android实现多线程下载文件(支持暂停、取消、断点续传)

多线程下载文件(支持暂停、取消、断点续传)

多线程同时下载文件即:在同一时间内通过多个线程对同一个请求地址发起多个请求,将需要下载的数据分割成多个部分,同时下载,每个线程只负责下载其中的一部分,最后将每一个线程下载的部分组装起来即可。

涉及的知识及问题

请求的数据如何分段 分段完成后如何下载和下载完成后如何组装到一起 暂停下载和继续下载的实现(wait()、notifyAll()、synchronized的使用) 取消下载和断点续传的实现

一、请求的数据如何分段

首先通过HttpURLConnection请求总文件大小,而后根据线程数计算每一个线程的下载量,在分配给每一个线程去下载

fileLength = conn.getContentLength(); //根据文件大小,先创建一个空文件 //“r“——以只读方式打开。调用结果对象的任何 write 方法都将导致抛出 IOException。 //“rw“——打开以便读取和写入。如果该文件尚不存在,则尝试创建该文件。 //“rws“—— 打开以便读取和写入,对于 “rw”,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。 //“rwd“——打开以便读取和写入,对于 “rw”,还要求对文件内容的每个更新都同步写入到底层存储设备。 RandomAccessFile raf = new RandomAccessFile(filePath, "rwd"); raf.setLength(fileLength); raf.close(); //计算各个线程下载的数据段 int blockLength = fileLength / threadCount;

二、分段完成后如何下载和下载完成后如何组装到一起

分段完成后给每一个线程的请求头设置Range参数,他允许客户端只请求文件的一部分数据,每一个线程只请求下载相应范围内的数据,使用RandomAccessFile(可随机读写的文件)写入到同一个文件里即可组装成目标文件Range,是在 HTTP/1.1里新增的一个 header field,它允许客户端实际上只请求文档的一部分(范围可以相互重叠)

Range的使用形式:

属性 解释 bytes=0-499 表示头500个字节 bytes=500-999 表示第二个500字节 bytes=-500 表示最后500个字节 bytes=500- 表示500字节以后的范围 bytes=0-0,-1 第一个和最后一个字节

HttpUrlConnection中设置请求头

URL url = new URL(loadUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setRequestProperty("Range", "bytes=" + startPosition + "-" + endPosition); conn.setConnectTimeout(5000); //若请求头加上Range这个参数,则返回状态码为206,而不是200 if (conn.getResponseCode() == 206) { InputStream is = conn.getInputStream(); RandomAccessFile raf = new RandomAccessFile(filePath, "rwd"); raf.seek(startPosition);//跳到指定位置开始写数据 }

三、暂停下载和继续下载的实现(wait()、notifyAll()、synchronized的使用)

关于synchronized只需记住一下五点:

当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。 然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。 尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。 第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。 以上规则对其它对象锁同样适用. protected void onPause() { if (mThreads != null) stateDownload = DOWNLOAD_PAUSE; } protected void onStart() { if (mThreads != null) synchronized (DOWNLOAD_PAUSE) { stateDownload = DOWNLOAD_ING; DOWNLOAD_PAUSE.notifyAll(); } }

对于wait()、notify()、notifyAll()需要注意的是

调用任何对象的wait()方法时,都必须先获得该对象的锁,即调用的wait()方法必须得写在synchronized(obj){…}之内 当调用对象的wait()方法后,该线程若想继续执行,必须得再次获得该对象的锁才可以 如果A1,A2,A3线程都在obj.wait(),则B调用object.notify()只能唤醒A1,A2,A3中的一个(具体哪一个由JVM决定) 当B调用object.notify/notifyAll的时候,B正持有object锁,因此,A1,A2,A3虽被唤醒,但是仍无法获得object锁直到B退出synchronized块,释放object锁后,A1,A2,A3中的一个/全部才有机会获得锁继续执行 synchronized (DOWNLOAD_PAUSE) { if (stateDownload.equals(DOWNLOAD_PAUSE)) { DOWNLOAD_PAUSE.wait(); } }

四、取消下载和断点续传的实现

取消下载即取消每个线程的执行,不建议直接使用Thread.stop()方法,安全的取消线程即run方法执行结束。只要控制住循环,就可以让run方法结束,也就是线程结束

while ((len = is.read(buffer)) != -1) { //是否继续下载 if (!isGoOn) break; }

断点续传即其实和重新下载是一样的,不过文件的大小和每一个线程下载时的起始位置和结束位置都不是重新计算的。而是上次取消下载时,每一个线程保存的当前位置和结束位置,让每一个线程接着上次的地方继续下载即可

SharedPreferences sp = mContext.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); //获取上次取消下载的进度,若没有则返回0 currLength = sp.getInt(CURR_LENGTH, 0); for (int i = 0; i < threadCount; i++) { //开始位置,获取上次取消下载的进度,默认返回i*blockLength,即第i个线程开始下载的位置 int startPosition = sp.getInt(SP_NAME + (i + 1), i * blockLength); //结束位置,-1是为了防止上一个线程和下一个线程重复下载衔接处数据 int endPosition = (i + 1) * blockLength - 1; //将最后一个线程结束位置扩大,防止文件下载不完全,大了不影响,小了文件失效 if ((i + 1) == threadCount) endPosition = endPosition * 2; mThreads[i] = new DownThread(i + 1, startPosition, endPosition); mThreads[i].start(); }

网络获取和读写SD卡都需要添加相应权限

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

下面贴上全部的代码,里面有详细的注释DownLoadFile.Java

import android.content.Context; import android.content.SharedPreferences; import android.os.Handler; import android.os.Message; import java.io.InputStream; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.URL; /** * Created by tianzhao on 2017/2/21 09:25. * 多线程下载文件 */ public class DownLoadFile { private static final String SP_NAME = "download_file"; private static final String CURR_LENGTH = "curr_length"; private static final int DEFAULT_THREAD_COUNT = 4;//默认下载线程数 //以下为线程状态 private static final String DOWNLOAD_INIT = "1"; private static final String DOWNLOAD_ING = "2"; private static final String DOWNLOAD_PAUSE = "3"; private Context mContext; private String loadUrl;//网络获取的url private String filePath;//下载到本地的path private int threadCount = DEFAULT_THREAD_COUNT;//下载线程数 private int fileLength;//文件总大小 //使用volatile防止多线程不安全 private volatile int currLength;//当前总共下载的大小 private volatile int runningThreadCount;//正在运行的线程数 private Thread[] mThreads; private String stateDownload = DOWNLOAD_INIT;//当前线程状态 private DownLoadListener mDownLoadListener; public void setOnDownLoadListener(DownLoadListener mDownLoadListener) { this.mDownLoadListener = mDownLoadListener; } interface DownLoadListener { //返回当前下载进度的百分比 void getProgress(int progress); void onComplete(); void onFailure(); } public DownLoadFile(Context mContext, String loadUrl, String filePath) { this(mContext, loadUrl, filePath, DEFAULT_THREAD_COUNT, null); } public DownLoadFile(Context mContext, String loadUrl, String filePath, DownLoadListener mDownLoadListener) { this(mContext, loadUrl, filePath, DEFAULT_THREAD_COUNT, mDownLoadListener); } public DownLoadFile(Context mContext, String loadUrl, String filePath, int threadCount) { this(mContext, loadUrl, filePath, threadCount, null); } public DownLoadFile(Context mContext, String loadUrl, String filePath, int threadCount, DownLoadListener mDownLoadListener) { this.mContext = mContext; this.loadUrl = loadUrl; this.filePath = filePath; this.threadCount = threadCount; runningThreadCount = 0; this.mDownLoadListener = mDownLoadListener; } /** * 开始下载 */ protected void downLoad() { //在线程中运行,防止anr new Thread(new Runnable() { @Override public void run() { try { //初始化数据 if (mThreads == null) mThreads = new Thread[threadCount]; //建立连接请求 URL url = new URL(loadUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(5000); conn.setRequestMethod("GET"); int code = conn.getResponseCode();//获取返回码 if (code == 200) {//请求成功,根据文件大小开始分多线程下载 fileLength = conn.getContentLength(); //根据文件大小,先创建一个空文件 //“r“——以只读方式打开。调用结果对象的任何 write 方法都将导致抛出 IOException。 //“rw“——打开以便读取和写入。如果该文件尚不存在,则尝试创建该文件。 //“rws“—— 打开以便读取和写入,对于 “rw”,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。 //“rwd“——打开以便读取和写入,对于 “rw”,还要求对文件内容的每个更新都同步写入到底层存储设备。 RandomAccessFile raf = new RandomAccessFile(filePath, "rwd"); raf.setLength(fileLength); raf.close(); //计算各个线程下载的数据段 int blockLength = fileLength / threadCount; SharedPreferences sp = mContext.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); //获取上次取消下载的进度,若没有则返回0 currLength = sp.getInt(CURR_LENGTH, 0); for (int i = 0; i < threadCount; i++) { //开始位置,获取上次取消下载的进度,默认返回i*blockLength,即第i个线程开始下载的位置 int startPosition = sp.getInt(SP_NAME + (i + 1), i * blockLength); //结束位置,-1是为了防止上一个线程和下一个线程重复下载衔接处数据 int endPosition = (i + 1) * blockLength - 1; //将最后一个线程结束位置扩大,防止文件下载不完全,大了不影响,小了文件失效 if ((i + 1) == threadCount) endPosition = endPosition * 2; mThreads[i] = new DownThread(i + 1, startPosition, endPosition); mThreads[i].start(); } } else { handler.sendEmptyMessage(FAILURE); } } catch (Exception e) { e.printStackTrace(); handler.sendEmptyMessage(FAILURE); } } }).start(); } /** * 取消下载 */ protected void cancel() { if (mThreads != null) { //若线程处于等待状态,则while循环处于阻塞状态,无法跳出循环,必须先唤醒线程,才能执行取消任务 if (stateDownload.equals(DOWNLOAD_PAUSE)) onStart(); for (Thread dt : mThreads) { ((DownThread) dt).cancel(); } } } /** * 暂停下载 */ protected void onPause() { if (mThreads != null) stateDownload = DOWNLOAD_PAUSE; } /** * 继续下载 */ protected void onStart() { if (mThreads != null) synchronized (DOWNLOAD_PAUSE) { stateDownload = DOWNLOAD_ING; DOWNLOAD_PAUSE.notifyAll(); } } protected void onDestroy() { if (mThreads != null) mThreads = null; } private class DownThread extends Thread { private boolean isGoOn = true;//是否继续下载 private int threadId; private int startPosition;//开始下载点 private int endPosition;//结束下载点 private int currPosition;//当前线程的下载进度 private DownThread(int threadId, int startPosition, int endPosition) { this.threadId = threadId; this.startPosition = startPosition; currPosition = startPosition; this.endPosition = endPosition; runningThreadCount++; } @Override public void run() { SharedPreferences sp = mContext.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); try { URL url = new URL(loadUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setRequestProperty("Range", "bytes=" + startPosition + "-" + endPosition); conn.setConnectTimeout(5000); //若请求头加上Range这个参数,则返回状态码为206,而不是200 if (conn.getResponseCode() == 206) { InputStream is = conn.getInputStream(); RandomAccessFile raf = new RandomAccessFile(filePath, "rwd"); raf.seek(startPosition);//跳到指定位置开始写数据 int len; byte[] buffer = new byte[1024]; while ((len = is.read(buffer)) != -1) { //是否继续下载 if (!isGoOn) break; //回调当前进度 if (mDownLoadListener != null) { currLength += len; int progress = (int) ((float) currLength / (float) fileLength * 100); handler.sendEmptyMessage(progress); } raf.write(buffer, 0, len); //写完后将当前指针后移,为取消下载时保存当前进度做准备 currPosition += len; synchronized (DOWNLOAD_PAUSE) { if (stateDownload.equals(DOWNLOAD_PAUSE)) { DOWNLOAD_PAUSE.wait(); } } } is.close(); raf.close(); //线程计数器-1 runningThreadCount--; //若取消下载,则直接返回 if (!isGoOn) { //此处采用SharedPreferences保存每个线程的当前进度,和三个线程的总下载进度 if (currPosition < endPosition) { sp.edit().putInt(SP_NAME + threadId, currPosition).apply(); sp.edit().putInt(CURR_LENGTH, currLength).apply(); } return; } if (runningThreadCount == 0) { sp.edit().clear().apply(); handler.sendEmptyMessage(SUCCESS); handler.sendEmptyMessage(100); mThreads = null; } } else { sp.edit().clear().apply(); handler.sendEmptyMessage(FAILURE); } } catch (Exception e) { sp.edit().clear().apply(); e.printStackTrace(); handler.sendEmptyMessage(FAILURE); } } public void cancel() { isGoOn = false; } } private final int SUCCESS = 0x00000101; private final int FAILURE = 0x00000102; private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { if (mDownLoadListener != null) { if (msg.what == SUCCESS) { mDownLoadListener.onComplete(); } else if (msg.what == FAILURE) { mDownLoadListener.onFailure(); } else { mDownLoadListener.getProgress(msg.what); } } } }; }

在MainActivity中的使用

import android.os.Bundle; import android.os.Environment; import android.support.v7.app.AppCompatActivity; import android.view.View; import android.widget.TextView; import android.widget.Toast; public class MainActivity extends AppCompatActivity { DownLoadFile downLoadFile; private String loadUrl = "http://gdown.baidu.com/data/wisegame/d2fbbc8e64990454/wangyiyunyinle_87.apk"; private String filePath = Environment.getExternalStorageDirectory()+"/"+"网易云音乐.apk"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final TextView tvprogress = (TextView) findViewById(R.id.tv_progress); downLoadFile = new DownLoadFile(this,loadUrl, filePath, 3); downLoadFile.setOnDownLoadListener(new DownLoadFile.DownLoadListener() { @Override public void getProgress(int progress) { tvprogress.setText("当前进度 :"+progress+" %"); } @Override public void onComplete() { Toast.makeText(MainActivity.this,"下载完成",Toast.LENGTH_SHORT).show(); } @Override public void onFailure() { Toast.makeText(MainActivity.this,"下载失败",Toast.LENGTH_SHORT).show(); } }); findViewById(R.id.bt).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { downLoadFile.downLoad(); } }); findViewById(R.id.bt_pause).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { downLoadFile.onPause(); } }); findViewById(R.id.bt_start).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { downLoadFile.onStart(); } }); findViewById(R.id.bt_cancel).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { downLoadFile.cancel(); } }); } @Override protected void onDestroy() { downLoadFile.onDestroy(); super.onDestroy(); } }

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

时间: 2024-09-12 09:58:29

android实现多线程下载文件(支持暂停、取消、断点续传)的相关文章

Android实现多线程下载文件的方法_Android

本文实例讲述了Android实现多线程下载文件的方法.分享给大家供大家参考.具体如下: 多线程下载大概思路就是通过Range 属性实现文件分段,然后用RandomAccessFile 来读写文件,最终合并为一个文件 首先看下效果图: 创建工程 ThreadDemo 首先布局文件 threaddemo.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android=&quo

Android实现多线程下载文件的方法

本文实例讲述了Android实现多线程下载文件的方法.分享给大家供大家参考.具体如下: 多线程下载大概思路就是通过Range 属性实现文件分段,然后用RandomAccessFile 来读写文件,最终合并为一个文件 首先看下效果图: 创建工程 ThreadDemo 首先布局文件 threaddemo.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android=&quo

Android版多线程下载 仿下载助手(最新)_Android

首先声明一点: 这里的多线程下载并不是指多个线程下载一个 文件,而是每个线程负责一个文件,今天给大家分享一个多线程下载的 例子.先看一下效果,点击下载开始下载,同时显示下载进度,下载完成,变成程安装,点击安装提示安装应用. 界面效果图: 线程池ThreadPoolExecutor ,先简单学习下这个线程池的使用 /** * Parameters: corePoolSize the number of threads to keep in the pool, even if they are id

Android实现Service下载文件,Notification显示下载进度的示例

先放个gif..最终效果如果: 主要演示了Android从服务器下载文件,调用Notification显示下载进度,并且在下载完毕以后点击通知会跳转到安装APK的界面,演示是在真实的网络环境中使用真实的URL进行演示,来看看代码: MainActivity代码非常简单,就是启动一个Service: public class MainActivity extends AppCompatActivity { String download_url="http://shouji.360tpcdn.co

java 多线程-为什么使用Java多线程下载文件时下载后的文件和服务器端文件大小一模一样但是无法打开

问题描述 为什么使用Java多线程下载文件时下载后的文件和服务器端文件大小一模一样但是无法打开 为什么使用Java多线程下载文件时下载后的文件和服务器端文件大小一模一样但是无法打开?? package com.miuitust.mutilethread; import java.io.File; import java.io.InputStream; import java.io.RandomAccessFile; import java.net.HttpURLConnection; impor

ios-IOS FTP多线程下载文件

问题描述 IOS FTP多线程下载文件 具体过程是这样的:线程里,创建了CFReadStreamCreateWithFTPUR一个流, scheduleInRunLoop然后open.接着就在CFRunLoopRun();接收是在CFNetwork框架里的- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode 中接收并些文件.这样的线程共有2个,我调试发现 只有一个loop循环里来消息,另一个trap着.下载

请问用link如何多线程下载文件从另一台计算机?

问题描述 请问用link如何多线程下载文件从另一台计算机? 请问用link如何多线程下载文件从另一台计算机?是不是分块同时下载?怎么样最快? 解决方案 linq和下载无关,你可以用thread或者threadpool.

Android通过SOCKET下载文件的方法_Android

本文实例讲述了Android通过SOCKET下载文件的方法.分享给大家供大家参考,具体如下: 服务端代码 import java.io.BufferedInputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.

Python实现多线程下载文件方法总结

今天把刚学python时收藏的几篇多线程下载文件的网页看了一下. 实现简单的多线程下载,需要关注如下几点: 1.文件的大小:可以从reponse header中提取,如"Content-Length:911"表示大小是911字节 2.任务拆分:指定各个线程下载的文件的哪一块,可以通过request header中添加"Range: bytes=300-400"(表示下载300~400byte的内容),注意可以请求的文件的range是[0, size-1]字节的. 3