官术网_书友最值得收藏!

上篇 UML

UML是面向對象分析與設計時的行業標準,談面向對象的分析、設計時就不能不談UML。

對軟件工程師而言,使用UML規范整個程序的開發,是最佳的選擇。

第1章 項目分析

1.1 為什么要選擇Android多線程斷點續傳下載器作為本書講解UML時的項目

Android多線程斷點續傳下載器涉及了Android應用開發大部分的核心知識點和難點:

(1)Android中主線程和非主線通信機制:Handler、Looper、Message、MessageQueue。

(2)多線程的編程、管理。

(3)Android網絡編程。

(4)IoC技術,自己動手實現設計模式中的Listener模式。

(5)Activity、Service、數據庫編程等。

(6)文件系統。

(7)緩存。

本章用一個項目去貫穿整個Android項目的學習,理論和實踐相結合,設計和編碼并舉。

1.2 細致剖析Android多線程斷點續傳下載器

首先看一下多線程斷點續傳下載器的運行效果圖,如下圖所示。

其實上面運行效果的基本原理如下圖所示。

1.Android多線程的實現思想

(1)可以根據記錄當前的下載位置,實現斷點下載。

如果現在需要下載一個大小為29MB的文件,當下載到5MB時,臨時有事情,關閉之后普通的下載器不能幫助我們繼續下載,而是必須重新開始,而多線程下載器(如迅雷)可以幫助我們記錄下上次下載的位置,當再次下載時可以從記錄的位置繼續下載。

(2)下載速度快。

使用多線程下載文件可以更快地完成文件的下載,多線程下載文件之所以快,是因為其搶占的服務器資源多。假設服務器同時最多服務100個用戶,在服務器中一條線程對應一個用戶,100條線程在計算機中并非并發執行,而是由CPU劃分時間片輪流執行,如果A應用使用了99條線程下載文件,那么相當于占用了99個用戶的資源。假設一秒內CPU分配給每條線程的平均執行時間是10ms, A應用在服務器中一秒內就得到了990ms的執行時間,而其他應用在一秒內只有10ms的執行時間。就如同一個水龍頭,在每秒出水量相等的情況下,放990ms的水肯定比放10ms的水要多。

實現多線程操作的可以分為:

● 取得網絡連接;

● 初始化多線程下載信息;

● “開辟”硬盤空間;

● 將網絡數據放入已申請的空間中;

● 關閉資源。

通過下圖來加深對多線程的理解。

此文件的大小是6MB,共有三條線程同時進行下載,實現過程如下所示。

(1)首先要根據要訪問的URL路徑去調用openConnection()方法,得到HttpUrlConnection對象。HttpUrlConnection調用它的方法得到下載文件的長度,然后設置本地文件的長度。

    Int  filesize = HttpURLConnection.getContentLength();
    RandomAccessFile file = new RandomAccessFile("QQWubiSetup.exe", "rw");

可以使用RandomAccessFile隨機訪問類。

RandomAccessFile和File的區別:

RandomAccessFile將FileInputStream和FileOutputStream整合到一起,而且支持將從文件的任意字節處讀或寫數據,File類只是將文件作為整體來處理文件的,不能讀寫文件。

    file.setLength(filesize);

調用setLength(filesize)方法設置文件的長度,file可以達到下載文件的長度,但是它的內部不存在我們要下載文件的的數據,而是File類的特有的一些初始化的數據。

(2)根據文件長度和線程數計算每條線程下載的數據長度和下載位置。例如,文件的長度為6MB,線程數為3,那么,每條線程下載的數據長度為2MB,每條線程開始下載的位置如上圖所示。

需要計算每一條線程需要下載的長度、每條線程下載的開始位置。例如,如果每一條線程是2MB,那么第一條線程就是從0開始,第二條就是從2開始,以此類推。

但是這樣就引出了一個問題:在下載時,怎樣去指定這些線程開始下載的位置呢?

HTTP協議已經為我們解決了這個問題,它可以給我們提供一個Range頭。

(3)使用HTTP的Range頭字段指定每條線程從文件的什么位置開始下載,例如,指定從文件的2MB位置開始下載文件,代碼如下所示。

    HttpURLConnection.setRequestProperty("Range", "bytes=2097152-");

我們設置的請求頭Range字段,就是bytes=2097152,2MB的字節,比如說指定了上圖的線程2,在下載的過程中只要設置了這個頭,那么它就會從文件的A→B開始下載。

每條線程在各自的文件下載完之后,需要將下載完的文件保存到一定的位置。這樣就引入了RandomAccessFile類。

(4)保存文件,使用RandomAccessFile類指定每條線程從本地文件的一定位置開始寫入數據。

下面的代碼就可以指定從文件的什么位置開始寫入數據。

    RandomAccessFile threadfile = new RandomAccessFile("QQWubiSetup.exe ", "rw");
    threadfile.seek(2097152);

用時序圖演示在此工程中實現多線程、斷點下載的思路,每條線程負責寫文件的某一段的數據,如下圖所示。

整個工程的結構圖如下圖所示。

多線程、斷點實現過程如下:

(1)首先設計main.xml頁面,當在DownLoadActivity.java中單擊下載按鈕時,就會觸發其單擊事件,在單擊事件內部中調用download()方法用于實現下載功能。

(2)編寫實現下載的download()方法,并在方法內開啟一個線程,在其run()方法中“new”FileDownloader類。返回下載文件的大小和已經下載的數量。

● 在FileDownloader類中構造線程下載器。

● 在download方法中調用FileService類中操作線程的下載記錄業務方法,得到各個線程的最后下載位置。

● 將DownloadProgressListener接口以對象的形式當做參數傳入download方法中。

(3)在FileDownloader類中的download方法中“new”DownloadThread類實現斷點多線程下載,并存儲在指定的文件中。DownloadThread線程返回文件是否下載完成。

(4)在FileDownloader類中的download方法中完成未下載完的補救方案。

(5)如果已經下載完成,則刪除數據庫中的文件,并通過DownloadProgressListener接口中的onDownloadSize方法得到已經下載文件的數量。

(6)將下載文件的大小和已經下載的數量已經返回給新開啟的線程中,通過Handle異步通信實現頁面重繪。將文件的下載進度顯示在UI界面上。

完成此功能需要解決的技術要點:

● 完成頁面UI和布局文件;

● 數據庫中記錄各線程已經下載的信息,對各個線程的下載記錄進行操作;

● 構造下載器;

● 實現下載功能,并同時可以得到實時的各個線程的下載數量;

● 完成下載的進度的實時更新;

● 得到下載文件的名字;

● 完成頁面的實時更新。

因為多線程文件下載涉及Web服務端和Android客戶端,所以需要分別建立這兩部分工程。

2.Android多線程斷點續傳下載之服務器端

服務端的核心功能是提供一個相對比較大的文件以供Android客戶端下載使用,具體建立過程如下所示。

(1)建立一個動態的Web工程“ServerForMultipleThreadDownloader”,如下圖所示。

一切采用默認設置,單擊“Finish”按鈕完成工程的創建。此時的工程視圖如下圖所示。

(2)把一個相對比較大的音頻文件加入到WebContent的根目錄下,例如,筆者這里加入的是筆者自己的CNN錄音文件,文件名為“CNNRecordingFromWangjialin.mp3”,加入后的工程視圖如下圖所示。

(3)發布服務端Web工程,如下圖所示。

發布后的Eclipse內置的瀏覽器顯示如下圖所示。

此時在該瀏覽器中輸入http://localhost:8080/ServerForMultipleThreadDownloader/CNNReco-rdingFromWangjialin.mp3,出現如下圖所示的頁面。

因為安裝了瀏覽器的播放器的緣故,此時筆者的計算機上正在播放自己模仿的CNN新聞播音。

至此,Web服務器端實現并發布成功。

3.Android多線程斷點續傳下載之Android客戶端

(1)新建Android工程,工程的名字為“MultipleThreadContinuableDownloaderForAndroid4”,如下圖所示。

單擊“Next”按鈕,選擇默認的Android 4.0平臺,如下圖所示。

單擊“Next”按鈕,把包名設為“com.wangjialin.internet.multipleThreadContinuableDown-loaderForAndroid4”,單擊“Finish”按鈕完成。此時的工程視圖如下圖所示。

(2)完成主界面main.xml,其具體內容如下所示。

    <? xml version="1.0" encoding="utf-8"? >
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">
        <! -- 下載路徑提示文字 -->
        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:text="@string/path"
            />
        <! -- 下載路徑輸入框,此處為了方便測試,設置了默認的路徑,可以根據需要在用戶界面處修改 -->
        <EditText
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:text="http://192.168.1.100:8080/ServerForMultipleThread Downloader/CNNRecordingFromWangjialin.mp3"
            android:id="@+id/path"
            />
        <! -- 水平LinearLayout布局,包括下載按鈕和暫停按鈕 -->
        <LinearLayout
            android:orientation="horizontal"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            >
            <! -- 下載按鈕,用于觸發下載事件 -->
            <Button
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/button"
                android:id="@+id/downloadbutton"
                />
            <! -- 暫停按鈕,在初始狀態下為不可用 -->
            <Button
            android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/stopbutton"
                android:enabled="false"
                android:id="@+id/stopbutton"
                />
        </LinearLayout>
 
        <! -- 水平進度條,用圖形化的方式實時顯示進步信息 -->
        <ProgressBar
            android:layout_width="fill_parent"
            android:layout_height="18dp"
            style="? android:attr/progressBarStyleHorizontal"
            android:id="@+id/progressBar"
            />
        <! -- 文本框,用于顯示實時下載的百分比 -->
        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:id="@+id/resultView"
            />
 
    </LinearLayout>

在main.xml中我們用到了水平進度條ProgressBar,這是Android所定義的,我們如果要訪問Android系統中定義的樣式,就必須加一個“? ”,也就是說要引用Android系統中的樣式。

style="? android:attr/progressBarStyleHorizontal"這里面的樣式和以前接觸過的css樣式很相似,主要定義頁面顯示的一個風格。我們在下載的過程中,還用到了百分率,我們用textview來表示,用到了Andorid的居中顯示android:gravity="center",內容可以居中對齊。

此時打開main.xml的Graphical Layout視圖,如下圖所示。

從上圖中可以看到,需要定義path、button、stopbutton等幾個字符串資源,具體的strings.xml內容如下所示。

    <? xml version="1.0" encoding="utf-8"? >
    <resources>
 
        <string name="hello">Hello World, MultipleThreadContinuableDownloader ForAndroid4Activity! </string>
        <string name="app_name">MultipleThreadContinuableDownloaderFor Android4</string>
        <string name="path">Download URL</string>
        <string name="button">Start Downloading</string>
        <string name="success">Downloading Completed Successfully</string>
        <string name="error">Downloading Error</string>
        <string name="stopbutton">Pause Downloading</string>
        <string name="sdcarderror">There is no SDCard or it is not allowed to write</string>
 
    </resources>

此時再看main.xml的Graphical Layout視圖就不會有顯示問題了,如下圖所示。

(3)完成數據庫的設計、實現以及對數據庫的操作。

數據庫中表的字段有id, downpath, threadid, downlength。

● id:代表數據記錄的主鍵。

● threadid:代表線程的id。

● downlength:代表線程下載的最后位置。

● downpath:代表當前線程下載的資源,因為一個下載器可能會同時下載很多資源。

此時建立自己的數據庫管理類,負責數據庫和數據表的創建、升級、初始化等工作。

創建數據庫管理類DBOpenHelper,該類需要繼承“android.database.sqlite.SQLiteOpen Helper”,如下圖所示。

DBOpenHelper的具體內容如下所示。

    package com.wangjialin.internet.service;
 
    import android.content.Context;
    import android.database.sqlite.SQLiteDatabase;
    import android.database.sqlite.SQLiteOpenHelper;
 
    /**
     * SQLite管理器,實現創建數據庫和表,但版本變化時實現對表的數據庫表的操作
     * @author think
     *
     */
    public class DBOpenHelper extends SQLiteOpenHelper {
        private static final String DBNAME = "eric.db";    //設置數據庫的名稱
        private static final int VERSION = 1;              //設置數據庫的版本
 
        /**
        * 通過構造方法
        * @param context應用程序的上、下文對象
        */
        public DBOpenHelper(Context context){
            super(context, DBNAME, null, VERSION);
        }
 
        @Override
        public void onCreate(SQLiteDatabase db){  //建立數據表
            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);   //調用onCreate方法重新創建數據表,也可以自己根據業務需onCreate(db);   //調用onCreate方法重新創建數據表,也可以自己根據業務需要創建新的的數據表
        }
 
    }

接下來建立數據庫業務操作類,如下圖所示。

單擊“Finish”按鈕完成創建,FileService的具體內容如下所示。

    package com.wangjialin.internet.service;
 
    import java.util.HashMap;
    import java.util.Map;
 
    import android.content.Context;
    import android.database.Cursor;
    import android.database.sqlite.SQLiteDatabase;
 
    /**
     * 業務Bean,實現對數據的操作
     * @author Wang Jialin
     *
     */
    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){Cursor cursor = db.rawQuery("select threadid, downlength from SQLiteDatabase db = openHelper.getReadableDatabase();  //獲取可讀SQLiteDatabase db = openHelper.getReadableDatabase();  //獲取可讀的數據庫句柄,一般情況下在該操作的內部實現中,其返回的其實是可寫的數據庫句柄filedownlog where downpath=? ", new String[]{path}); //根據下載路徑查詢所有線程下載數據,返回的Cursor指向第一條記錄之前Map<Integer, Integer> data = new HashMap<Integer, Integer>();//建立一個哈希表用于存放每條線程的已經下載的文件長度while(cursor.moveToNext()){//從第一條記錄開始開始遍歷Cursor對象
                data.put(cursor.getInt(0), cursor.getInt(1));  //把線程ID和該線程已下載的長度設置進data哈希表中
                data.put(cursor.getInt(cursor.getColumnIndexOrThrow
               ("threadid")), cursor.getInt(cursor.getColumnIndexOrThrow
               ("downlength")));
            }
            cursor.close();     //關閉cursor,釋放資源
            db.close();         //關閉數據庫
            return data;        //返回獲得的每條線程和每條線程的下載長度
        }
        /**
        * 保存每條線程已經下載的文件長度
        * @param path  下載的路徑
        * @param map現在的ID和已經下載的長度的集合
        */
       public void save(String path,  Map<Integer, Integer> map){
            SQLiteDatabase db = openHelper.getWritableDatabase();  //獲取可寫的數據庫句柄
            db.beginTransaction();  //開始事務,因為此處要插入多批數據
            try{
                for(Map.Entry<Integer, Integer> entry : map.entrySet()){
                //采用For-Each的方式遍歷數據集合
                    db.execSQL("insert into filedownlog(downpath, threadid,
                    downlength)values(? , ? , ?)",
                            new Object[]{path, entry.getKey(), entry.getValue
                           ()});   //插入特定下載路徑,特定線程ID,已經下載的數據
                }
                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(); //關閉數據庫,釋放資源
        }
 
    }

(4)實現文件下載類FileDownloader、具體線程DownloadThread以及進度監聽器接口DownloadProgressListener。

FileDownloader的建立如下圖所示。

FileDownloaderd的具體內容如下所示。

    package com.wangjialin.internet.service.downloader;
 
    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.wangjialin.internet.service.FileService;
 
    public class FileDownloader {
    private static final String TAG = "FileDownloader"; //設置標簽,方便Logcat日志記錄
    private static final int RESPONSEOK = 200; //響應碼為200,即訪問成功
    private Context context;                //應用程序的上、下文對象
    private FileService fileService;       //獲取本地數據庫的業務Bean
    private boolean exited;                 //停止下載標志
    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;              //根據數組長度返回線程數
    }
 
    /**
     * 退出下載
     */
    public void exit(){
        this.exited = true;                 //設置退出標志為true
    }
    public boolean getExited(){
        return this.exited;
    }
    /**
     * 獲取文件大小
     * @return
     */
    public int getFileSize(){
        return fileSize;                    //從類成員變量中獲取下載文件的大小
    }
 
    /**
     * 累計已下載大小
     * @param size
     */
    protected synchronized void append(int size){ //使用同步關鍵字解決并發訪問問題
        downloadedSize += size;         //把實時下載的長度加入到總下載長度中
    }
    /**
     * 更新指定線程最后下載的位置
     * @param threadId線程ID
     * @param pos最后下載的位置
     */
    protected synchronized void update(int threadId, int pos){
        this.data.put(threadId, pos);  //把制定線程ID的線程賦予最新的下載長度,以前的值會被覆蓋掉
        this.fileService.update(this.downloadUrl, threadId, pos);
                                            //更新數據庫中指定線程的下載長度
    }
    /**
     * 構建文件下載器
     * @param downloadUrl下載路徑
     * @param fileSaveDir文件保存目錄
     * @param threadNum下載線程數
     */
    public FileDownloader(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.mkdirs(); //如果指定的文
            件不存在,則創建目錄,此處可以創建多層目錄
            this.threads = new DownloadThread[threadNum];  //根據下載的線
            程數創建下載線程池
            HttpURLConnection conn =(HttpURLConnection)url.
            openConnection();   //建立一個遠程連接句柄,此時尚未真正連接
            conn.setConnectTimeout(5*1000); //設置連接超時時間為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頭字段集合
        if(conn.getResponseCode()==RESPONSEOK){  //此處的請求會打開
        返回流并獲取返回的狀態碼,用于檢查是否請求成功,當返回碼為200時執行下面
        的代碼
            this.fileSize = conn.getContentLength(); //根據響應獲取文件
            大小
            if(this.fileSize <= 0)throw new RuntimeException("Unkown
            file size ");   //當文件大小為小于等于零時拋出運行時異常
 
            String filename = getFileName(conn); //獲取文件名稱
            this.saveFile = new File(fileSaveDir, filename); //根據文件
            保存目錄和文件名構建保存文件
            Map<Integer, Integer> logdata = fileService.getData
           (downloadUrl); //獲取下載記錄
 
            if(logdata.size()>0){//如果存在下載記錄
                for(Map.Entry<Integer, Integer> entry : logdata.
                entrySet())//遍歷集合中的數據
                    data.put(entry.getKey(), entry.getValue()); //把各
                    條線程已經下載的數據長度放入data中
            }
 
            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("server response error ");
            //拋出運行時服務器返回異常
        }
    } catch(Exception e){
        print(e.toString());    //打印錯誤
            throw new RuntimeException("Can't connection this 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;  //如果遍歷到了返回頭末尾處,退出循環
                if("content-disposition".equals(conn.getHeaderField
                Key(i).toLowerCase())){//獲取content-disposition返回頭字段,里面可能會包含文件名
                    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"); //The file is opened for reading and writing. Every
            change of the file's content must be written synchronously to
            the target device.
            if(this.fileSize>0)randOut.setLength(this.fileSize);
            //設置文件的大小
            randOut.close();    //關閉該文件,使設置生效
            URL url = new URL(this.downloadUrl);   //A URL instance
            specifies the location of a resource on the internet as specified
            by RFC 1738
            if(this.data.size()! = this.threads.length){   //如果原先未曾下載或者原先的下載線程數與現在的線程數不一致
                this.data.clear();  //Removes all elements from this Map,
                leaving it empty.
                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 downloadedLength = this.data.get(i+1); //通過特定的線程ID獲取該線程已經下載的數據長度
                if(downloadedLength < this.block && this.downloadedSize <
                this.fileSize){//判斷線程是否已經完成下載,否則繼續下載
                    this.threads[i] = new DownloadThread(this, url,
                    this.saveFile, this.block, this.data.get(i+1), i+1);
                    //初始化特定ID的線程
                    this.threads[i].setPriority(7); //設置線程的優先級,
                    Thread.NORM_PRIORITY = 5 Thread.MIN_PRIORITY = 1 Thread.
                MAX_PRIORITY = 10
                    this.threads[i].start();    //啟動線程
                }else{
                    this.threads[i] = null; //表明在線程已經完成下載任務
                }
            }
            fileService.delete(this.downloadUrl);  //如果存在下載記錄,刪除它們,然后重新添加
            fileService.save(this.downloadUrl, this.data); //把已經下載的實時數據寫入數據庫
            boolean notFinished = true; //下載未完成
            while(notFinished){// 循環判斷所有線程是否完成下載
                Thread.sleep(900);
                notFinished = false; //假定全部線程下載完成
                for(int i = 0; i < this.threads.length; i++){
                    if(this.threads[i] ! = null && ! this.threads[i].
                    isFinished()){//如果發現線程未完成下載
                        notFinished = true; //設置標志為下載沒有完成
                        if(this.threads[i].getDownloadedLength()== -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("File downloads error"); //拋出文件下載異常
        }
        return this.downloadedSize;
    }
    /**
    * 獲取HTTP響應頭字段
    * @param http  HttpURLConnection對象
    * @return  返回頭字段的LinkedHashMap
    */
   public static Map<String, String> getHttpResponseHeader(HttpURL
   Connection http){
        Map<String, String> header = new LinkedHashMap<String, String>();
        //使用LinkedHashMap保證寫入和遍歷的時候的順序相同,而且允許空值存在
        for(int i = 0; ; i++){//此處為無限循環,因為不知道頭字段的數量
            String fieldValue = http.getHeaderField(i);
            //getHeaderField(int n)用于返回第n個頭字段的值。
 
            if(fieldValue == null)break; //如果第i個字段沒有值了,則表明頭字段部分已經循環完畢,此處使用Break退出循環
            header.put(http.getHeaderFieldKey(i), fieldValue);
            //getHeaderFieldKey(int n)用于返回第n個頭字段的鍵。
        }
        return header;
   }
   /**
    * 打印HTTP頭字段
    * @param http HttpURLConnection對象
    */
   public static void printResponseHeader(HttpURLConnection http){
        Map<String, String> header = getHttpResponseHeader(http);
        //獲取HTTP響應頭字段
        for(Map.Entry<String, String> entry : header.entrySet()){
        //使用For-Each循環的方式遍歷獲取的頭字段的值,此時遍歷的循序和輸入的順序相同
            String key = entry.getKey()! =null ? entry.getKey()+ ":" : "";
        //當有鍵的時候截獲取鍵,如果沒有則為空字符串
            print(key+ entry.getValue());  //答應鍵和值的組合
            }
        }
 
        /**
        * 打印信息
        * @param msg   信息字符串
        */
        private static void print(String msg){
            Log.i(TAG, msg);    //使用LogCat的Information方式打印信息
        }
    }

可以看出FileDownloader是使用了DownloadThread來執行具體的下載工作的,Download Thread的建立如下圖所示。

單擊“Finish”按鈕完成創建,DownloadThread的具體內容如下所示。

    package com.wangjialin.internet.service.downloader;
 
    import java.io.File;
    import java.io.InputStream;
    import java.io.RandomAccessFile;
    import java.net.HttpURLConnection;
    import java.net.URL;
 
    import android.util.Log;
 
    /**
    * 下載線程,根據具體下載地址、數據保存到的文件、下載塊的大小、已經下載的數據大小等信息進行下載
    * @author Wang Jialin
    *
    */
   public class DownloadThread extends Thread {
        private static final String TAG = "DownloadThread";    //定義TAG,方便日期的打印輸出
        private File saveFile;          //下載的數據保存到的文件
        private URL downUrl;            //下載的URL
        private int block;              //每條線程下載的大小
        private int threadId = -1;      //初始化線程ID設置
        private int downloadedLength;  //該線程已經下載的數據長度
        private boolean finished = false;  //該線程是否完成下載的標志
        private FileDownloader downloader; //文件下載器
 
        public DownloadThread(FileDownloader downloader, URL downUrl, File
        saveFile, int block, int downloadedLength, int threadId){
            this.downUrl = downUrl;
            this.saveFile = saveFile;
            this.block = block;
            this.downloader = downloader;
            this.threadId = threadId;
            this.downloadedLength = downloadedLength;
        }
 
        @Override
        public void run(){
            if(downloadedLength < block){//未下載完成
                try {
                    HttpURLConnection http =(HttpURLConnection)downUrl.
                    openConnection();   //開啟HttpURLConnection連接
                    http.setConnectTimeout(5 * 1000); //設置連接超時時間為5秒鐘
                    http.setRequestMethod("GET");  //設置請求的方法為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");   //設置通信編碼為UTF-8
                    int startPos = block *(threadId -1)+ downloadedLength;
                    //開始位置
                    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 + " starts to download from
        position "+ startPos);  //打印該線程開始下載的位置
        RandomAccessFile threadFile = new RandomAccessFile
       (this.saveFile, "rwd"); //If the file does not already
        exist then an attempt will be made to create it and it require
        that every update to the file's content be written
        synchronously to the underlying storage device.
        threadFile.seek(startPos); //文件指針指向開始下載的位置
        while(! downloader.getExited()&&(offset = inStream.
        read(buffer, 0, 1024))! = -1){//但用戶沒有要求停止下載,同時沒有到達請求數據的末尾時候會一直循環讀取數據
            threadFile.write(buffer, 0, offset);   //直接把數據寫到文件中
            downloadedLength += offset; //把新下載的已經寫到文件中的數據加入到下載長度中
            downloader.update(this.threadId, downloadedLength);
            //把該線程已經下載的數據長度更新到數據庫和內存哈希表中
            downloader.append(offset); //把新下載的數據長度加入到已經下載的數據總長度中
        }//該線程下載數據完畢或者下載被用戶停止
        threadFile.close(); //Closes this random access file stream
        and releases any system resources associated with the stream.
        inStream.close();   //Concrete implementations of this
        class should free any resources during close
        if(downloader.getExited())
        {
            print("Thread " + this.threadId + " has been paused");
        }
        else
        {
            print("Thread " + this.threadId + " download finish");
        }
 
        this.finished = true;   //設置完成標志為true,無論是下載完成還是用戶主動中斷下載
    } catch(Exception e){//出現異常
        this.downloadedLength = -1; //設置該線程已經下載的長度為-1
        print("Thread "+ this.threadId+ ":"+ e);  //打印出異常信息
    }
}
}
/**
        * 打印信息
        * @param msg   信息
        */
       private static void print(String msg){
            Log.i(TAG, msg);    //使用Logcat的Information方式打印信息
       }
 
       /**
        * 下載是否完成
        * @return
        */
       public boolean isFinished(){
            return finished;
       }
 
       /**
        * 已經下載的內容大小
        * @return如果返回值為-1,代表下載失敗
        */
       public long getDownloadedLength(){
            return downloadedLength;
       }
    }

同時在FileDownloader中需要使用DownloadProgressListener進行進度監聽,這是一個接口,如下圖所示。

DownloadProgressListener的具體內容如下所示。

    package com.wangjialin.internet.service.downloader;
 
    /**
     * 下載進度監聽器
     * @author Wang Jialin
     *
     */
    public interface DownloadProgressListener {
        /**
        * 下載進度監聽方法,獲取和處理下載點數據的大小
        * @param size數據大小
        */
        public void onDownloadSize(int size);
    }

(5)實現主Activity類。

MultipleThreadContinuableDownloaderForAndroid4Activity具體的內容如下所示。

    package com.wangjialin.internet.multipleThreadContinuableDownloaderFor
    Android4;
    import java.io.File;
    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;
 
    import com.eric.net.download.DownloadProgressListener;
    import com.eric.net.download.FileDownloader;
    /**
     * 主界面,負責下載界面的顯示、與用戶交互、響應用戶事件等
     * @author Wang Jialin
     *
     */
    public class MultipleThreadContinuableDownloaderForAndroid4Activity
    extends Activity {
        private static final int PROCESSING = 1;   //正在下載實時數據傳輸Message標志
        private static final int FAILURE = -1;     //下載失敗時的Message標志
 
        private EditText pathText;                  //下載輸入文本框
        private TextView resultView;                //現在進度顯示百分比文本框
    private Button downloadButton;     //下載按鈕,可以觸發下載事件
    private Button stopbutton;         //停止按鈕,可以停止下載
    private ProgressBar progressBar;   //下載進度條,實時圖形化的顯示進度信息
    //hanlder對象的作用是向創建Hander對象所在的線程所綁定的消息隊列發送消息并處理消息
    private Handler handler = new UIHander();
 
    private final class UIHander extends Handler{
     /**
      * 系統會自動調用的回調方法,用于處理消息事件
      * Mesaage一般會包含消息的標志和消息的內容以及消息的處理器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); //把獲取的浮點數計算結構轉化為整數
                resultView.setText(result+ "%");   //把下載的百分比顯示在界面顯示控件上
                if(progressBar.getProgress()== progressBar.getMax()){
                //當下載完成時
                    Toast.makeText(getApplicationContext(), R.string.
                    success, Toast.LENGTH_LONG).show(); //使用Toast技術,提示用戶下載完成
                }
                break;
 
            case -1:    //下載失敗時
                Toast.makeText(getApplicationContext(), R.string.error,
                Toast.LENGTH_LONG).show(); //提示用戶下載失敗
                break;
            }
        }
    }
 
    @Override
    public void onCreate(Bundle savedInstanceState){  //應用程序啟動時會首先調用且在應用程序整個生命周期中只會調用一次,適合于初始化工作
        super.onCreate(savedInstanceState); //使用父類的onCreate用做屏幕主界面的底層和基本繪制工作
        setContentView(R.layout.main);  //根據XML界面文件設置主界面
 
        pathText =(EditText)this.findViewById(R.id.path); //獲取下載URL
        的文本輸入框對象
        resultView =(TextView)this.findViewById(R.id.resultView);
        //獲取顯示下載百分比文本控件對象
        downloadButton =(Button)this.findViewById(R.id.downloadbutton);
        //獲取下載按鈕對象
        stopbutton =(Button)this.findViewById(R.id.stopbutton);
        //獲取停止下載按鈕對象
        progressBar =(ProgressBar)this.findViewById(R.id.progressBar);
        //獲取進度條對象
        ButtonClickListener listener = new ButtonClickListener();
        //聲明并定義按鈕監聽器對象
        downloadButton.setOnClickListener(listener); //設置下載按鈕的監聽器對象
        stopbutton.setOnClickListener(listener); //設置停止下載按鈕的監聽器對象
    }
    /**
    * 按鈕監聽器實現類
    * @author Wang Jialin
    *
    */
    private final class ButtonClickListener implements View.
    OnClickListener{
        public void onClick(View v){  //該方法在注冊了該按鈕監聽器的對象被單
        擊時會自動調用,用于響應單擊事件
            switch(v.getId()){    //獲取單擊對象的ID
            case R.id.downloadbutton:  //當單擊下載按鈕時
                String path = pathText.getText().toString(); //獲取下載路徑
                if(Environment.getExternalStorageState().equals
               (Environment.MEDIA_MOUNTED)){  //獲取SDCard是否存在,當SDCard存在時
                    File saveDir = Environment.getExternalStorage
                    Directory();    //獲取SDCard根目錄文件
                    File saveDir = Environment.getExternalStoragePublic
                    Directory(
                            Environment.DIRECTORY_MOVIES);
                    /*File saveDir = Environment.getExternalStoragePublic
                    Directory(
                            Environment.DIRECTORY_MUSIC); */
 
                    File saveDir =  getApplicationContext().getExternal
                    FilesDir(Environment.DIRECTORY_MOVIES);
                    download(path, saveDir);    //下載文件
                }else{  //當SDCard不存在時
                    Toast.makeText(getApplicationContext(), R.string.
                    sdcarderror, Toast.LENGTH_LONG).show(); //提示用戶SDCard
                    不存在
                }
                downloadButton.setEnabled(false);  //設置下載按鈕不可用
                stopbutton.setEnabled(true);   //設置停止下載按鈕可用
                break;
            case R.id.stopbutton:   //當單擊停止下載按鈕時
                exit(); //停止下載
                downloadButton.setEnabled(true);   //設置下載按鈕可用
                stopbutton.setEnabled(false);  //設置停止按鈕不可用
                break;
            }
        }
 
        ///////////////////////////////////////////////////////////////
        //由于用戶的輸入事件(單擊button,觸摸屏幕…)是由主線程負責處理的,如果主線程
        處于工作狀態
        //此時用戶產生的輸入事件如果沒能在5秒內得到處理,系統就會報"應用無響應"錯誤
        //所以在主線程里不能執行一件比較耗時的工作,否則會因主線程阻塞而無法處理用戶
        的輸入事件
        //導致"應用無響應"錯誤的出現,耗時的工作應該在子線程里執行
        ///////////////////////////////////////////////////////////////
 
        private DownloadTask task; //聲明下載執行者
        /**
        * 退出下載
        */
        public void exit(){
            if(task! =null)task.exit(); //如果有下載對象時,退出下載
        }
        /**
        * 下載資源,生命下載執行者并開辟線程開始現在
        * @param path  下載的路徑
        * @param saveDir   保存文件
        */
        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 FileDownloader loader; //文件下載器(下載線程的容器)
            /**
            * 構造方法,實現變量初始化
            * @param path  下載路徑
            * @param saveDir   下載要保存到的文件
            */
            public DownloadTask(String path, File saveDir){
                this.path = path;
                this.saveDir = saveDir;
            }
 
            /**
             * 退出下載
             */
            public void exit(){
                if(loader! =null)loader.exit(); //如果下載器存在的話則退出下載
            }
            DownloadProgressListener downloadProgressListener = new
            DownloadProgressListener(){   //開始下載,并設置下載的監聽器
V
                /**
                * 下載的文件長度會不斷的被傳入該回調方法
                */
                public void onDownloadSize(int size){
                    Message msg = new Message(); //新建立一個Message對象
                    msg.what = PROCESSING;      //設置ID為1;
                    msg.getData().putInt("size", size); //把文件下載的size設置進Message對象
                    handler.sendMessage(msg); //通過handler發送消息到消息隊列
                }
            };
            /**
             * 下載線程的執行方法,會被系統自動調用
             */
            public void run(){
                try {
                    loader = new FileDownloader(getApplicationContext(),
                    path, saveDir, 3);  //初始化下載
                    progressBar.setMax(loader.getFileSize());  //設置進度條的最大刻度
                    loader.download(downloadProgressListener);
 
                } catch(Exception e){
                    e.printStackTrace();
                    handler.sendMessage(handler.obtainMessage(FAILURE));
                    //下載失敗時向消息隊列發送消息
                    /*Message message = handler.obtainMessage();
                    message.what = FAILURE; */
                }
            }
        }
    }
 
  }

(6)因為要訪問網絡,同時要在SDCard中創建文件,所示要在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"/>

此時的AndroidManifest.xml的具體內容如下所示。

    <? xml version="1.0" encoding="utf-8"? >
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.wangjialin.internet.multipleThreadContinuableDownloader
        ForAndroid4"
        android:versionCode="1"
        android:versionName="1.0" >
 
        <uses-sdk android:minSdkVersion="8" />
V
        <application
          android:icon="@drawable/ic_launcher"
          android:label="@string/app_name" >
          <! -- 主Acitivity,提供用戶操作的界面和接口 -->
          <activity
              android:label="@string/app_name"
              android:name=".MultipleThreadContinuableDownloaderForAndroid4
              Activity" >
              <intent-filter >
                  <! -- 應用啟動時啟動入口Activity -->
                  <action android:name="android.intent.action.MAIN" />
                  <! -- 應用顯示在應用程序列表-->
                  <category android:name="android.intent.category.LAUNCHER" />
              </intent-filter>
          </activity>
        </application>
 
        <! -- 訪問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"/>
 
    </manifest>

(7)安裝并運行Android多線程斷點續傳下載程序,安裝并運行后的初始界面如下圖(左)所示。單擊“Start Downloading”開始進行下載,此時的狀態如下圖(右)所示。

當下載完成時,出現如下圖所示的視圖。

至此,Android多線程斷點續傳下載項目完成。

主站蜘蛛池模板: 阳曲县| 本溪| 同江市| 普洱| 安平县| 错那县| 肇庆市| 邮箱| 绵阳市| 柳州市| 奉化市| 财经| 苏尼特左旗| 江阴市| 迭部县| 鄢陵县| 乌鲁木齐市| 普定县| 惠水县| 阿荣旗| 莒南县| 濮阳市| 德兴市| 习水县| 通河县| 宁明县| 香港| 兰溪市| 大足县| 工布江达县| 澜沧| 金昌市| 白银市| 龙南县| 海阳市| 青阳县| 延长县| 越西县| 民权县| 北宁市| 牟定县|