异步网络下载案例(AsyncTask + 前台Service +
ServiceBestPractice项目(模块)GitHub地址
案例代码逻辑概述
interface DownloadListener
回调机制核心接口
class DownloadTask extends AsyncTask<String, Integer, Integer>
描述异步网络下载逻辑(网络请求,文件线上状态处理,文件本地状态处理,文件写入本地),
抽象调用接口对象方法;
class DownloadService extends Service
匿名类
方式具体实现回调接口的方法
而后将这个匿名类放入一个接口类实例
中
(回调方法负责状态处理,方式是:Toast、对downloadTask归为、开关通知等);(Binder
类定义中)- 实例化
DownloadTask
,把实现好的接口类实例传进去DownloadTask
的构造器;
为外部(如Activity)调用准备好业务Binder
实例class DownloadBinder extends Binder
;- 封装
NotificationManager
以及Notification
;getNotificationManager()
getNotification(String title, int progress)
- 在
Binder
类定义中执行DownloadTask
实例downloadTask.execute(downloadUrl);
并完成开关通知、删除文件逻辑;getNotificationManager();// 配置 NotificationManager!!!!!!!! startForeground(1, getNotification("Downloading...", 0)); ------------------------------------ file.delete(); ... getNotificationManager().cancel(1); stopForeground(true);
class MainActivity extends AppCompatActivity implements View.OnClickListener
- 实例化UI(主要是按钮);
- 启动、绑定、解绑服务;
startForegroundService(intent); startService(intent); unbindService(connection);
- 运行时权限动态申请;
- 准备监听事件,事件中通过
Service
的binder
对象来产生业务;
开始实战
- 创建ServiceBestPractice项目或模块。
首先在/build.gradle中dependencies下添加OKHttp库依赖(网络相关功能使用):
implementation 'com.squareup.okhttp3:okhttp:4.2.2'
- 运用回调机制编程,
定义一个回调接口,
用于对下载过程中的各种状态进行监听和回调:
(在DownloadTask的onPostExecute中抽象调用,
在DownloadService中具体实现)
public interface DownloadListener {
void onProgress(int progress);//通知当前下载进度
void onSuccess();//通知下载成功事件
void onFailed();//通知下载失败事件
void onPaused();//通知下载暂停事件
void onCanceled();//通知下载取消事件
}
- 编写下载功能,新建
DownloadTask类
继承自AsyncTask
:
/**
* <pre>
* author : 李蔚蓬(简书_凌川江雪)
* time : 2019/11/9 17:29
* desc :三个泛型参数,
* 第一个表示在执行AsyncTask时需传入一个字符串参数给后台任务,
* 第二个使用整型数据最为进度显示单位,
* 第三个表示使用整型数据来反馈结果执行
* </pre>
*/
public class DownloadTask extends AsyncTask<String, Integer, Integer> {
//定义四个整型常量分别表示下载的不同状态
public static final int TYPE_SUCCESS = 0;//表示下载取消
public static final int TYPE_FAILED = 1;//表示下载失败
public static final int TYPE_PAUSE = 2;//表示下载暂停
public static final int TYPE_CANCELED = 3;//表示下载取消
private DownloadListener listener;
//取消位以及暂停位
// 由外部调用,在doInBackground()中生效
private boolean isCanceled = false;
private boolean isPaused = false;
private int lastProgress;//记录上次的进度
//构造方法
public DownloadTask(DownloadListener listener){
//将下载的状态通过此参数进行回调,此处负责调用,外部具体编写逻辑
this.listener = listener;
}
//在后台执行具体的下载逻辑
// String... params:可变长参数列表,必须是String类型,转化为数组处理
@Override
protected Integer doInBackground(String... params) {
InputStream is = null;
RandomAccessFile savedFile = null;
File file = null;
try{
long downloadedLength = 0;//记录 已下载的文件 长度!!!!!!!
String downloadUrl = params[0];//获取 下载的URL地址!!!!!!!!!
// 根据URL地址解析出下载的文件名
String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
// 指定文件下载到 Environment.DIRECTORY_DOWNLOADS 目录下,即SD卡的Download目录
String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
//用以上的 文件下载路径 以及 要下载的文件名 得到 file句柄!!!!!!!!!!!!
file = new File(directory + fileName);
//判断是否已存在要下载的文件,
// 存在则 读取 已下载的字节数(以 启用 断点续传 功能)
if (file.exists()){
downloadedLength = file.length();
}
//获取 待下载文件 的总长度!!!!!!
// 判断 文件情况—— 有问题 或者 已下载完毕!!!!!
long contentLength = getContentLength(downloadUrl);
if (contentLength == 0){//总长度为0,说明文件有问题
return TYPE_FAILED;
}else if (contentLength == downloadedLength){//已下载字节和文件总字节相等,说明已经下载完成了
return TYPE_SUCCESS;
}
//注意这里,断点续传 功能!!!!!!!!!!
//使用.addHeader 往请求中添加一个Header,用于告诉服务器我们想要
// 从哪个字节开始下载(已下载部分不需再重新下载)
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.addHeader("RANGE", "bytes=" + downloadedLength + "-")
.url(downloadUrl)
.build();
Response response = client.newCall(request).execute();//得到服务器响应的数据
//使用 Java文件流方式 不断从网络上 读取数据!!
// 不断写入到本地,
// 直到文件全部下载完为止!!
if (response != null){
is = response.body().byteStream();
savedFile = new RandomAccessFile(file, "rw");//封装本地文件句柄
savedFile.seek(downloadedLength);//跳过已下载的字节
byte[] b = new byte[1024];
int total = 0; //本轮!!!下载的总长度!!
int len;
//使用 Java文件流方式 不断从网络上 读取数据!!
// 不断写入到本地,直到文件全部下载完为止!!
while ((len = is.read(b)) != -1){
//判断用户有没触发暂停或取消操作,如果有则返回相应值来中断下载
if (isCanceled){
return TYPE_CANCELED;
}else if (isPaused){
return TYPE_PAUSE;
}else {
//用户没有触发暂停或取消操作,继续下载
total += len;
savedFile.write(b, 0, len);
//计算已下载的百分比 == (本轮下载的长度 + 已经下载的长度)/ 要下载的 文件总长度
int progress = (int) ((total + downloadedLength) * 100 / contentLength);
publishProgress(progress);//抛出进度给 onProgressUpdate(),回调之!!!!
}
}
//执行到此,说明以上循环已执行完毕,文件下载完毕
response.body().close();
return TYPE_SUCCESS;
}
} catch (Exception e) {
e.printStackTrace();
}finally {
//分开关闭资源!!!!!!
try {
if (is != null){
is.close();
}
if (savedFile != null){
savedFile.close();
}
if (isCanceled && file != null){
file.delete();
}
} catch (Exception e) {
e.printStackTrace();
}
}
//不从上面成功退出则执行至此,证明失败!!!
return TYPE_FAILED;
}
/**
* 在界面更新当前的下载进度
*
* doInBackground()的每一次!!!while 读 输入流 ,
* 写入file,都会publishProgress(progress); 抛出进度
* 此时就会回调此方法!!! 对进度进行处理!!!
*
* @param values
*/
@Override
protected void onProgressUpdate(Integer... values) {
//获取当前下载进度,
// 参数来自 doInBackground()中 publishProgress()抛出的进度
int progress = values[0];
if (progress > lastProgress){//与上一次下载进度对比
listener.onProgress(progress);//有变化则调用DownloadListener的onProgress()通知下载进度更新
lastProgress = progress;//更新记录
}
}
/**
* 通知最终的下载结果
*
* 当任务执行完了,即doInBackground()一旦return,
* 其return的值就会传到这里,作为参数,
* 参数类型即定义泛型时的第三个参数
*
* 这里用了回调机制,listener负责抽象调用!!!
* 外部负责具体实现!!!
*/
@Override
protected void onPostExecute(Integer status) {
switch (status){//根据传入的下载状态进行回调
case TYPE_SUCCESS:
listener.onSuccess();
break;
case TYPE_FAILED:
listener.onFailed();
break;
case TYPE_PAUSE:
listener.onPaused();
break;
case TYPE_CANCELED:
listener.onCanceled();
break;
default:
break;
}
}
//取消位以及暂停位
// 由外部调用,在doInBackground()中生效
public void pauseDownload(){
isPaused = true;
}
public void cancelDownload(){
isCanceled = true;
}
private long getContentLength(String downloadUrl) throws IOException {
//请求得到需下载的文件
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(downloadUrl).build();
Response response = client.newCall(request).execute();
//得到文件长度
if (response != null && response.isSuccessful()){
long contentLength = response.body().contentLength();
response.close();
return contentLength;
}
return 0;
}
}
普及:关于RandomAccessFile
Java除了File类之外,还提供了专门处理文件的类,
即RandomAccessFile(随机访问文件)类。
该类是Java语言中功能最为丰富的文件访问类,
它提供了众多的文件访问方法。
RandomAccessFile类支持“随机访问”方式,
这里“随机”是指可以跳转到文件的任意位置处读写数据。
在访问一个文件的时候,不必把文件从头读到尾,
而是希望像访问一个数据库一样“随心所欲”地访问一个文件的某个部分,
这时使用RandomAccessFile类就是最佳选择。RandomAccessFile对象类有个位置指示器,指向当前读写处的位置,
当前读写n个字节后,文件指示器将指向这n个字节后面的下一个字节处
。
刚打开文件时,文件指示器指向文件的开头处,
可以移动文件指示器到新的位置,随后的读写操作将从新的位置开始。
RandomAccessFile类在数据等长记录格式文件的随机(相对顺序而言)读取时有很大的优势,
但该类仅限于操作文件,
不能访问其他的I/O设备,如网络、内存映像等;
RandomAccessFile
对象,
当前读写(read/write)
n个字节后,
文件指示器
将自动指向这n个字节后面的下一个字节处
。
RandomAccessFile
是面向文件(file对象)的,可以用来读写本地SD、硬盘;
BufferReader、BufferWriter
也有类似的指示器
,
使用readline()
、write()
,读写(read/write)
n个字节后,
指示器
将自动指向这n个字节后面的下一个字节处
。
只不过BufferReader、BufferWriter
面向的是IO流。
-
为了保证DownloadTask可一直在后台运行,
需创建一个下载的服务DownloadService
,
public class DownloadService extends Service {
private DownloadTask downloadTask;
private String downloadUrl;
private String notificationId = "nyd001";
private String notificationName = "downloadTask";
/**
* 创建DownloadListener 匿名内部类实例,
* 然后赋值给其父类类型DownloadListener引用
*
* 这里实现的方法!!
* 直接在DownloadTask 的 onPostExecute()中被调用
*
* 而onPostExecute() 中要调用那个回调方法
*
* 则由doInBackground() 的返回值位决定
*
* 而doInBackground() 的返回值 中
* 成功位 和 失败位 是 客观判断的结果
* 暂停位 和 取消位 可以 由人为点击置位
*/
private DownloadListener listener = new DownloadListener() {
/**
* 在 DownloadTask 中的 onProgressUpdate()处调用
* @param progress 来自对应的DownloadTask 的 doInBackground() 中的 publishProgress(progress);
*/
@Override
public void onProgress(int progress) {
//getNotification()是自定义的封装方法,
// 其中构造了一个用于显示下载进度的通知,
//调用NotificationManager的 notify() 去触发这个通知,
// 这样就可以在下拉状态栏中实时看到当前的下载进度了
getNotificationManager().notify(1, getNotification("Downloading...", progress));
}
@Override
public void onSuccess() {
downloadTask = null;
//下载成功时将正在下载的前台服务通知关闭
stopForeground(true);
//创建一个下载成功的通知
getNotificationManager().notify(1, getNotification("Download Success", -1));
Toast.makeText(DownloadService.this, "Download Success", Toast.LENGTH_SHORT).show();
}
@Override
public void onFailed() {
downloadTask = null;
//下载失败时将前台服务通知关闭,并创建一个下载失败的通知,
// !!!!!后面几个方法(暂停、取消)的逻辑 与此类似!!!!
stopForeground(true);
getNotificationManager().notify(1, getNotification("Download Failed", -1));
Toast.makeText(DownloadService.this, "Download Failed", Toast.LENGTH_SHORT).show();
}
@Override
public void onPaused() {
downloadTask = null;
Toast.makeText(DownloadService.this, "Paused", Toast.LENGTH_SHORT).show();
}
@Override
public void onCanceled() {
downloadTask = null;
stopForeground(true);
Toast.makeText(DownloadService.this, "Canceled", Toast.LENGTH_SHORT).show();
}
};
/**
* 创建DownloadBinder内部类,
* 把需要放给外部调用的Service服务方法写好,
* 实例化一个DownloadBinder内部类示例,在onBind()中返回,
* 这样,
* 当外部界面与本Service绑定,
* 就可以在 ServiceConnection实例 的 onServiceConnected 回调方法中,
* 获得这个 具备了 各种准备好的业务方法的 DownloadBinder(Binder、IBinder)实例了
*
*/
private DownloadBinder mBinder = new DownloadBinder();
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
//创建DownloadBinder内部类,
//把需要放给外部调用的Service服务方法写好
class DownloadBinder extends Binder {
/**
* 开启下载任务
* @param url 要下载的资源地址
*/
public void startDownload(String url){
if (downloadTask == null){
downloadUrl = url;
//创建DownloadTask实例
downloadTask = new DownloadTask(listener);
//传入下载地址,启动下载任务!!!!
downloadTask.execute(downloadUrl);
//让这个下载任务服务成为一个前台服务!!!
// 使用时在Activity处 先 startService(intent); 启动! 本服务DownloadService
//
// 然后 绑定本服务 bindService(intent, connection, BIND_AUTO_CREATE);!!!!
// 再调用本方法 downloadBinder【即这里的mBinder】.startDownload(url);
// 运行到下面的startForeground()!!
// 从而使刚刚已经启动(start)的服务变成前台服务!!!!!
//这样就会在 系统状态栏 中 创建一个持续运行的通知了
// .
// 注意这里有个id!!! 后续取消时 可以用!!
getNotificationManager();// 配置 NotificationManager!!!!!!!!
startForeground(1, getNotification("Downloading...", 0));
//!!!!!!!!!!!
Toast.makeText(DownloadService.this, "Downloading...", Toast.LENGTH_SHORT).show();
}
}
public void pauseDownload(){
if (downloadTask != null){
//使下载任务downloadTask 的 暂停位 置位
downloadTask.pauseDownload();
}
}
public void cancelDownload(){
if (downloadTask != null){
//首先,使下载任务downloadTask 的 取消位 置位,终止下载!!!!
downloadTask.cancelDownload();
//调用流程:
// downloadTask.cancelDownload();
// --> isCanceled = true; 取消位 置位
// .
// -->downloadTask 的 doInBackground 中 取消位 置位生效
// doInBackground() 中的 下载文件的while循环中
// if (isCanceled){ return TYPE_CANCELED;} 返回取消位 并终止下载!!!
// .
// -->onPostExecute() 接收到 doInBackground()返回的取消位
// (只要onPostExecute() 接收到了取消位, 便已经终止下载了!! 这时候回调接口...)
// .
// --> listener.onCanceled(); 回调 接口的 取消方法 ,
// 即这里 DownloadService 实现的方法, 接着进行下一步操作...
// .
// --> downloadTask = null;
}else {
//如果 downloadTask = null; 则 执行到此
//纵观 接口处几个方法 无论成功、失败、暂停、取消
// 都会执行 downloadTask = null;
// .
// 也就是说 只要 downloadTask 调用过 一次 接口方法!!!!
// 之后再调用 downloadBinder.cancelDownload(); 的话,
// 都会已 downloadTask = null;
// 即 会执行至此, 删除文件,关闭通知 !!!
if (downloadUrl != null){
//取消下载时需将文件删除,并将通知关闭
//获取file 的过程 同DownloadTask 的 doInBackground()
String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));//得到文件名
String directroy = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
File file = new File(directroy + fileName);
if (file.exists()){
file.delete();
}
//取消对应id 前台通知或者服务
getNotificationManager().cancel(1);
stopForeground(true);
Toast.makeText(DownloadService.this, "Canceled", Toast.LENGTH_SHORT).show();
}
}
}
}
//封装 NotificationManager
private NotificationManager getNotificationManager(){
NotificationManager notificationManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(notificationId, notificationName, NotificationManager.IMPORTANCE_HIGH);
notificationManager.createNotificationChannel(channel);
return notificationManager;
} else {
return notificationManager;
}
}
/**
* 封装进度条通知
* 返回一个封装配置好的 Notification
*
* Notification
* 遇 startForeground() 则成前台服务!!!
* 遇 NotificationManager.notify() 则成通知!!!
*/
private Notification getNotification(String title, int progress){
Intent intent = new Intent(this, MainActivity.class);
PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);
//拿着Notification 的 建造者Builder, 去各种配置(set()),
// 配置完毕了,调用builder.build(),返回 一个 Notification !!!
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, notificationId);
builder.setSmallIcon(R.mipmap.ic_launcher);
builder.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher));
builder.setContentIntent(pi);
builder.setContentTitle(title);
// if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// builder.setChannelId(notificationId);
// }
if (progress > 0){
//当progress大于或等于0时才需显示下载进度
builder.setContentText(progress + "%");
builder.setProgress(100, progress, false);//三个参数:通知的最大进度,通知的当前进度,是否使用模糊进度条
}
return builder.build();
}
}
.
普及
- 后端基本完成,编写前端,修改布局文件,
放置三个按钮分别用于开始下载、暂停下载和取消下载。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns: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"
tools:context=".MainActivity">
<Button
android:id="@+id/start_download"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start Download"/>
<Button
android:id="@+id/pause_download"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Pause Download"/>
<Button
android:id="@+id/cancel_download"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Cancel Download"/>
</LinearLayout>
修改MainActivity:
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
private DownloadService.DownloadBinder downloadBinder;
//创建了一个ServiceConnection 的 匿名内部类,
// 重写方法后 赋值给ServiceConnection 实例
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
downloadBinder = (DownloadService.DownloadBinder) service;
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initViews();
}
private void initViews() {
//初始化 UI 按钮
Button startDownload = (Button) findViewById(R.id.start_download);
Button pauseDownload = (Button) findViewById(R.id.pause_download);
Button cancelDownload = (Button) findViewById(R.id.cancel_download);
startDownload.setOnClickListener(this);
pauseDownload.setOnClickListener(this);
cancelDownload.setOnClickListener(this);
//启动服务 以及 绑定服务 二者在这里 缺一不可
Intent intent =new Intent(this, DownloadService.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent);//启动服务,保证服务一直在后台运行!!!
} else {
startService(intent);
}
bindService(intent, connection, BIND_AUTO_CREATE);//绑定服务,让MainActivity和服务进行通信!!!
//运行时权限申请
if (ContextCompat.checkSelfPermission(MainActivity.this,
Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(MainActivity.this, new String[]{
Manifest.permission.WRITE_EXTERNAL_STORAGE,}, 1);
}
}
@Override
public void onClick(View v) {
if (downloadBinder == null){
return;
}
switch (v.getId()){
case R.id.start_download:
String url = "https://raw.githubusercontent.com/guolindev/eclipse/master/eclipse-inst-win64.exe";
downloadBinder.startDownload(url);
break;
case R.id.pause_download:
downloadBinder.pauseDownload();
break;
case R.id.cancel_download:
downloadBinder.cancelDownload();
break;
default:
break;
}
}
//运行时权限申请结果
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode){
case 1:
for (int grantResult : grantResults) {
if (grantResult != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "拒绝权限将无法使用程序", Toast.LENGTH_SHORT).show();
finish();
}
}
break;
default:
}
}
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(connection);//解绑服务,避免内存泄漏
}
}
- 声明权限
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
注意Android 8.0 之后,开启前台服务需要关注一下几点!!!
开启服务需要用
startForegroundService(intent)
,
不能用startService(intent);
且调用完startForegroundService(intent)
之后,
五秒内需要调用startForeground()
!!!
否则app可能会ANR!
实战如上,
MainActivity
中initViews()
里边的startForegroundService(intent)
;需要为
NotificationManager
配置NotificationChannel
如DownloadService
里面的getNotificationManager()
需要为
Notification
设置channelId
如DownloadService
里面的的getNotification()
需要静态声明权限
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
参考文章:
运行测试:
- 首先是请求权限:
- Toast提示
- 开始+暂停+开始(断点续传)
-
开始+暂停+取消(重新下载)
- 下载完毕提示
- 下载完毕点击再开始,不会再下载
-
下载完毕点击取消会删除文件,再点击开始会重新下载
参考自《第一行代码》