Android下載文件(一)下載進度&斷點續傳

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修飾,它會保證修改的值當即更新到主存中,其餘線程訪問時會去主存中讀取新的值。而普通的共享變量不能保證可見性,由於普通共享變量被修改以後,何時被寫入主存是不肯定的,當其餘線程去讀取時,此時主存中可能仍是原來的舊值,所以沒法保證可見性。html

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

//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執行的順序也不作保證。android

3. 非原子性
volatile變量是不保證原子性的,可是須要注意的是 volatile關鍵字對long/double類型的get/set操做保證了原子性,詳見這裏數組

HttpURLConnection

Android基本網絡請求類,這個沒必要多說,接觸過Android開發的同窗也必定會了解,若是是Android新同窗請點我 。至於爲何我用HttpURLConnection而不用OKhttp或者Retrofit,由於最終我會開源一個Android下載文件的框架,因此不作過多的外部依賴。安全

RandomAccessFile

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

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,這樣就從源頭上解決了內存泄露。

後記

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

相關文章
相關標籤/搜索