首先,我们得来说下多线程下载实现的大致思路,以及在使用多线程下载过程应该需要注意的问题。
多线程下载实现的大致思路:
大致思路是这样的,也就是把整个一个文件资源分为若干个部分,然后开启若干个线程,并且使得每个线程负责下载每个子部分的文件,由于
线程下载是异步的,大大缩短了下载的时间,最后将所有的子部分写入到同一个文件中,最后重组得到一个完整的文件。
首先,我们得说下整个资源下载,我们通过网络请求然后可以得到一个文件的整体输入流,然后我们需要得到不是整个文件的输入流,而是
得到每个线程负责下载的子部分文件的输入流。然后得到这些指定大小的输入流,再次写入到我们本地文件中,写入流的时候也需要注意,
每个子部分输入流必须写入相对应的文件位置上,否则会造成后一个写入文件中的输入流会覆盖上一部分写入文件的输入流。
再次整理一下思路我们需要注意哪些问题:
问题一:如何获得整个下载资源的大小
问题二:获得整个文件资源的大小后,如何去拆分这个文件,如何去分配每个子部分文件,并且让不同的子线程来下载,也就是如何
确定每个子线程下载文件的区间(即每个子线程负责下载的子部分文件的开始下载和结束下载位置)
问题三:如何去获得我们需要的指定的大小的输入流,而不是获得由服务端一下把整个文件的输入流
问题四:如何使得每个子线程写入文件合适的起始位置,并且系统默认就是每次往文件中写数据的时候都是从0位置开始写,这样会出现后面
写入的数据,会覆盖前面写入的数据。
3、逐个击破解决上面几个问题,待这些问题都解决了,那么我们的多线程也就实现了
问题一的解决办法:
获取整个下载资源大小,这个很简单,可以直接通过HttpURLConnection网络请求而得到一个HttpURLConnection类型的连接对象
中的getContentLength来得到我们需要下载资源文件的大小。
更重要的是:我们拿到这个文件大小来干什么???其实说白了,就是仿造一个一样大小的空白文件来占用本地存储空间
为什么要这样做呢??其实细心的人就发现,当我们在下载电影或者文件的时候我们会发现在下载目录中会出现一个临时文件
而这个临时文件大小与我们要下载的文件的大小一致,并且这个文件此时是空白的。不信你可以右击查看属性文件大小,
为什么要占用空间,我们可以去设想一下这个情景,假如电脑中的储存空间只剩1G了,而你下载的电影正好1G,电影正在下载过程
假如没有提前占好空间的话,在下载过程中你又下载一个首歌,此时空间明显不足以装下这部电影,那请问这部电影将怎么办?
所以为了防止这种情况出现,也就出现所谓提前占用存储空间。
问题二的解决办法:
如何去拆分整个文件,因为我们要让每个线程去负责每个子部分文件的下载任务,所以直接按照线程的数目来分吧,但是有个问题
就是无法做到每个线程平均分配每个子部分文件的长度,所以我们就采用一个办法,假设有n个线程,就是让前n-1大小一样,最后一个
就包括一些剩余的零头吧,也就是最后一个线程处理最后剩余所有的子部分文件长度。
所以就有如下公式:
前n-1个线程的子部分文件尺寸: size=len/threadCount
这样也就很容易得到了每个线程负责子部分文件的长度
伪代码:
int size=length/threadCount;
for (int id = 0; id < threadCount; id++) {
//1、确定每个线程的下载区间
//2、开启对应子线程下载
int startIndex=id*size;//确定每个线程的子部分文件的开始位置
int endIndex=(id+1)*size-1;//确定每个线程的子部分文件的结束位置
if (id==threadCount-1) {//判断如果是最后一个线程,直接让它的子部分文件结束位置延伸最后即可,也即文件长度-1
endIndex=length-1;
}
System.out.println("第"+id+"个线程的下载区间为"+startIndex+"--"+endIndex);
问题三的解决办法:
如何去指定确定大小的输入流呢?在HttpURLConnection对象中有个setRequestProperty方法设置头部信息可以拿到拿到指定大小的输入流
conn.setRequestProperty("Range", "bytes="+startIndex+"-"+endIndex);
但是需要注意的一点是:你的请求的服务器必须支持多线程下载,并且才能拿到指定大小输入流
为什么要拿到指定大小的输入流为的就是与划分子部分文件长度对应起来,得到的对应指定大小的输入流通过输出流写入到相应大小的子部分文件中
问题四的解决办法:
防止默认设置(每次都从0位置开始写)的影响使得后面写入的数据会覆盖前面写入的数据,通过RandomAccessFile中的seek方法传入每个子部分文件开始的
位置,也就间接更改了默认每次都从0开始写,从每个子部分文件起始位置写,这样就不会覆盖前面的数据。
RandomAccessFile mAccessFile=new RandomAccessFile(file, "rwd");//"rwd"可读,可写
mAccessFile.seek(startIndex);//表示从不同的位置写文件
通过解决上面四个问题,把整个实现的思路梳理了一下,那么我将实现过程大致总结为以下5点:
1、得到下载资源文件的大小,产生相同大小的随机RandomAccessFile空白文件,来占用空间
2、并且把RandomAccessFile空白文件分割成若干个部分,并且确定每个子部分文件的下载空间
3、开启对应的子线程
4、从网络服务器拿到指定大小的部分输入流
5、从RandomAccessFile文件的不同的开始位置开始往其中写入我们得到对应的指定大小的输入流
1. package com.mikyou.multithread;
2.
3. import java.io.File;
4. import java.io.RandomAccessFile;
5. import java.net.HttpURLConnection;
6. import java.net.MalformedURLException;
7. import java.net.URL;
8.
9. /**
10. * @author zhongqihong
11. * 多线程下载
12. * */
13. public class Main {
14. public static final String PATH="http://120.203.56.190:8088/upload/mobilelist.xml";
15. public static int threadCount=3;//进行下载的线程数量
16.
17. public static void main(String[] args) {
18. try {
19. URL url=new URL(PATH);
20. HttpURLConnection conn=(HttpURLConnection) url.openConnection();
21. conn.setRequestMethod("GET");
22. conn.setConnectTimeout(8000);
23. conn.setReadTimeout(8000);
24. conn.connect();
25. if (conn.getResponseCode()==200) {
26. int length=conn.getContentLength();//返回文件大小
27. //占据文件空间
28. File file =new File("mobilelist.xml");
29. RandomAccessFile mAccessFile=new RandomAccessFile(file, "rwd");//"rwd"可读,可写
30. mAccessFile.setLength(length);//占据文件的空间
31. int size=length/threadCount;
32. for (int id = 0; id < threadCount; id++) {
33. //1、确定每个线程的下载区间
34. //2、开启对应子线程下载
35. int startIndex=id*size;
36. int endIndex=(id+1)*size-1;
37. if (id==threadCount-1) {
38. endIndex=length-1;
39. }
40. System.out.println("第"+id+"个线程的下载区间为"+startIndex+"--"+endIndex);
41. new DownLoadThread(startIndex, endIndex, PATH, id).start();
42. }
43. }
44. } catch (Exception e) {
45. e.printStackTrace();
46. }
47. }
48. }
1. package com.mikyou.multithread;
2.
3. import java.io.BufferedReader;
4. import java.io.File;
5. import java.io.FileInputStream;
6. import java.io.IOException;
7. import java.io.InputStream;
8. import java.io.InputStreamReader;
9. import java.io.RandomAccessFile;
10. import java.net.HttpURLConnection;
11. import java.net.MalformedURLException;
12. import java.net.ProtocolException;
13. import java.net.URL;
14.
15. public class DownLoadThread extends Thread{
16. private int startIndex,endIndex,threadId;
17. private String urlString;
18. public DownLoadThread(int startIndex,int endIndex,String urlString,int threadId) {
19. this.endIndex=endIndex;
20. this.startIndex=startIndex;
21. this.urlString=urlString;
22. this.threadId=threadId;
23. }
24. @Override
25. public void run() {
26. try {
27.
28.
29.
30. URL url=new URL(urlString);
31. HttpURLConnection conn=(HttpURLConnection) url.openConnection();
32. conn.setRequestMethod("GET");
33. conn.setConnectTimeout(8000);
34. conn.setReadTimeout(8000);
35. conn.setRequestProperty("Range", "bytes="+startIndex+"-"+endIndex);//设置头信息属性,拿到指定大小的输入流
36. if (conn.getResponseCode()==206) {//拿到指定大小字节流,由于拿到的使部分的指定大小的流,所以请求的code为206
37. InputStream is=conn.getInputStream();
38. File file =new File("mobilelist.xml");
39. RandomAccessFile mAccessFile=new RandomAccessFile(file, "rwd");//"rwd"可读,可写
40. mAccessFile.seek(startIndex);//表示从不同的位置写文件
41. byte[] bs=new byte[1024];
42. int len=0;
43. int current=0;
44. while ((len=is.read(bs))!=-1) {
45. mAccessFile.write(bs,0,len);
46. current+=len;
47. System.out.println("第"+threadId+"个线程下载了"+current);
48.
49. }
50. mAccessFile.close();
51. System.out.println("第"+threadId+"个线程下载完毕");
52.
53. }
54. } catch (Exception e) {
55. e.printStackTrace();
56. }
57. super.run();
58. }
59. }
实现android apk通知栏版本升级:
private static String savePath;
private static String saveFileName;
private static int id =1;
private static NotificationManager mNotifyManager;
private static NotificationCompat.Builder mBuilder ;
savePath = Environment.getExternalStorageDirectory() + "/HJXimg/";
saveFileName = savePath + "huixueyun_app_pro.apk";
mNotifyManager = (NotificationManager) homeActivity.getSystemService(Context.NOTIFICATION_SERVICE);
mBuilder = new NotificationCompat.Builder(homeActivity);
mBuilder.setContentTitle("版本升级").setContentText("下载中,请稍等……").setSmallIcon(R.mipmap.yh_logo);
mBuilder.setProgress(100, 0,false);
mNotifyManager.notify(id, mBuilder.build());
HttpUtils utils = new HttpUtils();
utils.download(fileurl, saveFileName, new RequestCallBack<File>() {
@Override
public void onFailure(HttpException arg0, String arg1) {
// TODO Auto-generated method stub
Toast.makeText(homeActivity,"下载失败!",Toast.LENGTH_SHORT).show();
}
@Override
public void onSuccess(ResponseInfo<File> arg0) {
// TODO Auto-generated method stub
LLog.e("argo--");
mBuilder.setProgress(0, 0,false).setContentTitle("下载完成").setContentText("");
mNotifyManager.notify(id, mBuilder.build());
installApk(saveFileName,homeActivity);
}
@Override
public void onLoading(long total, long current, boolean isUploading) {
int currentNum = (int) (100*current/total);
mBuilder.setProgress(100, currentNum,false);
mNotifyManager.notify(id, mBuilder.build());
}
});
}
private static void installApk(String saveFileName, HomeActivity homeActivity) {
File apkfile = new File(saveFileName);
if (!apkfile.exists()) {
return;
}
Intent i = new Intent(Intent.ACTION_VIEW);
i.setDataAndType(Uri.parse("file://" + apkfile.toString()), "application/vnd.android.package-archive");
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
homeActivity.startActivity(i);
}