1. 程式人生 > >Android 實現多執行緒下載檔案+斷點續傳

Android 實現多執行緒下載檔案+斷點續傳

                            Android 多執行緒下載檔案+斷點續傳

      在專案快要結束的時候,發現了app沒有版本更新的功能,於是找到一些過去的資料,在app上應用完成了版本更新,現在記錄一下apk的下載,也就是如何通過多執行緒將apk下載到本地。多執行緒檔案的下載,不是執行緒開的越多下載的越快(一般手機軟體、迅雷建議3-4個執行緒),多執行緒的下載還與伺服器給你的頻寬有影響下載我們先了解一下多執行緒下載的思路。

多執行緒下載的步驟分析:

  1、獲取伺服器檔案的大小。(connection.getContentLength();)

  2、在本地生成一個與服務大小相同的檔案。(提前申請好空間)

  3、開啟多個執行緒下載資料。(知道每個執行緒下載的開始位置和結束位置)

  4、知道每個執行緒什麼時候下載完畢。

每個執行緒下載開始位置和結束位置:

 blockSize=總長度/執行緒個數

第N個執行緒的開始位置 :n*blockSize  結束位置:(n+1)*blockSize-1

最後一個執行緒的開始位置:n*blockSize 結束位置總長度-1;

實現斷點續傳:

實現斷電續傳,就是把當前執行緒下載的位置給存起來,在此在下載的時候就是按照上次下載的位置繼續下載就可以了。

大體思路就這樣附上程式碼。

1.用來實現進度條的item.xml

<?xml version="1.0" encoding="utf-8"?>
<ProgressBar xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/progressBar1"
    style="?android:attr/progressBarStyleHorizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

2.activity_mian.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.itheima.download.MainActivity">
    <EditText
        android:id="@+id/et_path"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="http://192.168.100.17:7001/mfa.apk"
        android:hint="請輸入下載路徑" />

    <EditText
        android:id="@+id/et_threadCount"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="請輸入執行緒的數量" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="click"
        android:text="下載" />

    <LinearLayout
        android:id="@+id/ll_pb"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical" >
    </LinearLayout>

</LinearLayout>

3、MainActivity

package com.itheima.download;

import android.app.Activity;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ProgressBar;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

public class MainActivity extends Activity {

    private EditText et_path;
    private EditText et_threadCount;
    private LinearLayout ll_pb_layout;
    private String path;
    //代表正在執行的執行緒數量
    private static  int runningThread;
    private  int threadCount;
    //用來存進度條的引用
    private List<ProgressBar> pbLists;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //1找到我們關心的控制元件
        et_path= (EditText) findViewById(R.id.et_path);
        et_threadCount= (EditText) findViewById(R.id.et_threadCount);
        ll_pb_layout= (LinearLayout) findViewById(R.id.ll_pb);

        //2新增一個集合用來存進度條的引用
        pbLists=new ArrayList<ProgressBar>();
    }
    //點選按鈕實現下載的邏輯
    public void click(View view){
        //[1]獲取下載路徑
        path=et_path.getText().toString().trim();
        //[2]獲取下載執行緒的個數
        final String threadCounts=et_threadCount.getText().toString().trim();
        //[3]先移除進度條,在新增
        ll_pb_layout.removeAllViews();
        threadCount=Integer.parseInt(threadCounts);
        pbLists.clear();
        for (int i = 0; i < threadCount; i++) {
            //[3.1]把我定義的item佈局轉換成一個view物件
            ProgressBar pbView = (ProgressBar) View.inflate(getApplicationContext(), R.layout.item, null);

            //[3.2]把pbView 新增到集合中
            pbLists.add(pbView);

            //[4]動態的新增進度條
            ll_pb_layout.addView(pbView);
        }
        //開始聯網,獲取檔案的長度
        new Thread(){
            //[一 ☆☆☆☆]獲取伺服器檔案的大小   要計算每個執行緒下載的開始位置和結束位置
            @Override
            public void run(){
                try{
                    //1.建立一個URL物件,引數就是檔案地址
                    URL url=new URL(path);
                    //2.獲取HttpURLconnection 物件
                    HttpURLConnection connection= (HttpURLConnection) url.openConnection();
                    //3.設定連結方式
                    connection.setRequestMethod("GET");
                    //4.設定連結時間
                    connection.setReadTimeout(10000);
                    //5.獲取返回的狀態碼 200代表獲取伺服器全部資源成功 206代表獲取部分資源成功
                    int code=connection.getResponseCode();
                    if (code==200){
                        //6.獲取伺服器檔案的大小
                        int length=connection.getContentLength();
                        //7.把執行緒的數量賦值給正在執行的執行緒
                        runningThread=threadCount;
                        //列印檢視一下總長度
                        Log.d("MainActivity", "length:" + length);
                        //[二☆☆☆☆ ] 建立一個大小和伺服器一模一樣的檔案 目的提前把空間申請出來
                        RandomAccessFile accessFile=new RandomAccessFile(getFileName(path),"rw");
                        accessFile.setLength(length);

                        //8.計算每個執行緒下載大小
                        int bolckSize=length/threadCount;
                        //[三☆☆☆☆  計算每個執行緒下載的開始位置和結束位置 ]
                        for (int i=0;i<threadCount;i++){
                            int startIndex=i*bolckSize;
                            int endIndex=(i+1)*bolckSize-1;
                            //判斷是否為最後一個執行緒
                            if (i==threadCount-1){
                                endIndex=length-1;
                            }
                            Log.d("MainActivity", ("執行緒id:::" + i + "下載的位置" + ":" + startIndex + "-----" + endIndex));

                            //四 開啟執行緒去伺服器下載檔案
                            DownLoadThread downLoadThread = new DownLoadThread(startIndex, endIndex, i);
                            downLoadThread.start();
                        }

                    }

                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }.start();

    }
    //定義執行緒去伺服器下載資料
    private class DownLoadThread extends Thread{
        //通過構造方法把每個執行緒下載的開始位置和結束位置傳遞進來
        private int startIndex;
        private int endIndex;
        private int threadId;

        private int pbMaxSize; //代表當前執行緒下載的最大值
        //如果中斷過  獲取上次下載的位置
        private int pblastPostion;
        //構造方法
        public DownLoadThread(int startIndex,int endIndex,int threadId){
            this.startIndex=startIndex;
            this.endIndex=endIndex;
            this.threadId=threadId;
        }
        @Override
        public void run(){
            //實現伺服器下載檔案的邏輯
            try {
                //0.計算當前進度條的最大值
                pbMaxSize=endIndex-startIndex;
                //1.建立一個URL連結物件 引數是網址
                URL url=new URL(path);
                //2.建立HttpURLConnection 物件
                HttpURLConnection connection= (HttpURLConnection) url.openConnection();
                //3.設定連結的方式
                connection.setRequestMethod("GET");
                //4.設定請求時間
                connection.setConnectTimeout(10000);
                //[4.0]如果中間斷過  繼續上次的位置 繼續下載   從檔案中讀取上次下載的位置
                File file=new File(getFileName(getFileName(path)+threadId+".txt"));
                if (file.exists()&&file.length()>0){
                    FileInputStream fileInputStream=new FileInputStream(file);
                    BufferedReader reader=new BufferedReader(new InputStreamReader(fileInputStream));
                    //讀取出來的內容就是上一次下載的位置
                    String locations=reader.readLine();

                    int lastPosition=Integer.parseInt(locations);
                    //[4.0]給我們定義的進度條條位置 賦值
                    pblastPostion = lastPosition - startIndex;

                    //[4.0.1]要改變一下 startIndex 位置
                    startIndex = lastPosition + 1;

                    Log.d("DownLoadThread", ("執行緒id::" + threadId + "真實下載的位置" + ":" + startIndex + "-----" + endIndex));
                    //關閉流
                    fileInputStream.close();
                }
                //[4.1]設定一個請求頭Range (作用告訴伺服器每個執行緒下載的開始位置和結束位置)
                connection.setRequestProperty("Range", "bytes="+startIndex+"-"+endIndex);
                //5獲取伺服器返回的狀態碼 //200  代表獲取伺服器資源全部成功  206請求部分資源 成功
                int code =connection.getResponseCode();
                if (code==206){
                    //6建立隨機讀取檔案物件
                    RandomAccessFile accessFile=new RandomAccessFile(getFileName(path),"rw");
                    //6每個執行緒要從自己的位置開始寫
                    accessFile.seek(startIndex);
                    InputStream inputStream=connection.getInputStream();
                    //把資料寫入到檔案中
                    int len=-1;
                    //1MB
                    byte[] bytes=new byte[1024*1024];
                    //代表當前執行緒下載的大小
                    int total = 0;
                    while((len = inputStream.read(bytes))!=-1){
                        accessFile.write(bytes, 0, len);

                        total +=len;
                        //[8]實現斷點續傳 就是把當前執行緒下載的位置 給存起來 下次再下載的時候 就是按照上次下載的位置繼續下載 就可以了
                        int currentThreadPosition =  startIndex + total;  //比如就存到一個普通的.txt文字中

                        //[9]用來存當前執行緒下載的位置
                        RandomAccessFile raff = new RandomAccessFile(getFileName(path)+threadId+".txt", "rwd");
                        raff.write(String.valueOf(currentThreadPosition).getBytes());
                        raff.close();

                        //[10]設定一下當前進度條的最大值 和 當前進度
                        pbLists.get(threadId).setMax(pbMaxSize);//設定進度條的最大值
                        pbLists.get(threadId).setProgress(pblastPostion+total);//設定當前進度條的當前進度

                    }
                    //關閉流,釋放資源
                    accessFile.close();
                    Log.d("DownLoadThread", ("執行緒id:" + threadId + "---下載完畢了"));
                    //[10]把.txt檔案刪除  每個執行緒具體什麼時候下載完畢了 我們不知道
                    synchronized (DownLoadThread.class) {
                        runningThread--;
                        if (runningThread == 0) {
                            //說明所有的執行緒都執行完畢了 就把.txt檔案刪除
                            for (int i = 0; i < threadCount; i++) {
                                File  deleteFile = new File(getFileName(path)+i+".txt");
                                deleteFile.delete();
                            }
                        }
                    }

                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    //獲取檔案的名字 "http://192.168.100.96:7101/mfa.apk";
    public String getFileName(String path){
        int start =  path.lastIndexOf("/")+1;
        String substring = path.substring(start);
        String fileName = Environment.getExternalStorageDirectory().getPath()+"/"+substring;
        return fileName;
    }
}

記得要加上聯網和讀寫檔案的許可權。