先看一组效果图:
Android多线程断点下载之多线程下载原理
Android多线程断点下载之断点下载原理
工程目录结构如下:
比较简单,一个MainActivity,一个下载工具类,2个布局文件
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>
<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>
<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>
<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>
后台打印的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>