详解Android中的多线程断点下载
首先来看一下多线程下载的原理。多线程下载就是将同一个网络上的原始文件根据线程个数分成均等份,然后每个单独的线程下载对应的一部分,然后再将下载好的文件按照原始文件的顺序“拼接”起来就构
成了完整的文件了。这样就大大提高了文件的下载效率。对于文件下载来说,多线程下载是必须要考虑的环节。
多线程下载大致可分为以下几个步骤:
一.获取服务器上的目标文件的大小
显然这一步是需要先访问一下网络,只需要获取到目标文件的总大小即可。目的是为了计算每个线程应该分配的下载任务。
二.在本地创建一个跟原始文件同样大小的文件
在本地可以通过RandomAccessFile创建一个跟目标文件同样大小的文件,该api支持文件任意位置的读写操作。这样就给多线程下载提供了方便,每个线程只需在指定起始和结束脚标范围内写数据即可。
三.计算每个线程下载的起始位置和结束位置
我们可以把原始文件当成一个字节数组,每个线程只下载该“数组”的指定起始位置和指定结束位置之间的部分。在第一步中我们已经知道了“数组”的总长度。因此只要再知道总共开启的线程的个数就好计算每个线程要下载的范围了。每个线程需要下载的字节个数(blockSize)=总字节数(totalSize)/线程数(threadCount)。 假设给线程按照0,1,2,3...n的方式依次进行编号,那么第n个线程下载文件的范围为:
起始脚标startIndex=n*blockSize。
结束脚标endIndex=(n-1)*blockSize-1。
考虑到totalSize/threadCount不一定能整除,因此对已最后一个线程应该特殊处理,最后一个线程的起始脚标计算公式不变,但是结束脚标为endIndex=totalSize-1即可。
四.开启多个子线程开始下载
在子线程中实现读流操作,将conn.getInputStream()读到RandomAccessFile中。
五.记录下载进度
为每一个单独的线程创建一个临时文件,用于记录该线程下载的进度。对于单独的一个线程,每下载一部分数据就在本地文件中记录下当前下载的字节数。这样子如果下载任务异常终止了,那么下次重新开始下载时就可以接着上次的进度下载。
六.删除临时文件
当多个线程都下载完成之后,最后一个下载完的线程将所有的临时文件删除。
Android有界面可以跟用户进行良好的交互,在界面上让用户输入原文件地址、线程个数,然后点击确定开始下载。为了让用户可以清晰的看到每个线程下载的进度根据线程个数动态的生成等量的进度条(ProgressBar)。ProgressBar是一个进度条控件,用于显示一项任务的完成进度。其有两种样式,一种是圆形的,该种样式是系统默认的,由于无法显示具体的进度值,适合用于不确定要等待多久的情形下;另一种是长条形的,此类进度条有两种颜色,高亮颜色代表任务完成的总进度。对于我们下载任务来说,由于总任务(要下载的字节数)是明确的,当前已经完成的任务(已经下载的字节数)也是明确的,因此特别适合使用后者。由于在我们的需求里ProgressBar是需要根据线程的个数动态添加的,而且要求是长条形的。因此可以事先在布局文件中编写好ProgressBar的样式。当需要用到的时候再将该布局填充起来。ProgressBar的max属性代表其最大刻度值,progress属性代表当前进度值。使用方法如下:
ProgressBar.setMax(intmax);设置最大刻度值。
ProgressBar.setProgress(intprogress);设置当前进度值。
给ProgressBar设置最大刻度值和修改进度值可以在子线程中操作的,其内部已经特殊处理过了,因此不需要再通过handler发送Message让主线程修改进度。
下面就给出我们自己写的安卓环境下的多线程。
多线程下载界面布局如下,三个进度条分别表示三个子线程的下载进度。
<LinearLayoutxmlns: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" android:paddingBottom="@dimen/activity_vertical_margin"android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context=".MainActivity"> <EditText android:id="@+id/et_path" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入要下载的文件资源路径" android:text="http://192.168.1.104:8080/gg.exe"/> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="download" android:text="下载"/> <ProgressBar android:id="@+id/pb0" style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="wrap_content"/> <ProgressBar android:id="@+id/pb1" style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="wrap_content"/> <ProgressBar android:id="@+id/pb2" style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="wrap_content"/> </LinearLayout>
多线程下载的内部逻辑如下,其实这在开头已经有了,只不过是代码的实现了。
publicclassMainActivityextendsActivity{ privateEditTextet_path; privateProgressBarpb0; privateProgressBarpb1; privateProgressBarpb2; /** *开启几个线程从服务器下载数据 */ publicstaticintthreadCount=3; publicstaticintrunningThreadCount; privateStringpath; @Override protectedvoidonCreate(BundlesavedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //初始化控件 et_path=(EditText)findViewById(R.id.et_path); pb0=(ProgressBar)findViewById(R.id.pb0); pb1=(ProgressBar)findViewById(R.id.pb1); pb2=(ProgressBar)findViewById(R.id.pb2); } //下载按钮的点击事件 publicvoiddownload(Viewview){ path=et_path.getText().toString().trim(); if(TextUtils.isEmpty(path)||(!path.startsWith("http://"))){ Toast.makeText(this,"对不起路径不合法",0).show(); return; } newThread(){ publicvoidrun(){ try{ //1.获取服务器上的目标文件的大小 URLurl=newURL(path); HttpURLConnectionconn=(HttpURLConnection)url.openConnection(); conn.setConnectTimeout(5000); conn.setRequestMethod("GET"); intcode=conn.getResponseCode(); if(code==200){ intlength=conn.getContentLength(); System.out.println("服务器文件的长度为:"+length); //2.在本地创建一个跟原始文件同样大小的文件 RandomAccessFileraf=newRandomAccessFile(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+getFileName(path),"rw"); raf.setLength(length); raf.close(); //3.计算每个线程下载的起始位置和结束位置 intblocksize=length/threadCount; runningThreadCount=threadCount; for(intthreadId=0;threadId<threadCount;threadId++){ intstartIndex=threadId*blocksize; intendIndex=(threadId+1)*blocksize-1; if(threadId==(threadCount-1)){ endIndex=length-1; } //4.开启多个子线程开始下载 newDownloadThread(threadId,startIndex,endIndex).start(); } } }catch(Exceptione){ e.printStackTrace(); } }; }.start(); } privateclassDownloadThreadextendsThread{ /** *线程id */ privateintthreadId; /** *线程下载的理论开始位置 */ privateintstartIndex; /** *线程下载的结束位置 */ privateintendIndex; /** *当前线程下载到文件的那一个位置了. */ privateintcurrentPosition; publicDownloadThread(intthreadId,intstartIndex,intendIndex){ this.threadId=threadId; this.startIndex=startIndex; this.endIndex=endIndex; System.out.println(threadId+"号线程下载的范围为:"+startIndex +"~~"+endIndex); currentPosition=startIndex; } @Override publicvoidrun(){ try{ URLurl=newURL(path); HttpURLConnectionconn=(HttpURLConnection)url.openConnection(); //检查当前线程是否已经下载过一部分的数据了 Fileinfo=newFile(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+threadId+".position"); RandomAccessFileraf=newRandomAccessFile(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+getFileName(path),"rw"); if(info.exists()&&info.length()>0){ FileInputStreamfis=newFileInputStream(info); BufferedReaderbr=newBufferedReader(newInputStreamReader(fis)); currentPosition=Integer.valueOf(br.readLine()); conn.setRequestProperty("Range","bytes="+currentPosition+"-"+endIndex); System.out.println("原来有下载进度,从上一次终止的位置继续下载"+"bytes="+currentPosition+"-"+endIndex); fis.close(); raf.seek(currentPosition);//每个线程写文件的开始位置都是不一样的. }else{ //告诉服务器只想下载资源的一部分 conn.setRequestProperty("Range","bytes="+startIndex+"-"+endIndex); System.out.println("原来没有有下载进度,新的下载"+"bytes="+startIndex+"-"+endIndex); raf.seek(startIndex);//每个线程写文件的开始位置都是不一样的. } InputStreamis=conn.getInputStream(); byte[]buffer=newbyte[1024]; intlen=-1; while((len=is.read(buffer))!=-1){ //把每个线程下载的数据放在自己的空间里面. //System.out.println("线程:"+threadId+"正在下载:"+newString(buffer)); raf.write(buffer,0,len); //5.记录下载进度 currentPosition+=len; Filefile=newFile(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+threadId+".position"); RandomAccessFilefos=newRandomAccessFile(file,"rwd"); //System.out.println("线程:"+threadId+"写到了"+currentPosition); fos.write(String.valueOf(currentPosition).getBytes()); fos.close();//fileoutstream数据是不一定被写入到底层设备里面的,有可能是存储在缓存里面. //raf的rwd模式,数据是立刻被存储到底层硬盘设备里面. //更新进度条的显示 intmax=endIndex-startIndex; intprogress=currentPosition-startIndex; if(threadId==0){ pb0.setMax(max); pb0.setProgress(progress); }elseif(threadId==1){ pb1.setMax(max); pb1.setProgress(progress); }elseif(threadId==2){ pb2.setMax(max); pb2.setProgress(progress); } } raf.close(); is.close(); System.out.println("线程:"+threadId+"下载完毕了..."); Filef=newFile(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+threadId+".position"); f.renameTo(newFile(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+threadId+".position.finish")); synchronized(MainActivity.class){ runningThreadCount--; //6.删除临时文件 if(runningThreadCount<=0){ for(inti=0;i<threadCount;i++){ Fileft=newFile(Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+i+".position.finish"); ft.delete(); } } } }catch(Exceptione){ e.printStackTrace(); } } } /** *获取一个文件名称 *@parampath *@return */ publicStringgetFileName(Stringpath){ intstart=path.lastIndexOf("/")+1; returnpath.substring(start); } }
最后别忘了添加权限,在该工程中不仅用到了网络访问还用到了sdcard存储,因此需要添加两个权限。
<uses-permissionandroid:name="android.permission.INTERNET"/> <uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
另外,xUtils同样可以实现多线程下载。xUtils是开源免费的Android工具包,代码托管在github上。目前xUtils主要有四大模块:DbUtils模块,主要用于操作数据库的框架。ViewUtils模块,通过注解的方式可以对UI,资源和事件绑定进行管理。HttpUtils模块,提供了方便的网络访问,断点续传等功能。BitmapUtils模块,提供了强大的图片处理工具。我们在这里只简单实用xUtils工具中的HttpUtils工具。第三方包的使用较为简单,直接拷贝xUtils的jar包到libs目录下,然后添加依赖。
接下来就可以使用xUtils中的httpUtils的功能了:
HttpUtilshttp=newHttpUtils(); /** *参数1:原文件网络地址 *参数2:本地保存的地址 *参数3:是否支持断点续传,true:支持,false:不支持 *参数4:回调接口,该接口中的方法都是在主线程中被调用的, *也就是该接口中的方法都可以修改UI */ http.download(path,"/mnt/sdcard/xxx.exe",true,newRequestCallBack<File>(){ //下载成功后调用一次 @Override publicvoidonSuccess(ResponseInfo<File>arg0){ Toast.makeText(MainActivity.this,"下载成功",0).show(); } /** *每下载一部分就被调用一次,通过该方法可以知道当前下载进度 *参数1:原文件总字节数 *参数2:当前已经下载好的字节数 *参数3:是否在上传,对于下载,该值为false */ @Override publicvoidonLoading(longtotal,longcurrent,booleanisUploading){ pb0.setMax((int)total); pb0.setProgress((int)current); super.onLoading(total,current,isUploading); } //失败后调用一次 @Override publicvoidonFailure(HttpExceptionarg0,Stringarg1){ Toast.makeText(MainActivity.this,"下载失败"+arg1,0).show(); } });
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持毛票票!