Android入门(16)| 服务

文章目录

  • 概念
  • 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 对象

异步消息处理机制流程如图:
在这里插入图片描述

  1. 主线程中创建一个 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 构造方法详见

  1. 子线程需要进行 UI操作 时,创建一个 Message对象,并通过 Handler.sendMessage() 方法发送出去。
Message message = new Message();
message.what = WHAT_CODE;
handler.sendMessage(message);
  1. 该条消息会被添加到 MessageQueue 队列中等待被 Looper 取出并分发回 Handler.handleMessage() 方法中。
  2. 由于 Handler对象 是在主线程中创建的,因此 Handler.handleMessage() 方法中的 UI操作 也是在主线程中运行的。

AsyncTask

其原理也是基于异步消息处理机制,只是 Android 做好了封装。AsyncTask 是抽象类,继承时可以为其指定三个泛型参数:

  1. Params: 可在后台任务中使用。
  2. Progress: 后台任务执行时,如果需要在界面上显示当前进度,使用该参数指定的泛型作为进度单位。
  3. Result: 任务执行完毕后,若需要对结果进行返回,则使用该参数指定的泛型作为返回值类型。

举个例子:

class DownloadTask extends AsyncTask<Void, Integer, Boolean>{
}

上述自定义的 DownloadTask 三个参数的意义分别是:

  1. Void: 执行时无需将传入参数给后台任务。
  2. Integer: 使用整型数据作为进度显示单位。
  3. 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();}}
}
  1. onPreExecute: 在后台任务开始执行之前调用,用于界面上的初始化操作,如显示一个进度条对话框。
  2. doInBackground: 该方法中所有代码都在子线程中运行,可在此处理所有的耗时任务。任务一旦完成可以通过 return 语句将结果返回(如果 AsyncTask 第三个泛型参数指定的是 Void,则可以不返回执行结果)。该方法不可以进行 UI操作,如果要更新 UI元素,可以调用 publishProgress(Progress... values) 方法来完成。
  3. onProgressUpdate: 每次调用 publishProgress(Progress... values) 方法都会触发该方法执行,该方法的参数是后台任务中传递过来的,在这里可以进行UI操作,利用参数对界面元素进行更新
  4. 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;}}
}

serviceonCreate/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 表示绑定后自动创建服务。此时 MyServiceonCreate() 方法会执行,onStartCommand() 方法不会执行。
  • unbindService: 该方法解绑活动与服务,接受一个参数:ServiceConnection 实例

PS:任何一个服务在整个应用程序范围内都是通用的,意味着可以和多个活动绑定(绑定服务是异步的),绑定后都获得相同的 DownloadBinder 实例。


服务的生命周期

  • ContextstartService() 方法结束后会立刻回调 服务onStartCommand() 方法。(如果此前服务还未创建过,会先调用 服务onCreate() 方法)。
  • ContextstopService() 方法 或 服务stopSelf() 方法 可以停止服务。值得一提的是:
    • 服务onStartCommond() 方法 里面调用 stopSelf() 方法 时,服务不会马上停止,而是在 onStartCommond() 方法 执行结束才会停止。
    • 调用 stopSelf() 方法 之后,服务会执行 onDestory() 方法。
    • 如果 onStartCommond() 方法 中启动一个线程,调用 stopSelf() 方法,线程也不会被杀死。
  • ContextbindService() 方法可以获取一个服务的持久连接,结束后回调 服务 的 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参数 stringsAsyncTask 模板的 第一个参数,从 strings[0] 中我们可得到传入的下载资源的 url
  • 解析 url得到了 待下载文件 的 文件名,然后将文件下载到 Environment.DIRECTORY_DOWNLOADS(也就是 /storage/emulated/0/Android/data/com.example.activitytest/files/Download/<文件名>) 目录下。
  • 下载过程中用到了断点续传功能,HTTPHeader 中的 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 的匿名类实例
  • 通过 DownloadBinderDownloadService活动 通信,活动中通过点击按钮来调用这里的函数(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 能够一直在后台运行,绑定服务让 ServiceActivityDownloadTask 能够进行通信。
  • 活动销毁时注意解绑服务,以避免内存泄漏。

AndroidManifest.xml 权限声明

在这里插入图片描述

  • WRITE_EXTERNAL_STORAGE: 允许写入外部存储目录。
  • INTERNET: 网络访问权限。
  • FOREGROUND_SERVICE: 前台服务权限。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/443692.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2020德勤面试开始了吗_2020国考面试开始,近期面试公告汇总,附结构化小组面试流程...

2020年国家公务员考试面试环节逐步恢复考试&#xff0c;各个招录部门已经发布面试考察公告&#xff0c;对于进入面试环节的国考考生来说&#xff0c;有必要了解近期国考面试的招录动态&#xff0c;提前做好面试准备。2020国考国家统计局机关面试面试确认&#xff1a;请进入面试…

项目积压需求项目计划_需求变更频繁,项目经理如何做好需求管理?

项目实施过程中&#xff0c;项目经理常常面临一个重大挑战——需求变更。需求变更无处不在&#xff0c;市场条件变化、新业务出现、战略目标调整、客户需求修改、资源限制等&#xff0c;都会造成需求变更。需求变更会影响项目的时间、成本和质量&#xff0c;对整个项目和团队成…

Android | Sensor.TYPE_ORIENTATION被废弃后的解决办法

文章目录概述getOrientation 方法根据 旋转矩阵R 获取 设备旋转弧度getRotationMatrix 方法根据 地磁场、加速度传感器对象 获取 旋转矩阵R代码参考资料概述 Sensor.TYPE_ORIENTATION 常数在 API 8 中已弃用&#xff0c;官方推荐使用 SensorManager.getOrientation() 替代。关…

【JAVA 开发小问题】 | String操作合集

文章目录截取特定两个字符之间的字符串截取特定两个字符之间的字符串 利用正则表达式&#xff0c;图片来源

uniapp 刷新后数据都没有了_环境温度传感器都没有连接,竟然还会有数据?

福田欧曼GTL(福康发动机、康明斯2880系统)匹配ECoffit尿素泵●故障现象&#xff1a;OBD故障灯点亮&#xff0c;不烧尿素&#xff0c;油耗高&#xff0c;动力不足●故障码&#xff1a;●维修分析&#xff1a;①故障指出加热器问题&#xff0c;摸下尿素箱温度&#xff0c;发现烫手…

保姆级教学!Xcode 配置 OpenGL 环境

文章目录GLFW获取 GLFWGLAD获取 GLAD在 Xcode 中配置下载好的 GLFW 和 GLAD配置流程检测是否配置成功无关配置的题外话——Xcode 下安全的删除移动操作GLFW Graphics Library Framework&#xff08;图形库框架&#xff09;&#xff0c;可以让我们通过其封装好的 通用API 来正确…

Android入门(17)| 百度提供的 Android定位SDK

文章目录配置百度提供的 Android定位SDK用于发布的 SHA1用于测试的 SHA1使用百度定位实例配置百度提供的 Android定位SDK 详情参见官方文档&#xff0c;这里仅对获取 SHA1 做详细介绍&#xff1a; 用于发布的 SHA1 用于测试的 SHA1 使用百度定位实例 public class LocationAc…

ios 不被遮挡 阴影_为何你没见到日环食?你不知道的天象常识原来还有这么多 | 返朴...

关注风云之声提升思维层次导读说好的日环食呢&#xff0c;为什么上周很多人只等到了日偏食?日食月食的时间和种类是怎么预测的?你真的弄懂了各种日食和月食的成因吗&#xff1f;你了解它们有什么区别和联系&#xff0c;又遵循什么样的时间规律吗? 日食和月食发生的频率一样吗…

初识贝塞尔(bezier)曲线

文章目录资料援引贝塞尔曲线的用途一阶贝塞尔&#xff08;bezier&#xff09;曲线二阶贝塞尔&#xff08;bezier&#xff09;曲线三阶贝塞尔&#xff08;bezier&#xff09;曲线高阶贝塞尔&#xff08;bezier&#xff09;曲线三阶贝塞尔曲线求插值&#xff08;Slerp&#xff09…

python代码测试 vim_用 Hypothesis 快速测试你的 Python 代码

点击上方“Python编程时光”&#xff0c;选择“加为星标”第一时间关注Python技术干货&#xff01;介绍无论你使用哪种编程语言或框架&#xff0c;测试都非常重要。Hypothesis是 Python 的一个高级测试库。它允许编写测试用例时参数化&#xff0c;然后生成使测试失败的简单易懂…

Mac 下 CMake 的配置与使用

文章目录安装与配置编译单个源文件编译前的准备开始编译编译多个源文件多个源文件在同一目录下多个源文件在不同目录下math 目录下的 CMakeLists.txt根目录的 CMakeLists.txtoption 选项导入外部库本地导入&#xff08;find_package&#xff09;外部导入&#xff08;FetchConte…

五轴编程_沙井万丰数控数控编程五轴编程那个软件好用

沙井万丰数控数控编程五轴编程那个软件好用设计需要掌握很高很全面的知识和技能&#xff0c;模具做的好&#xff0c;产品质量好&#xff0c;模具结构合理&#xff0c;生产效率高&#xff0c;工厂效益好。正因如此&#xff0c;模具技术工在外打工的工资都非常的高。少则每月几千…

Linux学习:第二章-Linux安装

一虚拟机使用 VMware主要特点&#xff1a; 1、不需要分区或重新开机就能在同一台PC上使用两种以上的操作系统 2、本机系统可以与虚拟机系统网络通信 3、可以设定并且随时修改虚拟机操作系统的硬件环境 二安装方式 图形安装&#xff1a;直接回车 字符安装&#xff1a;linux tex…

keil3如何放大字体_国潮海报不会做?送你国风字体+图案笔刷+PSD素材+包装样机...

有很多朋友都问带鱼&#xff0c;国潮风的海报到底应该怎么做呢&#xff1f;首先你要知道什么是国潮风&#xff1a;国潮风就是现代文化和古代文化的碰撞&#xff0c;是年轻人的态度&#xff01;那么应该如何构图如何设计呢&#xff1f;如何配色如何搭配字体呢&#xff1f;这些方…

Google 开源项目风格指南学习笔记——C++篇

文章目录前言0. 缩写名词解释1. 头文件1.1. Self-contained 头文件1.2. 头文件保护1.3. 前置声明1.4 内联函数1.5. #include 的路径及顺序2. 作用域2.1. 命名空间2.2. 非成员函数、静态成员函数和全局函数2.3. 局部变量2.4. 静态和全局变量3. 类3.1. 构造函数的职责3.2. 隐式类…

hiveserver2启动不起来_给爱车配个充电宝,70迈汽车应急启动电源,让你远离搭电小广告...

说到汽车应急启动其实我有切身的痛&#xff0c;在哈尔滨零下35的严冬&#xff0c;晚上带着女神吃完饭&#xff0c;高高兴兴地吃完以后一上车&#xff0c;发现电瓶被冻没电了&#xff0c;天知道当时有多尴尬。马上叫了保险公司过来给搭电&#xff0c;结果在饭店从晚上8点一直等到…

Windows 下 VS 配置 OpenGL 环境

文章目录前言获取 GLFW打开 VS前言 感谢B站同学搬运YouTube上的教学视频。 获取 GLFW 从官网上下载GLFW macOS下64位二进制文件 打开 VS 新建解决方案 OpenGL test&#xff0c;并在解决方案中新建文件夹 Dependencies&#xff1a; 从下载好的 glfw 文件夹中找到最新版链接…

ubuntu 网卡双网口 配置_无线网卡m2 ngff keya keye、minipcie接口改转多口有线网卡实现软路...

小型主板及笔记本中的无线网卡m2ngffkeyakeye接口&#xff08;CNVI除外&#xff09;通过m2ngffkeyae转接pcie1x转接板&#xff0c;或者无线网卡的minipcie接口&#xff0c;通过minipcie转接pcie1x转接板可以改装有线网卡板卡&#xff0c;来实现软路由功能。m2ngffkeyae转接pcie…

OpenGL | 通过绘制一个三角形来入门 OpenGL 图形渲染管线

文章目录前言什么是 OpenGl &#xff1f;回顾openGL 的 Object显存结构工作阶段通过顶点缓冲对象将顶点数据初始化至缓冲中标准化设备坐标顶点缓冲对象 VBOglGenBuffersglBindBufferglBufferData建立了一个顶点和一个片段着色器着色器是什么&#xff1f;为什么需要使用着色器&a…

javascript特效_如何在网页添加鼠标点击特效

经常有同学问我怎么做到的&#xff0c;本论坛属于DZ当然用的是插件啦。偶然在网上找到一个关于wordpress的特效代码&#xff0c;分享给大家。WordPress 添加鼠标点击特效实际上这种教程在网上搜索一下有一大堆&#xff0c;已经是各大博主玩烂的东西了。不过既然给自己的博客加上…