文章目录
- 概念
- Android 多线程
- 继承 Thread
- 继承 Runable 接口
- 匿名类
- 异步消息处理
- AsyncTask
- 使用服务
- 框架
- 启动/停止服务
- 绑定/解绑服务
- 服务的生命周期
- 前台服务
- IntentService
- 完整版下载示例
- 下载过程的回调接口:DownloadListener
- 继承 AsyncTask 实现下载功能:DownloadTask
- 服务:DownloadService
- 活动:ServiceActivity
- AndroidManifest.xml 权限声明
概念
服务(Service)用于执行无需和用户交互但需要长期运行的任务,其不是独立进程,而是依赖于创建服务时所在的应用程序进程。应用程序进程死亡时,所有依赖于该进程的服务也都将停止运行。
Android 多线程
服务不会自动开启线程,因此为了防止主线程被阻塞,应该在服务内部手动创建子线程。
通常有三种线程的使用方式:
继承 Thread
新建一个类继承 Thread ,然后重写 run()
方法:
public class MyThread extends Thread{@Overridepublic void run() {// 处理耗时逻辑}
}
启动线程:
// new出实例,然后调用start方法
// 这样run()方法中代码就会在子线程中运行了
new MyThread().start();
继承 Runable 接口
使用继承的方式耦合性有点高(如父类添加新方法所有子类都要跟着添加),更多时候使用 Runnable接口 定义线程来降低耦合:
public class MyThread implements Runnable{@Overridepublic void run() {// 处理耗时逻辑}
}
启动线程:
MyThread myThread = new MyThread();
// 使用接收一个Runnable参数的 Thread() 构造方法来 new 一个匿名类
// 接着调用start方法,run()方法中代码就会在子线程中运行了
new Thread(myThread).start();
匿名类
无需专门定义一个类实现 Runnable接口,而是在代码中需要用到的地方创建匿名类,直接启动子线程执行耗时操作:
new Thread(new Runnable() {@Overridepublic void run() {// 处理耗时逻辑}}).start();
异步消息处理
这一点在上一篇博客中有深刻体会,不使用 runOnUiThread 跳回主线程,而是在子线程中直接操作 UI
的话会报错:Only the original thread that created a view hierarchy can touch its views
。
runOnUiThread()
方法其实就是一个异步消息处理机制的接口封装,异步消息处理主要由四部分组成:
Message
线程间传递的消息,可以携带少量信息。通过字段来携带数据,如:
- waht: 用户自定义的消息代码,每个
handler
各自包含自己的消息代码,所以不用担心自定义的消息跟其他handler
有冲突。 - arg1、arg2: 如果只需要存储几个整型数据,arg1、arg2 是
setData()
的低成本替代品。 - obj: Object对象。当使用 Message对象 在线程间传递消息时,如果它包含一个 Parcelable 的结构类(不是由应用程序实现的类),此字段必须为非空(
non-null
)。其他的数据传输则使用setData(Bundle)
方法。
Handler
用于 发送(使用 sendMessage() 方法) 和 处理(使用handleMessage() 方法) 消息。
MessageQueue
消息队列,存放所有通过 Handler 发送的消息。每个线程中只会有一个 MessageQueue 对象。
Looper
每个线程中的 MessageQueue 的管家,调用 Looper.loop()
方法后,会进入一个无限循环中,每当发现 MessageQueue 中存在一条消息,就把它取出,并传递到 Handler.handleMessage()
方法中。每个线程中只会有一个 Looper 对象。
异步消息处理机制流程如图:
- 主线程中创建一个 Handler对象,并重写
handleMessage()
方法。
// 隐式的Looper会导致操作丢失、程序崩溃和紊乱情况、Handler非期望等问题// 因此安卓11不允许使用无参数的Handler构造方法// 如果非得用隐式,用Looper.myLooper()作为参数// 否则可以使用Looper.getMainLooper()作为参数private Handler handler = new Handler(Looper.getMainLooper()){@Overridepublic void handleMessage(@NonNull Message msg) {switch (msg.what){// WHAT_CODE是自定义的what字段值case WHAT_CODE:// 执行UI操作break;}}}
关于ANDROID 11推荐使用的 Handler 构造方法详见
- 子线程需要进行 UI操作 时,创建一个 Message对象,并通过
Handler.sendMessage()
方法发送出去。
Message message = new Message();
message.what = WHAT_CODE;
handler.sendMessage(message);
- 该条消息会被添加到 MessageQueue 队列中等待被 Looper 取出并分发回
Handler.handleMessage()
方法中。 - 由于 Handler对象 是在主线程中创建的,因此
Handler.handleMessage()
方法中的 UI操作 也是在主线程中运行的。
AsyncTask
其原理也是基于异步消息处理机制,只是 Android 做好了封装。AsyncTask 是抽象类,继承时可以为其指定三个泛型参数:
- Params: 可在后台任务中使用。
- Progress: 后台任务执行时,如果需要在界面上显示当前进度,使用该参数指定的泛型作为进度单位。
- Result: 任务执行完毕后,若需要对结果进行返回,则使用该参数指定的泛型作为返回值类型。
举个例子:
class DownloadTask extends AsyncTask<Void, Integer, Boolean>{
}
上述自定义的 DownloadTask 三个参数的意义分别是:
- Void: 执行时无需将传入参数给后台任务。
- Integer: 使用整型数据作为进度显示单位。
- Boolean: 使用布尔型数据来反馈执行结果
自定义类继承 AsyncTask 时,常需要被重写的方法有:
class DownloadTask extends AsyncTask<Void, Integer, Boolean>{@Overrideprotected void onPreExecute() {progressDialog.show(); // 显示进度对话框}// 执行具体耗时任务@Overrideprotected Boolean doInBackground(Void... voids) {try{while (true){// 假设doDownload方法已实现,该方法用于计算下载速度并返回int downloadPercent = doDownload();// 当后台计算仍在运行时,可以从doInBackground调用此方法在Ul线程上发布更新;// 对该方法的每次调用都将触发UI线程上onProgressUpdate的执行;// 如果任务已取消,则不会调用onProgressUpdate。publishProgress(downloadPercent);if(downloadPercent >= 100){break;}}} catch (Exception e){return false;}// 下载完成后返回布尔型变量,调用onPostExecute方法return true;}// 进行UI操作@Overrideprotected void onProgressUpdate(Integer... values) {// 更新下载速度progressDialog.setMessage("Downloaded" + values[0] + "%");}// 执行后台任务的收尾工作@Overrideprotected void onPostExecute(Boolean aBoolean) {progressDialog.dismiss();// 根据下载结果弹出对应提示if (aBoolean) {Toast.makeText(context, "download succeeded", Toast.LENGTH_LONG).show();}else{Toast.makeText(context, "download failed", Toast.LENGTH_LONG).show();}}
}
- onPreExecute: 在后台任务开始执行之前调用,用于界面上的初始化操作,如显示一个进度条对话框。
- doInBackground: 该方法中所有代码都在子线程中运行,可在此处理所有的耗时任务。任务一旦完成可以通过
return
语句将结果返回(如果 AsyncTask 第三个泛型参数指定的是 Void,则可以不返回执行结果)。该方法不可以进行 UI操作,如果要更新 UI元素,可以调用publishProgress(Progress... values)
方法来完成。 - onProgressUpdate: 每次调用
publishProgress(Progress... values)
方法都会触发该方法执行,该方法的参数是后台任务中传递过来的,在这里可以进行UI操作,利用参数对界面元素进行更新。 - onPostExecute: 在 doInBackground 方法执行
return
语句后调用,可以利用返回的数据执行UI操作。
启动 DownloadTask 任务只需编写以下代码:
new DownloadTask().execute();
使用服务
框架
创建服务,框架如下:
public class MyService extends Service {public MyService() {}// Service中唯一的抽象方法,必须在子类中实现@Overridepublic IBinder onBind(Intent intent) {// TODO: Return the communication channel to the service.throw new UnsupportedOperationException("Not yet implemented");}// 服务创建时调用@Overridepublic void onCreate() {super.onCreate();}// 每次服务启动时调用@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {return super.onStartCommand(intent, flags, startId);}// 服务销毁时调用@Overridepublic void onDestroy() {super.onDestroy();}
}
每一个服务都需要在 AndroidManifest.xml
中注册才能生效:
启动/停止服务
启动服务的目的是让服务一直在后台运行。
活动布局文件:
一个按钮用来启动服务,另一个用来终止服务。
活动文件:
public class ServiceActivity extends AppCompatActivity implements View.OnClickListener {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.service_layout);Button button_start_service = findViewById(R.id.button_start_service);button_start_service.setOnClickListener(this);Button button_stop_service = findViewById(R.id.button_stop_service);button_stop_service.setOnClickListener(this);}@Overridepublic void onClick(View v) {switch (v.getId()){case R.id.button_start_service:Intent startIntent = new Intent(this, MyService.class);startService(startIntent);break;case R.id.button_stop_service:Intent stopIntent = new Intent(this, MyService.class);stopService(stopIntent);break;}}
}
除了通过 startService()
和 stopService()
来 启动/停止 服务,还可以在 MyService 中调用 stopSelf()
方法让服务停止。
绑定/解绑服务
绑定服务的目的是让服务和活动可以进行通信。
public class MyService extends Service {private static final String TAG = "MyService";// 用来和活动进行通信private DownloadBinder mBinder = new DownloadBinder();// Service中唯一的抽象方法,必须在子类中实现@Overridepublic IBinder onBind(Intent intent) {return mBinder;}public class DownloadBinder extends Binder{public void startDownload(){Log.e(TAG, "startDownload: ");}public int getProgress(){Log.e(TAG, "getProgress: ");return 0;}}
}
service 的 onCreate/onStartCommand/onStart
生命周期相关的方法总是在 主线程 上执行的,如果 bindService
在主线程上阻塞的话。service 就无法执行上述生命周期相关的方法,完成初始化工作。因为 绑定服务要在子线程上执行,因此绑定完成后必须通过 ServiceConnection 来回调到主线程。
public class ServiceActivity extends AppCompatActivity implements View.OnClickListener {private MyService.DownloadBinder downloadBinder;// 匿名类private ServiceConnection connection = new ServiceConnection(){// 成功绑定时调用@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {// 向下转型生成实例downloadBinder = (MyService.DownloadBinder) service;// 此时可以调用DownloadBinder的任何public方法downloadBinder.startDownload();downloadBinder.getProgress();}// 解绑时调用@Overridepublic void onServiceDisconnected(ComponentName name) {}};@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.service_layout);Button button_bind = findViewById(R.id.button_bind);button_bind.setOnClickListener(this);Button button_unbind = findViewById(R.id.button_unbind);button_unbind.setOnClickListener(this);}@Overridepublic void onClick(View v) {switch (v.getId()){case R.id.button_bind:Intent bindIntent = new Intent(this, MyService.class);bindService(bindIntent, connection, BIND_AUTO_CREATE); // 绑定服务break;case R.id.button_unbind:unbindService(connection); // 解绑服务break;}}
}
- bindService: 绑定活动与服务,该方法接受三个参数:
- Intent 对象
- ServiceConnection 实例
- 标志位:
BIND_AUTO_CREATE
表示绑定后自动创建服务。此时 MyService 的onCreate()
方法会执行,onStartCommand()
方法不会执行。
- unbindService: 该方法解绑活动与服务,接受一个参数:ServiceConnection 实例。
PS:任何一个服务在整个应用程序范围内都是通用的,意味着可以和多个活动绑定(绑定服务是异步的),绑定后都获得相同的 DownloadBinder 实例。
服务的生命周期
- Context 的
startService()
方法结束后会立刻回调 服务 的onStartCommand()
方法。(如果此前服务还未创建过,会先调用 服务 的onCreate()
方法)。 - Context 的
stopService()
方法 或 服务 的stopSelf()
方法 可以停止服务。值得一提的是:- 在 服务 的
onStartCommond()
方法 里面调用stopSelf()
方法 时,服务不会马上停止,而是在onStartCommond()
方法 执行结束才会停止。 - 调用
stopSelf()
方法 之后,服务会执行onDestory()
方法。 - 如果
onStartCommond()
方法 中启动一个线程,调用stopSelf()
方法,线程也不会被杀死。
- 在 服务 的
- Context 的
bindService()
方法可以获取一个服务的持久连接,结束后回调 服务 的onBind()
方法。调用方通过onBind()
返回的 IBinder 实例和 服务 进行通信。
PS:一个服务只要被启动(startService)或被绑定(bindService),就会一直处于运行状态,想要停止运行时,服务必须处于 停止(stopService) + 解绑(unbindService) 的状态,服务才能被销毁。
前台服务
系统内存不足时,可能会回收正在运行的后台服务;而前台服务可以一直保持运行,避免被回收。前台服务和普通服务的最大区别是,会一直在系统状态栏显示一个正在运行的图标,下拉状态栏可以显示详细信息。这其实就用到了之前通知一文的知识。
// 服务创建时调用@Overridepublic void onCreate() {super.onCreate();Log.e(TAG, "onCreate executed");Intent intent = new Intent(this, ServiceActivity.class);PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);String id = "1";NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);if(Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O){String name = getString(R.string.app_name);// 创建通知通道// 第一个参数要和NotificationCompat.Builder的channelId一样// 第三个参数是通知的重要程度NotificationChannel notificationChannel = new NotificationChannel(id, name,NotificationManager.IMPORTANCE_HIGH);manager.createNotificationChannel(notificationChannel);}Notification notification = new NotificationCompat.Builder(this, id).setContentTitle("天气").setContentText("天气内容").setWhen(System.currentTimeMillis()).setSmallIcon(R.mipmap.cloud).setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.cloud))// 点击通知后执行的意图.setContentIntent(pi).build();// 不使用NotificationManager.notify()显示// 而使用startForeground显示startForeground(1, notification);}
PS:实现通知的代码都是之前介绍过的,唯一不同的就是显示通知是通过 startForeground()
方法,而非 NotificationManager.notify()
方法。
IntentService
通常在 onStartCommand()
方法中开启子线程来执行耗时逻辑,并在子线程中逻辑处理完毕后调用 stopSelf()
方法来自动结束服务:
// 每次服务启动时调用@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {Log.e(TAG, "onStartCommand executed");new Thread(new Runnable() {@Overridepublic void run() {stopSelf();}}).start();return super.onStartCommand(intent, flags, startId);}
而 Android 提供了 IntentService 类来封装上面的逻辑,我们可以通过继承它来实现自定义类以满足所需功能:
public class MyIntentService extends IntentService {private static final String TAG = "MyIntentService";// name用于命名工作线程,仅对调试很重要public MyIntentService() {super("MyIntentService");}@Overrideprotected void onHandleIntent(@Nullable Intent intent) {Log.e(TAG, "onHandleIntent: Thread id is" + Thread.currentThread().getId());}@Overridepublic void onDestroy() {super.onDestroy();Log.e(TAG, "onDestroy: MyIntentService");}
}
在活动中通过按钮调用它:
点击按钮后的输出结果:
完整版下载示例
下载过程的回调接口:DownloadListener
// 回调接口,监听下载过程中的各种状态
public interface DownloadListener {void onProgress(int progress); // 当前下载进度void onSuccess(); // 下载成功void onFailed(); // 失败void onPaused(); // 暂停void onCanceled(); // 取消下载
}
继承 AsyncTask 实现下载功能:DownloadTask
DownloadTask 实现了具体的下载功能。
// 自实现的下载任务的异步消息处理机制
public class DownloadTask extends AsyncTask<String, Integer, Integer> {private static final String TAG = "DownloadTask";public static final int TYPE_SUCCESS = 0;public static final int TYPE_FAILED = 1;public static final int TYPE_PAUSED= 2;public static final int TYPE_CANCELED = 3;private final DownloadListener listener;Context context;private boolean isCanceled = false;private boolean isPaused = false;private int lastProgress; // 上一次的下载进度public DownloadTask(DownloadListener listener, Context context){this.listener = listener;this.context = context;}// 执行具体耗时任务——下载逻辑@Overrideprotected Integer doInBackground(String... strings) {Log.e(TAG, "doInBackground: 下载开始");Log.e(TAG, "子线程 id is " + Thread.currentThread().getId());InputStream inputStream = null;// RandomAccessFile的一个重要使用场景就是网络请求中的多线程下载及断点续传RandomAccessFile savedFile = null;File file = null;try{long downloadedLength = 0; // 已下载的文件长度String downloadUrl = strings[0]; // 从传入的参数中得到欲下载资源的URL// lastIndexOf返回downloadUrl最后一次出现“/”的索引位置,截取该“/”到结尾的部分作为fileNameString fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));// 下载目录为SD卡的Download目录/*String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();*/// /Android/data/com.example.activitytest/files/DocumentsString directory = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).getPath();file = new File(directory + fileName);Log.e(TAG, "doInBackground file: " + file);// 文件存在则说明上次的下载行为被中断了// 此时需要用downloadedLength记录已下载的字节数,辅助完成断点续传功能if(file.exists()){downloadedLength = file.length();Log.e(TAG, "doInBackground: file exists, downloadedLength: " + downloadedLength);}else {Log.e(TAG, "doInBackground: file not exists, downloadedLength: " + downloadedLength);}long contentLength = getContentLength(downloadUrl); // 待下载文件总长度if(contentLength == 0){ // 长度为0说明文件有问题Log.e(TAG, "doInBackground: contentLength == 0");return TYPE_FAILED;}else if (contentLength == downloadedLength){// 已下载字节和文件总字节相等,说明下载已完成Log.e(TAG, "doInBackground: 下载过了");return TYPE_SUCCESS;}OkHttpClient client = new OkHttpClient();Request request = new Request.Builder()// 断点下载,指定从哪个字节开始下载.addHeader("RANGE", "bytes=" + downloadedLength + "-").url(downloadUrl).build();Response response = client.newCall(request).execute();if(response != null){Log.e(TAG, "服务器的确认报文");inputStream = response.body().byteStream(); // 字节输入流Log.e(TAG, "doInBackground inputStream: " + inputStream);savedFile = new RandomAccessFile(file, "rw");Log.e(TAG, "doInBackground saveFile: " + savedFile);savedFile.seek(downloadedLength); // 跳过已下载字节Log.e(TAG, "断点重续 over");byte[] bArray = new byte[1024];int total = 0; // 已读字节int len; // 读入缓冲区的字节总数// 不断将网络数据写入本地while ((len = inputStream.read(bArray)) != -1){Log.e(TAG, "doInBackground: 不断将网络数据写入本地");if (isCanceled) {return TYPE_CANCELED;} else if (isPaused) {return TYPE_PAUSED;} else {total += len;savedFile.write(bArray, 0, len);Log.e(TAG, "doInBackground: total: " + total + " len: " + len+ " contentLength: " + contentLength);// 计算已下载的百分比int progress = (int) ((total + downloadedLength) * 100 / contentLength);// 当后台计算仍在运行时,可以从doInBackground调用此方法在Ul线程上发布更新;// 对该方法的每次调用都将触发UI线程上onProgressUpdate的执行;// 如果任务已取消,则不会调用onProgressUpdate。publishProgress(progress);int tmp = 100;Log.e(TAG, "doInBackground: 计算下载百分比已完成,progress:"+ progress + " " + tmp);}}response.body().close();Log.e(TAG, "doInBackground: 下载已完成");return TYPE_SUCCESS;}} catch (Exception e){e.printStackTrace();} finally {try {if (inputStream != null) {inputStream.close();}if (savedFile != null) {savedFile.close();}if(isCanceled && file != null){boolean res = file.delete();Log.e(TAG, "doInBackground: file.delete() is res: " + res);}} catch (Exception e){e.printStackTrace();}}return TYPE_FAILED;}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();Log.e(TAG, "getContentLength: contentLength: " + contentLength);return contentLength;}return 0;}// 进行UI操作——更新下载进度@Overrideprotected void onProgressUpdate(Integer... values) {// 更新下载速度int progress = values[0];if (progress > lastProgress){// 调用DownloadListener的onProgress通知下载进度的更新listener.onProgress(progress);lastProgress = progress;}}// 执行后台任务的收尾工作——通知下载结果@Overrideprotected void onPostExecute(Integer integer) {switch (integer){case TYPE_SUCCESS:listener.onSuccess();break;case TYPE_FAILED:listener.onFailed();break;case TYPE_PAUSED:listener.onPaused();break;case TYPE_CANCELED:listener.onCanceled();break;}}// 暂停下载,修改 isPaused 标记public void pauseDownload(){isPaused = true;}// 取消下载,修改 isCanceled 标记public void cancelDownload(){isCanceled = true;}
}
doInBackground
doInBackground
的 参数 strings 是 AsyncTask 模板的 第一个参数,从strings[0]
中我们可得到传入的下载资源的url
。- 解析
url
得到了 待下载文件 的 文件名,然后将文件下载到Environment.DIRECTORY_DOWNLOADS(也就是 /storage/emulated/0/Android/data/com.example.activitytest/files/Download/<文件名>)
目录下。 - 下载过程中用到了断点续传功能,HTTP 的 Header 中的 RANGE 参数就是为标识断点续传功能而存在的。而 RandomAccessFile 类型的一个重要使用场景就是网络请求中的多线程下载及断点续传。
关于RandomAccessFile详见本文
关于HTTP断点续传详见本文 - 通过文件流不断从网络读取数据写到本地,在此期间还需判断用户有无触发暂停或取消操作。
onProgressUpdate
和上一次下载进度相比,有变化则回调 DownloadListener.onProgress()
方法 通知下载进度更新。
onPostExecute
根据 AsyncTask 模板的第三个参数 Integer
对应的状态参数来进行回调。
服务:DownloadService
DownloadService 保证 DownloadTask 能够一直在后台运行。
public class DownloadService extends Service {private static final String TAG = "DownloadService";private DownloadTask downloadTask;private String downloadUrl;// 匿名类实例private final DownloadListener listener = new DownloadListener() {@Overridepublic void onProgress(int progress) {getNotificationManager().notify(1, getNotification("Downloading...", progress));}@Overridepublic void onSuccess() {downloadTask = null;// 下载成功时将前台服务通知关闭stopForeground(true);// 创建一个下载成功的通知getNotificationManager().notify(1, getNotification("Download Success", -1));Toast.makeText(DownloadService.this, "Download Success",Toast.LENGTH_LONG).show();}@Overridepublic void onFailed() {downloadTask = null;// 下载失败时将前台服务通知关闭stopForeground(true);// 创建一个下载失败的通知getNotificationManager().notify(1, getNotification("Download Failed", -1));Toast.makeText(DownloadService.this, "Download Failed",Toast.LENGTH_LONG).show();}@Overridepublic void onPaused() {downloadTask = null;Toast.makeText(DownloadService.this, "Download Paused",Toast.LENGTH_LONG).show();}@Overridepublic void onCanceled() {downloadTask = null;Toast.makeText(DownloadService.this, "Download Canceled",Toast.LENGTH_LONG).show();}};// 和活动通信private final DownloadBinder mBinder = new DownloadBinder();@Overridepublic IBinder onBind(Intent intent) {return mBinder;}public class DownloadBinder extends Binder {public void startDownload(String url){if (downloadTask == null) {downloadUrl = url;downloadTask = new DownloadTask(listener, DownloadService.this);downloadTask.execute(downloadUrl); // execute通过url开启下载Log.e(TAG, "startDownload: downloadTask 已执行");// 前台显示startForeground(1, getNotification("Downloading...", 0));Toast.makeText(DownloadService.this, "Downloading...",Toast.LENGTH_LONG).show();Log.e(TAG, "startDownload: 通知已显示");}Log.e(TAG, "startDownload: over");}public void pauseDownload(){if(downloadTask != null){downloadTask.pauseDownload();}Log.e(TAG, "getProgress: pauseDownload over");}public void cancelDownload(){if(downloadTask != null){downloadTask.cancelDownload();}else {if (downloadUrl != null) {// 取消下载时需删除文件String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));String directory = DownloadService.this.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).getPath();File file = new File(directory + fileName);if (file.exists()){boolean res = file.delete();Log.e(TAG, "cancelDownload: file.delete() is res: " + res);}// 并关闭通知getNotificationManager().cancel(1);stopForeground(true);Toast.makeText(DownloadService.this, "Canceled",Toast.LENGTH_LONG).show();}}Log.e(TAG, "cancelDownload: cancelDownload over");}}private NotificationManager getNotificationManager() {Log.e(TAG, "getNotificationManager: 生成通知管理器已完成");return (NotificationManager) getSystemService(NOTIFICATION_SERVICE);}private Notification getNotification(String title, int progress) {String id = "1"; // NotificationCompat.Builder 和 NotificationChannel 的 id 参数if(Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O){String name = getString(R.string.app_name);NotificationChannel notificationChannel = new NotificationChannel(id, name,NotificationManager.IMPORTANCE_HIGH);notificationChannel.enableLights(true);notificationChannel.setLightColor(Color.RED);notificationChannel.setShowBadge(true);notificationChannel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);// 通知更新时声音关掉,避免每次更新进度都会弹出提示音notificationChannel.setSound(null, null);getNotificationManager().createNotificationChannel(notificationChannel);}Intent intent = new Intent(this, ServiceActivity.class);PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);NotificationCompat.Builder builder = new NotificationCompat.Builder(this, id);builder.setSmallIcon(R.mipmap.download);builder.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.download));builder.setContentIntent(pi);builder.setContentTitle(title);if (progress > 0) { // >0时才有显示下载进度的需求builder.setContentText(progress + "%");// 第三个参数表述是否适用模糊进度条builder.setProgress(100, progress, false);Log.e(TAG, "getNotification: 显示下载进度");}Log.e(TAG, "getNotification: 生成通知已完成");return builder.build();}
}
- 实现了下载过程的回调接口 DownloadTask 的匿名类实例
- 通过 DownloadBinder 让 DownloadService 和 活动 通信,活动中通过点击按钮来调用这里的函数(
startDownload()
、pauseDownload()
、cancelDownload()
)。其实例mBinder
通过onBind()
方法返回,onBind()
方法 在bindService()
方法 调用后被回调。 - 安卓 8.0 版本以上使用 Notification 时要添加 NotificationChannel。
NotificationCompat.Builder.setProgress(100, progress, false);
第一个参数:传入通知的最大进度;第二个参数:传入通知的当前进度;第三个参数:是否使用模糊进度条。
不使用模糊进度条:
使用模糊进度条:
活动:ServiceActivity
public class ServiceActivity extends AppCompatActivity implements View.OnClickListener {private static final String TAG = "ServiceActivity";private DownloadService.DownloadBinder downloadBinder;// 作为 bindService 的第二个参数private final ServiceConnection connection = new ServiceConnection() {@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {// 生成实例,以便服务和活动的通信downloadBinder = (DownloadService.DownloadBinder) service;}@Overridepublic void onServiceDisconnected(ComponentName name) {}};@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.service_layout);Button button_start_download = findViewById(R.id.button_start_download);button_start_download.setOnClickListener(this);Button button_pause_download = findViewById(R.id.button_pause_download);button_pause_download.setOnClickListener(this);Button button_cancel_download = findViewById(R.id.button_cancel_download);button_cancel_download.setOnClickListener(this);Intent intent = new Intent(this, DownloadService.class);startService(intent); // 启动服务bindService(intent, connection, BIND_AUTO_CREATE); // 绑定服务if (ContextCompat.checkSelfPermission(ServiceActivity.this,Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {ActivityCompat.requestPermissions(ServiceActivity.this, new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE }, 1);}else {Log.e(TAG, "拥有权限,无需授权");}}@Overridepublic void onClick(View v) {if (downloadBinder == null) {return;}switch (v.getId()){case R.id.button_start_download:Log.e(TAG, "主线程 id is " + Thread.currentThread().getId());String url = "https://dl.hdslb.com/mobile/latest/android64/iBiliPlayer-bili.apk?t=1647227157000";downloadBinder.startDownload(url);break;case R.id.button_pause_download:downloadBinder.pauseDownload();break;case R.id.button_cancel_download:downloadBinder.cancelDownload();break;}}@Overridepublic void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {switch (requestCode){case 1:if (grantResults.length > 0 && grantResults[0] != PackageManager.PERMISSION_GRANTED){Toast.makeText(this, "拒绝权限将无法使用程序", Toast.LENGTH_LONG).show();finish();}Log.e(TAG, "已完成申请授权");break;}}@Overrideprotected void onDestroy() {super.onDestroy();unbindService(connection); // 解绑服务,防止内存泄漏}
}
- 在绑定成功时在
ServiceConnection.onServiceConnected()
方法中生成 DownloadService.DownloadBinder 的实例,以便于活动和服务之间进行通信。 - 启动服务保证 DownloadTask 能够一直在后台运行,绑定服务让 ServiceActivity 和 DownloadTask 能够进行通信。
- 活动销毁时注意解绑服务,以避免内存泄漏。
AndroidManifest.xml 权限声明
- WRITE_EXTERNAL_STORAGE: 允许写入外部存储目录。
- INTERNET: 网络访问权限。
- FOREGROUND_SERVICE: 前台服务权限。