Android中的多線程斷點下載

来源:http://www.cnblogs.com/huangjie123/archive/2016/12/19/6198573.html
-Advertisement-
Play Games

多線程下載就是將同一個網路上的原始文件根據線程個數分成均等份,然後每個單獨的線程下載對應的一部分 ...


      首先來看一下多線程下載的原理。多線程下載就是將同一個網路上的原始文件根據線程個數分成均等份,然後每個單獨的線程下載對應的一部分,然後再將下載好的文件按照原始文件的順序“拼接”起來就構
成了完整的文件了。這樣就大大提高了文件的下載效率。對於文件下載來說,多線程下載是必須要考慮的環節。

      多線程下載大致可分為以下幾個步驟:

       一.獲取伺服器上的目標文件的大小
              顯然這一步是需要先訪問一下網路,只需要獲取到目標文件的總大小即可。目的是為了計算每個線程應該分配的下載任務。

      二. 在本地創建一個跟原始文件同樣大小的文件
             在本地可以通過RandomAccessFile 創建一個跟目標文件同樣大小的文件,該api 支持文件任意位置的讀寫操作。這樣就給多線程下載提供了方便,每個線程只需在指定起始和結束腳標範圍內寫數據即可。

      三.計算每個線程下載的起始位置和結束位置
             我們可以把原始文件當成一個位元組數組,每個線程只下載該“數組”的指定起始位置和指定結束位置之間的部分。在第一步中我們已經知道了“數組”的總長度。因此只要再知道總共開啟的線程的個數就好計算每個線程要下載的範圍了。每個線程需要下載的位元組個數(blockSize)=總位元組數(totalSize)/線程數(threadCount)。       假設給線程按照0,1,2,3...n 的方式依次進行編號,那麼第n 個線程下載文件的範圍為:
               起始腳標startIndex=n*blockSize。
              結束腳標endIndex=(n-1)*blockSize-1。
            考慮到totalSize/threadCount 不一定能整除,因此對已最後一個線程應該特殊處理,最後一個線程的起始腳標計算公式不變,但是結束腳標為endIndex=totalSize-1即可。
     四.開啟多個子線程開始下載
            在子線程中實現讀流操作,將conn.getInputStream()讀到RandomAccessFile中。
     五.記錄下載進度
           為每一個單獨的線程創建一個臨時文件,用於記錄該線程下載的進度。對於單獨的一個線程,每下載一部分數據就在本地文件中記錄下當前下載的位元組數。這樣子如果下載任務異常終止了,那麼下次重新開始下載時就可以接著上次的進度下載。
     六. 刪除臨時文件
           當多個線程都下載完成之後,最後一個下載完的線程將所有的臨時文件刪除。

        Android有界面可以跟用戶進行良好的交互,在界面上讓用戶輸入原文件地址、線程個數,然後點擊確定開始下載。為了讓用戶可以清晰的看到每個線程下載的進度根據線程個數動態的生成等量的進度條(ProgressBar)。ProgressBar 是一個進度條控制項,用於顯示一項任務的完成進度。其有兩種樣式,一種是圓形的,該種樣式是系統預設的,由於無法顯示具體的進度值,適合用於不確定要等待多久的情形下;另一種是長條形的,此類進度條有兩種顏色,高亮顏色代表任務完成的總進度。對於我們下載任務來說,由於總任務(要下載的位元組數)是明確的,當前已經完成的任務(已經下載的位元組數)也是明確的,因此特別適合使用後者。由於在我們的需求里ProgressBar 是需要根據線程的個數動態添加的,而且要求是長條形的。因此可以事先在佈局文件中編寫好ProgressBar 的樣式。當需要用到的時候再將該佈局填充起來。ProgressBar 的max 屬性代表其最大刻度值,progress 屬性代表當前進度值。使用方法如下:
           ProgressBar.setMax(int max);設置最大刻度值。
           ProgressBar.setProgress(int progress);設置當前進度值。
       給ProgressBar 設置最大刻度值和修改進度值可以在子線程中操作的,其內部已經特殊處理過了,因此不需要再通過handler發送Message 讓主線程修改進度。

      下麵就給出我們自己寫的安卓環境下的多線程。

       多線程下載界面佈局如下,三個進度條分別表示三個子線程的下載進度。

<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"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <EditText
        android:id="@+id/et_path"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="請輸入要下載的文件資源路徑"
        android:text="http://192.168.1.104:8080/gg.exe" />

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

    <ProgressBar
        android:id="@+id/pb0"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <ProgressBar
        android:id="@+id/pb1"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <ProgressBar
        android:id="@+id/pb2"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

 

      多線程下載的內部邏輯如下,其實這在開頭已經了,只不過是代碼的實現了。

public class MainActivity extends Activity {
    private EditText et_path;
    private ProgressBar pb0;
    private ProgressBar pb1;
    private ProgressBar pb2;
    /**
     * 開啟幾個線程從伺服器下載數據
     */
    public static int threadCount = 3;

    public static int runningThreadCount;
    private String path;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //初始化控制項
        et_path = (EditText) findViewById(R.id.et_path);
        pb0 = (ProgressBar) findViewById(R.id.pb0);
        pb1 = (ProgressBar) findViewById(R.id.pb1);
        pb2 = (ProgressBar) findViewById(R.id.pb2);
    }

    //下載按鈕的點擊事件
    public void download(View view) {
        path = et_path.getText().toString().trim();
        if (TextUtils.isEmpty(path) || (!path.startsWith("http://"))) {
            Toast.makeText(this, "對不起路徑不合法", 0).show();
            return;
        }
        new Thread(){
            public void run() {
                try {
                    //1.獲取伺服器上的目標文件的大小
                    URL url = new URL(path);
                    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                    conn.setConnectTimeout(5000);
                    conn.setRequestMethod("GET");
                    int code = conn.getResponseCode();
                    if (code == 200) {
                        int length = conn.getContentLength();
                        System.out.println("伺服器文件的長度為:" + length);
                        //2.在本地創建一個跟原始文件同樣大小的文件
                        RandomAccessFile raf = new RandomAccessFile(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+getFileName(path), "rw");
                        raf.setLength(length);
                        raf.close();
                        //3.計算每個線程下載的起始位置和結束位置
                        int blocksize = length / threadCount;
                        runningThreadCount = threadCount;
                        for (int threadId = 0; threadId < threadCount; threadId++) {
                            int startIndex = threadId * blocksize;
                            int endIndex = (threadId + 1) * blocksize - 1;
                            if (threadId == (threadCount - 1)) {
                                endIndex = length - 1;
                            }
                            //4.開啟多個子線程開始下載
                            new DownloadThread(threadId, startIndex, endIndex).start();
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            };
        }.start();
    }

    
    private class DownloadThread extends Thread {
        /**
         * 線程id
         */
        private int threadId;
        /**
         * 線程下載的理論開始位置
         */
        private int startIndex;
        /**
         * 線程下載的結束位置
         */
        private int endIndex;
        /**
         * 當前線程下載到文件的那一個位置了.
         */
        private int currentPosition;

        public DownloadThread(int threadId, int startIndex, int endIndex) {
            this.threadId = threadId;
            this.startIndex = startIndex;
            this.endIndex = endIndex;
            System.out.println(threadId + "號線程下載的範圍為:" + startIndex
                    + "   ~~   " + endIndex);
            currentPosition = startIndex;
        }

        @Override
        public void run() {
            try {
                URL url = new URL(path);
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                //檢查當前線程是否已經下載過一部分的數據了 
                File info = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+threadId+".position");
                RandomAccessFile raf = new RandomAccessFile(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+getFileName(path), "rw");
                if(info.exists()&&info.length()>0){
                    FileInputStream fis = new FileInputStream(info);
                    BufferedReader br = new BufferedReader(new InputStreamReader(fis));
                    currentPosition = Integer.valueOf(br.readLine());
                    conn.setRequestProperty("Range", "bytes="+currentPosition+"-"+endIndex);
                    System.out.println("原來有下載進度,從上一次終止的位置繼續下載"+"bytes="+currentPosition+"-"+endIndex);
                    fis.close();
                    raf.seek(currentPosition);//每個線程寫文件的開始位置都是不一樣的.
                }else{
                //告訴伺服器 只想下載資源的一部分
                    conn.setRequestProperty("Range", "bytes="+startIndex+"-"+endIndex);
                    System.out.println("原來沒有有下載進度,新的下載"+ "bytes="+startIndex+"-"+endIndex);
                    raf.seek(startIndex);//每個線程寫文件的開始位置都是不一樣的.
                }
                InputStream is = conn.getInputStream();
                byte[] buffer = new byte[1024];
                int len = -1;
                while((len = is.read(buffer))!=-1){
                    //把每個線程下載的數據放在自己的空間裡面.
//                    System.out.println("線程:"+threadId+"正在下載:"+new String(buffer));
                    raf.write(buffer,0, len);
                    //5.記錄下載進度
                    currentPosition+=len;
                    File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+threadId+".position");
                    RandomAccessFile fos = new RandomAccessFile(file,"rwd");
                    //System.out.println("線程:"+threadId+"寫到了"+currentPosition);
                    fos.write(String.valueOf(currentPosition).getBytes());
                    fos.close();//fileoutstream數據是不一定被寫入到底層設備裡面的,有可能是存儲在緩存裡面.
                    //raf 的 rwd模式,數據是立刻被存儲到底層硬碟設備裡面.
                    
                    //更新進度條的顯示
                    int max = endIndex - startIndex;
                    int progress = currentPosition - startIndex;
                    if(threadId==0){
                        pb0.setMax(max);
                        pb0.setProgress(progress);
                    }else if(threadId==1){
                        pb1.setMax(max);
                        pb1.setProgress(progress);
                    }else if(threadId==2){
                        pb2.setMax(max);
                        pb2.setProgress(progress);
                    }
                }
                raf.close();
                is.close();
                System.out.println("線程:"+threadId+"下載完畢了...");
                File f = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+threadId+".position");
                f.renameTo(new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+threadId+".position.finish"));
                synchronized (MainActivity.class) {
                    runningThreadCount--;
                    //6.刪除臨時文件
                    if(runningThreadCount<=0){
                        for(int i=0;i<threadCount;i++){
                            File ft = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+i+".position.finish");
                            ft.delete();
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    /**
     * 獲取一個文件名稱
     * @param path
     * @return
     */
    public String getFileName(String path){
        int start = path.lastIndexOf("/")+1;
        return path.substring(start);
    }
}

       最後別忘了添加許可權,在該工程中不僅用到了網路訪問還用到了sdcard 存儲,因此需要添加兩個許可權。

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

      另外,xUtils同樣可以實現多線程下載。xUtils 是開源免費的Android 工具包,代碼托管在github 上。目前xUtils 主要有四大模塊:DbUtils 模塊,主要用於操作資料庫的框架。ViewUtils 模塊,通過註解的方式可以對UI,資源和事件綁定進行管理。HttpUtils 模塊,提供了方便的網路訪問,斷點續傳等功能。BitmapUtils 模塊,提供了強大的圖片處理工具。我們在這裡只簡單實用xUtils 工具中的HttpUtils 工具。第三方包的使用較為簡單,直接拷貝xUtils的jar包到libs目錄下,然後添加依賴。

     接下來就可以使用xUtils中的httpUtils的功能了:

HttpUtils http = new HttpUtils();
        /**
         * 參數1:原文件網路地址
         * 參數2:本地保存的地址
         * 參數3:是否支持斷點續傳,true:支持,false:不支持
         * 參數4:回調介面,該介面中的方法都是在主線程中被調用的,
         * 也就是該介面中的方法都可以修改UI
         */
        http.download(path, "/mnt/sdcard/xxx.exe", true, new RequestCallBack<File>() {
            
            //下載成功後調用一次
            @Override
            public void onSuccess(ResponseInfo<File> arg0) {
                Toast.makeText(MainActivity.this, "下載成功", 0).show();
            }
            
            /**
             * 每下載一部分就被調用一次,通過該方法可以知道當前下載進度
             * 參數1:原文件總位元組數
             * 參數2:當前已經下載好的位元組數
             * 參數3:是否在上傳,對於下載,該值為false
             */
            @Override
            public void onLoading(long total, long current, boolean isUploading) {
                pb0.setMax((int) total);
                pb0.setProgress((int) current);
                super.onLoading(total, current, isUploading);
            }

            //失敗後調用一次
            @Override
            public void onFailure(HttpException arg0, String arg1) {
                Toast.makeText(MainActivity.this, "下載失敗"+arg1, 0).show();
            }
        });

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 由OpenDigg 出品的安卓開源項目周報第二期來啦。我們的安卓開源周報集合了OpenDigg一周來新收錄的優質的安卓開發方面的開源項目,方便安卓開發人員便捷的找到自己需要的項目工具等。 ...
  • 今天拉同事最新的代碼,編譯時老是報如下錯誤: Error:Could not find com.android.tools.build:gradle:2.2.0.Searched in the following locations: file:/D:/software/android-studio ...
  • 轉載請標明出處:http://www.cnblogs.com/zhaoyanjun/p/6202369.html 本文出自 "【趙彥軍的博客】" 在Android Studio項目裡面有個local.properties文件,這個文件可以放一些系統配置。比如:sdk路徑、ndk路徑。 當然我們也可以 ...
  • 最近為了滿足蘋果的 https 要求, 經過努力終於寫出了方法 驗證 SSL 證書是否滿足 ATS 要求 nscurl --ats-diagnostics --verbose https://你的功能變數名稱 PASS 符合要求 輸出滿足 ATS 的證書 openssl s_client -connect ...
  • 首先想強調一下“語音識別”四個字字面意義上的需求:用戶說話然後馬上把用戶說的話轉成文字顯示!,這才是開發者真正需要的功能。 做需求之前其實是先谷歌百度一下看有沒有造好的輪子直接用,結果真的很呵呵,都是標著這個庫深入學習的標題,裡面調用一下api從URL里取出一個本地語音文件進行識別,這就沒了? 最基 ...
  • 一、SharedPreferences保存數據介紹 如果有想要保存的相對較小鍵值集合,應使用SharedPreferences API。SharedPreferences對象指向包含鍵值對的文件並提供讀寫這些文件的簡單方法。每個SharedPreferences文件由框架進行管理並且可以專用或共用。 ...
  • 微信小程式提交審核需要選擇資質服務範圍,如果服務範圍不對,審核會不通過, 開發小程式之前,最好先查詢所開發小程式的資質範圍,否則無法通過微信審核。 小程式的資質範圍查詢地址,數據同步微信官方 https://weixin.hotapp.cn/weixinmob ...
  • CocoaPods是什麼? 當你開發iOS應用時,會經常使用到很多第三方開源類庫,比如JSONKit,AFNetWorking等等。可能某個類庫又用到其他類庫,所以要使用它,必須得另外下載其他類庫,而其他類庫又用到其他類庫,“子子孫孫無窮盡也”,這也許是比較特殊的情況。總之小編的意思就是,手動一個個 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...