Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android技術基礎 >> 7.3.3 Android 文件下載(2)

7.3.3 Android 文件下載(2)

編輯:Android技術基礎

本節引言:

本節給大家帶來的Android中的多線程斷點續傳的代碼解析,呵呵,為什麼叫解析呢?因為我 也寫不出來,( ╯□╰ )!先來說說斷點的意思吧!所謂的斷點就是:使用數據庫記錄每天線程所 下載的進度!每次啟動時根據線程id查詢某線程的下載進度,在繼續下載!聽上去蠻簡單的, 要你寫十有八九寫不出,這很正常,所以本節看懂最好,看不懂也沒什麼,會用和改就好! 好的,開始本節內容~


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<Integer, Integer> getData(String path)
  {
    //獲得可讀數據庫句柄,通常內部實現返回的其實都是可寫的數據庫句柄
    SQLiteDatabase db = openHelper.getReadableDatabase();
    //根據下載的路徑查詢所有現場的下載數據,返回的Cursor指向第一條記錄之前
    Cursor cursor = db.rawQuery("select threadid, downlength from filedownlog where downpath=?",
        new String[]{path});
    //建立一個哈希表用於存放每條線程已下載的文件長度
    Map<Integer,Integer> data = new HashMap<Integer, Integer>();
    //從第一條記錄開始遍歷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<Integer,Integer> map)
  {
    SQLiteDatabase db = openHelper.getWritableDatabase();
    //開啟事務,因為此處需要插入多條數據
    db.beginTransaction();
    try{
      //使用增強for循環遍歷數據集合
      for(Map.Entry<Integer, Integer> 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<Integer, Integer> data = new ConcurrentHashMap<Integer, Integer>();  //緩存個條線程的下載的長度
  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 <= 0)throw new RuntimeException("不知道文件大小");  //文件長度小於等於0時拋出運行時異常
        String filename = getFileName(conn);      //獲取文件名稱
        this.saveFile = new File(fileSaveDir,filename);  //根據文件保存目錄和文件名保存文件
        Map<Integer,Integer> logdata = fileService.getData(downloadUrl);    //獲取下載記錄
        //如果存在下載記錄
        if(logdata.size() > 0)
        {
          //遍歷集合中的數據,把每條線程已下載的數據長度放入data中
          for(Map.Entry<Integer, Integer> entry : logdata.entrySet())
          {
            data.put(entry.getKey(), entry.getValue());
          }
        }
        //如果已下載的數據的線程數和現在設置的線程數相同時則計算所有現場已經下載的數據總長度
        if(this.data.size() == this.threads.length)
        {
          //遍歷每條線程已下載的數據
          for(int i = 0;i < this.threads.length;i++)
          {
            this.downloadedSize += this.data.get(i+1);
          }
          print("已下載的長度" + this.downloadedSize + "個字節");
        }
        //使用條件運算符求出每個線程需要下載的數據長度
        this.block = (this.fileSize % this.threads.length) == 0?
            this.fileSize / this.threads.length:
              this.fileSize / this.threads.length + 1;
      }else{
        //打印錯誤信息
        print("服務器響應錯誤:" + conn.getResponseCode() + conn.getResponseMessage());
        throw new RuntimeException("服務器反饋出錯");
      }
      
      
    }catch (Exception e) 
    {
      print(e.toString());   //打印錯誤
      throw new RuntimeException("無法連接URL");
    }
  }

  
  /**
   * 獲取文件名
   * */
  private String getFileName(HttpURLConnection conn)
  {
    //從下載的路徑的字符串中獲取文件的名稱
    String filename = this.downloadUrl.substring(this.downloadUrl.lastIndexOf('/') + 1);
    if(filename == null || "".equals(filename.trim())){     //如果獲取不到文件名稱
    for(int i = 0;;i++)  //使用無限循環遍歷
    {
      String mine = conn.getHeaderField(i);     //從返回的流中獲取特定索引的頭字段的值
      if (mine == null) break;          //如果遍歷到了返回頭末尾則退出循環
      //獲取content-disposition返回字段,裡面可能包含文件名
      if("content-disposition".equals(conn.getHeaderFieldKey(i).toLowerCase())){
        //使用正則表達式查詢文件名
        Matcher m = Pattern.compile(".*filename=(.*)").matcher(mine.toLowerCase());
        if(m.find()) return m.group(1);    //如果有符合正則表達式規則的字符串,返回
      }
    }
    filename = UUID.randomUUID()+ ".tmp";//如果都沒找到的話,默認取一個文件名
    //由網卡標識數字(每個網卡都有唯一的標識號)以及CPU時間的唯一數字生成的一個16字節的二進制作為文件名
    }
      return filename;
  }
  
  /**
   *  開始下載文件
   * @param listener 監聽下載數量的變化,如果不需要了解實時下載的數量,可以設置為null
   * @return 已下載文件大小
   * @throws Exception
   */
  //進行下載,如果有異常的話,拋出異常給調用者
  public int download(DownloadProgressListener listener) throws Exception{
    try {
      RandomAccessFile randOut = new RandomAccessFile(this.saveFile, "rwd");
      //設置文件大小
      if(this.fileSize>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 < this.threads.length; i++) {
          this.data.put(i+1, 0);//初始化每條線程已經下載的數據長度為0
        }
        this.downloadedSize = 0;   //設置已經下載的長度為0
      }
      
      for (int i = 0; i < this.threads.length; i++) {//開啟線程進行下載
        int downLength = this.data.get(i+1);   
        //通過特定的線程id獲取該線程已經下載的數據長度
        //判斷線程是否已經完成下載,否則繼續下載 
        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);
          //設置線程優先級,Thread.NORM_PRIORITY = 5;
          //Thread.MIN_PRIORITY = 1;Thread.MAX_PRIORITY = 10,數值越大優先級越高
          this.threads[i].setPriority(7);   
          this.threads[i].start();    //啟動線程
        }else{
          this.threads[i] = null;   //表明線程已完成下載任務
        }
      }
      
      fileService.delete(this.downloadUrl);           
      //如果存在下載記錄,刪除它們,然後重新添加
      fileService.save(this.downloadUrl, this.data);  
      //把下載的實時數據寫入數據庫中
      boolean notFinish = true;             
      //下載未完成
      while (notFinish) {               
      // 循環判斷所有線程是否完成下載
        Thread.sleep(900);
        notFinish = false;                
        //假定全部線程下載完成
        for (int i = 0; i < this.threads.length; i++){
          if (this.threads[i] != null && !this.threads[i].isFinish()) {
          //如果發現線程未完成下載
            notFinish = true;                   
            //設置標志為下載沒有完成
            if(this.threads[i].getDownLength() == -1){        
            //如果下載失敗,再重新在已下載的數據長度的基礎上下載
            //重新開辟下載線程,設置線程的優先級
              this.threads[i] = new DownloadThread(this, url, this.saveFile, this.block, this.data.get(i+1), i+1);
              this.threads[i].setPriority(7);
              this.threads[i].start();
            }
          }
        }       
        if(listener!=null) listener.onDownloadSize(this.downloadedSize);
        //通知目前已經下載完成的數據長度
      }
      if(downloadedSize == this.fileSize) fileService.delete(this.downloadUrl);
      //下載完成刪除記錄
    } catch (Exception e) {
      print(e.toString());
      throw new Exception("文件下載異常");
    }
    return this.downloadedSize;
  }
  
  
  /**
   * 獲取Http響應頭字段
   * @param http
   * @return
   */
  public static Map<String, String> getHttpResponseHeader(HttpURLConnection http) {
    //使用LinkedHashMap保證寫入和便利的時候的順序相同,而且允許空值
    Map<String, String> header = new LinkedHashMap<String, String>();
    //此處使用無線循環,因為不知道頭字段的數量
    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<String, String> header = getHttpResponseHeader(http);
    //使用增強for循環遍歷取得頭字段的值,此時遍歷的循環順序與輸入樹勳相同
    for(Map.Entry<String, String> 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 < block){//未下載完成
      try {
        HttpURLConnection http = (HttpURLConnection) downUrl.openConnection();
        http.setConnectTimeout(5 * 1000);
        http.setRequestMethod("GET");
        http.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, */*");
        http.setRequestProperty("Accept-Language", "zh-CN");
        http.setRequestProperty("Referer", downUrl.toString()); 
        http.setRequestProperty("Charset", "UTF-8");
        int startPos = block * (threadId - 1) + downLength;//開始位置
        int endPos = block * threadId -1;//結束位置
        http.setRequestProperty("Range", "bytes=" + startPos + "-"+ endPos);//設置獲取實體數據的范圍
        http.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)");
        http.setRequestProperty("Connection", "Keep-Alive");
        
        InputStream inStream = http.getInputStream();     //獲得遠程連接的輸入流
        byte[] buffer = new byte[1024];           //設置本地數據的緩存大小為1MB
        int offset = 0;                   //每次讀取的數據量
        print("Thread " + this.threadId + " start download from position "+ startPos);  //打印該線程開始下載的位置
        
        RandomAccessFile threadfile = new RandomAccessFile(this.saveFile, "rwd");
        threadfile.seek(startPos);
        //用戶沒有要求停止下載,同時沒有達到請求數據的末尾時會一直循環讀取數據
        while (!downloader.getExited() && (offset = inStream.read(buffer, 0, 1024)) != -1) {
          threadfile.write(buffer, 0, offset);          //直接把數據寫入到文件中
          downLength += offset;             //把新線程已經寫到文件中的數據加入到下載長度中
          downloader.update(this.threadId, downLength); //把該線程已經下載的數據長度更新到數據庫和內存哈希表中
          downloader.append(offset);            //把新下載的數據長度加入到已經下載的數據總長度中
        }
        threadfile.close();
        inStream.close();
        print("Thread " + this.threadId + " download finish");
        this.finish = true;                               //設置完成標記為true,無論下載完成還是用戶主動中斷下載
      } catch (Exception e) {
        this.downLength = -1;               //設置該線程已經下載的長度為-1
        print("Thread "+ this.threadId+ ":"+ e);
      }
    }
  }
  private static void print(String msg){
    Log.i(TAG, msg);
  }
  /**
   * 下載是否完成
   * @return
   */
  public boolean isFinish() {
    return finish;
  }
  /**
   * 已經下載的內容大小
   * @return 如果返回值為-1,代表下載失敗
   */
  public long getDownLength() {
    return 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:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/LinearLayout1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.jay.example.multhreadcontinuabledemo.MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="請輸入要下載的文件地址" />
  <EditText 
      android:id="@+id/editpath"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="http://10.13.20.32:8080/Test/twelve.mp3"    
  />
  <Button 
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/btndown"
    android:text="下載"    
  />
  <Button 
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/btnstop"
    android:text="停止"
    android:enabled="false"    
  />
  
  <ProgressBar
     android:layout_width="fill_parent" 
     android:layout_height="18dp" 
     
     android:id="@+id/progressBar"
    />
    
   <TextView  
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:gravity="center"
    android:id="@+id/textresult"
    android:text="顯示實時下載的百分比"
    />
  
</LinearLayout>

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權限 -->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- 在SDCard中創建與刪除文件權限 -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
<!-- 往SDCard寫入數據權限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

參考代碼下載:

多線程斷點下載器demo:MulThreadContinuableDemo.zip

多線程斷點下載+在線音樂播放器:多線程斷點下載+在線音樂播放器.zip


本節小結:

好的,本節關於Android多線程斷點下載的代碼解析就這麼多,夠嗆的是把,不過還是 那句話,有別人造好的輪子,為什麼還要自己造呢?況且現在的我們還能力造出來, 不是麼,So,暫時弄懂,會用,知道怎麼改就好~嗯,就說這麼多,謝謝~

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved