Android入門之利用OKHttp實(shí)現(xiàn)斷點(diǎn)續(xù)傳功能
簡介
今天我們將繼續(xù)使用OkHttp組件并制作一個基于多線程的可斷點(diǎn)續(xù)傳的下載器來結(jié)束Android OkHttp組件的所有知識內(nèi)容。在這一課里我們會在上一次課程的基礎(chǔ)上增加SQLite的使用以便于我們的App可以暫存下載時的實(shí)時進(jìn)度,每次下載開始都會判斷是覆蓋式還是續(xù)傳式下載。同時由于Android自帶的進(jìn)度條太丑了,我們對它稍稍進(jìn)行了一些美化??梢哉f今天這篇教程也是一篇階段性的功能整合實(shí)驗(yàn)。
下面開始進(jìn)入課程。
課程目標(biāo)

1.使用SQLite進(jìn)行下載時的進(jìn)度信息的暫存;
2.自定義ProgressBar的樣式;
斷點(diǎn)下載的原理
如果你認(rèn)真的在看完了上篇教程后并且脫離我的Sample代碼自己動手實(shí)現(xiàn)了一個多線程下載器的話那么今天這篇教程對于你來說會變得相當(dāng)?shù)暮唵巍?/p>
因?yàn)樗^的斷點(diǎn)下載就是把每一條線程當(dāng)前在下載的信息存入一個SQLite的表內(nèi)。而斷點(diǎn)下載就是通過暫存的信息去改變RandomAcessFile在寫入時的seek。
當(dāng)然這里面還伴隨著一些小技巧,我們需要我們的APP的“STOP”動作可以打斷正在下載的進(jìn)度,打斷后如果再次點(diǎn)擊了“DOWNLOAD”按鈕,此時各子線程做的任務(wù)為“續(xù)傳”,續(xù)傳的進(jìn)度是否完成了呢這也需要子線程和主線程間進(jìn)行狀態(tài)通信。
需要知道每個子線程運(yùn)行是否已經(jīng)結(jié)束了
這邊并不是需要知道每個子線程的返回、中間態(tài)。我們只是需要知道每一個子線程是否運(yùn)行完了。
在平時開發(fā)中我們經(jīng)常會面臨這樣的一種情況。比如說我們外部需要長時間的等待?或者也有開發(fā)搞了一個全局的棧去計算、也有用future接口的。很多時候往往為了取一個狀態(tài),開發(fā)創(chuàng)造了一堆的“輪子”,導(dǎo)致了整個項(xiàng)目代碼過于復(fù)雜以及不好調(diào)試。因此這些手法都不是很優(yōu)雅。今天筆者給各位推薦一種更為優(yōu)雅的寫法,以便于在外部判斷每一個子線程是否都運(yùn)行完畢了。
使用狀態(tài)反轉(zhuǎn)來不斷check子線程狀態(tài)

其實(shí)它的核心思路是:
- 在外部有一個無限while 循環(huán),while(notFinish);
- 循環(huán)入口上手就把循環(huán)終止, notFinish=false;
- 接著依次檢查每一個子線程內(nèi)的一個狀態(tài)值-finish,這個值在每個子線程內(nèi)任務(wù)結(jié)束后會設(shè)為true。只要這個值在外部被檢測到不為true,那么把外部循環(huán)的狀態(tài)再改為notFinish=true,以使得外部循環(huán)不斷運(yùn)行直到所有子線程檢測下來都確為finish,此時外部的while循環(huán)跳出;

每個子線程下載的實(shí)時信息存儲

我們設(shè)計了一個這樣的表結(jié)構(gòu)用來存儲下載的實(shí)時信息。
- 每次下載進(jìn)程開始時,先根據(jù)下載URL去該表中查出所有的下載信息。比如說我們開啟了3個線程,那么對于同一個URL:/test.zip可以根據(jù)download_path查出3條數(shù)據(jù)。把3條數(shù)據(jù)的download_length相加拼在一起,如果<當(dāng)前遠(yuǎn)程文件size說明上次下載沒有完成,那么繼續(xù)下載。否則新建一個空文件并把這個空文件的長度設(shè)定為遠(yuǎn)程資源文件的長度;
- 每個子線程在下載時不斷根據(jù)download_path update這張表里的數(shù)據(jù)把當(dāng)前的實(shí)時進(jìn)度寫進(jìn)去;
- 下載完后根據(jù)download_path清空這個表里的數(shù)據(jù);
Http Get請求如何支持?jǐn)帱c(diǎn)續(xù)傳
Request.addHeader("Range", "bytes=" + startPos + "-" + endPos)
假設(shè)線程編號從1開始,開了3個子線程,共有1-3個線程,線程編號為1-3,此處的startPos和endPos的計算公式如下:
- startPos=每個線程分頁下載文件大小*線程編號+上一次下載進(jìn)度,如果線程為1號線程那么startPos=上一次的下載進(jìn)度;
- endPos=每個線程分頁下載文件大小*當(dāng)前線程編號-1,-1代表“不計算文件末尾結(jié)束符”;
int startPos = block * (threadId - 1) + downLength;//開始位置 int endPos = block * threadId - 1;//結(jié)束位置
自定義Android里的ProgressBar的樣式
第一步:
res\values\colors.xml文件中加入一個ProgressBar的底色theme_progressbar
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="theme_progressbar">#D0E3F7</color>
</resources>
這是一個很淺很淡的藍(lán)色。
第二步:
res\drawable\下,新建一個progressbar_color.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<!-- 背景 gradient是漸變,corners定義的是圓角 -->
<item android:id="@android:id/background">
<shape>
<corners android:radius="3dp"/>
<solid android:color="@color/theme_progressbar" />
</shape>
</item>
<!-- 進(jìn)度條 -->
<item android:id="@android:id/progress">
<clip>
<shape>
<corners android:radius="3dp"/>
<solid android:color="#FF51AAE6" />
</shape>
</clip>
</item>
</layer-list>第三步:
在activity_main.xml文件里定義progressbar時引用這個progressbar_color.xml文件。
<ProgressBar
android:id="@+id/progressBarDownload"
style="@android:style/Widget.DeviceDefault.Light.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:max="100"
android:progressDrawable="@drawable/progressbar_color"
android:visibility="visible" />
以上內(nèi)容都準(zhǔn)備好了,我們就可以進(jìn)入全代碼了。
項(xiàng)目結(jié)構(gòu)

前端代碼
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/buttonDownload"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="download"
android:layout_marginRight="10dp"
android:textSize="20sp" />
<Button
android:id="@+id/buttonStop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="stop"
android:textSize="20sp" />
</LinearLayout>
<ProgressBar
android:id="@+id/progressBarDownload"
style="@android:style/Widget.DeviceDefault.Light.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:max="100"
android:progressDrawable="@drawable/progressbar_color"
android:visibility="visible" />
</LinearLayout>后端代碼
DbOpeerateHelper.java
package org.mk.android.demo.http;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
public class DbOperateHelper extends SQLiteOpenHelper {
private static final String TAG = "DemoContinueDownload";
private static final String DB_NAME = "dw_manager.db";
private static final String DB_TABLE = "dw_infor";
private static final int DB_VERSION = 1;
public DbOperateHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
}
private static final String DB_CREATE =
"CREATE TABLE dw_infor ("
+"dw_id INTEGER PRIMARY KEY AUTOINCREMENT,"
+"download_path VARCHAR,"
+"thread_id INTEGER,"
+"download_length INTEGER);";
@Override
public void onCreate(SQLiteDatabase db) {
Log.i(TAG, ">>>>>>execute create table->" + DB_CREATE);
db.execSQL(DB_CREATE);
Log.i(TAG, ">>>>>>db init successfully");
}
@Override
public void onUpgrade(SQLiteDatabase db, int _oldVersion, int _newVersion) {
//db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
//onCreate(_db);
db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
onCreate(db);
}
}DBService.java
package org.mk.android.demo.http;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class DBService {
private static final String TAG = "DemoContinueDownload";
private DbOperateHelper dbHelper;
private static final String DB_NAME = "dw_manager.db";
private static final String DB_TABLE = "dw_infor";
private static final int DB_VERSION = 1;
public DBService(Context ctx){
dbHelper=new DbOperateHelper(ctx,DB_NAME,null,DB_VERSION);
}
/**
* 獲得指定URI的每條線程已經(jīng)下載的文件長度
* @param downloadPath
* @return
* */
public List<DWManagerInfor> getData(String downloadPath)
{
//獲得可讀數(shù)據(jù)庫句柄,通常內(nèi)部實(shí)現(xiàn)返回的其實(shí)都是可寫的數(shù)據(jù)庫句柄
//根據(jù)下載的路徑查詢所有現(xiàn)場的下載數(shù)據(jù),返回的Cursor指向第一條記錄之前
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor = db.rawQuery("select thread_id, download_length from dw_infor where download_path=?",
new String[]{downloadPath});
List<DWManagerInfor> data=new ArrayList<DWManagerInfor>();
try {
//從第一條記錄開始遍歷Cursor對象
//cursor.moveToFirst();
while (cursor.moveToNext()) {
DWManagerInfor dwInfor =new DWManagerInfor();
dwInfor.setThreadId(cursor.getInt(cursor.getColumnIndexOrThrow("thread_id")));
dwInfor.setDownloadLength(cursor.getInt(cursor.getColumnIndexOrThrow("download_length")));
data.add(dwInfor);
}
}catch(Exception e){
Log.e(TAG,">>>>>>getData from db error: "+e.getMessage(),e);
}finally{
try {
cursor.close();//關(guān)閉cursor,釋放資源;
}catch(Exception e){}
try {
db.close();
}catch(Exception e){}
}
return data;
}
/**
* 保存每條線程已經(jīng)下載的文件長度
*/
public void save(String downloadPath, Map<Integer,Integer> map)
{
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.beginTransaction();
try{
//使用增強(qiáng)for循環(huán)遍歷數(shù)據(jù)集合
for(Map.Entry<Integer, Integer> entry : map.entrySet())
{
db.execSQL("insert into dw_infor(download_path, thread_id, download_length) values(?,?,?)",
new Object[]{downloadPath, entry.getKey(),entry.getValue()});
}
//設(shè)置一個事務(wù)成功的標(biāo)志,如果成功就提交事務(wù),如果沒調(diào)用該方法的話那么事務(wù)回滾
//就是上面的數(shù)據(jù)庫操作撤銷
db.setTransactionSuccessful();
}catch(Exception e){
Log.e(TAG,">>>>>>save download infor into db error: "+e.getMessage(),e);
}finally{
//結(jié)束一個事務(wù)
db.endTransaction();
try{
db.close();
}catch(Exception e){}
}
}
public int updateItem(DWManagerInfor dwInfor)throws Exception{
SQLiteDatabase db = dbHelper.getWritableDatabase();
try{
ContentValues newValues = new ContentValues();
newValues.put("download_length",dwInfor.getDownloadLength());
newValues.put("thread_id",dwInfor.getThreadId());
newValues.put("download_path",dwInfor.getDownloadPath());
return db.update(DB_TABLE,newValues,"thread_id='"+dwInfor.getThreadId()+"' and download_path='"+dwInfor.getDownloadPath()+"'",null);
}catch(Exception e){
Log.e(TAG,"update item error: "+e.getMessage(),e);
throw new Exception("update item error: "+e.getMessage(),e);
}finally{
try{
db.close();
}catch(Exception e){}
}
}
public void delete(String path)
{
SQLiteDatabase db = dbHelper.getWritableDatabase();
try {
String deleteSql = "delete from dw_infor where download_path=?";
db.execSQL(deleteSql, new Object[]{path});
}catch(Exception e){
Log.e(TAG,">>>>>>delete from path->"+path+" error: "+e.getMessage(),e);
}finally{
try{
db.close();
}catch(Exception e){}
}
}
public long addItem(DWManagerInfor dwInfor)throws Exception{
SQLiteDatabase db = dbHelper.getWritableDatabase();
try{
ContentValues newValues = new ContentValues();
newValues.put("download_path", dwInfor.getDownloadPath());
newValues.put("thread_id", dwInfor.getThreadId());
newValues.put("download_length", dwInfor.getDownloadLength());
Log.i(TAG, "addItem successfully");
return db.insert(DB_TABLE, null, newValues);
}catch(Exception e){
Log.e(TAG,">>>>>>addItem into db error: "+e.getMessage(),e);
throw new Exception(">>>>>>addItem into db error: "+e.getMessage(),e);
}finally{
try{
db.close();
}catch(Exception e){}
}
}
}DownloadProgressListener.java
package org.mk.android.demo.http;
public interface DownloadProgressListener {
public void onDownloadSize(int size);
}
DWManagerInfor.java
package org.mk.android.demo.http;
import java.io.Serializable;
public class DWManagerInfor implements Serializable {
private int dwId=0;
private int threadId=0;
public int getDwId() {
return dwId;
}
public void setDwId(int dwId) {
this.dwId = dwId;
}
public int getThreadId() {
return threadId;
}
public void setThreadId(int threadId) {
this.threadId = threadId;
}
public int getDownloadLength() {
return downloadLength;
}
public void setDownloadLength(int downloadLength) {
this.downloadLength = downloadLength;
}
public String getDownloadPath() {
return downloadPath;
}
public void setDownloadPath(String downloadPath) {
this.downloadPath = downloadPath;
}
private int downloadLength=0;
private String downloadPath="";
}DownloadService.java
這是一個主要的用于啟動多線程下載和操作斷點(diǎn)信息的類,在這個類內(nèi)會分出3個子線程,每個子線程內(nèi)又把這個類的this傳入在子線程內(nèi)進(jìn)行回調(diào)、寫下載時的實(shí)時信息入庫、傳遞子線程狀態(tài),因此它是一個核心類。
package org.mk.android.demo.http;
import android.content.Context;
import android.os.Environment;
import android.util.Log;
import androidx.annotation.NonNull;
import org.apache.commons.io.FilenameUtils;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.URL;
import java.sql.Array;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class DownloadService {
private static final String TAG = "DemoContinueDownload";
private File saveFile;
private int downloadedSize = 0; //已下載的文件長度
private Context context = null;
private int threadCount = 3;
private int fileSize = 0;
private int block = 0;
private Map<Integer, Integer> data = new ConcurrentHashMap<Integer, Integer>(); //緩存?zhèn)€條線程的下載的長度
//private DBAdapter dbAdapter = null;
private DBService dbService=null;
private DownloadThread[] threads; //根據(jù)線程數(shù)設(shè)置下載的線程池
private boolean exited = false;
private String downloadUrl = "";
public DownloadService(Context context, String downloadUrl) {
this.context = context;
//dbAdapter = new DBAdapter(context);
dbService=new DBService(context);
this.threads = new DownloadThread[threadCount];
this.downloadUrl = downloadUrl;
}
public int getFileSize() {
return this.fileSize;
}
/**
* 退出下載
*/
public void exit() {
Log.i(TAG, ">>>>>>觸發(fā)了exited");
this.exited = true; //將退出的標(biāo)志設(shè)置為true;
}
public boolean getExited() {
return this.exited;
}
/**
* 累計已下載的大小
* 使用同步鎖來解決并發(fā)的訪問問題
*/
protected synchronized void append(int size) {
//把實(shí)時下載的長度加入到總的下載長度中
downloadedSize += size;
}
/**
* 更新指定線程最后下載的位置
*
* @param threadId 線程id
* @param pos 最后下載的位置
*/
protected synchronized void update(int threadId, int pos) {
try {
this.data.put(threadId, pos);
//dbAdapter.open();
DWManagerInfor dwInfor = new DWManagerInfor();
dwInfor.setDownloadPath(this.downloadUrl);
dwInfor.setThreadId(threadId);
dwInfor.setDownloadLength(pos);
//dbAdapter.updateItem(dwInfor);
dbService.updateItem(dwInfor);
} catch (Exception e) {
Log.e(TAG, ">>>>>>update error: " + e.getMessage(), e);
}
//把指定線程id的線程賦予最新的下載長度,以前的值會被覆蓋掉
this.data.put(threadId, pos);
//更新數(shù)據(jù)庫中制定線程的下載長度
}
private String generateFile(long fileLength, boolean generateFile) throws Exception {
String end = downloadUrl.substring(downloadUrl.lastIndexOf("."));
URL url = new URL(downloadUrl);
//String downloadFilePath = "Cache_" + System.currentTimeMillis() + end;
String urlFileName = FilenameUtils.getName(url.getPath());
RandomAccessFile file = null;
try {
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
String fileName = Environment.getExternalStorageDirectory().getCanonicalPath() + "/" + urlFileName;
Log.i(TAG, ">>>>>>需要操作的文件名為->" + fileName);
Log.i(TAG,">>>>>>downloadedSize->"+downloadedSize+" fileLength->"+fileLength);
if (generateFile) {
if(downloadedSize==0||downloadedSize>=fileLength) {
Log.i(TAG,">>>>>>新建文件并設(shè)定長度->"+fileLength);
file = new RandomAccessFile(fileName, "rwd");
file.setLength(fileLength);
file.close();
}else{
Log.i(TAG,">>>>>>文件存在,返回文件名進(jìn)行續(xù)傳");
}
}
return fileName;
} else {
throw new Exception("SD卡不可讀寫");
}
} catch (Exception e) {
throw new Exception("GenerateTempFile error: " + e.getMessage(), e);
} finally {
try {
file.close();
} catch (Exception e) {
}
}
}
public int getRemainDownloadLen(int threadCount, long fileLength) {
int block = 0;
try {
block = (int) fileLength % threadCount == 0 ? (int) fileLength / threadCount :
(int) fileLength / threadCount + 1;
} catch (Exception e) {
Log.e(TAG, ">>>>>>getRemainDownloadLen error: " + e.getMessage(), e);
}
return block;
}
public void download(boolean generateFile, DownloadProgressListener downloadProgressListener) throws Exception {
try {
fileSize = getDownloadFileSize(downloadUrl);
//把所有的DB內(nèi)已經(jīng)存在的size放入全局的data中,以作緩存
List<DWManagerInfor> dwInforList = new ArrayList<DWManagerInfor>();
dwInforList = dbService.getData(downloadUrl);
Log.i(TAG, ">>>>>>in download method the dwInforList size->" + dwInforList.size());
if (dwInforList.size() > 0) {
for (DWManagerInfor dwInfor : dwInforList) {
downloadedSize += dwInfor.getDownloadLength();
this.data.put(dwInfor.getThreadId(), dwInfor.getDownloadLength());
}
} else {
for (int i = 0; i < threadCount; i++) {
this.data.put(i + 1, 0);
}
}
this.block = getRemainDownloadLen(3, fileSize);
Log.i(TAG,">>>>>>downloadSize->"+downloadedSize);
String saveFileName = generateFile(this.fileSize, generateFile);//生成一個Random空文件并把文件長度設(shè)置好
Log.i(TAG, ">>>>>>開始生成線程進(jìn)行分: " + this.threadCount + " 條線程并行下載...每條線程的block->" + this.block);
Log.i(TAG, ">>>>>>全局data size->" + data.size());
for (int i = 0; i < this.threads.length; i++) {//開啟線程進(jìn)行下載
int downLength = 0;
if (data.size() > 0) {
downLength = this.data.get(i + 1);
}
Log.i(TAG, ">>>>>>開啟前發(fā)覺當(dāng)前下載進(jìn)度為->" + downLength);
//通過特定的線程id獲取該線程已經(jīng)下載的數(shù)據(jù)長度
//判斷線程是否已經(jīng)完成下載,否則繼續(xù)下載
if (downLength < this.block && this.downloadedSize < this.fileSize) {
//初始化特定id的線程
//this.threads[i] = new DownloadThread(this, url, this.saveFile, this.block, this.data.get(i+1),
// i+1);
this.threads[i] = new DownloadThread(this, downloadUrl, saveFileName, this.block,
this.data.get(i + 1), i + 1);
//設(shè)置線程優(yōu)先級,Thread.NORM_PRIORITY = 5;
//Thread.MIN_PRIORITY = 1;Thread.MAX_PRIORITY = 10,數(shù)值越大優(yōu)先級越高
this.threads[i].setPriority(7);
this.threads[i].start(); //啟動線程
} else {
Log.i(TAG, "當(dāng)前線程不用下載,因?yàn)楫?dāng)前線程己下載長度downLength->" + downLength + " block->" + this.block);
this.threads[i] = null; //表明線程已完成下載任務(wù)
}
}
//dbAdapter.open();
dbService.delete(downloadUrl);
dbService.save(downloadUrl, this.data);
//把下載的實(shí)時數(shù)據(jù)寫入數(shù)據(jù)庫中
boolean notFinish = true;
//下載未完成
while (notFinish) {
// 循環(huán)判斷所有線程是否完成下載
Thread.sleep(300);
notFinish = false;
for (int i = 0; i < threadCount; i++) {
if (this.threads[i] != null && !this.threads[i].isFinish()) {
//如果發(fā)現(xiàn)線程未完成下載
notFinish = true;
//設(shè)置標(biāo)志為下載沒有完成,以便于外層while循環(huán)不斷check;
}
}
if (downloadProgressListener != null) {
downloadProgressListener.onDownloadSize(this.downloadedSize);
}
//通知目前已經(jīng)下載完成的數(shù)據(jù)長度
}
if (downloadedSize == this.fileSize) {
dbService.delete(downloadUrl);
}
} catch (Exception e) {
Log.e(TAG, ">>>>>>download error: " + e.getMessage(), e);
throw new Exception(">>>>>>download error: " + e.getMessage(), e);
}
}
public int getDownloadFileSize(String downloadUrl) throws Exception {
int size = -1;
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)//設(shè)置連接超時時間
.readTimeout(10, TimeUnit.SECONDS).build();//設(shè)置讀取超時時間
Request request = new Request.Builder().url(downloadUrl)//請求接口,如果需要傳參拼接到接口后面
.build(); //創(chuàng)建Request對象
Response response = null;
try {
Call call = client.newCall(request);
response = call.execute();
if (200 == response.code()) {
Log.d(TAG, ">>>>>>response.code()==" + response.code());
Log.d(TAG, ">>>>>>response.message()==" + response.message());
try {
size = (int) response.body().contentLength();
Log.d(TAG, ">>>>>>file length->" + size);
//fileSizeListener.onHttpResponse((int) size);
} catch (Exception e) {
Log.e(TAG, ">>>>>>get remote file size error: " + e.getMessage(), e);
}
}
} catch (Exception e) {
Log.e(TAG, ">>>>>>open connection to path->" + downloadUrl + "\nerror: " + e.getMessage(), e);
throw new Exception(">>>>>>getDownloadFileSize from->" + downloadUrl + "\nerror: " + e.getMessage(), e);
} finally {
try {
response.close();
} catch (Exception e) {
}
}
return size;
}
}DownloadThread.java
這個類就是每一個子線程的實(shí)現(xiàn)了。在這個類里每一個子線程會啟動OkHttp并使用http-header: Range去做斷點(diǎn)下載。
值得注意的是,如果你的http-header帶著Rnage去做請求,你得到的response code不是200還是206即:partial content。
package org.mk.android.demo.http;
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class DownloadThread extends Thread {
private static final String TAG = "DemoContinueDownload";
private String downloadUrl; //下載的URL
private int block; //每條線程下載的大小
private int threadId = 1; //初始化線程id設(shè)置
private int downLength; //該線程已下載的數(shù)據(jù)長度
private boolean finish = false; //該線程是否完成下載的標(biāo)志
private DownloadService downloader;
private String saveFileName = "";
//private FileDownloadered downloader; //文件下載器
public DownloadThread(DownloadService downloader, String downloadUrl, String saveFileName, int block,
int downLength, int threadId) {
this.downloader = downloader;
this.downloadUrl = downloadUrl;
this.saveFileName = saveFileName;
this.block = block;
this.downLength = downLength;
this.threadId = threadId;
}
@Override
public void run() {
Log.i(TAG, ">>>>>>downloadLength->" + downLength + " block->" + block);
if (downLength < block) {
int startPos = block * (threadId - 1) + downLength;//開始位置
int endPos = block * threadId - 1;//結(jié)束位置
OkHttpClient client = new OkHttpClient.Builder().connectTimeout(10, TimeUnit.SECONDS)//設(shè)置連接超時時間
.readTimeout(10, TimeUnit.SECONDS)//設(shè)置讀取超時時間
.build();
Request request = new Request.Builder().get().url(downloadUrl)//請求接口,如果需要傳參拼接到接口后面
.addHeader("Referer", downloadUrl)
.addHeader("Accept", "image/gif, image/jpeg, image/pjpeg, image/pjpeg, " +
"application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, " +
"application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, " +
"application/vnd.ms-powerpoint, application/msword, */*")
.addHeader("connection", "keep-alive")
.addHeader("Range", "bytes=" + startPos + "-" + endPos)
.addHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; .NET " +
"CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET " +
"CLR 3.5.30729)")
.build(); //創(chuàng)建Request對象
//Log.i(TAG, ">>>>>>線程" + threadId + "開始下載...Range: bytes=" + startPos + "-" + endPos);
Call call = client.newCall(request);
//異步請求
call.enqueue(new Callback() {
//失敗的請求
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
Log.e(TAG, ">>>>>>下載進(jìn)程加載->" + downloadUrl + " error:" + e.getMessage(), e);
finish = true;
}
//結(jié)束的回調(diào)
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
Log.i(TAG, ">>>>>>連接->" + downloadUrl + " 己經(jīng)連接,進(jìn)入下載...");
InputStream is = null;
Log.i(TAG, ">>>>>>當(dāng)前:response code->" + response.code());
RandomAccessFile threadFile = null;
try {
if (response.code() == 200 || response.code() == 206) {
Log.i(TAG, ">>>>>>response.code()==" + response.code());
//Log.i(TAG, ">>>>>>response.message()==" + response.message());
is = response.body().byteStream();
byte[] buffer = new byte[1024];
int offset = 0;
int length = 0;
threadFile = new RandomAccessFile(saveFileName, "rwd");
threadFile.seek(startPos);
while (!downloader.getExited() && (offset = is.read(buffer, 0, 1024)) != -1) {
//Log.i(TAG,">>>>>>offset write->"+offset);
threadFile.write(buffer, 0, offset);
downLength += offset;
downloader.update(threadId, downLength);
downloader.append(offset);
}
//Log.i(TAG,"current offset->"+offset);
//Log.i(TAG, ">>>>>>線程" + threadId + "已下載完成");
finish = true;
threadFile.close();
}
} catch (Exception e) {
downLength = -1; //設(shè)置該線程已經(jīng)下載的長度為-1
Log.e(TAG, ">>>>>>線程:" + threadId + " 下載出錯: " + e.getMessage(), e);
finish = true;
} finally {
try {
threadFile.close();
;
} catch (Exception e) {
}
try {
is.close();
;
} catch (Exception e) {
}
}
}
});
}
}
/**
* 下載是否完成
*
* @return
*/
public boolean isFinish() {
return finish;
}
/**
* 已經(jīng)下載的內(nèi)容大小
*
* @return 如果返回值為-1,代表下載失敗
*/
public long getDownLength() {
return downLength;
}
}MainActivity.java
package org.mk.android.demo.http;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.content.Intent;
import android.database.sqlite.SQLiteDatabase;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.provider.Settings;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private SQLiteDatabase db;
private Context context;
//private DBAdapter dbAdapter;
private DBService dbService;
private Button buttonDownload;
private Button buttonStop;
private DownloadTask downloadTask;
private ProgressBar progressBarDownload;
private static final String TAG = "DemoContinueDownload";
//private static final String DOWNLOAD_URL = "http://www.jszjenergy.cn/data/upload/image/20191231/1577758425809614.jpg";
private static final String DOWNLOAD_URL = "https://7-zip.org/a/7z2201.exe";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
context = getApplicationContext();
//dbAdapter = new DBAdapter(context);
dbService=new DBService(context);
progressBarDownload = (ProgressBar) findViewById(R.id.progressBarDownload);
buttonDownload = (Button) findViewById(R.id.buttonDownload);
buttonStop = (Button) findViewById(R.id.buttonStop);
//progressBarDownload.setVisibility(View.GONE);
buttonDownload.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Log.i(TAG, ">>>>>>version.SDK->" + Build.VERSION.SDK_INT);
if (!Environment.isExternalStorageManager()) {
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
startActivity(intent);
return;
}
}
downloadTask = new DownloadTask();
downloadTask.start();
} catch (Exception e) {
Log.e(TAG, ">>>>>>downloadTest error: " + e.getMessage(), e);
}
}
});
buttonStop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (downloadTask != null) {
downloadTask.exit();
}
}
});
}
private Handler downloadHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(@NonNull Message msg) {
Log.i(TAG, ">>>>>>receive handler Message msg.what is: " + msg.what);
switch (msg.what) {
case 101:
progressBarDownload.setVisibility(View.VISIBLE);
//progressBarDownload.setProgress();
int inputNum = msg.getData().getInt("pgValue");
progressBarDownload.setProgress(inputNum);
if (inputNum >= 100) {
Toast.makeText(context, "下載完成", Toast.LENGTH_LONG).show();
}
break;
}
return false;
}
});
private class DownloadTask extends Thread {
private DownloadService loader;
/**
* 退出下載
*/
public void exit() {
if (loader != null) {
loader.exit();
}
}
@Override
public void run() {
try {
loader = new DownloadService(context, DOWNLOAD_URL);
//dbAdapter = new DBAdapter(context);
//dbAdapter.open();
//dbAdapter.delete(DOWNLOAD_URL);
loader.download(true, new DownloadProgressListener() {
@Override
public void onDownloadSize(int size) {
int fileSize=loader.getFileSize();
Log.i(TAG, ">>>>>>下載中,當(dāng)前尺寸: " + size+" totalSize->"+fileSize);
float progress = ((float) size / (float) fileSize) * 100;
int pgValue = (int) progress;
Message msg = new Message();
msg.what = 101;
Bundle bundle = new Bundle();
bundle.putInt("pgValue", pgValue);
msg.setData(bundle);
downloadHandler.sendMessage(msg);
}
});
} catch (Exception e) {
Log.e(TAG, ">>>>>>downloadTest error: " + e.getMessage(), e);
} finally {
//dbAdapter.close();
}
}
}
}為了正確運(yùn)行上述內(nèi)容你需要在gradle的build文件內(nèi)加入OkHttp和commons-io的依賴包。
implementation 'com.squareup.okhttp3:okhttp:3.10.0'
implementation group: 'commons-io', name: 'commons-io', version: '2.6'
運(yùn)行后的效果
當(dāng)你無論如何stop再download再stop或者下載完后多次再download,那么當(dāng)文件被成功下載后,會在Android的資源列表里此處顯示下載的資源。

它位于data\media\0下。
為了驗(yàn)證你下載的正確性,你可以把這個資源右鍵->另存出去。然后雙擊這個安裝程序,如果它可以正確安裝那么說明你的斷點(diǎn)下載是正確了。
結(jié)束今天的課程,不妨自己動一下手試試看吧。
附、AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- 在SDCard中創(chuàng)建與刪除文件權(quán)限 -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"
tools:ignore="ProtectedPermissions" />
<!-- 往SDCard寫入數(shù)據(jù)權(quán)限 -->
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<!--外部存儲的寫權(quán)限-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!--外部存儲的讀權(quán)限-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:networkSecurityConfig="@xml/network_config"
android:requestLegacyExternalStorage="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.DemoContinueDownloadProcess"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
</application>
</manifest>
以上就是Android入門之利用OKHttp實(shí)現(xiàn)斷點(diǎn)續(xù)傳功能的詳細(xì)內(nèi)容,更多關(guān)于Android OKHttp斷點(diǎn)續(xù)傳的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android程序打開和對輸入法的操作(打開/關(guān)閉)
整理了一下Android下對輸入法的操作:打開輸入法窗口、關(guān)閉出入法窗口、如果輸入法打開則關(guān)閉,如果沒打開則打開、獲取輸入法打開的狀態(tài)2013-05-05
在Android中 獲取正在運(yùn)行的Service 實(shí)例
本篇文章小編為大家介紹,在Android中 獲取正在運(yùn)行的Service 實(shí)例。需要的朋友參考下2013-04-04
android判斷一個Activity是否處于棧頂?shù)膶?shí)例
下面小編就為大家分享一篇android判斷一個Activity是否處于棧頂?shù)膶?shí)例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-03-03
Android 中ScrollView與ListView沖突問題的解決辦法
這篇文章主要介紹了Android 中ScrollView與ListView沖突問題的解決辦法的相關(guān)資料,希望通過本文能幫助到大家,讓大家掌握解決問題的辦法,需要的朋友可以參考下2017-10-10

