1. 程式人生 > >Android 多執行緒下載檔案原理霸氣解析介紹 -----檔案的下載(3)

Android 多執行緒下載檔案原理霸氣解析介紹 -----檔案的下載(3)

1、首先我們先建立好下載的位置–根據url建立檔案。

  /**
 * <p>Title: FlieStorageManager</p >
 * <p>Description: TODO</p >
 * <p>Company: ihaveu</p >
 *
 * @author MaWei
 * @date 2018/2/5
 */
public class FlieStorageManager {

    public static FlieStorageManager sManager = new FlieStorageManager();

    private
Context mContext; public static FlieStorageManager getInstance(){ return sManager; } public void init(Context mContext){ this.mContext = mContext; } public FlieStorageManager() { } /** * 根據url 設定檔名返回檔案 * @param url * @return */ public
File getFileByName(String url){ File parent; // 判斷手機有沒有掛載SD卡 if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { // 通過Context.getExternalCacheDir()方法可以獲取到 SDCard/Android/data/你的應用包名/cache/目錄,一般存放臨時快取資料 parent = mContext.getExternalCacheDir(); }else
{ // 沒有SD卡儲存在系統目錄的cache下 parent = mContext.getCacheDir(); } String fileName = Md5Uills.generateCode(url); // 路徑、 檔名 File file = new File(parent, fileName); if(!file.exists()) { try { file.createNewFile(); } catch (IOException e) { e.printStackTrace(); } } return file; } }

我們的路徑很明顯放在的cach目錄下。

那麼我們簡單封裝一下okhttp聯網的框架的幾個請求。
然後給大家看一下簡單的單執行緒下載是怎麼實現的

/**
 * <p>Title: HttpManager</p >
 * <p>Description: 聯網請求工具類</p >
 * <p>Company: ihaveu</p >
 *
 * @author MaWei
 * @date 2018/2/5
 */
public class HttpManager {

    public static HttpManager mManager = new HttpManager();
    /** 請求失敗*/
    public static final int NETWORK_ERROR_CODE = 1;
    /** 獲取不到檔案的總長度*/
    public static final int CONTENT_LENGTH_ERROR_CODE = 2;
    /** 佇列中存在下載的任務*/
    public static final int TASK_RUNNING_ERROR_CODE = 3;
    /** okhttp請求類*/
    private OkHttpClient mClient;
    private Context mContext;

    private void init(Context mContext){
        this.mContext = mContext;
    }

    public HttpManager() {
        this.mClient = new OkHttpClient();
    }

    public static HttpManager getInstance(){
        return mManager;
    }

    /**
     * 同步請求
     *
     * @param url
     * @return
     */
    public Response syncRequest(String url) {
        Request request = new Request.Builder().url(url).build();
        try {
            return mClient.newCall(request).execute();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 同步請求(根據指定位置下載)
     * 根據請求頭 Range欄位 指定下載的位置
     */
    public Response syncRequestByRange(String url, long start, long end) {
        Request request = new Request.Builder().url(url)
                .addHeader("Range", "bytes=" + start + "-" + end)
                .build();
        try {
            return mClient.newCall(request).execute();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }


    /**
     * 非同步呼叫
     * @param url
     * @param callback
     */
    public void asyncRequest(final String url, Callback callback) {
        Request request = new Request.Builder().url(url).build();
        mClient.newCall(request).enqueue(callback);
    }


    /**
     * 非同步請求(單執行緒下載)
     */
    public void asyncSingleThreadRequest(final String url, final DownLoadCallBack callback) {
        Request request = new Request.Builder().url(url).build();
        // okhttp的非同步請求
        mClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {

                if (!response.isSuccessful() && callback != null) {
                    callback.fail(NETWORK_ERROR_CODE, "請求失敗");
                }

                // 根據url建立一個檔案
                File file = FlieStorageManager.getInstance().getFileByName(url);

                // 建立位元組陣列,以這個位元組陣列 讀寫
                byte[] buffer = new byte[1024 * 500];
                int len;
                FileOutputStream fileOut = new FileOutputStream(file);
                // 返回的response轉成資料流
                InputStream inStream = response.body().byteStream();
                // 讀取返回資料流 寫到建立的檔案中
                while ((len = inStream.read(buffer, 0, buffer.length)) != -1) {
                    fileOut.write(buffer, 0, len);
                    fileOut.flush();
                }
                // 寫入完成
                callback.success(file);

            }
        });
    }
}

然後看一下我們的回撥介面:

/**
 * <p>Title: DownLoadCallBack</p >
 * <p>Description: TODO</p >
 * <p>Company: ihaveu</p >
 *
 * @author MaWei
 * @date 2018/2/5
 */
public interface DownLoadCallBack {

    void success(File file);

    void fail(int errorCode, String errorMessage);

    void progress(int progress);
}

可以看出HttpManager 是我簡單封好一個okhttp工具類。 這個工具類有同步請求的回撥,非同步的回撥。

我還給大家簡單謝了一個單執行緒下載的例子。就是,大家直接呼叫asyncSingleThreadRequest()這個方法,就是單執行緒的方法。裡面的註釋我也都寫好了,很簡單相信看一眼就明白了吧。

單執行緒下載的業務流程: 根據url建立好一個檔案,然後在非同步請求回撥中通過響應的**response.body().byteSream(),轉換的響應資料的流,然後通過流的形式寫在我們檔案中。

OK,我們繼續下面操作。

我們接著建立一個多執行緒下載的管理類:

/**
 * <p>Title: DownloadManager</p >
 * <p>Description: 下載、執行緒管理類</p >
 * <p>Company: ihaveu</p >
 *
 * @author MaWei
 * @date 2018/2/6
 */
public class DownloadManager {

    private static DownloadManager mManager = new DownloadManager();
    /** 核心和最大執行緒數量*/
    private final static int MAX_THREAD = 2;

    /**
     * 建立執行緒池
     * 引數1:核心執行緒數量
     * 引數2:執行緒池最大數量
     * 引數3:執行緒存活時間
     * 引數4:設定時間等級
     * 引數5:先不管
     * 原理:執行緒池首先會建立核心執行緒, 如果在執行時,超過最大數量會丟擲異常
     */
    private static final ThreadPoolExecutor sThreadPool = new ThreadPoolExecutor(MAX_THREAD, MAX_THREAD, 60,
            TimeUnit.MICROSECONDS, new LinkedBlockingDeque<Runnable>(), new ThreadFactory() {

        private AtomicInteger mInteger = new AtomicInteger(1);
        @Override
        public Thread newThread(Runnable runnable) {
            // 指定Runnable  和執行緒名稱
            Thread mThread = new Thread(runnable, "download thread #" + mInteger.getAndIncrement());
            return mThread;
        }
    });

    public static DownloadManager getInstance(){
        return mManager;
    }

    public DownloadManager() {
    }

    /**
     * 判斷每個執行緒下載多長的資料,並多執行緒下載
     */
    public void downLoad(final String url, final DownLoadCallBack callBack){
        HttpManager.getInstance().asyncRequest(url, new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                if(response == null && callBack != null) {
                    callBack.fail(HttpManager.NETWORK_ERROR_CODE, "網路出問題了");
                    return;
                }

                long length = response.body().contentLength();
                if(length == -1) {
                    // 獲取不到檔案的總長度
                    callBack.fail(HttpManager.CONTENT_LENGTH_ERROR_CODE, "contenLength -1");
                    return;
                }

                processDownload(url, length, callBack);
            }
        });
    }

    /**
     * 下載
     * @param length     下載檔案的長度
     */
    private void processDownload(String url, long length, DownLoadCallBack callBack) {
        // 計算每一個執行緒下載的大小
        long threadDownloadSize = length / MAX_THREAD;
        // 分配每一個執行緒下載
        for(int i = 0; i < MAX_THREAD; i++) {
            // 計算每一個執行緒從多少下載 比如長度100  2個執行緒  0-49  50-99, 下面是計算的演算法
            long startSize = i * threadDownloadSize;
            long endSize = (i + 1) * threadDownloadSize - 1;

            // 執行下載
            sThreadPool.execute(new DownloadRunnable(startSize, endSize, url, callBack));
        }
    }
}

總結:首先建立一個核心執行緒執行緒、最大執行緒數量為2 的執行緒池來管理執行緒。
然後我們發起非同步請求,先訪問下載的檔案,得到檔案的總長度後,然後根據執行緒的數量平分每個執行緒下載的長度, 然後通過執行緒池下載檔案。

OK,接下來看一下核心的執行緒類是怎麼執行的。

/**
 * <p>Title: DownloadRunnable</p >
 * <p>Description: 下載執行的執行緒</p >
 * <p>Company: ihaveu</p >
 *
 * @author MaWei
 * @date 2018/2/6
 */
public class DownloadRunnable  implements Runnable{

    /** 指定下載開始位置*/
    private long mStart;
    /** 指定下載結束位置*/
    private long mEnd;
    /** 請求url*/
    private String mUrl;
    /** 結果回撥*/
    private DownLoadCallBack mCallBack;

    public DownloadRunnable(long mStart, long mEnd, String mUrl, DownLoadCallBack mCallBack) {
        this.mStart = mStart;
        this.mEnd = mEnd;
        this.mUrl = mUrl;
        this.mCallBack = mCallBack;
    }

    @Override
    public void run() {
        // 下載完成後返回的結果 response
        Response response = HttpManager.getInstance().syncRequestByRange(mUrl, mStart, mEnd);

        if(response == null && mCallBack != null) {
            mCallBack.fail(HttpManager.NETWORK_ERROR_CODE, "網路出問題了");
            return;
        }

        // 獲取本地下載儲存的檔案
        File file = FlieStorageManager.getInstance().getFileByName(mUrl);

        // 多個執行緒對檔案指定的位置寫入資料(因為是多執行緒下載,多個執行緒肯定會對一個檔案可讀 可寫 可修改)
        try {
            // 引數1:指定操作的檔案  引數2:可讀 可寫 可修改
            RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rwd");
            // 指定一個偏移,下載的起始位置
            randomAccessFile.seek(mStart);
            byte[] buffer = new byte[1024];
            int len = 0;
            InputStream inputStream = response.body().byteStream();
            // 讀取返回來的資料, 寫入本地檔案中
            while((len = inputStream.read(buffer, 0, buffer.length)) != -1) {
                randomAccessFile.write(buffer, 0, len);
            }

            // 下載成功
            mCallBack.success(file);

        } catch (FileNotFoundException e) {

            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

這就是通過執行緒池執行的Runnable,看執行緒run方法中,是通過url建立一個檔案,然後通過這個檔案轉換成RandomAccessFile檔案, 目的是 “多個執行緒對檔案指定的位置寫入資料(因為是多執行緒下載,多個執行緒肯定會對一個檔案可讀 可寫 可修改)” 然後這個檔案通過seek方法,指定檔案從什麼位置開始下載。那麼不同執行緒下載不同得位置就OK了。 但是需要提醒的是,多執行緒同時下載是同步操作。

OK那麼多執行緒下載就完成了, 接下來我們只需要在你需要的地方直接呼叫就可以實現多執行緒下載了。
例子(多執行緒下載,下載完成顯示在ImageView上):

   private void multipleDownFileImage() {
        DownloadManager.getInstance().downLoad("http://szimg.mukewang.com/5763765d0001352105400300-360-202.jpg", new DownLoadCallBack() {
            @Override
            public void success(final File file) {
                Log.e("file", "file success: " + file.getAbsolutePath());
                Log.e("file", "file : " + file.length());

                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
                        image.setImageBitmap(bitmap);
                    }
                });
            }

            @Override
            public void fail(int errorCode, String errorMessage) {

            }

            @Override
            public void progress(int progress) {

            }
        });
    }

有需要直接copy程式碼就可以用,接下來我會給大家介紹一些優化的地方,和用資料庫儲存下載的資料,然後繼續下載。 敬請期待!