Android 實現檔案的單執行緒斷點續傳下載

Android 實現檔案的單執行緒斷點續傳下載

1.實際效果:

效果圖
開始介面


2.程式碼實現

完成這個小專案需要:

  • 基礎網路知識(Http)
  • 瞭解android介面處理機制
  • Service的繫結與解綁
  • BroadCastReceiver的註冊與訊息的處理
  • 本地檔案的I/O處理
  • 資料庫基礎
  • 事件回撥原理

在這裡我採用了資料庫框架GreenDao,方便實現想要的效果,自己獨立寫幾個類來操作SQLite資料庫也是可以的。關於怎麼使用GreenDao,這裡不做敘述了,網上一大堆教程,這裡給個教程連結http://m.blog.csdn.net/article/details?id=51893092

下面是專案框架
這裡寫圖片描述

主要有服務類DownloadService,廣播類ProgressReceiver,下載類DownloadTask,執行緒資訊類ThreadInfo,和MainActivity與App(繼承Application,用於初始化資料庫)。

首先來看看我的佈局檔案acitivty_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@ id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.chen.capton.filedownload.MainActivity">
<TextView
android:id="@ id/info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="檔案資訊:"
android:layout_alignEnd="@ id/pause" />
<ProgressBar
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@ id/info"
android:layout_alignParentStart="true"
android:id="@ id/progressBar" />
<Button
android:text="開始"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@ id/progressBar"
android:layout_alignParentStart="true"
android:id="@ id/start" />
<Button
android:text="暫停"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:enabled="false"
android:id="@ id/pause"
android:layout_below="@ id/progressBar"
android:layout_toEndOf="@ id/start" />
<TextView
android:text="進度"
android:gravity="right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:id="@ id/progressText"
android:layout_alignParentEnd="true"
android:layout_toEndOf="@ id/info" />
</RelativeLayout>

配置檔案menifests.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.chen.capton.filedownload">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application
android:name=".App"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".DownloadService"
android:enabled="true"
android:exported="true">
</service>
</application>
</manifest>

最主要的MainActivity:
洋洋灑灑100多行

package com.chen.capton.filedownload;
import android.content.ComponentName;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
private TextView fileInfoText,progressText;
private Button startBtn,pauseBtn;
private ProgressBar mProgressBar;
private ProgressReceiver mReceiver;
private final String REFRESH_PROGRESS="REFRESH_PROGRESS"; //設定Action,與ProgressReceiver,DownloadService中的Action都一致
private final String url="http://192.168.1.103/app.zip";  //設定url
private final int maxProgress=100;                        //設定進度條最大進度
private boolean isContinue;                               //是否暫停的標識
private Handler handler=new Handler(){
public void handleMessage(Message msg){
mProgressBar.setProgress(msg.what);//由於傳送的是空訊息,直接用What作為進度引數使用
progressText.setText("完成度:" msg.what "%"); 
}
}; //用於介面更新的handler。將它作為引數傳遞至ProgressReceiver,供其傳送Message,然後根據Message更新進度
private DownloadService mService;
private ServiceConnection conn=new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//獲取繫結好的DownloadService物件
mService= ((DownloadService.MyBinder)service).getService();
}
@Override
public void onServiceDisconnected(ComponentName name) {}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initComponent(); //初始化ProgressReceiver,DownloadService
setContentView(R.layout.activity_main);
initView(); //初始化檢視控制元件
setListener(); //設定點選事件
}
private void initView() {
fileInfoText= (TextView) findViewById(R.id.info);
progressText= (TextView) findViewById(R.id.progressText);
startBtn= (Button) findViewById(R.id.start);
pauseBtn= (Button) findViewById(R.id.pause);
mProgressBar= (ProgressBar) findViewById(R.id.progressBar);
mProgressBar.setMax(maxProgress);
}
/*
* 初始化ProgressReceiver,DownloadService
* 繫結DownloadService,註冊ProgressReceiver
* */
private void initComponent() {
Intent intent=new Intent(this,DownloadService.class);
bindService(intent,conn,BIND_AUTO_CREATE);
mReceiver=new ProgressReceiver(handler);
IntentFilter filter=new IntentFilter();
filter.addAction(REFRESH_PROGRESS);
registerReceiver(mReceiver,filter);
}
private void setListener() {
startBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(!isContinue) {
mService.startMission(url,maxProgress);
v.setEnabled(false);
pauseBtn.setEnabled(true);
fileInfoText.setText(getFileName(url));
}else {
mService.continueMission();
v.setEnabled(false);
pauseBtn.setEnabled(true);
}
}
});
pauseBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mService.pauseMission();
v.setEnabled(false);
startBtn.setEnabled(true);
isContinue=true;
startBtn.setText("繼續");
}
});
}
/*
* 獲取檔名
* */
private String getFileName(String url){
int start=url.lastIndexOf("/") 1;
int end=url.length();
return url.substring(start,end);
}
@Override
protected void onDestroy() {
/*
* 解繫結DownloadService,登出ProgressReceiver
* */
unregisterReceiver(mReceiver);
unbindService(conn);
super.onDestroy();
}
}

用於初始化資料庫的App類:
當然也可以在MainActivity中初始化資料庫,為了程式碼簡潔和更快地初始化資料庫,就寫著這裡了。

package com.chen.capton.filedownload;
import android.app.Application;
import android.database.sqlite.SQLiteDatabase;
/**
* Created by CAPTON on 2017/1/9.
*/
public class App extends Application {
public static App instances;
@Override
public void onCreate() {
super.onCreate();
setDatabase();
instances = this;
}
public static App getInstances(){
return instances;
}
/**
* 設定greenDao
*/
private DaoMaster.DevOpenHelper mHelper;
private SQLiteDatabase db;
private DaoMaster mDaoMaster;
private DaoSession mDaoSession;
private void setDatabase() {
mHelper = new DaoMaster.DevOpenHelper(this, "Dishes-db", null);
db = mHelper.getWritableDatabase();
mDaoMaster = new DaoMaster(db);
mDaoSession = mDaoMaster.newSession();
}
public DaoSession getDaoSession() {
return mDaoSession;
}
public SQLiteDatabase getDb() {
return db;
}
}

接下來就是貼各個類的程式碼了

服務類DownloadService:

package com.chen.capton.filedownload;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
/*
* 實現DownloadTask.RefreshProgressListener介面
* */
public class DownloadService extends Service implements DownloadTask.RefreshProgressListener{
private final String REFRESH_PROGRESS="REFRESH_PROGRESS"; //設定Action
private DownloadTask task;
private Intent intent;
@Override
public IBinder onBind(Intent intent) {
return new MyBinder();
}
class MyBinder extends Binder {
public DownloadService getService(){
intent=new Intent();
intent.setAction(REFRESH_PROGRESS);
return DownloadService.this;
}
}
/*
* 供DownloadTask回撥的方法,用於傳送重新整理進度的廣播
* */
@Override
public void refressProgress(int progress) {
intent.putExtra("progress",progress); //將進度值寫入intent
sendBroadcast(intent);  //傳送廣播
}
public void startMission(String url,int maxProgress){
task=new DownloadTask(url,maxProgress);
task.setRefreshProgressListener(this);
task.startMission();
};
public void pauseMission(){
task.pauseMission();
}
public void continueMission(){
task.continueMission();
}
}

廣播類ProgressReceiver:

package com.chen.capton.filedownload;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
/**
* Created by CAPTON on 2017/1/9.
*/
public class ProgressReceiver extends BroadcastReceiver {
private Handler handler;  //儲存從MainActivity傳來的handler;
private final String REFRESH_PROGRESS="REFRESH_PROGRESS";//設定Action
public ProgressReceiver(Handler handler) {
this.handler=handler;
}
@Override
public void onReceive(Context context, Intent intent) {
//判斷Action是否一致
if(intent.getAction().equals(REFRESH_PROGRESS)){
//從傳來的intent中獲取進度值
int progress=intent.getIntExtra("progress",0); 
//將帶有進度值的intent傳送出去,交與MainActivity中的handler處理
handler.sendEmptyMessage(progress); 
}
}
}

下載類(重點)DownloadTask:

package com.chen.capton.filedownload;
import android.os.Environment;
import android.util.Log;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* Created by CAPTON on 2017/1/9.
*/
public class DownloadTask {
private String url;           //下載地址
private ThreadInfo threadInfo;//執行緒資訊
private ThreadInfoDao dao; //資料庫入口物件
private DownloadThread thread; //下載執行緒
public boolean isPause;//是否斷開連線的標誌位,很關鍵,呵呵
private int maxProgress; //最大進度
public int filedLength;  //檔案長度
public int finishedLength; //完成的檔案長度(待儲存的檔案進度)
private RefreshProgressListener listener;//進度重新整理的監聽器用於呼叫Service的更新方法
public DownloadTask(String url,int maxProgress) {
this.url = url;
this.maxProgress=maxProgress;
threadInfo=new ThreadInfo();
DaoSession session=App.getInstances().getDaoSession();  //從App中獲取初始化好的DaoSession物件
dao=session.getThreadInfoDao();
thread=new DownloadThread(url,threadInfo,maxProgress);
}
/*
* 開始下載執行緒
* */
public void startMission(){
thread.start();
}
/*
* 暫停任務,即跳出while迴圈,將執行緒資訊儲存到資料庫
* */
public void pauseMission(){
isPause=true;
ThreadInfo info=dao.queryBuilder().where(ThreadInfoDao.Properties.Id.eq(1)).build().unique();
//第一次儲存進度時插入紀錄,之後更新紀錄即可
if(info==null) {
info=new ThreadInfo(null, filedLength, finishedLength);
dao.insert(info);
}else {
info.setFileLength(filedLength);
info.setFinishedLength(finishedLength);
dao.update(info);
}
}
/*
*  從資料庫讀取上次儲存的執行緒資訊,新建執行緒從指定位置下載剩下的部分
* */
public void continueMission(){
isPause=false;
ThreadInfo info=dao.queryBuilder().where(ThreadInfoDao.Properties.Id.eq(1)).build().unique();
if(info!=null){
thread=new DownloadThread(url,info,maxProgress);
thread.start();
}
}
/*
* 下載執行緒,核心
* */
class DownloadThread extends Thread{
private String url;
private ThreadInfo threadInfo;
private HttpURLConnection conn; //httpUrl連線
private InputStream is;   //輸入流
private File fileDir; //建立一個資料夾存放檔案
private File file;
private RandomAccessFile raFile;  //可隨機讀寫的File類,實際又不是繼承File,呵呵,斷點續傳必用。
private int maxProgress;
public DownloadThread(String url, ThreadInfo threadInfo, int maxProgress) {
this.url = url;
this.threadInfo=threadInfo;
this.maxProgress=maxProgress;
fileDir=new File(Environment.getExternalStorageDirectory(),"test");
if(!fileDir.exists()){
fileDir.mkdir(); //第一次下載時,應該沒有text目錄,新建一個
}
file=new File(fileDir,getFileName(url));
try {
raFile=new RandomAccessFile(file,"rw"); //設定檔案讀寫模式,"rw"為可讀可寫
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
public void run(){
/*
* 第一次連線,先獲取檔案長度,供後續設定檔案的傳輸範圍(byte)
* */
try {
URL Url = new URL(url);
HttpURLConnection conn= (HttpURLConnection) Url.openConnection();
conn.setRequestMethod("GET");
conn.setReadTimeout(3000);
filedLength=conn.getContentLength();
threadInfo.setFileLength(filedLength);
conn.disconnect();
} catch (IOException e) {
e.printStackTrace();
}
/*
* 第二次連線,根據檔案長度,上次儲存的進度,設定傳輸範圍,建立連線開始下載
* */
try {
URL Url=new URL(url);
conn= (HttpURLConnection) Url.openConnection();
conn.setRequestMethod("GET");  //設定連線方式
conn.setReadTimeout(5000);  //設定連線超時
//設定傳輸的範圍,例如"bytes=0-1231540",‘-’後面沒寫說明結束端為傳輸檔案的最後一位元組
conn.setRequestProperty("Range","bytes=" threadInfo.getFinishedLength() "-" threadInfo.getFileLength());
conn.connect();
is=conn.getInputStream();  //從連線物件中獲取輸入流
//資料輸入流,也可以用BufferedInputStream;
DataInputStream dis=new DataInputStream(is);
try {
//設定一定的延時,等待伺服器響應報文
Thread.sleep(1800);
} catch (InterruptedException e) {
e.printStackTrace();
}
/*
* 根據響應碼判斷是否成功連線伺服器
* */
if(conn.getResponseCode()==HttpURLConnection.HTTP_OK||
conn.getResponseCode()==HttpURLConnection.HTTP_PARTIAL) {
raFile.seek(threadInfo.getFinishedLength());//跳轉至上一次暫停時儲存的位置
byte[] b = new byte[1024];  //設定byte陣列,大小適度即可;
int len;    //每次寫入b中的實際位元組數
long now=System.currentTimeMillis(); //設定迴圈初始時間
while ((len = dis.read(b)) != -1) {
raFile.write(b, 0, len); //將儲存在b中的資料寫入檔案
finishedLength  = len;   //累加下載長度
/*
* 判斷檔案(下載)寫入消耗的時間是否大於100ms,若是才跟新進度,不設定的話,
* 重新整理頻率=檔案長度(很大的數)/1kb,會明顯地限制傳輸速度
* */
if(System.currentTimeMillis()-now>=100) {
now=System.currentTimeMillis();
//根據公式算出實際進度大小,然後呼叫DownloadService的實現方法refressProgress(int progress);
listener.refressProgress((int) ((long) finishedLength * maxProgress / threadInfo.getFileLength()));
}else {
/*當檔案(下載)寫入消耗的時間小於100ms時,判斷是否下載完成,若是則把進度設定為最大這個判斷存在的意義在於,當檔案下載完全時,消耗時間又小於100ms,進度顯示為100%, 若不設定,則進度顯示會卡在90%-100%之間,檔案越小,顯示誤差越大*/
if (finishedLength>=threadInfo.getFileLength()){
listener.refressProgress(maxProgress);
}
}
if (isPause) {
break;
}
}
threadInfo.setFinishedLength(finishedLength); //儲存下載資訊
//寫入完畢或者暫停則斷開連線
is.close();
conn.disconnect();
}
} catch (IOException e) {
e.printStackTrace();
}
}
};
/*
* 設定回撥方法,和回撥介面,讓DownloadService實現介面用於重新整理進度。
* */
public void setRefreshProgressListener(RefreshProgressListener listener){
this.listener=listener;
}
public interface RefreshProgressListener{
void refressProgress(int progress);
}
private String getFileName(String url){
int start=url.lastIndexOf("/") 1;
int end=url.length();
return url.substring(start,end);
}
}

執行緒資訊類ThreadInfo:

package com.chen.capton.filedownload;
import org.greenrobot.greendao.annotation.Entity;
import org.greenrobot.greendao.annotation.Id;
import org.greenrobot.greendao.annotation.Generated;
/**
* Created by CAPTON on 2017/1/9.
*/
//宣告此類是GreenDao框架的Entity實體類
@Entity
public class ThreadInfo {
//宣告這是自增的唯一鍵
@Id
private Long id;
private int fileLength;
private int finishedLength;
/*
*用Android Studio 點選Build選項下的"Make Project",後面的程式碼會自動生成
*/
@Generated(hash = 956576157)
public ThreadInfo(Long id, int fileLength, int finishedLength) {
this.id = id;
this.fileLength = fileLength;
this.finishedLength = finishedLength;
}
@Generated(hash = 930225280)
public ThreadInfo() {
}
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
public int getFileLength() {
return this.fileLength;
}
public void setFileLength(int fileLength) {
this.fileLength = fileLength;
}
public int getFinishedLength() {
return this.finishedLength;
}
public void setFinishedLength(int finishedLength) {
this.finishedLength = finishedLength;
}
}

程式碼就全部貼完了,至於思路是怎麼屢清楚的,主要看你對各個知識點掌握的熟練程度。


3.設計思路

寫這個小demo,我的思路是:
❶,先要明確目標:檔案的單執行緒斷點續傳,首先單執行緒先不管,後面還有多執行緒呢,斷點續傳是重點,說道斷點續傳你就應該明白要儲存暫停時的進度了,儲存進度需要用什麼途徑?SharePreference,SQLite(這裡採用的途徑),檔案儲存?如果是單執行緒,只需要儲存一個進度資訊,SharePreference是最方便了,把檔案長度,已下載長度兩個引數寫進xml檔案裡就行了,需要續傳檔案時從xml裡讀就行了,當然這只是最簡單的情況;如果是多執行緒下載時,比如10個執行緒,你就需要至少20個名稱空間來儲存引數,這樣很麻煩,讀寫資訊都很麻煩,不如資料庫來的方便了。至於用檔案儲存資訊就不要去想了,更麻煩,可以自行琢磨。
❷,資訊的儲存方式確定了,接下來是考慮如何下載檔案了,當然不要去用什麼框架來下載了,自己手寫,從http連線開始寫,到檔案寫入,關閉http連線為止,都自己寫。這裡Http連線用到的是HttpUrlConnection,也可以用HttpClient。檔案的來源弄懂了,然後是檔案輸出,斷點續傳要用到RandomAccessFile這個類,可以實現檔案的隨機位置的讀寫,當然這個類並不是繼承File類,而是實現 DataOutput, DataInput, Closeable這幾個介面,所以我們在匯入資料的時候用DataInputStream比較好,BufferdInputStream也是可以的。
❸檔案如何下載寫入和儲存搞定了,接下來是進度重新整理的問題,這個就涉及到Service,BroadCastReceiver,Handler這三大塊的知識了,把這些基礎知識先掌握好。其實我剛開始學的時候沒用Service,BroadCastReceiver,直接在MainActivity裡寫,當然程式碼量就嚇人了,結構也看起來很複雜,不過感覺下載速度確實最快(估計是Service不用一直髮送訊息給BroadCastReceiver,傳送一個訊息雖然很快,但是進度是不斷重新整理的,積少成多,我們的裝置要處理的訊息就多了,效率就下降了,從而影響檔案傳輸速度了)。
❹各大類的呼叫關係
(1)MainActivity(點選”開始”,”暫停”按鈕)呼叫DownloadService內的方法
(2)DownloadService繼續呼叫DownloadTask的相應方法
(3)DownloadTask開始其中的下載執行緒,執行緒下載一定位元組的後,回撥DownloadService的refressProgress(int progress)方法
(4)refressProgress方法傳送廣播給ProgressReceiver,ProgressReceiver根據發來的資訊通過Handler轉發到MainActivity的Handler中
(5)MainActivity中的Handler收到最終訊息,更新UI。
若是點選”暫停”按鈕,“開始”按鈕變為“繼續”按鈕,->(3)中跳出檔案讀取的迴圈,並把進度寫入資料庫,再次點選“繼續”按鈕續傳檔案,DownloadTask讀取資料庫資料,重新開始(3)(4)(5)。

前面的是大致思路,具體細節要深入到程式碼裡去剖析,如果你是大神,餘光一瞥就能理解每一行程式碼的用意;如果你對各個知識掌握的還不夠熟練,可能就卡在某處了。幾乎所有重要的方法和類我都一一註釋了其用意了,剩下一些繁文縟節就沒有敘述了,希望大家都能明白。

建議:
❶最好在個人電腦上構建一個區域網伺服器,通過區域網來測試下載任務(不用流量),我用的是WAMP,簡單粗暴,把檔案丟進根目錄下的“www”目錄即可通過類似“http://192.168.1.10x/xxx.xxx“的url找到你的檔案。建好伺服器啟動後發現手機無法訪問地址,可以試試關閉電腦的防火牆(百試不厭),當然用完最好恢復回去。
❷檔案最好選擇一些裝置需要檢查其完整性的格式,如zip,rar,apk, 嘗試開啟檔案來檢驗是否下載完整
❸手機最好已經root,方便檢視資料庫的資訊(注意:GreenDao輸出的資料庫格式可能不是以db結尾(受初始化時的命名決定),手動改成”.db”結尾就可以開啟了)。這裡推薦“RE檔案管理器”這款軟體來檢視root後的手機資料夾。當然不root的話,如果是資料出錯,就手動寫程式碼檢查資料庫嘍。


4.相關檔案

單執行緒斷點續傳 demo apk 連結:http://pan.baidu.com/s/1sltoHrB 密碼:v4tj
完整專案demo 連結:http://pan.baidu.com/s/1kVBbTcj 密碼:qcb7