Android中的多執行緒斷點續傳

liuxuhui發表於2021-09-09

Android多執行緒斷點下載的程式碼流程解析:

執行效果圖

圖片描述

實現流程全解析


Step 1:建立一個用來記錄執行緒下載資訊的表

建立資料庫表,於是乎我們建立一個資料庫的管理器類,繼承SQLiteOpenHelper類 重寫onCreate()與onUpgrade()方法,我們建立的表欄位如下: 圖片描述

DBOpenHelper.java

package com.jay.example.db;import android.content.Context;import android.database.sqlite.SQLiteDatabase;import android.database.sqlite.SQLiteDatabase.CursorFactory;import android.database.sqlite.SQLiteOpenHelper;public class DBOpenHelper extends SQLiteOpenHelper {
  public DBOpenHelper(Context context) {
    super(context, "downs.db", null, 1);
  }
  @Override
  public void onCreate(SQLiteDatabase db) {
    //資料庫的結構為:表名:filedownlog 欄位:id,downpath:當前下載的資源,
    //threadid:下載的執行緒id,downlength:執行緒下載的最後位置
    db.execSQL("CREATE TABLE IF NOT EXISTS filedownlog " +
        "(id integer primary key autoincrement," +
        " downpath varchar(100)," +
        " threadid INTEGER, downlength INTEGER)");
  }
  @Override
  public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    //當版本號發生改變時呼叫該方法,這裡刪除資料表,在實際業務中一般是要進行資料備份的
    db.execSQL("DROP TABLE IF EXISTS filedownlog");
    onCreate(db);
  }}

Step 2:建立一個資料庫操作類

我們需要建立什麼樣的方法呢?

  • ①我們需要一個根據URL獲得每條執行緒當前下載長度的方法

  • ②接著,當我們的執行緒新開闢後,我們需要往資料庫中插入與該執行緒相關引數的方法

  • ③還要定義一個可以實時更新下載檔案長度的方法

  • ④我們執行緒下載完,還需要根據執行緒id,刪除對應記錄的方法

FileService.java

package com.jay.example.db;import java.util.HashMap;import java.util.Map;import android.content.Context;import android.database.Cursor;import android.database.sqlite.SQLiteDatabase;/*
 * 該類是一個業務bean類,完成資料庫的相關操作
 * */public class FileService {
  //宣告資料庫管理器
  private DBOpenHelper openHelper;
  
  //在構造方法中根據上下文物件例項化資料庫管理器
  public FileService(Context context) {
    openHelper = new DBOpenHelper(context);
  }
  
  /**
   * 獲得指定URI的每條執行緒已經下載的檔案長度
   * @param path
   * @return 
   * */
  public Map getData(String path)
  {
    //獲得可讀資料庫控制程式碼,通常內部實現返回的其實都是可寫的資料庫控制程式碼
    SQLiteDatabase db = openHelper.getReadableDatabase();
    //根據下載的路徑查詢所有現場的下載資料,返回的Cursor指向第一條記錄之前
    Cursor cursor = db.rawQuery("select threadid, downlength from filedownlog where downpath=?",
        new String[]{path});
    //建立一個雜湊表用於存放每條執行緒已下載的檔案長度
    Map data = new HashMap();
    //從第一條記錄開始遍歷Cursor物件
    cursor.moveToFirst();
    while(cursor.moveToNext())
    {
      //把執行緒id與該執行緒已下載的長度存放到data雜湊表中
      data.put(cursor.getInt(0), cursor.getInt(1));
      data.put(cursor.getInt(cursor.getColumnIndexOrThrow("threadid")),
          cursor.getInt(cursor.getColumnIndexOrThrow("downlength")));
    }
    cursor.close();//關閉cursor,釋放資源;
    db.close();
    return data;
  }
  
  /**
   * 儲存每條執行緒已經下載的檔案長度
   * @param path 下載的路徑
   * @param map 現在的di和已經下載的長度的集合
  */
  public void save(String path,Map map)
  {
    SQLiteDatabase db = openHelper.getWritableDatabase();
    //開啟事務,因為此處需要插入多條資料
    db.beginTransaction();
    try{
      //使用增強for迴圈遍歷資料集合
      for(Map.Entry entry : map.entrySet())
      {
        //插入特定下載路徑特定執行緒ID已經下載的資料
        db.execSQL("insert into filedownlog(downpath, threadid, downlength) values(?,?,?)",
            new Object[]{path, entry.getKey(), entry.getValue()});
      }
      //設定一個事務成功的標誌,如果成功就提交事務,如果沒呼叫該方法的話那麼事務回滾
      //就是上面的資料庫操作撤銷
      db.setTransactionSuccessful();
    }finally{
      //結束一個事務
      db.endTransaction();
    }
    db.close();
  }
  
  /**
   * 實時更新每條執行緒已經下載的檔案長度
   * @param path
   * @param map
   */
  public void update(String path,int threadId,int pos)
  {
    SQLiteDatabase db = openHelper.getWritableDatabase();
    //更新特定下載路徑下特定執行緒已下載的檔案長度
    db.execSQL("update filedownlog set downlength=? where downpath=? and threadid=?",
        new Object[]{pos, path, threadId});
    db.close();
  }
  
  
  /**
   *當檔案下載完成後,刪除對應的下載記錄
   *@param path 
   */
  public void delete(String path)
  {
    SQLiteDatabase db = openHelper.getWritableDatabase();
    db.execSQL("delete from filedownlog where downpath=?", new Object[]{path});
    db.close();
  }
  }

Step 3:建立一個檔案下載器類

好了,資料庫管理器與操作類都完成了接著就該弄一個檔案下載器類了,在該類中又要完成 什麼操作呢?要做的事就多了:

定義一堆變數,核心是執行緒池threads和同步集合ConcurrentHashMap,用於快取執行緒下載長度的
定義一個獲取執行緒池中執行緒數的方法;
定義一個退出下載的方法,
獲取當前檔案大小的方法
累計當前已下載長度的方法,這裡需要新增一個synchronized關鍵字,用來解決併發訪問的問題
更新指定執行緒最後的下載位置,同樣也需要用同步
在構造方法中完成檔案下載,執行緒開闢等操作
獲取檔名的方法:先擷取提供的url最後的'/'後面的字串,如果獲取不到,再從頭欄位查詢,還是 找不到的話,就使用網路卡標識數字+cpu的唯一數字生成一個16個位元組的二進位制作為檔名
開始下載檔案的方法
獲取http響應頭欄位的方法
列印http頭欄位的方法
12.列印日誌資訊的方法

FileDownloadered.java:

package com.jay.example.service;import java.io.File;import java.io.RandomAccessFile;import java.net.HttpURLConnection;import java.net.URL;import java.util.LinkedHashMap;import java.util.Map;import java.util.UUID;import java.util.concurrent.ConcurrentHashMap;import java.util.regex.Matcher;import java.util.regex.Pattern;import android.content.Context;import android.util.Log;import com.jay.example.db.FileService;public class FileDownloadered {
  
  private static final String TAG = "檔案下載類";  //設定一個查log時的一個標誌
  private static final int RESPONSEOK = 200;    //設定響應碼為200,代表訪問成功
  private FileService fileService;        //獲取本地資料庫的業務Bean
  private boolean exited;             //停止下載的標誌
  private Context context;            //程式的上下文物件
  private int downloadedSize = 0;               //已下載的檔案長度
  private int fileSize = 0;           //開始的檔案長度
  private DownloadThread[] threads;        //根據執行緒數設定下載的執行緒池
  private File saveFile;              //資料儲存到本地的檔案中
  private Map data = new ConcurrentHashMap();  //快取個條執行緒的下載的長度
  private int block;                            //每條執行緒下載的長度
  private String downloadUrl;                   //下載的路徑
  
  
  /**
   * 獲取執行緒數
   */
  public int getThreadSize()
  {
    //return threads.length;
    return 0;
  }
  
  /**
   * 退出下載
   * */
  public void exit()
  {
    this.exited = true;    //將退出的標誌設定為true;
  }
  public boolean getExited()
  {
    return this.exited;
  }
  
  /**
   * 獲取檔案的大小
   * */
  public int getFileSize()
  {
    return fileSize;
  }
  
  /**
   * 累計已下載的大小
   * 使用同步鎖來解決併發的訪問問題
   * */
  protected synchronized void append(int size)
  {
    //把實時下載的長度加入到總的下載長度中
    downloadedSize += size;
  }
  
  /**
   * 更新指定執行緒最後下載的位置
   * @param threadId 執行緒id
   * @param pos 最後下載的位置
   * */
  protected synchronized void update(int threadId,int pos)
  {
    //把指定執行緒id的執行緒賦予最新的下載長度,以前的值會被覆蓋掉
    this.data.put(threadId, pos);
    //更新資料庫中制定執行緒的下載長度
    this.fileService.update(this.downloadUrl, threadId, pos);
  }
  
  
  /**
   * 構建檔案下載器
   * @param downloadUrl 下載路徑
   * @param fileSaveDir 檔案的儲存目錄
   * @param threadNum  下載執行緒數
   * @return 
   */
  public FileDownloadered(Context context,String downloadUrl,File fileSaveDir,int threadNum)
  {
    try {
      this.context = context;     //獲取上下文物件,賦值
      this.downloadUrl = downloadUrl;  //為下載路徑賦值
      fileService = new FileService(this.context);   //例項化資料庫操作的業務Bean類,需要傳一個context值
      URL url = new URL(this.downloadUrl);     //根據下載路徑例項化URL
      if(!fileSaveDir.exists()) fileSaveDir.mkdir();  //如果檔案不存在的話指定目錄,這裡可建立多層目錄
      this.threads = new DownloadThread[threadNum];   //根據下載的執行緒數量建立下載的執行緒池
      
      
      HttpURLConnection conn = (HttpURLConnection) url.openConnection();   //建立遠端連線控制程式碼,這裡並未真正連線
      conn.setConnectTimeout(5000);      //設定連線超時事件為5秒
      conn.setRequestMethod("GET");      //設定請求方式為GET
      //設定使用者端可以接收的媒體型別
      conn.setRequestProperty("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, */*");
      
      conn.setRequestProperty("Accept-Language", "zh-CN");  //設定使用者語言
      conn.setRequestProperty("Referer", downloadUrl);    //設定請求的來源頁面,便於服務端進行來源統計
      conn.setRequestProperty("Charset", "UTF-8");    //設定客戶端編碼
      //設定使用者代理
      conn.setRequestProperty("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)");
      
      conn.setRequestProperty("Connection", "Keep-Alive");  //設定connection的方式
      conn.connect();      //和遠端資源建立正在的連結,但尚無返回的資料流
      printResponseHeader(conn);   //列印返回的Http的頭欄位集合
      //對返回的狀態碼進行判斷,用於檢查是否請求成功,返回200時執行下面的程式碼
      if(conn.getResponseCode() == RESPONSEOK)
      {
        this.fileSize = conn.getContentLength();  //根據響應獲得檔案大小
        if(this.fileSize  logdata = fileService.getData(downloadUrl);    //獲取下載記錄
        //如果存在下載記錄
        if(logdata.size() > 0)
        {
          //遍歷集合中的資料,把每條執行緒已下載的資料長度放入data中
          for(Map.Entry entry : logdata.entrySet())
          {
            data.put(entry.getKey(), entry.getValue());
          }
        }
        //如果已下載的資料的執行緒數和現在設定的執行緒數相同時則計算所有現場已經下載的資料總長度
        if(this.data.size() == this.threads.length)
        {
          //遍歷每條執行緒已下載的資料
          for(int i = 0;i 0) randOut.setLength(this.fileSize);
      randOut.close();    //關閉該檔案,使設定生效
      URL url = new URL(this.downloadUrl);
      if(this.data.size() != this.threads.length){
      //如果原先未曾下載或者原先的下載執行緒數與現在的執行緒數不一致
        this.data.clear();
        //遍歷執行緒池
        for (int i = 0; i  getHttpResponseHeader(HttpURLConnection http) {
    //使用LinkedHashMap保證寫入和便利的時候的順序相同,而且允許空值
    Map header = new LinkedHashMap();
    //此處使用無線迴圈,因為不知道頭欄位的數量
    for (int i = 0;; i++) {
      String mine = http.getHeaderField(i);  //獲取第i個頭欄位的值
      if (mine == null) break;      //沒值說明頭欄位已經迴圈完畢了,使用break跳出迴圈
      header.put(http.getHeaderFieldKey(i), mine); //獲得第i個頭欄位的鍵
    }
    return header;
  }
  /**
   * 列印Http頭欄位
   * @param http
   */
  public static void printResponseHeader(HttpURLConnection http){
    //獲取http響應的頭欄位
    Map header = getHttpResponseHeader(http);
    //使用增強for迴圈遍歷取得頭欄位的值,此時遍歷的迴圈順序與輸入樹勳相同
    for(Map.Entry entry : header.entrySet()){
      //當有鍵的時候則獲取值,如果沒有則為空字串
      String key = entry.getKey()!=null ? entry.getKey()+ ":" : "";
      print(key+ entry.getValue());      //列印鍵和值得組合
    }
  }
  
  /**
   * 列印資訊
   * @param msg 資訊字串
   * */
  private static void print(String msg) {
    Log.i(TAG, msg);
  }}

Step 4:自定義一個下載執行緒類

這個自定義的執行緒類要做的事情如下:

  • ① 首先肯定是要繼承Thread類啦,然後重寫Run()方法

  • ② Run()方法:先判斷是否下載完成,沒有得話:開啟URLConnection連結,接著RandomAccessFile 進行資料讀寫,完成時設定完成標記為true,發生異常的話設定長度為-1,列印異常資訊

  • ③列印log資訊的方法

  • ④判斷下載是否完成的方法(根據完成標記)

  • ⑤獲得已下載的內容大小

DownLoadThread.java:

package com.jay.example.service;import java.io.File;import java.io.InputStream;import java.io.RandomAccessFile;import java.net.HttpURLConnection;import java.net.URL;import android.util.Log;public class DownloadThread extends Thread {
  private static final String TAG = "下載執行緒類";    //定義TAG,在列印log時進行標記
  private File saveFile;              //下載的資料儲存到的檔案
  private URL downUrl;              //下載的URL
  private int block;                //每條執行緒下載的大小
  private int threadId = -1;            //初始化執行緒id設定
  private int downLength;             //該執行緒已下載的資料長度
  private boolean finish = false;         //該執行緒是否完成下載的標誌
  private FileDownloadered downloader;      //檔案下載器

  public DownloadThread(FileDownloadered downloader, URL downUrl, File saveFile, int block, int downLength, int threadId) {
    this.downUrl = downUrl;
    this.saveFile = saveFile;
    this.block = block;
    this.downloader = downloader;
    this.threadId = threadId;
    this.downLength = downLength;
  }
  
  @Override
  public void run() {
    if(downLength 

Step 5:建立一個DownloadProgressListener介面監聽下載進度

FileDownloader中使用了DownloadProgressListener進行進度監聽, 所以這裡需要建立一個介面,同時定義一個方法的空實現:

DownloadProgressListener.java:

package com.jay.example.service;public interface DownloadProgressListener {
  public void onDownloadSize(int downloadedSize);}

Step 6:編寫我們的佈局程式碼

另外呼叫android:enabled="false"設定元件是否可點選, 程式碼如下

activity_main.xml:



    
  
  
  
  
  
    
   
  

Step 7:MainActivity的編寫

最後就是我們的MainActivity了,完成元件以及相關變數的初始化; 使用handler來完成介面的更新操作,另外耗時操作不能夠在主執行緒中進行, 所以這裡需要開闢新的執行緒,這裡用Runnable實現,詳情見程式碼 吧

MainActivity.java:

package com.jay.example.multhreadcontinuabledemo;import java.io.File;import com.jay.example.service.FileDownloadered;import android.app.Activity;import android.os.Bundle;import android.os.Environment;import android.os.Handler;import android.os.Message;import android.view.View;import android.widget.Button;import android.widget.EditText;import android.widget.ProgressBar;import android.widget.TextView;import android.widget.Toast;public class MainActivity extends Activity {

  private EditText editpath;
  private Button btndown;
  private Button btnstop;
  private TextView textresult;
  private ProgressBar progressbar;
  private static final int PROCESSING = 1;   //正在下載實時資料傳輸Message標誌
  private static final int FAILURE = -1;     //下載失敗時的Message標誌
  
  
  private Handler handler = new UIHander();
    
    private final class UIHander extends Handler{
    public void handleMessage(Message msg) {
      switch (msg.what) {
      //下載時
      case PROCESSING:
        int size = msg.getData().getInt("size");     //從訊息中獲取已經下載的資料長度
        progressbar.setProgress(size);         //設定進度條的進度
        //計算已經下載的百分比,此處需要轉換為浮點數計算
        float num = (float)progressbar.getProgress() / (float)progressbar.getMax();
        int result = (int)(num * 100);     //把獲取的浮點數計算結果轉換為整數
        textresult.setText(result+ "%");   //把下載的百分比顯示到介面控制元件上
        if(progressbar.getProgress() == progressbar.getMax()){ //下載完成時提示
          Toast.makeText(getApplicationContext(), "檔案下載成功", 1).show();
        }
        break;

      case FAILURE:    //下載失敗時提示
        Toast.makeText(getApplicationContext(), "檔案下載失敗", 1).show();
        break;
      }
    }
    }
  
  
  
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    editpath = (EditText) findViewById(R.id.editpath);
    btndown = (Button) findViewById(R.id.btndown);
    btnstop = (Button) findViewById(R.id.btnstop);
    textresult = (TextView) findViewById(R.id.textresult);
    progressbar = (ProgressBar) findViewById(R.id.progressBar);
    ButtonClickListener listener = new ButtonClickListener();
    btndown.setOnClickListener(listener);
    btnstop.setOnClickListener(listener);
    
    
  }
  
  
  private final class ButtonClickListener implements View.OnClickListener{
    public void onClick(View v) {
      switch (v.getId()) {
      case R.id.btndown:
        String path = editpath.getText().toString();
        if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
          File saveDir = Environment.getExternalStorageDirectory();
          download(path, saveDir);
        }else{
          Toast.makeText(getApplicationContext(), "sd卡讀取失敗", 1).show();
        }
        btndown.setEnabled(false);
        btnstop.setEnabled(true);
        break;

      case R.id.btnstop:
        exit();
        btndown.setEnabled(true);
        btnstop.setEnabled(false);
        break;
      }
    }
    /*
    由於使用者的輸入事件(點選button, 觸控螢幕....)是由主執行緒負責處理的,如果主執行緒處於工作狀態,
    此時使用者產生的輸入事件如果沒能在5秒內得到處理,系統就會報“應用無響應”錯誤。
    所以在主執行緒裡不能執行一件比較耗時的工作,否則會因主執行緒阻塞而無法處理使用者的輸入事件,
    導致“應用無響應”錯誤的出現。耗時的工作應該在子執行緒裡執行。
     */
    private DownloadTask task;
    /**
     * 退出下載
     */
    public void exit(){
      if(task!=null) task.exit();
    }
    private void download(String path, File saveDir) {//執行在主執行緒
      task = new DownloadTask(path, saveDir);
      new Thread(task).start();
    }
    
    
    /*
     * UI控制元件畫面的重繪(更新)是由主執行緒負責處理的,如果在子執行緒中更新UI控制元件的值,更新後的值不會重繪到螢幕上
     * 一定要在主執行緒裡更新UI控制元件的值,這樣才能在螢幕上顯示出來,不能在子執行緒中更新UI控制元件的值
     */
    private final class DownloadTask implements Runnable{
      private String path;
      private File saveDir;
      private FileDownloadered loader;
      public DownloadTask(String path, File saveDir) {
        this.path = path;
        this.saveDir = saveDir;
      }
      /**
       * 退出下載
       */
      public void exit(){
        if(loader!=null) loader.exit();
      }
      
      public void run() {
        try {
          loader = new FileDownloadered(getApplicationContext(), path, saveDir, 3);
          progressbar.setMax(loader.getFileSize());//設定進度條的最大刻度
          loader.download(new com.jay.example.service.DownloadProgressListener() {
            public void onDownloadSize(int size) {
              Message msg = new Message();
              msg.what = 1;
              msg.getData().putInt("size", size);
              handler.sendMessage(msg);
            }
          });
        } catch (Exception e) {
          e.printStackTrace();
          handler.sendMessage(handler.obtainMessage(-1));
        }
      }     
    }
    }}

Step 8:AndroidManifest.xml檔案中新增相關許可權

<!-- 訪問internet許可權 --&gt<!-- 在SDCard中建立與刪除檔案許可權 --&gt<!-- 往SDCard寫入資料許可權 --&gt

原文連結:http://www.apkbus.com/blog-830047-61319.html

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/756/viewspace-2814837/,如需轉載,請註明出處,否則將追究法律責任。

相關文章