多线程下载器(不含数据库部分)
1、写在前面:
虽然demo中程序框架已搭建完成,但是由于笔者时间原因,暂时只完成了核心部分:多线程下载的部分,其他数据库、服务通知、暂停部分还未添加到项目中。
2、相关知识点:
(1)Java线程及停止线程的方式
(2)Java RandomAccessFile文件操作
(3)HttpURLConnection相关range字段的配置
(4)Sqlite同步操作
2、核心思想:
(1)通过HttpURLConnection判断服务器是否支持断电续传:
<1>否->直接开启普通的多线程下载(遇到断网等情况便会重新下载)
<2>是->开启普通的多线程下载,但是每个线程都含有自己的下载进度信息,以便断网或用户暂停开始重新下载重新开启下载。笔者在针对不同的下载尺寸智能的分配不同的线程数量去下载资源,通过设置缓冲区大小来提高下载速度。
3、核心技术:
(1)HttpURLConnection的配置
(2)RandomAccessFile随机文件的读取以及缓冲区的设置
(3)线程的暂停与启动
4、分析结果:
(1)将功能划分为三大部分:下载器(统一的外部接口)、存储器(内部的存储实现)、通知服务(用户交互部分)。
(2)项目结构:
5、核心代码:
package com.jx.downloader; import java.io.BufferedInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import android.content.Context; import android.os.Environment; import android.text.TextUtils; import android.util.Log; import com.jx.dbhelper.DownloadRecordDB; import com.jx.model.DownloadModel; /** * 自定义下载器: 1、根据即将下载的内容大小智能的分配下载线程数量,每个线程通过Downloader携带自身线程的下载信息(下载起点、终点、线程名称) * 2、如果服务器支持断电续传则开启数据库 * ,在下载处于暂停的状态时(导致下载暂停的原因可能是手动暂停或者网络不佳),自动保存下载信息到数据库,在取消下载的时候自动清空数据库信息, * 重新开始下载的时候,读取内容重新下载(一般情况本地存储变量还未被回收,不必从数据库重新读取) 2、使用RandomAccessFile存储下载的内容 * * @author J_X 2016年3月19日09:58:00 */ public class JX_Downloader { /** * 0~3M的下载范围默认开启1个线程 */ private final static long BELOW_Three_M_SIZE = 5 * 1024 * 1024; /** * 3~6M的下载范围默认开启2个线程 */ private final static long BELOW_SIX_M_SIZE = 6 * 1024 * 1024; /** * 10~18M的下载范围默认开启4个线程 */ private final static long BELOW_EIGHTEEN_M_SIZE = 18 * 1024 * 1024; /** * 线程是否暂停的标示 */ private static volatile boolean isStopDownloading = false; /** * 下载器的标示id,以为可以创建多个下载器 */ private long downloaderID; /** * 需要下载的资源链接 */ private String resourceUrl; /** * 下载链接的遵守URL协议的对象 */ private URL resourceURL; /** * 需要下载的资源的总字节数 */ private long resourceSize; /** * 需要的下载线程的总数量 */ private int totalTreadNum; /** * 资源所在服务器是否支持断点续传功能 */ private boolean isSupportLoadingAndSaving; /** * 下载资源存储的数据库 */ private DownloadRecordDB downlaodDB; /** * 文件存储目录 */ private String savaFileName; /** * 构造器暂无子类所以没有提供默认构造函数 * * @param context * 上下文 * @param downloadUrl * 将要被下载的链接 * @param saveFileName * 带后缀的下载的文件存储名称 */ public JX_Downloader(Context context, String downloadUrl, String saveFileName) throws MalformedURLException { // TODO Auto-generated constructor stub resourceUrl = downloadUrl; this.resourceURL = new URL(resourceUrl); this.downlaodDB = new DownloadRecordDB(context); // 如果用户没有设置下载后缀,提供默认存储文件夹 if (TextUtils.isEmpty(saveFileName)) { this.savaFileName = "JX_DownLoader.txt"; } else { this.savaFileName = saveFileName; } } /** * 开启下载任务,对外提供方便下载方法 */ public void startDownload() { new Thread(new Runnable() { @Override public void run() { // TODO Auto-generated method stub try { readyDownload(); } catch (IOException e) { // TODO Auto-generated catch block Log.e("Debug", "downloading fail!!!"); } } }).start(); } /** * 核心下载程序(采用类似门面模式方法处理,方法有严格的执行顺序要求) * * @throws IOException * */ private void readyDownload() throws IOException { // 判断当前连接所在服务器是否支持断点续传下载 // this.isSupportLoadingAndSaving = false; // /** 不支持的给出友好提示,只进行多线程下载的任务 */ // if (!isSupportLoadingAndSaving) { // Log.e("Debug", "Server don't support pause_save download!"); // } else { // /** 支持的情况:1、开启数据库存储各个线程进度 */ // } /* 多线程下载流程 */ // 1、计算需要下载的资源大小 HttpURLConnection httpURLConnection = settingRequestHttp(null); if (httpURLConnection.getResponseCode() == 200) { // TODO: 需要进一步优化网络 Log.e("Debug", "Coonected sucessfully"); } resourceSize = httpURLConnection.getContentLength(); if (resourceSize <= 0) { Log.e("Debug", "unkown file Length and return"); return; } else { Log.e("Debug", "file length: " + resourceSize + "bytes"); } // 2、按照用户设置或者资源大小智能的设置下载线程总数量 int tempThreadNum = setTotalTreadNum(resourceSize); // 3、根据分配的线程数量,来将资源“等分”,并设置header ArrayList<DownloadModel> downloadArray = new ArrayList<DownloadModel>(); if (isSupportLoadingAndSaving) { // 从数据库读取 downloadArray = downlaodDB.getAllInfo(); } else { // 从本地方法读取 downloadArray = initResoureSize(tempThreadNum, resourceSize); } // 4、划分线程进行下载 for (int i = 0; i < downloadArray.size(); ++i) { DownloadModel tempModel = downloadArray.get(i); MyRunnable runnable = new MyRunnable(tempModel); Thread thread = new Thread(runnable); thread.start(); } } /** * 配置Http信息,准备开始下载数据 * * @throws IOException */ private HttpURLConnection settingRequestHttp(DownloadModel model) throws IOException { HttpURLConnection coon = (HttpURLConnection) this.resourceURL .openConnection(); coon.setConnectTimeout(3 * 1000); coon.setRequestMethod("GET"); coon.setRequestProperty( "Accept", "image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*"); coon.setRequestProperty("Accept-Language", "zh-CN"); coon.setRequestProperty("Referer", resourceUrl); coon.setRequestProperty("Charset", "UTF-8"); coon.setRequestProperty( "User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)"); coon.setRequestProperty("Connection", "Keep-Alive"); // 断点续传的核心代码,设置下载区间,注意是bytes=在这吃过亏 if (model != null) { coon.setRequestProperty( "Range", "bytes=" + model.getDownloadedLength() + "-" + model.getDownloadLengthSum()); } return coon; } /** * 多线程下载共享的run方法 * * @author Administrator * */ private class MyRunnable implements Runnable { private DownloadModel rModel; public MyRunnable(DownloadModel model) { this.rModel = model; } @Override public void run() { try { Log.i("Debug", rModel.getThreadName()); HttpURLConnection coon = settingRequestHttp(rModel); if (coon.getResponseCode() == 200) { Log.e("Debug", "Coonected sucessfully"); } resourceSize = coon.getContentLength(); if (resourceSize <= 0) { Log.e("Debug", "unkown file Length and return"); return; } else { readResourceAndSave(coon, rModel); } } catch (IOException e) { e.printStackTrace(); } } } /** * 将网络数据从网上读取下来 * * @throws IOException */ private void readResourceAndSave(HttpURLConnection coon, DownloadModel model) throws IOException { //创建文件夹 File savDir = Environment.getExternalStorageDirectory(); File file = null; if (Environment.getExternalStorageState().equals( Environment.MEDIA_MOUNTED)) { if (!savDir.exists()) { savDir.mkdirs(); } file = new File(savDir, this.savaFileName); } RandomAccessFile rFile = new RandomAccessFile(file, "rwd"); // 跳转到当前线程对文件的读写起始位置 rFile.seek(model.getDownloadedLength()); InputStream finput = coon.getInputStream(); if (finput != null) { Log.e("Debug", "finput=>" + finput.toString()); } else { Log.e("Debug", "finput is null and return"); } Log.e("Debug", "rFile.getFilePointer=>" + rFile.getFilePointer()); BufferedInputStream bInput = new BufferedInputStream(finput); // 设置3KB的缓冲区,加快下载速度,这里后期改成按字符或者整行读取方式优化 int size = 3 * 1024; byte[] readBytes = new byte[size]; int readNum = 0; long sum = 0; // 当还有数据可读且线程没有暂停 long needReadNum = model.getDownloadLengthSum() - model.getDownloadedLength(); while ((readNum = bInput.read(readBytes, 0, size)) > 0 && sum < needReadNum && !isStopDownloading) { sum += readNum; // 如果请求的尺寸下一次读取即将超过总需求数量时,修正读取内容的大小保证不多读取 if ((sum + size) > needReadNum) { size = (int) (needReadNum - sum); } // 写入到文件中 rFile.write(readBytes, 0, readNum); } Log.e("Debug", model.getThreadName() + "finish downloading:" + rFile.length() + "-bytes"); rFile.close(); finput.close(); } /** * 计算需要下载的资源大小 * * @param url * 待下载的资源内容链接 * @return 返回资源所占的字节数 */ private ArrayList<DownloadModel> initResoureSize(int tNum, long contentLength) { long tempLastSize = contentLength;// 剩余的下载内容大小 ArrayList<DownloadModel> downloadArray = new ArrayList<DownloadModel>(); long commonSize = contentLength / tNum; for (int i = 0; i < tNum; ++i) { DownloadModel downloadModel = new DownloadModel(); // 还未开始下载,所以已下载的大小为0byte,他的长度代表下一个线程的下载的起点 downloadModel.setDownloadedLength(i * commonSize); long loadEnding = tempLastSize; // 剩下的最后一个下载线程的下载大小=总线程-其他线程下载的大小和 if (i == tNum - 1) { loadEnding = contentLength; } else { loadEnding = commonSize * i + commonSize; } tempLastSize = contentLength - commonSize; downloadModel.setDownloadLengthSum(loadEnding); downloadModel.setThreadName("JX_Download_Thread" + i); // 如果支持断点续传,就存储到数据库中,否则暂时存储到ArrayList中 if (isSupportLoadingAndSaving) { downlaodDB.insert(downloadModel); } else { downloadArray.add(downloadModel); } } return downloadArray; } /** * 根据资源大小智能的设置下载线程总数量 * * @param totalTreadNum * 设置的下载数据量 */ private int setTotalTreadNum(long contentLength) { // 如果用户没有设置下载的线程数量则根据大小智能设置 if (0 == this.totalTreadNum) { if (contentLength <= 0) { Log.i("Debug", "下载的内容太小!"); return 0; } else if (contentLength < BELOW_Three_M_SIZE) { this.totalTreadNum = 1; } else if (contentLength < BELOW_SIX_M_SIZE) { this.totalTreadNum = 2; } else if (contentLength < BELOW_EIGHTEEN_M_SIZE) { this.totalTreadNum = 4; } else { this.totalTreadNum = 5; } } return this.totalTreadNum; } /** * 判断当前连接所在服务器是否支持断点续传下载 * * @return 默认不支持断电续传 */ public boolean isSupportLoadingAndSaving() { return isSupportLoadingAndSaving; } public long getDownloaderID() { return downloaderID; } public void setDownloaderID(long downloaderID) { this.downloaderID = downloaderID; } public int getTotalTreadNum() { return totalTreadNum; } public String getResourceUrl() { return resourceUrl; } public long getResourceSize() { return resourceSize; } }
下载器使用:
package com.jx.main; import java.net.MalformedURLException; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import com.example.mutilthreaddownloader.R; import com.jx.downloader.JX_Downloader; /** * 2016年3月19日09:41:00 * * @author J_X 多线程测试类 */ public class MainActivity extends Activity { JX_Downloader downloader; Button btnDownlaod; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btnDownlaod = (Button)findViewById(R.id.btn_start_downlaoding); btnDownlaod.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub try { downloader = new JX_Downloader(MainActivity.this, "****.apk""****.apk); downloader.startDownload(); } catch (MalformedURLException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } } }); } }
小结:
下载器的速度,出来外部因素网速,服务器传输速度,内部因素主要是线程的数量和缓冲区能够影响下载速度,但是笔者在魅族酷派小米虚拟机上同一wifi相同线程数量以及同样大小缓冲区下,下载速度小米的下载速度奇慢,还有待考证具体原因。
时间: 2024-11-08 22:06:22