Android下載檔案(一)下載進度&斷點續傳

李季_發表於2019-03-04

Android下載檔案(一)下載進度&斷點續傳

索引

  • Android下載檔案(一)下載進度&斷點續傳
  • Android下載檔案(二)單任務多執行緒併發&斷點續傳(待續)
  • Android下載檔案(三)自定義進度條(待續)
  • Android下載檔案(四)任務資訊持久化儲存(待續)
  • Android下載檔案(五)IPC(待續)
  • Android下載檔案(六)XDownloader(待續)

    前言

    從接觸Android開發至今也快兩年了,一路走過來可以說是站在巨人的肩膀上前進,真的很感激為開源世界作出貢獻的人。話說回來,搞了這麼久的開發卻一直在用別人的勞動成果也不是回事,所以我決定寫幾篇文章分享我對Android下載檔案的理解,並在最後整合並開源一個框架,也是對我在Android之旅中的一個小小的總結。

    注意:本人能力有限,如有錯誤、不合理、可優化的地方 請務必告知我!複製程式碼

    實現效果

    本節主要講解Android下載檔案的進度獲取和斷點續傳,效果如下

    所需知識點

  • volatile
  • RandomAccessFile
  • HttpURLConnection
  • Handler

    volatile

    volatile是java中修飾變數的關鍵字,在這裡重點講下其特性,後面會用到。
    如需深入理解請參考 《深入理解Java虛擬機器》12.3.3 對於volatile型變數的特殊規則

1. 保證可見性
根據JVM記憶體模型得知,JVM將記憶體分為主記憶體與工作記憶體兩個部分,所有的變數都存放在主記憶體中。而每條執行緒有自己的工作記憶體,其存放部分主存中變數的拷貝,執行緒對變數的操作必須在工作記憶體中完成,然後更新到主存中。
當一個共享變數被volatile修飾,它會保證修改的值立即更新到主存中,其他執行緒訪問時會去主存中讀取新的值。而普通的共享變數不能保證可見性,因為普通共享變數被修改之後,什麼時候被寫入主存是不確定的,當其他執行緒去讀取時,此時主存中可能還是原來的舊值,因此無法保證可見性。

2. 禁止指令重排
當程式碼編譯時JVM會對指令執行的順序進行優化,但volatile不會,如下所示

//x、y為非volatile變數
//flag為volatile變數
x = 2;        //語句1
y = 0;        //語句2
flag = true;  //語句3
x = 4;        //語句4
y = -1;       //語句5複製程式碼

語句3必定在語句1/2後執行,但語句1/2順序不做保證,同理,語句3也必定在語句4/5前面執行,語句4/5執行的順序也不做保證。

3. 非原子性
volatile變數是不保證原子性的,但是需要注意的是 volatile關鍵字對long/double型別的get/set操作保證了原子性,詳見這裡

HttpURLConnection

Android基本網路請求類,這個不必多說,接觸過Android開發的同學也一定會了解,如果是Android新同學請點我 。至於為什麼我用HttpURLConnection而不用OKhttp或者Retrofit,因為最終我會開源一個Android下載檔案的框架,所以不做過多的外部依賴。

RandomAccessFile

這個類很特殊,雖然是java.io包下的,但是隻實現了DataOutput, DataInput, Closeable這三個介面,唯一父類是Object。其功能是隨機讀寫檔案,換句話說就是可以在一個檔案的任何位置讀取或者寫入。在本文中用它來實現檔案下載的斷點續傳。

Handler

Android開發必然涉及到的東西,新同學請點我

###準備好了,開始擼程式碼
1.首先下載檔案需要下載連結/下載路徑/檔名等屬性,所以我們寫一個JavaBean,這裡用到了volatile關鍵字,詳見註釋

public class TaskInfo {
    private String name;//檔名
    private String path;//檔案路徑
    private String url;//連結
    private long contentLen;//檔案總長度
    /**
     * 迄今為止java虛擬機器都是以32位作為原子操作,而long與double為64位,當某執行緒
     * 將long/double型別變數讀到暫存器時需要兩次32位的操作,如果在第一次32位操作
     * 時變數值改變,其結果會發生錯誤,簡而言之,long/double是非執行緒安全的,volatile
     * 關鍵字修飾的long/double的get/set方法具有原子性。
     */
    private volatile long completedLen;//已完成長度

    getter/setter省略複製程式碼

2.下載檔案需要在子執行緒中進行,所以我們寫一個類,實現Runnable介面,方便任務的建立

public class DownloadRunnable implements Runnable {
    private TaskInfo info;//下載資訊JavaBean
    private boolean isStop;//是否暫停

    /**
     * 構造器
     * @param info 任務資訊
     */
    public DownloadRunnable(TaskInfo info) {
        this.info = info;
    }

    /**
     * 停止下載
     */
    public void stop() {
        isStop = true;
    }

    /**
     * Runnable的run方法,進行檔案下載
     */
    @Override
    public void run() {
        HttpURLConnection conn;//http連線物件
        BufferedInputStream bis;//緩衝輸入流,從伺服器獲取
        RandomAccessFile raf;//隨機讀寫器,用於寫入檔案,實現斷點續傳
        int len = 0;//每次讀取的陣列長度
        byte[] buffer = new byte[1024 * 8];//流讀寫的緩衝區
        try {
            //通過檔案路徑和檔名例項化File
            File file = new File(info.getPath() + info.getName());
            //例項化RandomAccessFile,rwd模式
            raf = new RandomAccessFile(file, "rwd");
            conn = (HttpURLConnection) new URL(info.getUrl()).openConnection();
            conn.setConnectTimeout(120000);//連線超時時間
            conn.setReadTimeout(120000);//讀取超時時間
            conn.setRequestMethod("GET");//請求型別為GET
            if (info.getContentLen() == 0) {//如果檔案長度為0,說明是新任務需要從頭下載
                //獲取檔案長度
                info.setContentLen(Long.parseLong(conn.getHeaderField("content-length")));
            } else {//否則設定請求屬性,請求制定範圍的檔案流
                conn.setRequestProperty("Range", "bytes=" + info.getCompletedLen() + "-" + info.getContentLen());
            }
            raf.seek(info.getCompletedLen());//移動RandomAccessFile寫入位置,從上次完成的位置開始
            conn.connect();//連線
            bis = new BufferedInputStream(conn.getInputStream());//獲取輸入流並且包裝為緩衝流
            //從流讀取位元組陣列到緩衝區
            while (!isStop && -1 != (len = bis.read(buffer))) {
                //把位元組陣列寫入到檔案
                raf.write(buffer, 0, len);
                //更新任務資訊中的完成的檔案長度屬性
                info.setCompletedLen(info.getCompletedLen() + len);
            }
            if (len == -1) {//如果讀取到檔案末尾則下載完成
                Log.i("tag", "下載完了");
            } else {//否則下載系手動停止
                Log.i("tag", "下載停止了");
            }
        } catch (IOException e) {
            e.printStackTrace();
            Log.i("tag",e.toString());
        }
    }
}複製程式碼

3.任務開始/停止和進度回撥

public class MainActivity3 extends AppCompatActivity {

    private ProgressBar bar;//進度條
    private TaskInfo info;//任務資訊
    private DownloadRunnable runnable;//下載任務
    //用於更新進度的Handler
    private Handler handler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            //使用Handler製造一個200毫秒為週期的迴圈
            handler.sendEmptyMessageDelayed(1, 200);
            //計算下載進度
            int l = (int) ((float) info.getCompletedLen() / (float) info.getContentLen() * 100);
            //設定進度條進度
            bar.setProgress(l);
            if (l>=100) {//當進度>=100時,取消Handler迴圈
                handler.removeCallbacksAndMessages(null);
            }
            return true;
        }
    });

    @Override
    protected void onDestroy() {
        //在Activity銷燬時移除回撥和msg,並置空,防止記憶體洩露
        if(handler != null){
            handler.removeCallbacksAndMessages(null);
            handler = null;
        }
        super.onDestroy();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main3);
        //例項化任務資訊物件
        info = new TaskInfo("aa.apk"
                , Environment.getExternalStorageDirectory().getAbsolutePath() 
                + "/Download/"
                , "https://download.alicdn.com/wireless/taobao4android/latest/702757.apk");
        bar = (ProgressBar) findViewById(R.id.bar);
        //設定進度條的最大值
        bar.setMax(100);
    }

    /**
     * 開始下載按鈕監聽
     * @param view
     */
    public void start(View view) {
        //建立下載任務
        runnable = new DownloadRunnable(info);
        //開始下載任務
        new Thread(runnable).start();
        //開始Handler迴圈
        handler.sendEmptyMessageDelayed(1, 200);
    }

    /**
     * 停止下載按鈕監聽
     * @param view
     */
    public void stop(View view) {
        //呼叫DownloadRunnable中的stop方法,停止下載
        runnable.stop();
        runnable = null;//強迫症,不用的物件手動置空
    }
}複製程式碼

Q:為什麼進度資訊不用handler傳送到主執行緒,而是直接從主記憶體中的TaskInfo獲取下載進度?
A:單個執行緒任務確實可以用handler攜帶下載資訊進行執行緒切換,但是我們過後會涉及到多執行緒下載,一個下載任務甚至可以達到128執行緒併發,這麼多子執行緒“同時”向主執行緒傳遞訊息,主執行緒壓力太大會造成“掉幀”,也就是我們所說的卡頓,並且TaskInfo中所有屬性的均具有原子性,不會出現執行緒安全問題。

Q:Handler是非靜態的不會造成記憶體洩露嗎?
A:不會,造成記憶體洩露的原因是Message持有Handler,Handler持有Activity,造成Message-Handler-Activity的引用鏈,導致在Activity銷燬時無法被GC回收。但在Activity銷燬時移除未處理的Message,這樣就從源頭上解決了記憶體洩露。

後記

再次強調,本人能力有限,難免有知識上的空缺或者疏漏,如有不足之處請告知!我會用業餘時間繼續更新,感謝您的閱讀。

相關文章