Android 实现文件的单线程断点续传下载
2017-01-09 20:45
375 查看
1.实际效果:
2.代码实现
完成这个小项目需要:基础网络知识(Http)
了解android界面处理机制
Service的绑定与解绑
BroadCastReceiver的注册与消息的处理
本地文件的I/O处理
数据库基础
事件回调原理
在这里我采用了数据库框架GreenDao,方便实现想要的效果,自己独立写几个类来操作SQLite数据库也是可以的。关于怎么使用GreenDao,这里不做叙述了,网上一大堆教程,这里给个教程链接http://m.blog.csdn.net/article/details?id=51893092。
下面是项目框架
主要有服务类DownloadService,广播类ProgressReceiver,下载类DownloadTask,线程信息类ThreadInfo,和MainActivity与App(继承Application,用于初始化数据库)。
首先来看看我的布局文件acitivty_main.xml:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" 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="com.chen.capton.filedownload.MainActivity"> <TextView android:id="@+id/info" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="文件信息:" android:layout_alignEnd="@+id/pause" /> <ProgressBar style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@+id/info" android:layout_alignParentStart="true" android:id="@+id/progressBar" /> <Button android:text="开始" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/progressBar" android:layout_alignParentStart="true" android:id="@+id/start" /> <Button android:text="暂停" android:layout_width="wrap_content" android:layout_height="wrap_content" android:enabled="false" android:id="@+id/pause" android:layout_below="@+id/progressBar" android:layout_toEndOf="@+id/start" /> <TextView android:text="进度" android:gravity="right" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:id="@+id/progressText" android:layout_alignParentEnd="true" android:layout_toEndOf="@+id/info" /> </RelativeLayout>
配置文件menifests.xml:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.chen.capton.filedownload"> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <application android:name=".App" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <service android:name=".DownloadService" android:enabled="true" android:exported="true"> </service> </application> </manifest>
最主要的MainActivity:
洋洋洒洒100多行
package com.chen.capton.filedownload; import android.content.ComponentName; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; public class MainActivity extends AppCompatActivity { private TextView fileInfoText,progressText; private Button startBtn,pauseBtn; private ProgressBar mProgressBar; private ProgressReceiver mReceiver; private final String REFRESH_PROGRESS="REFRESH_PROGRESS"; //设置Action,与ProgressReceiver,DownloadService中的Action都一致 private final String url="http://192.168.1.103/app.zip"; //设置url private final int maxProgress=100; //设置进度条最大进度 private boolean isContinue; //是否暂停的标识 private Handler handler=new Handler(){ public void handleMessage(Message msg){ mProgressBar.setProgress(msg.what);//由于发送的是空消息,直接用What作为进度参数使用 progressText.setText("完成度:"+msg.what+"%"); } }; //用于界面更新的handler。将它作为参数传递至ProgressReceiver,供其发送Message,然后根据Message更新进度 private DownloadService mService; private ServiceConnection conn=new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { //获取绑定好的DownloadService对象 mService= ((DownloadService.MyBinder)service).getService(); } @Override public void onServiceDisconnected(ComponentName name) {} }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); initComponent(); //初始化ProgressReceiver,DownloadService setContentView(R.layout.activity_main); initView(); //初始化视图控件 setListener(); //设置点击事件 } private void initView() { fileInfoText= (TextView) findViewById(R.id.info); progressText= (TextView) findViewById(R.id.progressText); startBtn= (Button) findViewById(R.id.start); pauseBtn= (Button) findViewById(R.id.pause); mProgressBar= (ProgressBar) findViewById(R.id.progressBar); mProgressBar.setMax(maxProgress); } /* * 初始化ProgressReceiver,DownloadService * 绑定DownloadService,注册ProgressReceiver * */ private void initComponent() { Intent intent=new Intent(this,DownloadService.class); bindService(intent,conn,BIND_AUTO_CREATE); mReceiver=new ProgressReceiver(handler); IntentFilter filter=new IntentFilter(); filter.addAction(REFRESH_PROGRESS); registerReceiver(mReceiver,filter); } private void setListener() { startBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(!isContinue) { mService.startMission(url,maxProgress); v.setEnabled(false); pauseBtn.setEnabled(true); fileInfoText.setText(getFileName(url)); }else { mService.continueMission(); v.setEnabled(false); pauseBtn.setEnabled(true); } } }); pauseBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mService.pauseMission(); v.setEnabled(false); startBtn.setEnabled(true); isContinue=true; startBtn.setText("继续"); } }); } /* * 获取文件名 * */ private String getFileName(String url){ int start=url.lastIndexOf("/")+1; int end=url.length(); return url.substring(start,end); } @Override protected void onDestroy() { /* * 解绑定DownloadService,注销ProgressReceiver * */ unregisterReceiver(mReceiver); unbindService(conn); super.onDestroy(); } }
用于初始化数据库的App类:
当然也可以在MainActivity中初始化数据库,为了代码简洁和更快地初始化数据库,就写着这里了。
package com.chen.capton.filedownload; import android.app.Application; import android.database.sqlite.SQLiteDatabase; /** * Created by CAPTON on 2017/1/9. */ public class App extends Application { public static App instances; @Override public void onCreate() { super.onCreate(); setDatabase(); instances = this; } public static App getInstances(){ return instances; } /** * 设置greenDao */ private DaoMaster.DevOpenHelper mHelper; private SQLiteDatabase db; private DaoMaster mDaoMaster; private DaoSession mDaoSession; private void setDatabase() { mHelper = new DaoMaster.DevOpenHelper(this, "Dishes-db", null); db = mHelper.getWritableDatabase(); mDaoMaster = new DaoMaster(db); mDaoSession = mDaoMaster.newSession(); } public DaoSession getDaoSession() { return mDaoSession; } public SQLiteDatabase getDb() { return db; } }
接下来就是贴各个类的代码了
服务类DownloadService:
package com.chen.capton.filedownload; import android.app.Service; import android.content.Intent; import android.os.Binder; import android.os.IBinder; /* * 实现DownloadTask.RefreshProgressListener接口 * */ public class DownloadService extends Service implements DownloadTask.RefreshProgressListener{ private final String REFRESH_PROGRESS="REFRESH_PROGRESS"; //设置Action private DownloadTask task; private Intent intent; @Override public IBinder onBind(Intent intent) { return new MyBinder(); } class MyBinder extends Binder { public DownloadService getService(){ intent=new Intent(); intent.setAction(REFRESH_PROGRESS); return DownloadService.this; } } /* * 供DownloadTask回调的方法,用于发送刷新进度的广播 * */ @Override public void refressProgress(int progress) { intent.putExtra("progress",progress); //将进度值写入intent sendBroadcast(intent); //发送广播 } public void startMission(String url,int maxProgress){ task=new DownloadTask(url,maxProgress); task.setRefreshProgressListener(this); task.startMission(); }; public void pauseMission(){ task.pauseMission(); } public void continueMission(){ task.continueMission(); } }
广播类ProgressReceiver:
package com.chen.capton.filedownload; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.os.Handler; /** * Created by CAPTON on 2017/1/9. */ public class ProgressReceiver extends BroadcastReceiver { private Handler handler; //保存从MainActivity传来的handler; private final String REFRESH_PROGRESS="REFRESH_PROGRESS";//设置Action public ProgressReceiver(Handler handler) { this.handler=handler; } @Override public void onReceive(Context context, Intent intent) { //判断Action是否一致 if(intent.getAction().equals(REFRESH_PROGRESS)){ //从传来的intent中获取进度值 int progress=intent.getIntExtra("progress",0); //将带有进度值的intent发送出去,交与MainActivity中的handler处理 handler.sendEmptyMessage(progress); } } }
下载类(重点)DownloadTask:
package com.chen.capton.filedownload; import android.os.Environment; import android.util.Log; import java.io.BufferedReader; import java.io.DataInputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.URL; /** * Created by CAPTON on 2017/1/9. */ public class DownloadTask { private String url; //下载地址 private ThreadInfo threadInfo;//线程信息 private ThreadInfoDao dao; //数据库入口对象 private DownloadThread thread; //下载线程 public boolean isPause;//是否断开连接的标志位,很关键,呵呵 private int maxProgress; //最大进度 public int filedLength; //文件长度 public int finishedLength; //完成的文件长度(待保存的文件进度) private RefreshProgressListener listener;//进度刷新的监听器用于调用Service的更新方法 public DownloadTask(String url,int maxProgress) { this.url = url; this.maxProgress=maxProgress; threadInfo=new ThreadInfo(); DaoSession session=App.getInstances().getDaoSession(); //从App中获取初始化好的DaoSession对象 dao=session.getThreadInfoDao(); thread=new DownloadThread(url,threadInfo,maxProgress); } /* * 开始下载线程 * */ public void startMission(){ thread.start(); } /* * 暂停任务,即跳出while循环,将线程信息保存到数据库 * */ public void pauseMission(){ isPause=true; ThreadInfo info=dao.queryBuilder().where(ThreadInfoDao.Properties.Id.eq(1)).build().unique(); //第一次保存进度时插入纪录,之后更新纪录即可 if(info==null) { info=new ThreadInfo(null, filedLength, finishedLength); dao.insert(info); }else { info.setFileLength(filedLength); info.setFinishedLength(finishedLength); dao.update(info); } } /* * 从数据库读取上次保存的线程信息,新建线程从指定位置下载剩下的部分 * */ public void continueMission(){ isPause=false; ThreadInfo info=dao.queryBuilder().where(ThreadInfoDao.Properties.Id.eq(1)).build().unique(); if(info!=null){ thread=new DownloadThread(url,info,maxProgress); thread.start(); } } /* * 下载线程,核心 * */ class DownloadThread extends Thread{ private String url; private ThreadInfo threadInfo; private HttpURLConnection conn; //httpUrl连接 private InputStream is; //输入流 private File fileDir; //建立一个文件夹存放文件 private File file; private RandomAccessFile raFile; //可随机读写的File类,实际又不是继承File,呵呵,断点续传必用。 private int maxProgress; public DownloadThread(String url, ThreadInfo threadInfo, int maxProgress) { this.url = url; this.threadInfo=threadInfo; this.maxProgress=maxProgress; fileDir=new File(Environment.getExternalStorageDirectory(),"test"); if(!fileDir.exists()){ fileDir.mkdir(); //第一次下载时,应该没有text目录,新建一个 } file=new File(fileDir,getFileName(url)); try { raFile=new RandomAccessFile(file,"rw"); //设置文件读写模式,"rw"为可读可写 } catch (FileNotFoundException e) { e.printStackTrace(); } } public void run(){ /* * 第一次连接,先获取文件长度,供后续设置文件的传输范围(byte) * */ try { URL Url = new URL(url); HttpURLConnection conn= (HttpURLConnection) Url.openConnection(); conn.setRequestMethod("GET"); conn.setReadTimeout(3000); filedLength=conn.getContentLength(); threadInfo.setFileLength(filedLength); conn.disconnect(); } catch (IOException e) { e.printStackTrace(); } /* * 第二次连接,根据文件长度,上次保存的进度,设置传输范围,建立连接开始下载 * */ try { URL Url=new URL(url); conn= (HttpURLConnection) Url.openConnection(); conn.setRequestMethod("GET"); //设置连接方式 conn.setReadTimeout(5000); //设置连接超时 //设置传输的范围,例如"bytes=0-1231540",‘-’后面没写说明结束端为传输文件的最后一字节 conn.setRequestProperty("Range","bytes="+threadInfo.getFinishedLength()+"-"+threadInfo.getFileLength()); conn.connect(); is=conn.getInputStream(); //从连接对象中获取输入流 //数据输入流,也可以用BufferedInputStream; DataInputStream dis=new DataInputStream(is); try { //设置一定的延时,等待服务器响应报文 Thread.sleep(1800); } catch (InterruptedException e) { e.printStackTrace(); } /* * 根据响应码判断是否成功连接服务器 * */ if(conn.getResponseCode()==HttpURLConnection.HTTP_OK|| conn.getResponseCode()==HttpURLConnection.HTTP_PARTIAL) { raFile.seek(threadInfo.getFinishedLength());//跳转至上一次暂停时保存的位置 byte[] b = new byte[1024]; //设置byte数组,大小适度即可; int len; //每次写入b中的实际字节数 long now=System.currentTimeMillis(); //设置循环初始时间 while ((len = dis.read(b)) != -1) { raFile.write(b, 0, len); //将保存在b中的数据写入文件 finishedLength += len; //累加下载长度 /* * 判断文件(下载)写入消耗的时间是否大于100ms,若是才跟新进度,不设置的话, * 刷新频率=文件长度(很大的数)/1kb,会明显地限制传输速度 * */ if(System.currentTimeMillis()-now>=100) { now=System.currentTimeMillis(); //根据公式算出实际进度大小,然后调用DownloadService的实现方法refressProgress(int progress); listener.refressProgress((int) ((long) finishedLength * maxProgress / threadInfo.getFileLength())); }else { /*当文件(下载)写入消耗的时间小于100ms时,判断是否下载完成,若是则把进度设置为最大这个判断存在的意义在于,当文件下载完全时,消耗时间又小于100ms,进度显示为100%, 若不设置,则进度显示会卡在90%-100%之间,文件越小,显示误差越大*/ if (finishedLength>=threadInfo.getFileLength()){ listener.refressProgress(maxProgress); } } if (isPause) { break; } } threadInfo.setFinishedLength(finishedLength); //保存下载信息 //写入完毕或者暂停则断开连接 is.close(); conn.disconnect(); } } catch (IOException e) { e.printStackTrace(); } } }; /* * 设置回调方法,和回调接口,让DownloadService实现接口用于刷新进度。 * */ public void setRefreshProgressListener(RefreshProgressListener listener){ this.listener=listener; } public interface RefreshProgressListener{ void refressProgress(int progress); } private String getFileName(String url){ int start=url.lastIndexOf("/")+1; int end=url.length(); return url.substring(start,end); } }
线程信息类ThreadInfo:
package com.chen.capton.filedownload; import org.greenrobot.greendao.annotation.Entity; import org.greenrobot.greendao.annotation.Id; import org.greenrobot.greendao.annotation.Generated; /** * Created by CAPTON on 2017/1/9. */ //声明此类是GreenDao框架的Entity实体类 @Entity public class ThreadInfo { //声明这是自增的唯一键 @Id private Long id; private int fileLength; private int finishedLength; /* *用Android Studio 点击Build选项下的"Make Project",后面的代码会自动生成 */ @Generated(hash = 956576157) public ThreadInfo(Long id, int fileLength, int finishedLength) { this.id = id; this.fileLength = fileLength; this.finishedLength = finishedLength; } @Generated(hash = 930225280) public ThreadInfo() { } public Long getId() { return this.id; } public void setId(Long id) { this.id = id; } public int getFileLength() { return this.fileLength; } public void setFileLength(int fileLength) { this.fileLength = fileLength; } public int getFinishedLength() { return this.finishedLength; } public void setFinishedLength(int finishedLength) { this.finishedLength = finishedLength; } }
代码就全部贴完了,至于思路是怎么屡清楚的,主要看你对各个知识点掌握的熟练程度。
3.设计思路
写这个小demo,我的思路是:❶,先要明确目标:文件的单线程断点续传,首先单线程先不管,后面还有多线程呢,断点续传是重点,说道断点续传你就应该明白要保存暂停时的进度了,保存进度需要用什么途径?SharePreference,SQLite(这里采用的途径),文件保存?如果是单线程,只需要保存一个进度信息,SharePreference是最方便了,把文件长度,已下载长度两个参数写进xml文件里就行了,需要续传文件时从xml里读就行了,当然这只是最简单的情况;如果是多线程下载时,比如10个线程,你就需要至少20个命名空间来保存参数,这样很麻烦,读写信息都很麻烦,不如数据库来的方便了。至于用文件存储信息就不要去想了,更麻烦,可以自行琢磨。
❷,信息的保存方式确定了,接下来是考虑如何下载文件了,当然不要去用什么框架来下载了,自己手写,从http连接开始写,到文件写入,关闭http连接为止,都自己写。这里Http连接用到的是HttpUrlConnection,也可以用HttpClient。文件的来源弄懂了,然后是文件输出,断点续传要用到RandomAccessFile这个类,可以实现文件的随机位置的读写,当然这个类并不是继承File类,而是实现 DataOutput, DataInput, Closeable这几个接口,所以我们在导入数据的时候用DataInputStream比较好,BufferdInputStream也是可以的。
❸文件如何下载写入和保存搞定了,接下来是进度刷新的问题,这个就涉及到Service,BroadCastReceiver,Handler这三大块的知识了,把这些基础知识先掌握好。其实我刚开始学的时候没用Service,BroadCastReceiver,直接在MainActivity里写,当然代码量就吓人了,结构也看起来很复杂,不过感觉下载速度确实最快(估计是Service不用一直发送消息给BroadCastReceiver,发送一个消息虽然很快,但是进度是不断刷新的,积少成多,我们的设备要处理的消息就多了,效率就下降了,从而影响文件传输速度了)。
❹各大类的调用关系:
(1)MainActivity(点击”开始”,”暂停”按钮)调用DownloadService内的方法
(2)DownloadService继续调用DownloadTask的相应方法
(3)DownloadTask开始其中的下载线程,线程下载一定字节的后,回调DownloadService的refressProgress(int progress)方法
(4)refressProgress方法发送广播给ProgressReceiver,ProgressReceiver根据发来的信息通过Handler转发到MainActivity的Handler中
(5)MainActivity中的Handler收到最终消息,更新UI。
若是点击”暂停”按钮,“开始”按钮变为“继续”按钮,->(3)中跳出文件读取的循环,并把进度写入数据库,再次点击“继续”按钮续传文件,DownloadTask读取数据库数据,重新开始(3)(4)(5)。
前面的是大致思路,具体细节要深入到代码里去剖析,如果你是大神,余光一瞥就能理解每一行代码的用意;如果你对各个知识掌握的还不够熟练,可能就卡在某处了。几乎所有重要的方法和类我都一一注释了其用意了,剩下一些繁文缛节就没有叙述了,希望大家都能明白。
建议:
❶最好在个人电脑上构建一个局域网服务器,通过局域网来测试下载任务(不用流量),我用的是WAMP,简单粗暴,把文件丢进根目录下的“www”目录即可通过类似“http://192.168.1.10x/xxx.xxx“的url找到你的文件。建好服务器启动后发现手机无法访问地址,可以试试关闭电脑的防火墙(百试不厌),当然用完最好恢复回去。
❷文件最好选择一些设备需要检查其完整性的格式,如zip,rar,apk, 尝试打开文件来检验是否下载完整
❸手机最好已经root,方便查看数据库的信息(注意:GreenDao输出的数据库格式可能不是以db结尾(受初始化时的命名决定),手动改成”.db”结尾就可以打开了)。这里推荐“RE文件管理器”这款软件来查看root后的手机文件夹。当然不root的话,如果是数据出错,就手动写代码检查数据库喽。
4.相关文件
单线程断点续传 demo apk 链接:http://pan.baidu.com/s/1sltoHrB 密码:v4tj完整项目demo 链接:http://pan.baidu.com/s/1kVBbTcj 密码:qcb7
相关文章推荐
- Android 实现文件的下载
- Android 实现文件的下载
- struts2中通过Action以InputStream的下载文件以及在iOS以及Android端接收的实现方式
- Android文件下载进度条的实现
- android 之文件下载的实现思路
- Android文件下载进度条的实现代码
- Android实现单线程异步多文件下载的代码
- Android实现文件下载
- Android文件下载进度条的实现
- Android实现单一文件下载
- Android 实现文件的下载
- Android中如何实现文件下载
- Android通过http下载文件实现小记
- Android文件下载(实现断点续传)
- Android 利用缓存机制实现文件下载
- Android 实现apk文件下载并自动安装
- android自定义Dialog实现文件下载和下载进度
- Android中如何实现文件下载
- Android文件下载(实现断点续传)的学习
- struts2中通过Action以InputStream的下载文件以及在iOS以及Android端接收的实现方式