android多線程斷點續傳下載

jopen 8年前發布 | 14K 次閱讀 Android開發 移動開發

最近在研究下載文件的斷點續傳,主要是能夠記錄下載位置,退出應用下次再進來的時候也能從當前的位置開始下載,代碼為多線程+數據庫,代碼能夠正常運行,但是我在開發的過程中遇到了幾個問題,有的沒找到解決方案,分享出來,希望有的大神能夠指點一下:

  1. 使用HttpURLConnection 獲取文件大小的時候,速度在4.x手機上非常慢,但是找了許多中文網站上的代碼基本都沒有解決,后來google一下,發現添加conn.setRequestMethod(“HEAD”)就可以了,獲取包頭就可以了,這樣conn.disconnect()關閉連接就能夠快很多了
  2. 同樣的使用HttpURLConnection下載,用戶暫停下載或者下載完成conn.disconnect()的調用有時候會花費十幾秒的時間,這個找了很久,4.X的bug,解決方法有,但是挺麻煩的,google一下就能找到,我也去參考了其他軟件的斷點續傳,發現也有這問題,不知道有沒有方便一點的解決方法,望大神解答
  3. RandomAccessFile的多線程低效率問題,看網上說在低配置手機上RandomAccessFile的效率低,網絡支持3M/s的下載,但是讀寫速度最多只有1M/s,那么不管網速多快,下載速度就是1M/s,不知道能不能找到一個代替的方案
  4. 非常偶現的情況,下載一個超過100M的文件,有時候會莫名其妙的下載到90多M,下載線程HttpURLConnection就自動關閉了鏈接,但是我只是偶現了一兩次,不知道是不是我代碼的問題,估計是吧
  5. 在下載過程中,由于網絡不穩定或者服務器錯誤,可能會下載不到數據,所以在線程中檢測,如果3s之內沒有下載到任何數據,就檢測網絡是否連接,如果沒有鏈接,就直接拋出網絡錯誤狀態,要不然就拋出服務器錯誤狀態
  6. 有一個臺灣朋友,提出來的,非常感謝他,服務器狀態錯誤,read函數會阻塞,無法返回,這個時候,我最先使用的是futureTask,非常強暴的將下載線程cancel,防止一直阻塞,雖然說可以成功,但是畢竟不好,他提出來使用setReadTimeout函數,將read函數設置為阻塞5s,在5s之內如果得不到數據,就會拋出SocketTimeoutException,下載線程就會成功關閉,這個方法非常好。

代碼我貼出來吧,主體就是一個類,附加一個數據庫類,主要是線程之間的交互吧,有一個開始線程,主要功能是當數據庫中沒有該下載文件信息的時候去創建,如果有,就去數據庫中獲取,并且開始數個下載線程開始下載文件;當用戶暫停的時候會啟動停止線程去結束開始線程(如果沒有執行完成)和下載線程,這時候第2個問題就會出現,關閉下載線程時,4.x的手機關閉http連接的速度慢的驚人。

FileDownloadManager類

public class FileDownloadManager {

    /** 開啟線程 */
    private static final int STATE_START_THREADS = -1;
    /** 更新狀態 */
    private static final int STATE_UPDATE_PROGRESS = 0;
    /** 下載狀態,正在獲取文件大小 */
    public static final int STATE_GETSIZE = 1;
    /** 下載狀態,開始下載 */
    public static final int STATE_STARTING = 2;
    /** 下載狀態,正在停止 */
    public static final int STATE_STOPING = 3;
    /** 下載狀態,停止成功 */
    public static final int STATE_STOPED = 4;
    /** 下載狀態,下載完成 */
    public static final int STATE_FINISH = 5;
    /** 下載狀態,正在刪除 */
    public static final int STATE_DELETING = 6;
    /** 下載狀態,刪除成功 */
    public static final int STATE_DELETE = 7;
    /** 下載狀態,網絡錯誤 */
    public static final int STATE_NET_ERROR = 8;
    /** 下載狀態,服務器錯誤 */
    public static final int STATE_SERVER_ERROR = 9;

    /** 當前文件的下載狀態,默認為停止成功,即為下載完成,且隨時可以開始下載 */
    private int currentState = STATE_STOPED;

    /** 下載一個文件所開啟的線程數量 */
    private final int THREAD_NUM = 4;
    /** 下載一個文件的所有線程信息 */
    private ArrayList<DownloadInfo> infos;
    /** 開始下載線程 */
    private Thread startDownloadThread;
    /** 結束下載線程 */
    private Thread stopDownloadThread;
    /** 刪除下載線程 */
    private Thread deleteDownloadThread;

    /** 下載一個文件的線程隊列 */
    private ArrayList<DownloadThread> threads;

    /** 更新下載信息,更新界面線程 */
    private UpdateThread updateThread;
    /** 數據庫操作對象 */
    private DownloadDBHelper helper;
    /** 該文件下載名 */
    private String fileName;
    /** 該文件下載的url */
    private String url;
    /** 該文件下載路徑,默認為SD卡file目錄 */
    private String path = FileUtils.getExternalStorageFilePath();
    /** 該文件下載的文件大小 */
    private long fileSize = 0;
    /** 該文件下載的完成度 */
    private long completeSize = 0;
    /** 通知更新進度handler */
    private ProgressChangeHandler progressChangeHandler;
    /** 文件下載進度更新 */
    private IDownloadProgressChangedListener listener;
    /** 文件的下載狀態,true表示正在下載 */
    private boolean downloadState = false;
    /** 文件是否下載完成 */
    private boolean isDownloadFinish = false;
    /** 下載線程是否結束標識 */
    private CountDownLatch countDownLatch;

    /**
     * @param url 文件下載url
     * @param fileName 文件名,默認將會下載到sd卡file目錄下
     */
    public FileDownloadManager(String url, String fileName){
        this(url, fileName, null);
    }

    /**
     * @param url 文件下載url
     * @param fileName 文件名
     * @param path 文件下載路徑,不需要帶文件名
     */
    public FileDownloadManager(String url, String fileName, String path){
        this.url = url;
        if (path != null)
            this.path = path;
        if (!this.path.substring(this.path.length()-1).equals("/")){
            this.path += "/";
        }

        //保存該文件原路徑
        this.fileName = this.path + fileName;

        //將文件名字先進行md5,最后等文件下載完成之后再更改文件名字
        String md5 = CommonUtils.md5(fileName);
        this.path += md5;

        helper = new DownloadDBHelper();
        infos = new ArrayList<>();
        threads = new ArrayList<>();
        progressChangeHandler = new ProgressChangeHandler(this);
        checkFileFinish();
    }

    /**
     * 檢測該文件是否已經下載完成
     */
    private boolean checkFileFinish(){
        if (isDownloadFinish || isFileDownloadFinish(helper.getInfo(url))){
            isDownloadFinish = true;
            progressChangeHandler.sendEmptyMessage(STATE_FINISH);
            return true;
        }
        return false;
    }

    /**
     * 開啟下載
     */
    public void start(){
        if (!CommonUtils.isNetworkAvailable()){
            progressChangeHandler.sendEmptyMessage(STATE_NET_ERROR);
            T.getInstance().showShort("網絡錯誤");
            return;
        }

        if (checkFileFinish()){
            T.getInstance().showShort("文件已下載完成");
            return;
        }

        if (downloadState){
            T.getInstance().showShort("已經啟動下載");
            return;
        }

        if (currentState == STATE_STOPING){
            T.getInstance().showShort("文件還未停止成功");
            return;
        }

        if (currentState == STATE_DELETING){
            T.getInstance().showShort("文件正在刪除");
            return;
        }

        if (currentState == STATE_DELETE){
            T.getInstance().showShort("文件已刪除");
            return;
        }

        downloadState = true;

        //開啟下載任務
        startDownload(helper.getInfo(url));
    }

    /**
     * 停止下載
     */
    public void stop(){
        downloadState = false;
        //停止更新界面線程,一定要保證最多只有一個更新線程在執行
        if (updateThread != null && updateThread.isAlive())
            updateThread.canRun = false;

        if (checkFileFinish()){
            T.getInstance().showShort("文件已下載完成");
            return;
        }

        if (currentState == STATE_STOPING){
            T.getInstance().showShort("正在停止,稍后...");
            return;
        }

        if (currentState == STATE_STOPED){
            T.getInstance().showShort("已停止");
            return;
        }

        if (currentState == STATE_DELETING){
            T.getInstance().showShort("文件正在刪除");
            return;
        }

        if (currentState == STATE_DELETE){
            T.getInstance().showShort("文件已刪除");
            return;
        }

        stopDownload();
    }

    /**
     * 刪除該文件下載的相關所有信息,成功之后,該對象將自動置為null
     */
    public void delete(){
        //停止下載線程
        downloadState = false;
        //停止更新界面線程,一定要保證最多只有一個更新線程在執行
        if (updateThread != null && updateThread.isAlive())
            updateThread.canRun = false;

        if (currentState == STATE_DELETING){
            T.getInstance().showShort("正在刪除,稍后...");
            return;
        }

        if (currentState == STATE_DELETE){
            T.getInstance().showShort("已刪除");
            return;
        }

        if (currentState == STATE_STOPING){
            T.getInstance().showShort("正在停止,稍后...");
            return;
        }
        deleteDownload();
    }

    private void startDownload(final ArrayList<HashMap<String, String>> maps){
        startDownloadThread = new Thread(new Runnable() {
            @Override
            public void run() {
                if (stopDownloadThread!=null && stopDownloadThread.isAlive()){
                    Looper.prepare();
                    T.getInstance().showShort("正在停止,稍后...");
                    Looper.loop();
                    Looper.myLooper().quit();
                    return;
                }

                if (deleteDownloadThread!=null && deleteDownloadThread.isAlive()){
                    Looper.prepare();
                    T.getInstance().showShort("文件正在刪除");
                    Looper.loop();
                    Looper.myLooper().quit();
                    return;
                }

                //如果沒有下載信息,則需要創建
                if (infos==null || infos.size()==0) {
                    if (maps == null || maps.size() == 0) {
                        createDownloadInfos();
                    } else {
                        revertDownloadInfos(maps);
                    }
                }

                //更新文件狀態為正在下載
                progressChangeHandler.sendEmptyMessage(STATE_STARTING);

                //上次的線程完成之后才能開啟新的下載線程開始下載
                threads.clear();
                for (DownloadInfo info : infos){
                    DownloadThread thread = new DownloadThread(info);
                    threads.add(thread);
                }
                L.i("準備開啟下載線程");
                //初始化多線程標識
                countDownLatch = new CountDownLatch(THREAD_NUM);

                progressChangeHandler.sendEmptyMessage(STATE_START_THREADS);
            }
        });
        startDownloadThread.start();
    }

    private void stopDownload(){
        stopDownloadThread = new Thread(new Runnable() {
            @Override
            public void run() {
                if (deleteDownloadThread!=null && deleteDownloadThread.isAlive()){
                    Looper.prepare();
                    T.getInstance().showShort("文件正在刪除");
                    Looper.loop();
                    return;
                }

                stopStartThread(STATE_STOPING);
                stopDownloadThread(STATE_STOPING);

                //確保開始線程和下載線程都已經執行完成之后才能將狀態修改為停止成功
                progressChangeHandler.sendEmptyMessage(STATE_STOPED);
            }
        });
        stopDownloadThread.start();
    }

    private void deleteDownload(){
        deleteDownloadThread = new Thread(new Runnable() {
            @Override
            public void run() {
                if (stopDownloadThread!=null && stopDownloadThread.isAlive()){
                    Looper.prepare();
                    T.getInstance().showShort("正在停止,稍后...");
                    Looper.loop();
                    Looper.myLooper().quit();
                    return;
                }

                stopStartThread(STATE_DELETING);
                stopDownloadThread(STATE_DELETING);

                //確保開始線程,停止線程和下載線程都已經執行完成之后才能開始刪除下載的相關信息
                helper.deleteInfos(url);
                File file = new File(path);
                if (file.exists()){
                    file.delete();
                }else{
                    L.w("FileDownloadManager deleteDownload file not exist");
                }
                progressChangeHandler.sendEmptyMessage(STATE_DELETE);
            }
        });
        deleteDownloadThread.start();
    }

    /**
     * 停止開始線程
     */
    private void stopStartThread(int sendState){
        boolean state;
        do {
            //如果開始線程還未停止,比如用戶摁完開始下載之后快速摁停止下載或刪除,
            // 這時需要更新狀態,直到開始線程完成
            state = (startDownloadThread!=null&&startDownloadThread.isAlive());
            L.i("開始線程還未結束");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            progressChangeHandler.sendEmptyMessage(sendState);
        }while (state);
    }

    /**
     * 停止下載線程
     */
    private void stopDownloadThread(int sendState){
        boolean state;

        //檢測下載線程,確保下載線程要全部執行完成
        state = threads.size()>0;
        while(state){
            state = countDownLatch.getCount()>0;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //還有線程在執行,所以狀態還需要相應變更
            progressChangeHandler.sendEmptyMessage(sendState);
        }
    }

    /**
     * 第一次下載文件,無下載記錄,重新創建
     */
    private void createDownloadInfos(){
        try {
            //更新狀態為正在獲取文件大小
            progressChangeHandler.sendEmptyMessage(STATE_GETSIZE);
            URL url = new URL(FileDownloadManager.this.url);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setConnectTimeout(5 * 1000);
            conn.setRequestMethod("GET");
            //添加這句話,要不然disconnect()會花費很多時間
            conn.setRequestMethod("HEAD");
            conn.setAllowUserInteraction(true);
            conn.connect();
            if (conn.getResponseCode()==200) {
                fileSize = conn.getContentLength();
                File file = FileUtils.checkAndCreateFile(path);
                // 本地訪問文件
                RandomAccessFile accessFile = new RandomAccessFile(file, "rwd");
                accessFile.setLength(fileSize);
                accessFile.close();
            }
            conn.disconnect();
        } catch (Exception e) {
            e.printStackTrace();
            L.e("獲取文件長度發生錯誤", e);
            return;
        }

        //開始計算每個線程下載的字節范圍
        long startPos = 0;
        //最后一個線程所下載的字節數一定要小于等于前面的線程保證文件完整性
        long perSize = (long) Math.ceil((fileSize*1.0) / (THREAD_NUM*1.0));
        for (int i=0; i<THREAD_NUM; i++){
            DownloadInfo info = new DownloadInfo();
            info.id = i;
            info.startPos = startPos;
            startPos += perSize;
            startPos -= 1;
            if (startPos >= fileSize)
                startPos = fileSize-1;
            info.endPos = startPos;
            info.completeSize = 0;
            //下一個任務的開始位置要+1
            startPos ++;
            infos.add(info);
        }
        helper.insertInfos(url, infos);
    }

    /**
     * 恢復以前的下載信息
     */
    private void revertDownloadInfos(ArrayList<HashMap<String, String>> maps){
        ArrayList<String> columns = DownloadDB.TABLES.DOWNLOAD.getTableColumns();
        for (HashMap<String, String> map : maps){
            DownloadInfo info = new DownloadInfo();
            info.id = Integer.parseInt(map.get(columns.get(0)));
            info.startPos = Long.parseLong(map.get(columns.get(2)));
            info.endPos = Long.parseLong(map.get(columns.get(3)));
            info.completeSize = Long.parseLong(map.get(columns.get(4)));
            completeSize += info.completeSize;
            fileSize = fileSize>info.endPos ? fileSize:info.endPos;
            infos.add(info);
        }
    }

    /**
     * 獲取該文件的總下載字節數
     */
    private long getCompleteSize(){
        completeSize = 0;
        for (DownloadThread thread : threads)
            completeSize += thread.getCompleteSize();
        return completeSize;
    }

    /**
     * 檢測該文件是否下載完成
     */
    private boolean isFileDownloadFinish(ArrayList<HashMap<String, String>> maps){
        boolean result = true;
        if (maps==null || maps.size() == 0){
            return false;
        }
        ArrayList<String> columns = DownloadDB.TABLES.DOWNLOAD.getTableColumns();
        for (HashMap<String, String> map : maps){
            //如果完成字節數不足end-start,代表該線程未完成,所以需要繼續下載
            if (Long.parseLong(map.get(columns.get(4))) <
                    (Long.parseLong(map.get(columns.get(3)))- Long.parseLong(map.get(columns.get(2))))){
                result = false;
                break;
            }
        }
        return result;
    }

    public void setListener(IDownloadProgressChangedListener listener){
        this.listener = listener;
    }

    /**
     * 下載進度更新接口
     */
    public interface IDownloadProgressChangedListener{
        void onProgressChanged(long completeSize, long totalSize);
        void onStateChanged(int state);
    }

    /**
     * 開啟更新界面線程和開啟下載線程
     */
    private void startThreads(){
        //準備開啟線程
        updateThread = new UpdateThread();
        updateThread.start();
        for (Thread thread : threads)
            thread.start();
    }

    /**
     * 下載完成之后的處理工作
     */
    private void finishDownload(){
        downloadState = false;
        isDownloadFinish = true;
        progressChangeHandler.sendEmptyMessage(STATE_FINISH);
        if (updateThread!=null && updateThread.isAlive())
            updateThread.canRun = false;

        File file = new File(path);
        File newFile = new File(fileName);
        if (file.exists()){
            file.renameTo(newFile);
        }
        helper.deleteInfos(url);
    }

    private static class ProgressChangeHandler extends Handler{
        private WeakReference<FileDownloadManager> activityWeakReference;

        public ProgressChangeHandler(FileDownloadManager manager){
            activityWeakReference = new WeakReference<>(manager);
        }

        @Override
        public void handleMessage(Message msg) {
            if (msg.what == STATE_START_THREADS){
                L.i("開啟線程");
                activityWeakReference.get().currentState = STATE_START_THREADS;
                activityWeakReference.get().startThreads();
            }
            //下載進度更新
            else if (msg.what == STATE_UPDATE_PROGRESS) {
                if (activityWeakReference.get().getCompleteSize() >= activityWeakReference.get().fileSize) {
                    activityWeakReference.get().finishDownload();
                    T.getInstance().showShort("下載完成");
                }
                if (activityWeakReference.get().listener != null)
                    activityWeakReference.get().listener.onProgressChanged
                            (activityWeakReference.get().getCompleteSize(), activityWeakReference.get().fileSize);
            }
            //下載狀態更新
            else {
                L.i("state"+msg.what);
                if (activityWeakReference.get().currentState != msg.what){
                    //如果在下載過程中網絡發生錯誤,或者服務器發生錯誤,就不再更新后面的停止狀態
                    if ((activityWeakReference.get().currentState == STATE_NET_ERROR
                            || activityWeakReference.get().currentState == STATE_SERVER_ERROR)
                            && (msg.what==STATE_STOPING || msg.what==STATE_STOPED)){
                        return;
                    }
                    activityWeakReference.get().currentState = msg.what;
                    if (activityWeakReference.get().listener != null){
                        activityWeakReference.get().listener.onStateChanged(activityWeakReference.get().currentState);
                    }
                }
            }
        }
    }

    /**
     * 單獨一個線程下載的信息
     */
    public class DownloadInfo {
        public int id;
        public long startPos;
        public long endPos;
        public volatile long completeSize;
    }

    /**
     * 下載線程
     */
    private class DownloadThread extends Thread{
        private DownloadInfo info;
        HttpURLConnection connection = null;
        RandomAccessFile randomAccessFile = null;
        InputStream is = null;

        public DownloadThread(DownloadInfo info){
            this.info = info;
        }

        public long getCompleteSize(){
            return info.completeSize;
        }

        public void initConnection() throws Exception{
            URL url = new URL(FileDownloadManager.this.url);
            connection = (HttpURLConnection) url.openConnection();
            connection.setConnectTimeout(2000);
            connection.setReadTimeout(5000);
            connection.setRequestMethod("GET");
            connection.setAllowUserInteraction(true);

            // 設置范圍,格式為Range:bytes x-y;
            connection.setRequestProperty("Range", "bytes=" + (info.startPos + info.completeSize) + "-" + info.endPos);
        }

        @Override
        public void run() {
            try {
                initConnection();
                randomAccessFile = new RandomAccessFile(path, "rwd");
                // 將要下載的字節寫到上次寫的末尾
                randomAccessFile.seek(info.startPos + info.completeSize);
                //偶爾發現,下載的資源有時候403 FileNotFoundException,這種情況重新連接一次,不成功就Log出錯誤日志
                try {
                    is = connection.getInputStream();
                }catch (Exception e){
                    //重新連接
                    initConnection();
                    try {
                        is = connection.getInputStream();
                    }catch (Exception ee){
                        L.e("無法連接"+info.startPos+"~"+info.endPos+"處資源", ee);
                        return;
                    }
                }
                byte[] buffer = new byte[1024 * 8];
                int length;
                while (((length = is.read(buffer)) != -1) && downloadState) {
                    randomAccessFile.write(buffer, 0, length);
                    info.completeSize += length;
                }
                L.i("結束下載線程");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                countDownLatch.countDown();
                try {
                    //android 4.x disconnect或者close會耗費很長的時間,解決了很長時間,暫未找到方法,有的聯系我
                    is.close();
                    randomAccessFile.close();
                    connection.disconnect();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 更新數據庫和界面線程
     */
    private class UpdateThread extends Thread{

        public boolean canRun = true;
        private long lastCompleteSize = 0;
        /** 重試時間3s */
        private final int RETRYCOUNT = 3;
        private int retry;

        @Override
        public void run() {
            retry = RETRYCOUNT;
            try {
                while (canRun) {
                    // 更新數據庫中的下載信息
                    helper.updateInfos(url, infos);

                    L.i("lastCompleteSize  " + lastCompleteSize);
                    L.i("更新界面  " + getCompleteSize() + "   fileSize" + fileSize);
                    //由于網絡不穩定,或者是服務器不穩定導致的無法返回數據
                    if (lastCompleteSize == getCompleteSize()){
                        retry--;
                        if (retry <= 0){
                            //停止下載線程,并提示用戶重試
                            FileDownloadManager.this.stop();
                            //有網絡的情況下為服務器錯誤
                            if (CommonUtils.isNetworkAvailable()){
                                L.e("服務器錯誤,請重試");
                                progressChangeHandler.sendEmptyMessage(STATE_SERVER_ERROR);
                            }else{
                                L.e("網絡錯誤,請連接網絡后重試");
                                progressChangeHandler.sendEmptyMessage(STATE_NET_ERROR);
                            }
                        }
                    }else{
                        retry = RETRYCOUNT;
                    }

                    lastCompleteSize = getCompleteSize();
                    //更新界面
                    progressChangeHandler.sendEmptyMessage(STATE_UPDATE_PROGRESS);
                    //每隔1秒操作數據庫和更新界面,防止頻繁的更新
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

github地址,隨時更新,隨時優化

來自: http://blog.csdn.net/self_study/article/details/50505865

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!