Android 多线程断点下载

来源:互联网 发布:网络语开挂是什么意思 编辑:程序博客网 时间:2024/06/06 16:44

项目简介:

该项目演示啊Android中文件的多线程断点下载

详细介绍:

该应用涉及到的只是有:
1.HttpURLConnection的使用
2.多线程的使用
3.文件的断点下载
4.图像进度条的使用
5.文字进度条的使用

该应用下载界面,点击按钮后开始下载文件,并将下载进度显示在进度条上(包括文字进度和图像进度)

注意:

1.图像进度条,谷歌官方已经封装好了,不需要再UI线程中执行,它会自动从子线程传递数据到UI线程,所以,用户可以在子线程中直接刷新进度条
2.文字进度实际上是自己计算来一个百分比,然后显示在TextView控件上,涉及到刷新UI,所以用户要把数据传到UI线程再执行刷新操作
3.多线程下载中,需要用的随机文件存储RandomAccessFile,否则可能会发生覆盖现象,并且RandomAccessFile可以实现同步更新。所以在线程中尽量使用RandomAccessFile,而不是使用其他的流
4.多线程中,一定要注意线程的安全,注意使用同步语句块和静态变量的使用
5.synchronized的参数不能是空参,否则永远不会执行该语句块

步骤:

1.创建一个android项目,布局文件中仅有四个组件,如下图所示:

这里写图片描述

2.在MainActivity中添加一下代码:

public class MainActivity extends Activity {    private EditText ed_path;    // 图形进度条    private ProgressBar pb;    // 文字进度    private TextView tv;    // 定义变量,该变量表示总的线程数    private static int THREAD_COUNT = 5;    // 定义变量,白变量表示已经下载完成的线程数    private static int THREAD_COMPLETE = 0;    // 定义一个变量,该变量是用来充当锁的,是为了程序同步的时候所使用的,可以为任意字符串    private static String LOCK = "lock";    // 定义变量,该变量表示当前文件下载的长度    private int currentTotal = 0;    Handler handler = new Handler() {        public void handleMessage(Message msg) {            switch (msg.what) {            case 0:                Toast.makeText(getApplicationContext(), "出现未知错误", 0).show();                break;            case 1:                Toast.makeText(getApplicationContext(), "无法连接网络", 0).show();                break;            case 2:                Toast.makeText(getApplicationContext(), "SD卡容量不足或者无法读写", 0).show();                break;            case 3:                Toast.makeText(getApplicationContext(), "文件下载完毕", 0).show();                // 下载完毕,强制把文字进度设为100%                tv.setText("100%");                break;            case 4:                // 刷新文字进度条                // 计算过程中,可能会超出int的范围,最好强转成long                // 大文件下载,可能会使最后的文字进度为99%,所以下载完成后,手动更改                int max = pb.getMax();                int pro = pb.getProgress();                tv.setText((long) pro * 100 / max + "%");                break;            }        }    };    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        requestWindowFeature(Window.FEATURE_NO_TITLE);        setContentView(R.layout.activity_main);        pb = (ProgressBar) findViewById(R.id.pb);        ed_path = (EditText) findViewById(R.id.ed_path);        tv = (TextView) findViewById(R.id.tv);    }    // 点击按钮后,开始下载    public void download(View view) {        // 把进度条设置为空,文字进度设为0%        pb.setProgress(0);        tv.setText("0%");        final String path = ed_path.getText().toString();        // 开启一个子线程,获取所要下载的文件的基本信息以及设定每个线程的起始位置和终止位置        Thread thread = new Thread() {            @SuppressLint("NewApi")            @Override            public void run() {                try {                    URL url = new URL(path);                    // 获取HttpURLConnection的对象并设置属性                    HttpURLConnection conn = (HttpURLConnection) url.openConnection();                    conn.setConnectTimeout(5000);                    conn.setReadTimeout(5000);                    conn.setRequestMethod("POST");                    if (conn.getResponseCode() == 200) {                        // 获取需要下载的资源的总长度                        int length = conn.getContentLength();                        // 设置进度条最大长度                        pb.setMax(length);                        // 判断SD卡空间是否足够.由于可能不仅仅有文件的长度,还附带其他一些资源的额长度,所以SD容量要稍微比资源文件大                        boolean b = CheckSD.checkCapcity(length);                        if (b) {                            // 设置目标文件以及目标文件的大小,这里将目标文件放在SD卡中.所以,这里要用到随机文件流                            File targetFile = new File(Environment.getExternalStorageDirectory(), getFileName(path));                            RandomAccessFile raf = new RandomAccessFile(targetFile, "rwd");                            raf.setLength(length);                            raf.close();                            // 计算每个线程需要下载的字节数                            int everyLength = length / THREAD_COUNT;                            // 确定每个线程的开始位置和终止位置                            for (int i = 0; i < THREAD_COUNT; i++) {                                int startIndex = i * everyLength;                                int endIndex = (i + 1) * everyLength - 1;                                // 如果是最后一个线程,可能存在余数,把最后一个线程的位置写死                                if (i == THREAD_COUNT - 1) {                                    endIndex = length - 1;                                }                                // 开启线程,准备下载                                new Thread(new DownLoad(i, url, startIndex, endIndex, targetFile)).start();                            }                        } else {                            // SD卡空间不足或者SD卡无法读写,发送消息给主线程                            handler.sendEmptyMessage(2);                        }                    } else {                        // 无法连接网络,发送消息给主线程                        handler.sendEmptyMessage(1);                    }                } catch (Exception e) {                    // 请求中出错,发送消息给主线程                    handler.sendEmptyMessage(0);                    e.printStackTrace();                }            }        };        thread.start();    }    /**     * 根据给定的URL字符串,截取该字符串的额最后几位作为下载后文件的名称     */    protected String getFileName(String path) {        int index = path.lastIndexOf("/");        String fileName = path.substring(index + 1);        return fileName;    }    /**     * 创建一个内部类,该类是开启各个子线程下载资源文件     */    class DownLoad implements Runnable {        int threadID;        URL url;        int startIndex;        int endIndex;        File targetFile;        /**         * @param threadID         *            当前线程名称         * @param url         *            资源的URL         * @param startIndex         *            开始位置         * @param endIndex         *            终止位置         * @param targetFile         *            目标文件         */        public DownLoad(int threadID, URL url, int startIndex, int endIndex, File targetFile) {            super();            this.threadID = threadID;            this.url = url;            this.startIndex = startIndex;            this.endIndex = endIndex;            this.targetFile = targetFile;        }        @Override        public void run() {            try {                // 创建一个临时进度文件,该文件用来记载当前线程已经下载的字节数.该文件直接以线程名命名,位置放在目标文件的同一位置                File progressFile = new File(targetFile.getParent(), threadID + ".txt");                // 判断临时进度文件是否存在                if (progressFile.exists()) {                    // 取出临时进度文件中的数据,并更改当前线程下载的起始位置                    BufferedReader br = new BufferedReader(new FileReader(progressFile));                    int data = Integer.parseInt(br.readLine());                    br.close();                    startIndex += data;                    // 把当前所有线程已经下载的字节数显示在进度条中                    currentTotal += data;                    pb.setProgress(currentTotal);                    // 发送消息给主线程,更新文字进度                    handler.sendEmptyMessage(4);                }                Log.i("HHH", threadID + "  的区间是:   " + startIndex + "-----" + endIndex);                // 获取HttpURLConnection的对象并设置属性                HttpURLConnection conn = (HttpURLConnection) url.openConnection();                conn.setConnectTimeout(5000);                conn.setReadTimeout(5000);                conn.setRequestMethod("POST");                // 设置请求数据的区间.如果没有设置,系统默认从头开始请求。那么,每个线程请求的数据都是从头开始,无法做到多线程下载                conn.setRequestProperty("Range", "bytes=" + startIndex + "-" + endIndex);                // 判断响应码。请求部分资源的响应码是206                if (conn.getResponseCode() == 206) {                    // 设置文件写入的位置移动到startIndex,如果不指定写入位置,那么文件会覆盖掉                    RandomAccessFile raf = new RandomAccessFile(targetFile, "rwd");                    raf.seek(startIndex);                    // 获取资源中的输入流                    InputStream is = conn.getInputStream();                    // 进行读写操作                    int i = 0;                    int currentProgress = 0;// 记录当前线程已经下载到的位置                    int len = 0;                    byte[] buffer = new byte[1024];                    while ((len = is.read(buffer)) != -1) {                        raf.write(buffer, 0, len);                        currentProgress += len;                        currentTotal += len;                        i++;                        // 把当前线程已经下载的总字节数写入临时进度文件中                        // 要求每个更新操作都要同步写到进度临时文件中,所以最好是用随机文件存储流                        // 每次读写都记录下来已经更新UI都比较耗时,所以隔一段时间记录一下(这里每次下载30K就记录一下)                        if (i % 30 == 1 || len != 1024) {                            RandomAccessFile progressRaf = new RandomAccessFile(progressFile, "rwd");                            progressRaf.write((currentProgress + "").getBytes());                            progressRaf.close();                            // 设置进度条和文字进度                            pb.setProgress(currentTotal);                            handler.sendEmptyMessage(4);                        }                    }                    is.close();                    raf.close();                    // 当前线程下载完成,记录下已经完成的线程数量                    THREAD_COMPLETE++;                    Log.i("HHH", threadID + "线程下载完毕 ,当前完成线程数为:  " + THREAD_COMPLETE);                } else {                    handler.sendEmptyMessage(1);                }            } catch (Exception e) {                handler.sendEmptyMessage(0);            }            // 当所有的线程完成时,发送消息给主线程,并且删除所有的临时进度文件。这里需要用到同步语句块            synchronized (LOCK) {                if (THREAD_COMPLETE == THREAD_COUNT) {                    for (int j = 0; j < THREAD_COUNT; j++) {                        File f = new File(targetFile.getParent(), j + ".txt");                        if (f.exists()) {                            f.delete();                            THREAD_COMPLETE--;                        }                    }                    // 发送消息给主线程,下载完毕                    handler.sendEmptyMessage(3);                }            }        }    }}

3.其中这句代码:

boolean b = CheckSD.checkCapcity(length);

这是个自己写的工具类,就是用来判断当前手机中的SD卡空间是否足够
代码如下所示:

@SuppressLint("NewApi")@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)public class CheckSD {    // 判断SD卡状态    public static boolean checkStatus() {        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {            return true;        }        return false;    }    // 判断SD卡容量是否足够    public static boolean checkCapcity(int length) {        boolean b = false;        if (checkStatus()) {            // 输入sd卡的路径            String path = Environment.getExternalStorageDirectory().getPath();            // 读取sd卡的存储空间            // 调用C的系统库Statfs            StatFs fs = new StatFs(path);            long availableBolocks = 0;            long bolockSize = 0;            // 判断当前Android系统的版本等级。            if (Build.VERSION.SDK_INT >= 18) {                availableBolocks = fs.getAvailableBlocksLong();                bolockSize = fs.getBlockSizeLong();            } else {                availableBolocks = fs.getAvailableBlocks();                bolockSize = fs.getBlockSize();            }            long available = availableBolocks * bolockSize;            if (available > length) {                b = true;            }        }        return b;    }}

4.最后在清单文件中添加权限:

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

在布局文件中,我直接把资源文件的URL下载了第一行和第二行(实际上这是一个EditText组件),“点击按钮“实际上是个Button组件,灰色的横杠是个ProgressBar组件,”0%“是个TextView组件,由于背景是黑色的,所以看不清这些组件的边框。

0 0
原创粉丝点击