1.上篇实现了单线程的单文件下载,本篇将讲述多个文件的多线程下载,
在此之前希望你先弄懂上篇
2.本篇将用到上篇之外的技术:
多线程、线程池(简)、RecyclerView、数据库多线程访问下的注意点、volatile AtomicLong(简)
最终静态的效果
最终动态的效果
一、分析一下多线程下载单个文件的原理:
1.线程分工方式
大家都知道,一个文件是很多的字节组成的,字节又是由二进制的位组成,如果把一个字节当成一块砖。
那下载就像把服务器的砖头搬到手机里,然后摆在一个文件里摆好,搬完了,文件满了,任务就完成了
然后文件是电影就能播,是图片就能看,app就能安装。
对于下载一个文件,上篇讲的单线程下载相当于一个人一块一块地搬。
而本篇的多线程则是雇几个人来搬,可想而知效率是更高的。
那我开一千个线程岂不是秒下?如 果你要搬1000 块砖,找1000 个人,效率固然高,
但人家也不是白干活,相对于3 个人搬,你要多付333 倍的工资,也就是开线程要消耗的,适量即可。
一个字节的丢失就可能导致一个文件的损坏,可想而知要多个人一起干活必须分工明确
不然一块砖搬错了,整个文件就报废了,下面看一下线程怎么分工,拿3个线程下载1000字节来说:
2.多线程下载的流程图
整体架构和单线程的下载类似,最大的改变的是:
由于多线程需要管理,使用一个DownLoadTask来管理一个文件的所有下载线程,其中封装了下载和暂停逻辑。
在DownLoadTask#download方法里,如果数据库没有信息,则进行线程的任务分配及线程信息的创建,并插入数据库。
DownLoadThread作为DownLoadTask的内部类,方便使用。最后在download方法一一创建DownLoadThread并开启,
将DownLoadThread存入集合管理,在DownLoadTask#pause方法里,将集合中的线程全部关闭即可
二、代码实现:
1.RecyclerView的使用:
用RecyclerView将单个条目便成一个列表界面
1).增加URL常量
public static final String URL_JUEJIN = "https://imtt.dd.qq.com/16891/4611E43165D203CB6A52E65759FE7641.apk?fsname=com.daimajia.gold_5.6.2_196.apk&csr=1bbd" ;
public static final String URL_QQ = "https://qd.myapp.com/myapp/qqteam/Androidlite/qqlite_3.7.1.704_android_r110206_GuanWang_537057973_release_10000484.apk" ;
public static final String URL_YOUDAO = "http://codown.youdao.com/note/youdaonote_android_6.3.5_youdaoweb.apk" ;
public static final String URL_WEIXIN = "http://gdown.baidu.com/data/wisegame/3d4de3ae1d2dc7d5/weixin_1360.apk" ;
public static final String URL_YOUDAO_CIDIAN = "http://codown.youdao.com/dictmobile/youdaodict_android_youdaoweb.apk" ;
2).初始化数据
* 初始化数据
* @return
@NonNull
private ArrayList<FileBean> initData() {
FileBean juejin = new FileBean(0 , Cons.URL_JUEJIN, "掘金.apk" , 0 , 0 )
FileBean yunbiji = new FileBean(1 , Cons.URL_YOUDAO, "有道云笔记.apk" , 0 , 0 )
FileBean qq = new FileBean(2 , Cons.URL_QQ, "QQ.apk" , 0 , 0 )
FileBean weiChat = new FileBean(3 , Cons.URL_WEIXIN, "微信.apk" , 0 , 0 )
FileBean cidian = new FileBean(4 , Cons.URL_YOUDAO_CIDIAN, "有道词典.apk" , 0 , 0 )
ArrayList<FileBean> fileBeans = new ArrayList<>()
fileBeans.add(juejin)
fileBeans.add(yunbiji)
fileBeans.add(qq)
fileBeans.add(weiChat)
fileBeans.add(cidian)
return fileBeans
3).RecyclerView适配器
上篇在Activity中的按钮中实现的下载和暂停intent,这里放在RVAdapter里
* 作者:张风捷特烈<br/>
* 时间:2018/11/13 0013:11:58<br/>
* 邮箱:1981462002@qq.com<br/>
* 说明:RecyclerView适配器
public class RVAdapter extends RecyclerView.Adapter<MyViewHolder> {
private Context mContext
private List<FileBean> mData
public RVAdapter(Context context, List<FileBean> data) {
mContext = context
mData = data
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(mContext).inflate(R.layout.item_pb, parent, false )
view.setOnClickListener(v -> {
//TODO 点击条目
return new MyViewHolder(view)
@Override
public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
FileBean fileBean = mData.get(position)
holder.mBtnStart.setOnAlphaListener(v -> {
ToastUtil.showAtOnce(mContext, "开始下载: " + fileBean.getFileName())
Intent intent = new Intent(mContext, DownLoadService.class)
intent.setAction(Cons.ACTION_START)
intent.putExtra(Cons.SEND_FILE_BEAN, fileBean)
mContext.startService(intent)
holder.mBtnStop.setOnAlphaListener(v -> {
Intent intent = new Intent(mContext, DownLoadService.class)
intent.setAction(Cons.ACTION_STOP)
intent.putExtra(Cons.SEND_FILE_BEAN, fileBean)
mContext.startService(intent)
ToastUtil.showAtOnce(mContext, "停止下载: " + fileBean.getFileName())
holder.mTVFileName.setText(fileBean.getFileName())
holder.mPBH.setProgress((int) fileBean.getLoadedLen())
holder.mPBV.setProgress((int) fileBean.getLoadedLen())
@Override
public int getItemCount() {
return mData.size()
* 更新进度
* @param id 待更新的文件id
* @param progress 进度数
public void updateProgress(int id, int progress) {
mData.get(id).setLoadedLen(progress)
notifyDataSetChanged()
* ViewHolder
class MyViewHolder extends RecyclerView.ViewHolder {
public ProgressBar mPBH
public ProgressBar mPBV
public AlphaImageView mBtnStart
public AlphaImageView mBtnStop
public TextView mTVFileName
public MyViewHolder(View itemView) {
super(itemView)
mPBH = itemView.findViewById(R.id.id_pb_h)
mPBV = itemView.findViewById(R.id.id_pb_v)
mBtnStart = itemView.findViewById(R.id.id_btn_start)
mBtnStop = itemView.findViewById(R.id.id_btn_stop)
mTVFileName = itemView.findViewById(R.id.id_tv_file_name)
4).设置适配器:MainActivity中
mAdapter = new RVAdapter (this , fileBeans);
mIdRvPage.setAdapter (mAdapter);
mIdRvPage.setLayoutManager (new LinearLayoutManager (this , LinearLayoutManager.VERTICAL, false ));
2.DownLoadTask的分析:
DownLoadTask最重要的在于:管理一个文件下载的所有线程,download是暴漏出的下载方法。pause停止。
比如开三个线程,该类的mDownLoadThreads就将线程存到集合里,以便使用
DownLoadThread 和上篇核心逻辑基本一至,这里作为DownLoadTask内部类,方便使用其中的变量
还有就是由于是多线程,每个执行的快慢不定,判断结束的标识必须三个线程都结束才代表下载结束
另外使用Timer定时器来发送进度,在DownLoadThread发送会导致几个线程中的进度不统一,影响视觉
三个线程共同工作
暂停时数据库情况
* 作者:张风捷特烈<br/>
* 时间:2018/11/13 0013:15:21<br/>
* 邮箱:1981462002@qq.com<br/>
* 说明:下载一个文件的任务(mDownLoadThreads储存该文件任务的所有线程)
public class DownLoadTask {
private FileBean mFileBean;
private DownLoadDao mDao;
private Context mContext;
private int mThreadCount;
public boolean isDownLoading;
private Timer mTimer;
private List<DownLoadThread> mDownLoadThreads;
private volatile AtomicLong mLoadedLen = new AtomicLong ();
public static ExecutorService sExe = Executors.newCachedThreadPool ();
public DownLoadTask (FileBean fileBean, Context context, int threadCount) {
mFileBean = fileBean;
mContext = context;
mThreadCount = threadCount;
mDao = new DownLoadDaoImpl (context);
mDownLoadThreads = new ArrayList<>();
mTimer = new Timer ();
* 下载逻辑
public void download () {
List<ThreadBean> threads = mDao.getThreads (mFileBean.getUrl());
if (threads.size() == 0 ) {
long len = mFileBean.getLength () / mThreadCount;
for (int i = 0 ; i < mThreadCount; i++) {
ThreadBean threadBean = null;
if (i != mThreadCount - 1 ) {
threadBean = new ThreadBean (
i, mFileBean.getUrl(), len * i , (i + 1 ) * len - 1 , 0 );
} else {
threadBean = new ThreadBean (
i, mFileBean.getUrl(), len * i , mFileBean.getLength (), 0 );
threads.add (threadBean);
mDao.insertThread (threadBean);
for (ThreadBean info : threads) {
DownLoadThread thread = new DownLoadThread (info);
sExe.execute (thread);
thread.isDownLoading = true;
isDownLoading = true;
mDownLoadThreads.add (thread);
mTimer.schedule (new TimerTask() {
@Override
public void run() {
Intent intent = new Intent (Cons.ACTION_UPDATE);
mContext.sendBroadcast (intent);
intent.putExtra (Cons.SEND_LOADED_PROGRESS,
(int) (mLoadedLen.get() * 100 / mFileBean.getLength ()));
intent.putExtra (Cons.SEND_FILE_ID, mFileBean.getId());
mContext.sendBroadcast (intent);
}, 1000 , 1000 );
public void pause () {
for (DownLoadThread downLoadThread : mDownLoadThreads) {
downLoadThread.isDownLoading = false;
isDownLoading = false;
* 下载的核心线程类
public class DownLoadThread extends Thread {
private ThreadBean mThreadBean;
public boolean isDownLoading;
public DownLoadThread (ThreadBean threadBean) {
mThreadBean = threadBean;
@Override
public void run() {
if (mThreadBean == null) {
return;
HttpURLConnection conn = null;
RandomAccessFile raf = null;
InputStream is = null;
try {
URL url = new URL (mThreadBean.getUrl());
conn = (HttpURLConnection) url.openConnection ();
conn.setConnectTimeout (5000 );
conn.setRequestMethod ("GET");
long start = mThreadBean.getStart () + mThreadBean.getLoadedLen ();
conn.setRequestProperty ("Range", "bytes=" + start + "-" + mThreadBean.getEnd());
File file = new File (Cons.DOWNLOAD_DIR, mFileBean.getFileName());
raf = new RandomAccessFile (file, "rwd");
raf.seek (start);
mLoadedLen.set (mLoadedLen.get() + mThreadBean.getLoadedLen ());
if (conn.getResponseCode() == 206 ) {
is = conn.getInputStream ();
byte[] buf = new byte[1024 * 4] ;
int len = 0 ;
while ((len = is.read(buf)) != -1 ) {
raf.write (buf, 0 , len);
mLoadedLen.set (mLoadedLen.get() + len);
mThreadBean.setLoadedLen (mThreadBean.getLoadedLen() + len);
if (!this.isDownLoading) {
mDao.updateThread (mThreadBean.getUrl(), mThreadBean.getId (),
mThreadBean.getLoadedLen ());
return;
isDownLoading = false;
checkIsAllOK ();
} catch (Exception e) {
e.printStackTrace ();
} finally {
if (conn != null) {
conn.disconnect ();
try {
if (raf != null) {
raf.close ();
if (is != null) {
is.close ();
} catch (IOException e) {
e.printStackTrace ();
* 检查是否所有线程都已经完成了
private synchronized void checkIsAllOK () {
boolean allFinished = true;
for (DownLoadThread downLoadThread : mDownLoadThreads) {
if (downLoadThread.isDownLoading) {
allFinished = false;
break;
if (allFinished) {
mTimer.cancel ();
mDao.deleteThread (mThreadBean.getUrl());
Intent intent = new Intent ();
intent.setAction (Cons.ACTION_FINISH);
intent.putExtra (Cons.SEND_FILE_BEAN, mFileBean);
mContext.sendBroadcast (intent);
3.Service 的修改
稍微不同的就是一个下载任务变成了多个下载任务,这里使用安卓特有的SparseArray来存储
* 作者:张风捷特烈<br/>
* 时间:2018/11/12 0012:12:23<br/>
* 邮箱:1981462002@qq.com<br/>
* 说明:下载的服务
public class DownLoadService extends Service {
private SparseArray<DownLoadTask> mTaskMap = new SparseArray <>();
* 处理消息使用的Handler
private Handler mHandler = new Handler () {
@Override
public void handleMessage (Message msg) {
switch (msg.what) {
case Cons.MSG_CREATE_FILE_OK:
FileBean fileBean = (FileBean) msg.obj;
ToastUtil.showAtOnce(DownLoadService.this , "文件长度:" + fileBean.getLength());
DownLoadTask task = new DownLoadTask (fileBean, DownLoadService.this , 3 );
task.download();
mTaskMap.put(fileBean.getId(), task);
break ;
@Override
public int onStartCommand (Intent intent, int flags, int startId) {
if (intent.getAction() != null ) {
switch (intent.getAction()) {
case Cons.ACTION_START:
FileBean fileBean = (FileBean) intent.getSerializableExtra(Cons.SEND_FILE_BEAN);
DownLoadTask start = mTaskMap.get(fileBean.getId());
if (start != null ) {
if (start.isDownLoading) {
return super .onStartCommand(intent, flags, startId);
DownLoadTask.sExe.execute(new LinkURLThread (fileBean, mHandler));
break ;
case Cons.ACTION_STOP:
FileBean stopFile = (FileBean) intent.getSerializableExtra(Cons.SEND_FILE_BEAN);
DownLoadTask task = mTaskMap.get(stopFile.getId());
if (task != null ) {
task.pause();
break ;
return super .onStartCommand(intent, flags, startId);
@Nullable
@Override
public IBinder onBind (Intent intent) {
return null ;
4.广播的处理:
这里多了一个下载完成的Action,并且由MainActivity传入进度条,改为mAdapter.updateProgress刷新视图
* 作者:张风捷特烈<br/>
* 时间:2018/11/12 0012:16:05<br/>
* 邮箱:1981462002@qq.com<br/>
* 说明:更新ui的广播接收者
public class UpdateReceiver extends BroadcastReceiver {
private RVAdapter mAdapter;
public UpdateReceiver (RVAdapter adapter) {
mAdapter = adapter;
@Override
public void onReceive(Context context, Intent intent) {
if (Cons .ACTION_UPDATE .equals(intent.getAction())) {
int loadedProgress = intent.getIntExtra(Cons .SEND_LOADED_PROGRESS , 0 );
int id = intent.getIntExtra(Cons .SEND_FILE_ID , 0 );
mAdapter.updateProgress(id, loadedProgress);
} else if (Cons .ACTION_FINISH .equals(intent.getAction())) {
FileBean fileBean = (FileBean ) intent.getSerializableExtra(Cons .SEND_FILE_BEAN );
mAdapter.updateProgress(fileBean.getId(), 0 );
ToastUtil .showAtOnce(context, "文佳下载完成:" + fileBean.getFileName());
三、数据库的多线程操作注意点:
1.DownLoadDBHelper的单例
为了避免不同线程拿到的DownLoadDBHelper对象不同,这里使用单例模式
private static DownLoadDBHelper sDownLoadDBHelper;
public static DownLoadDBHelper newInstance (Context context ) {
if (sDownLoadDBHelper == null ) {
synchronized (DownLoadDBHelper.class ) {
if (sDownLoadDBHelper == null ) {
sDownLoadDBHelper = new DownLoadDBHelper(context);
return sDownLoadDBHelper;
2.在变动数据库的方法上加同步:db.DownLoadDaoImpl
避免多个线程修改数据库产生冲突
public synchronized void insertThread (ThreadBean threadBean)
public synchronized void deleteThread (String url)
public synchronized void updateThread (String url, int threadId, long loadedLen)
你看完上下两篇,基本上就能够实现这样的效果了:
回过头来看一看,也并非难到无法承受的地步,多想想,思路贯通之后还是很好理解的。
后记:捷文规范
1.本文成长记录及勘误表
Swift
3.6w
pekonchan
JavaScript
1.7w
crazysunj
Android
ZXing
9439
JavaScript
Visual Studio Code
1.7w
腾讯IVWEB团队
Node.js
JavaScript
3.4w
JavaScript
ECMAScript 6
15.4w
张风捷特烈
VIP.5 如鱼得水
万花过尽知无物 @ 编程之王