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.pswp.cn/news/443692.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/443692.shtml
英文地址,請注明出處:http://en.pswp.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;發現燙手…

Android | 再探 RecyclerView 之名詞解析

文章目錄Adapter、ViewHolderchild viewLayoutManagerRecyclerScrapDirtyIndexPositionlayout position 和 adapter position四級緩存瀏覽本文前推薦先閱讀 Android入門&#xff08;九&#xff09;| 滾動控件 ListView 與 RecyclerView Adapter、ViewHolder Adapter: A subcla…

Linux學習:第一章-Linux簡介

一 UNIX發展史1 1965年&#xff0c;美國麻省理工學院&#xff08;MIT&#xff09;、通用電氣公司&#xff08;GE&#xff09;及AT&T的貝爾實驗室聯合開發Multics工程計劃&#xff0c;其目標是開發一種交互式的具有多道程序處理能力的分時操作系統&#xff0c;但因Multics追…

尼爾機器人技能快捷鍵_《尼爾機械紀元》連招操作技巧

《尼爾機械紀元》中的每個角色都可以裝備一個輕武器和一個重武器&#xff0c;技能招式也很豐富&#xff0c;下面為大家帶來了《尼爾機械紀元》連招操作技巧&#xff0c;希望對你們有所幫助。連擊技能展示視頻視頻原址&#xff1a;點擊進入基本沒有什么太難的連段&#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 文件夾中找到最新版鏈接…