文章目录
概念
服务(Service)用于执行无需和用户交互但需要长期运行的任务,其不是独立进程,而是依赖于创建服务时所在的应用程序进程。应用程序进程死亡时,所有依赖于该进程的服务也都将停止运行。
Android 多线程
服务不会自动开启线程,因此为了防止主线程被阻塞,应该在服务内部手动创建子线程。
通常有三种线程的使用方式:
继承 Thread
新建一个类继承 Thread ,然后重写 run()
方法:
public class MyThread extends Thread{
@Override
public void run() {
// 处理耗时逻辑
}
}
启动线程:
// new出实例,然后调用start方法
// 这样run()方法中代码就会在子线程中运行了
new MyThread().start();
继承 Runable 接口
使用继承的方式耦合性有点高(如父类添加新方法所有子类都要跟着添加),更多时候使用 Runnable接口 定义线程来降低耦合:
public class MyThread implements Runnable{
@Override
public void run() {
// 处理耗时逻辑
}
}
启动线程:
MyThread myThread = new MyThread();
// 使用接收一个Runnable参数的 Thread() 构造方法来 new 一个匿名类
// 接着调用start方法,run()方法中代码就会在子线程中运行了
new Thread(myThread).start();
匿名类
无需专门定义一个类实现 Runnable接口,而是在代码中需要用到的地方创建匿名类,直接启动子线程执行耗时操作:
new Thread(new Runnable() {
@Override
public 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()){
@Override
public 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>{
@Override
protected void onPreExecute() {
progressDialog.show(); // 显示进度对话框
}
// 执行具体耗时任务
@Override
protected 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操作
@Override
protected void onProgressUpdate(Integer... values) {
// 更新下载速度
progressDialog.setMessage("Downloaded" + values[0] + "%");
}
// 执行后台任务的收尾工作
@Override
protected 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中唯一的抽象方法,必须在子类中实现
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}
// 服务创建时调用
@Override
public void onCreate() {
super.onCreate();
}
// 每次服务启动时调用
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}
// 服务销毁时调用
@Override
public void onDestroy() {
super.onDestroy();
}
}
每一个服务都需要在 AndroidManifest.xml
中注册才能生效:
启动/停止服务
启动服务的目的是让服务一直在后台运行。
活动布局文件:
一个按钮用来启动服务,另一个用来终止服务。
活动文件:
public class ServiceActivity extends AppCompatActivity implements View.OnClickListener {
@Override
protected 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);
}
@Override
public 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中唯一的抽象方法,必须在子类中实现
@Override
public 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(){
// 成功绑定时调用
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// 向下转型生成实例
downloadBinder = (MyService.DownloadBinder) service;
// 此时可以调用DownloadBinder的任何public方法
downloadBinder.startDownload();
downloadBinder.getProgress();
}
// 解绑时调用
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected 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);
}
@Override
public 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) 的状态,服务才能被销毁。
前台服务
系统内存不足时,可能会回收正在运行的后台服务;而前台服务可以一直保持运行,避免被回收。前台服务和普通服务的最大区别是,会一直在系统状态栏显示一个正在运行的图标,下拉状态栏可以显示详细信息。这其实就用到了之前通知一文的知识。
// 服务创建时调用
@Override
public 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()
方法来自动结束服务:
// 每次服务启动时调用
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.e(TAG, "onStartCommand executed");
new Thread(new Runnable() {
@Override
public 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");
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
Log.e(TAG, "onHandleIntent: Thread id is" + Thread.currentThread().getId());
}
@Override
public 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;
}
// 执行具体耗时任务——下载逻辑
@Override
protected 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最后一次出现“/”的索引位置,截取该“/”到结尾的部分作为fileName
String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
// 下载目录为SD卡的Download目录
/*String directory = Environment.getExternalStoragePublicDirectory
(Environment.DIRECTORY_DOWNLOADS).getPath();*/
// /Android/data/com.example.activitytest/files/Documents
String 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操作——更新下载进度
@Override
protected void onProgressUpdate(Integer... values) {
// 更新下载速度
int progress = values[0];
if (progress > lastProgress){
// 调用DownloadListener的onProgress通知下载进度的更新
listener.onProgress(progress);
lastProgress = progress;
}
}
// 执行后台任务的收尾工作——通知下载结果
@Override
protected 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() {
@Override
public void onProgress(int progress) {
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_LONG).show();
}
@Override
public void onFailed() {
downloadTask = null;
// 下载失败时将前台服务通知关闭
stopForeground(true);
// 创建一个下载失败的通知
getNotificationManager().notify(1, getNotification("Download Failed", -1));
Toast.makeText(DownloadService.this, "Download Failed",
Toast.LENGTH_LONG).show();
}
@Override
public void onPaused() {
downloadTask = null;
Toast.makeText(DownloadService.this, "Download Paused",
Toast.LENGTH_LONG).show();
}
@Override
public void onCanceled() {
downloadTask = null;
Toast.makeText(DownloadService.this, "Download Canceled",
Toast.LENGTH_LONG).show();
}
};
// 和活动通信
private final DownloadBinder mBinder = new DownloadBinder();
@Override
public 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() {
@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.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, "拥有权限,无需授权");
}
}
@Override
public 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;
}
}
@Override
public 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;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(connection); // 解绑服务,防止内存泄漏
}
}
- 在绑定成功时在
ServiceConnection.onServiceConnected()
方法中生成 DownloadService.DownloadBinder 的实例,以便于活动和服务之间进行通信。 - 启动服务保证 DownloadTask 能够一直在后台运行,绑定服务让 ServiceActivity 和 DownloadTask 能够进行通信。
- 活动销毁时注意解绑服务,以避免内存泄漏。
AndroidManifest.xml 权限声明
- WRITE_EXTERNAL_STORAGE: 允许写入外部存储目录。
- INTERNET: 网络访问权限。
- FOREGROUND_SERVICE: 前台服务权限。