Android多线程断点下载之部署到Android App

先看一组效果图:




Android多线程断点下载之部署到Android App_第1张图片



Android多线程断点下载之部署到Android App_第2张图片


Android多线程断点下载之多线程下载原理

Android多线程断点下载之断点下载原理

工程目录结构如下:

比较简单,一个MainActivity,一个下载工具类,2个布局文件

Android多线程断点下载之部署到Android App_第3张图片

activity_main.xml:

<span style="font-family:Courier New;font-size:14px;"><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=".MainActivity">

    <TextView
        android:textSize="18sp"
        android:text="请输入下载文件的路径"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <!--输入url路径-->
    <EditText
        android:id="@+id/edt_url"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="http://" />

    <!--输入下载的线程数量-->
    <TextView
        android:textSize="18sp"
        android:text="请设置线程的数量"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <EditText
        android:id="@+id/edt_count"
        android:inputType="number"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="3" />

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="下载"
        android:id="@+id/btn_download" />
    <!--动态添加线程下载进度条的父控件-->
    <LinearLayout
        android:orientation="vertical"
        android:id="@+id/ll_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    </LinearLayout>
</LinearLayout>
</span>

进度条布局pb_layout.xml:

<span style="font-family:Courier New;font-size:14px;"><?xml version="1.0" encoding="utf-8"?>
<ProgressBar xmlns:android="http://schemas.android.com/apk/res/android"
    style="?android:attr/progressBarStyleHorizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

</ProgressBar>
</span>

MultiThreadDownloadUtil.java工具类:

<span style="font-family:Courier New;font-size:14px;">package com.example.chenys.mutilethreaddownload;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;

/**
 * 多线程断点下载工具类
 *
 * @author Chenys
 */
public class MultiThreadDownloadUtil {
    /**
     * 线程的数量,默认为开启3个线程
     */
    private static int threadCount = 3;
    /**
     * 每个下载区块的大小
     */
    private static long blocksize;
    /**
     * 当前正在工作的线程个数
     */
    private static int runningThreadCount;
    /**
     * 线程下载缓冲区,默认为10M
     */
    private static int defaultBuff = 1024 * 1024 * 10;

    /**
     * 文件保存的文件夹
     */
    private static String saveFolder = "download";
    private static Context mContext;

    /**
     * 断点下载文件的方法
     *
     * @param path
     */
    public static void downloadFile(Context context, String path, DownloadResponseHandler downloadResponseHandler) {
        HttpURLConnection conn = null;
        try {
            mContext = context;
            if (!path.startsWith("http://")) {
                path = "http://" + path;
            }
            URL url = new URL(path);
            conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(3000);
            conn.setReadTimeout(1000);
            int code = conn.getResponseCode();
            if (code == 200) {
                // 得到服务器端返回的文件大小,单位byte,字节
                long size = conn.getContentLength();
                Log.d("chenys", "服务器文件的大小:" + size);
                // 计算每个blocksize的大小和记录当前线程的总数
                blocksize = size / threadCount;
                runningThreadCount = threadCount;
                /**
                 * 1.首先在本地创建一个文件大小跟服务器一模一样的空白文件,RandomAccessFile类的实例支持对随机访问文件的读取和写入
                 * 参数1:目标文件 参数2:打开该文件的访问模式,"r" 以只读方式打开 ,"rw" 打开以便读取和写入
                 */
                File file = new File(getFileSavePath(), getFileName(path));
                RandomAccessFile raf = new RandomAccessFile(file, "rw");
                raf.setLength(size);// 设置文件的大小
                // 2.开启若干个子线程,分别下载对应的资源
                for (int i = 1; i <= threadCount; i++) {
                    long startIndex = (i - 1) * blocksize; // 由于服务端下载文件是从0开始的
                    long endIndex = i * blocksize - 1;
                    if (i == threadCount) {
                        // 最后一个线程
                        endIndex = size - 1;
                    }
                    Log.d("chenys", "开启线程:" + i + "下载的位置" + startIndex + "~" + endIndex);

                    //开启线程
                    new DownloadThread(path, i, startIndex, endIndex, downloadResponseHandler).start();
                }
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
            downloadResponseHandler.onFailed("下载失败,请确认你的URL下载地址是否可用");
        } catch (ProtocolException e) {
            e.printStackTrace();
            downloadResponseHandler.onFailed("下载失败");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            downloadResponseHandler.onFailed("下载失败,请检查你的sd卡是否存在");
        } catch (IOException e) {
            e.printStackTrace();
            downloadResponseHandler.onFailed("下载失败,请检查你的网络是否存在,或者sd卡空间不足");
        } finally {
            if (null != conn) {
                conn.disconnect();
                conn = null;
                System.gc();
            }
        }

    }

    /**
     * 自定义下载线程
     *
     * @author Chenys
     */
    private static class DownloadThread extends Thread {
        private int threadId; // 线程id
        private long startIndex; // 开始下载的位置
        private long endIndex; // 结束下载的位置
        private String path;
        private DownloadResponseHandler downloadResponseHandler;

        public DownloadThread(String path, int threadId, long startIndex, long endIndex, DownloadResponseHandler downloadResponseHandler) {
            this.path = path;
            this.threadId = threadId;
            this.startIndex = startIndex;
            this.endIndex = endIndex;
            this.downloadResponseHandler = downloadResponseHandler;
        }

        /**
         * 执行下载任务
         */
        public void run() {
            HttpURLConnection conn = null;
            //计算每个线程的下载总长度
            long totalSize = endIndex - startIndex;
            try {
                //创建记录资源当前下载到什么位置的临时文件
                File tempFile = new File(getFileSavePath(), threadId + ".temp");
                //记录当前线程下载的大小
                int currentSize = 0;
                URL url = new URL(path);
                conn = (HttpURLConnection) url.openConnection();
                conn.setRequestMethod("GET");
                //接着从上一次断点的位置继续下载数据
                if (tempFile.exists() && tempFile.length() > 0) {
                    FileInputStream lastDownload = new FileInputStream(tempFile);
                    BufferedReader br = new BufferedReader(new InputStreamReader(lastDownload));
                    //获取当前线程上次下载的总大小是多少
                    String lastSizeStr = br.readLine();
                    int lastSize = Integer.parseInt(lastSizeStr);
                    Log.d("chenys", "上次线程" + threadId + "下载的总大小:" + lastSize);
                    //更新startIndex的位置和每条线程下载的当前大小数
                    startIndex += lastSize;
                    currentSize += lastSize;
                    lastDownload.close();
                }

                //设置http协议请求头: 指定每条线程从文件的什么位置开始下载,下载到什么位置为止
                conn.setRequestProperty("Range", "bytes=" + startIndex + "-" + endIndex);
                conn.setConnectTimeout(5000);
                int code = conn.getResponseCode();
                Log.d("chenys", "服务器返回码code=" + code);// 如果是下载一部分资源,那么返回码是206
                //获取服务器返回的流
                InputStream is = conn.getInputStream();
                File file = new File(getFileSavePath(), getFileName(path));
                RandomAccessFile raf = new RandomAccessFile(file, "rw");
                //指定文件的下载起始位置
                raf.seek(startIndex);
                Log.d("chenys", "第" + threadId + "个线程写文件的开始位置" + String.valueOf(startIndex));
                //开始下载文件
                int len = 0;
                byte[] buff = new byte[defaultBuff]; //10M的缓冲数组,提高下载速度
                while ((len = is.read(buff)) != -1) {
                    RandomAccessFile rf = new RandomAccessFile(tempFile, "rwd");//注意:这里采用的模式是"rwd",即无缓存模式的读写
                    //写数据到目标文件
                    raf.write(buff, 0, len);
                    //记录当前下载记录的位置到文件中
                    currentSize += len;
                    rf.write(String.valueOf(currentSize).getBytes());
                    rf.close();
                    //回调显示下载的进度
                    downloadResponseHandler.onProgress(threadId - 1, totalSize, currentSize);
                }
                is.close();
                raf.close();

            } catch (MalformedURLException e) {
                e.printStackTrace();
                downloadResponseHandler.onFailed("下载失败,请确认你的URL下载地址是否可用");
            } catch (ProtocolException e) {
                e.printStackTrace();
                downloadResponseHandler.onFailed("下载失败");
            } catch (IOException e) {
                e.printStackTrace();
                downloadResponseHandler.onFailed("下载失败,请检查你的网络是否可用");
            } finally {
                //以下是处理清理缓存的任务
                synchronized (MultiThreadDownloadUtil.class) {
                    Log.d("chenys", "线程" + threadId + "下载完毕了");
                    runningThreadCount--;
                    if (runningThreadCount < 1) {
                        Log.d("chenys", "所有的线程都工作完毕了,删除临时文件");
                        for (int i = 1; i <= threadCount; i++) {
                            File tempFile = new File(getFileSavePath(), i + ".temp");
                            Log.d("chenys", "删除临时文件成功与否" + tempFile.delete());
                            downloadResponseHandler.onSuccess("下载完成");
                            //下载完毕后,恢复线程数量为允许设置的状态
                            SharedPreferences sp = mContext.getSharedPreferences("threadCount_sp_name", Context.MODE_PRIVATE);
                            sp.edit().putBoolean("is_count_setted", false).commit();
                        }
                        if (null != conn) {
                            conn.disconnect();
                            conn = null;
                            System.gc();
                        }
                    }
                }
            }
        }
    }

    /**
     * 设置总线程个数的方法,如果第一次已经设置过了,则不能则设置,只有等当前的任务下载完毕后才能设置
     *
     * @param threadCount
     */
    public static void setThreadCount(Context context,int threadCount) {
        SharedPreferences sp = context.getSharedPreferences("threadCount_sp_name", Context.MODE_PRIVATE);
        boolean setted = sp.getBoolean("isCountSetted", false);
        Log.d("chenys", "是否已经设置过下载线程数" + setted);
        if (threadCount > 0 && !setted) {
            MultiThreadDownloadUtil.threadCount = threadCount;
            SharedPreferences.Editor edit = sp.edit();
            edit.putInt("thread_count", threadCount);
            edit.putBoolean("is_count_setted", true).commit();
        } else {
            int lastCountSet = sp.getInt("thread_count", 3);
            Toast.makeText(context, "你上次设置的下载线程数是:" + lastCountSet + "当前任务现在完毕后可重新设置线程数", Toast.LENGTH_LONG).show();
            MultiThreadDownloadUtil.threadCount = lastCountSet;
        }
    }

    /**
     * 默认的线程个数
     * @return
     */
    public static int getDefaultThreadCount() {
        return 3;
    }

    /**
     * 设置保存下载文件的文件夹
     *
     * @param folderName
     */
    public static void setDownloadDir(String folderName) {
        if (!TextUtils.isEmpty(folderName)) {
            MultiThreadDownloadUtil.saveFolder = folderName;
        }
    }

    /**
     * 设置缓存区大小
     */
    public static void setCacheSize(int defaultBuff) {
        if (defaultBuff < 0) {
            MultiThreadDownloadUtil.defaultBuff = defaultBuff;
        }
    }

    /**
     * 获取文件的保存路径
     *
     * @return
     */
    private static String getFileSavePath() {
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            String path = Environment.getExternalStorageDirectory() + File.separator +
                    mContext.getPackageName() + File.separator + saveFolder;
            File fileDir = new File(path);
            if (!fileDir.exists()) {
                fileDir.mkdirs();
            }
            return path;
        }
        return null;
    }

    /**
     * 获取文件名
     */
    private static String getFileName(String path) {
        int index = path.lastIndexOf("/");
        String fileName = path.substring(index + 1);
        return fileName;
    }

    /**
     * 下载成功或失败的回调接口
     */
    interface DownloadResponseHandler {
        //下载成功时回调
        void onSuccess(String resultCode);
        //下载失败时回调
        void onFailed(String result);
        //下载过程中回调,用于显示下载的进度
        void onProgress(int threadId, long totalBytes, long currentBytes);
    }
}
</span>

MainActivity.java:

<span style="font-family:Courier New;font-size:14px;">package com.example.chenys.mutilethreaddownload;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.List;


/**
 * 多线程断点下载
 */
public class MainActivity extends Activity {

    private EditText mUrlEdt;   //url地址输入框
    private EditText mCountEdt; //线程个数输入框
    private Button mDowloadBtn; //下载按钮
    private LinearLayout mLlContainer; //存放进度条的父控件
    private List<ProgressBar> mPbList = new ArrayList<>();//存放进度条的集合

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mUrlEdt = (EditText) findViewById(R.id.edt_url);
        mCountEdt = (EditText) findViewById(R.id.edt_count);
        mDowloadBtn = (Button) findViewById(R.id.btn_download);
        mLlContainer = (LinearLayout) findViewById(R.id.ll_container);
        mDowloadBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                mDowloadBtn.setEnabled(false);//禁用下载按钮
                downLoad();
            }
        });
    }

    /**
     * 消息处理Handler
     */
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            Toast.makeText(MainActivity.this, (String) msg.obj, Toast.LENGTH_LONG).show();
            mDowloadBtn.setEnabled(true);//恢复下载按钮
            super.handleMessage(msg);
        }
    };

    /**
     * 下载按钮的点击后的处理逻辑
     */
    public void downLoad() {
        final String path = mUrlEdt.getText().toString().trim();
        final String count = mCountEdt.getText().toString().trim();
        if (TextUtils.isEmpty(path)) {
            Toast.makeText(this, "对比起,下载路径不能为空", Toast.LENGTH_LONG).show();
            return;
        }
        if (!TextUtils.isEmpty(count)) {
            //设置下载的线程个数
            int threadCount = Integer.parseInt(count);
            Log.d("chenys", "线程个数" + threadCount);
            MultiThreadDownloadUtil.setThreadCount(MainActivity.this,threadCount);
            //清空旧的的进度条
            mLlContainer.removeAllViews();
            //根据count的个数添加进度条个数
            for (int i = 1; i <= threadCount; i++) {
                ProgressBar pb = (ProgressBar) View.inflate(MainActivity.this, R.layout.layout_pb, null);
                TextView textView = new TextView(MainActivity.this);
                textView.setText("线程" + i + "下载进度:");
                mLlContainer.addView(textView);
                mLlContainer.addView(pb);
                mPbList.add(pb);//将进度条添加到集合中

            }
        } else {
            Toast.makeText(MainActivity.this, "当前未选则线程个数,将开启默认线程进行下载", Toast.LENGTH_LONG).show();
            for (int i = 1; i <= MultiThreadDownloadUtil.getDefaultThreadCount(); i++) {
                ProgressBar pb = (ProgressBar) View.inflate(MainActivity.this, R.layout.layout_pb, null);
                mLlContainer.addView(pb);
                mPbList.add(pb);//将进度条添加到集合中
            }
        }

        Toast.makeText(MainActivity.this, "开始下载了", Toast.LENGTH_SHORT).show();
        //执行下载任务
        new Thread(new Runnable() {
            @Override
            public void run() {

                MultiThreadDownloadUtil.downloadFile(MainActivity.this, path, new MultiThreadDownloadUtil.DownloadResponseHandler() {
                    @Override
                    public void onSuccess(String result) {
                        Log.d("chenys", result);
                        Message msg = Message.obtain();
                        msg.obj = result;
                        handler.sendMessage(msg);
                    }

                    @Override
                    public void onFailed(String result) {
                        Log.d("chenys", result);
                        Message msg = Message.obtain();
                        msg.obj = result;
                        handler.sendMessage(msg);
                    }

                    @Override
                    public void onProgress(int threadId, long totalBytes, long currentBytes) {
                        mPbList.get(threadId).setMax((int) totalBytes);
                        mPbList.get(threadId).setProgress((int) currentBytes);
                        Log.d("chenys", "threadId=" + threadId + " totalBytes=" + totalBytes + " currentBytes=" + currentBytes);

                    }
                });
            }
        }).start();
    }

    @Override
    protected void onDestroy() {
        handler.removeCallbacksAndMessages(null);
        super.onDestroy();
    }
}
</span>

下载完毕后的结果:

Android多线程断点下载之部署到Android App_第4张图片

后台打印的log:

<span style="font-family:Courier New;font-size:14px;">05-16 14:16:58.506    3537-3537/? D/chenys﹕ 线程个数3
05-16 14:16:58.516    3537-3537/? D/chenys﹕ 是否已经设置过下载线程数false
05-16 14:16:58.876    3537-4028/? D/chenys﹕ 服务器文件的大小:23250432
05-16 14:16:59.846    3537-4028/? D/chenys﹕ 开启线程:1下载的位置0~7750143
05-16 14:16:59.856    3537-4028/? D/chenys﹕ 开启线程:2下载的位置7750144~15500287
05-16 14:16:59.876    3537-4028/? D/chenys﹕ 开启线程:3下载的位置15500288~23250431
05-16 14:17:00.196    3537-4035/? D/chenys﹕ 服务器返回码code=206
05-16 14:17:00.216    3537-4036/? D/chenys﹕ 服务器返回码code=206
05-16 14:17:00.246    3537-4036/? D/chenys﹕ 第2个线程写文件的开始位置7750144
05-16 14:17:00.246    3537-4035/? D/chenys﹕ 第1个线程写文件的开始位置0
05-16 14:17:00.336    3537-4039/? D/chenys﹕ 服务器返回码code=206
05-16 14:17:00.386    3537-4039/? D/chenys﹕ 第3个线程写文件的开始位置15500288
05-16 14:17:01.427    3537-4035/? D/chenys﹕ threadId=0 totalBytes=7750143 currentBytes=78545
05-16 14:17:01.446    3537-4039/? D/chenys﹕ threadId=2 totalBytes=7750143 currentBytes=78537
05-16 14:17:01.476    3537-4036/? D/chenys﹕ threadId=1 totalBytes=7750143 currentBytes=78538
05-16 14:17:01.546    3537-4039/? D/chenys﹕ threadId=2 totalBytes=7750143 currentBytes=125257
05-16 14:17:01.597    3537-4035/? D/chenys﹕ threadId=0 totalBytes=7750143 currentBytes=125265
05-16 14:17:01.597    3537-4035/? D/chenys﹕ threadId=0 totalBytes=7750143 currentBytes=160305
05-16 14:17:01.606    3537-4036/? D/chenys﹕ threadId=1 totalBytes=7750143 currentBytes=134018
05-16 14:17:01.626    3537-4035/? D/chenys﹕ threadId=0 totalBytes=7750143 currentBytes=169065
05-16 14:17:01.626    3537-4036/? D/chenys﹕ threadId=1 totalBytes=7750143 currentBytes=174898
..........
05-16 14:17:01.657    3537-4035/? D/chenys﹕ threadId=0 totalBytes=7750143 currentBytes=186585
05-16 14:17:01.676    3537-4036/? D/chenys﹕ threadId=1 totalBytes=7750143 currentBytes=192418
05-16 14:17:01.718    3537-4039/? D/chenys﹕ threadId=2 totalBytes=7750143 currentBytes=157377
05-16 14:17:01.757    3537-4036/? D/chenys﹕ threadId=1 totalBytes=7750143 currentBytes=209938
05-16 14:17:26.087    3537-4036/? D/chenys﹕ threadId=1 totalBytes=7750143 currentBytes=7734778
05-16 14:17:26.097    3537-4036/? D/chenys﹕ threadId=1 totalBytes=7750143 currentBytes=7749378
05-16 14:17:26.117    3537-4036/? D/chenys﹕ threadId=1 totalBytes=7750143 currentBytes=7750144
05-16 14:17:26.117    3537-4036/? D/chenys﹕ 线程2下载完毕了
05-16 14:17:26.127    3537-4036/? D/chenys﹕ 所有的线程都工作完毕了,删除临时文件
05-16 14:17:26.177    3537-4036/? D/chenys﹕ 删除临时文件成功与否true
05-16 14:17:26.187    3537-4036/? D/chenys﹕ 下载完成
05-16 14:17:26.326    3537-4036/? D/chenys﹕ 删除临时文件成功与否true
05-16 14:17:26.326    3537-4036/? D/chenys﹕ 下载完成
05-16 14:17:26.346    3537-4036/? D/chenys﹕ 删除临时文件成功与否true
05-16 14:17:26.397    3537-4036/? D/chenys﹕ 下载完成</span>



你可能感兴趣的:(Android多线程断点下载之部署到Android App)