您好,登錄后才能下訂單哦!
這篇文章主要介紹“在Android中Service和AIDL怎么使用”的相關知識,小編通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強,希望這篇“在Android中Service和AIDL怎么使用”文章能幫助大家解決問題。
Service
(服務) 是一個一種可以在后臺執行長時間操作而沒有用戶界面的應用組件。服務可由其他應用組件啟動(如
Activity
),若沒進行綁定,服務一旦啟動將在后臺一直運行,即使啟動服務的組件(Activity
)已銷毀也不受影響。組件也可以綁定到服務,以與之進行交互,甚至是執行進程間通信 (
IPC
),這時組件銷毀時服務也會停止。
該類中的常用方法如下:
public abstract class Service extends ContextWrapper implements ComponentCallbacks2, ContentCaptureManager.ContentCaptureClient { // 創建時回調 public void onCreate() { } // startService 時回調 public @StartResult int onStartCommand(Intent intent, @StartArgFlags int flags, int startId) { onStart(intent, startId); return mStartCompatibility ? START_STICKY_COMPATIBILITY : START_STICKY; } // bindService 時的回調 @Nullable public abstract IBinder onBind(Intent intent); // unbindService 時的回調 public boolean onUnbind(Intent intent) { return false; } // 當 onUnbind 返回 true 時,重新綁定時的回調 public void onRebind(Intent intent) { } // 銷毀停止時的回調 public void onDestroy() { } // 內部的停止方法,若想停止并獲取結果使用 stopSelfResult(startId) public final void stopSelf() { stopSelf(-1); } }
這里 onStartCommand()
的返回值很重要,共有如下四種不同的取值:
START_STICKY
粘性的
onStartCommand()
使用這個返回值執行后,如果 service
進程被 kill 掉,保留 service
的狀態為開始狀態,但不保留遞送的 intent
對象;系統隨后會嘗試重新創建 service
,由于服務狀態為開始狀態,所以創建服務后一定會調用 onStartCommand (Intent, int, int)
方法。如果在此期間沒有任何啟動命令被傳遞到 service
, 那么參數 Intent
將為 null
。
START_NOT_STICKY
非粘性的
使用這個返回值時 , 如果在執行完 onStartCommand()
后 , 服務被異常 kill
掉 ,系統不會自動重啟該服務。
START_REDELIVER_INTENT
重傳 Intent
使用這個返回值時,如果在執行完 onStartCommand()
后,服務被異常 kill 掉,系統會自動重啟該服務 , 并將 Intent 的值傳入。
START_STICKY_COMPATIBILITY
START_STICKY
的兼容版本 , 但不保證服務被 kill
后一定能重啟。
提到生命周期就不得不說到關于 Service 的兩種啟動方法:
startService
bindService
此方式啟動的 Service 會一直無限運行,只有調用了它的 stopService()
或 stopSelf()
方法時,才會停止運行并銷毀。
生命周期:
startService:
若 service 沒被創建,調用 startService()
后會執行 onCreate()
----> onStartCommand()
,若 service 已創建,startService()
只會執行 onStartCommand()
。
stopService:
啟動的服務和調用者之間是典型的 client-server 模式,調用者是 client,Service 則是 server 端。
Service 只有一個,但綁定到 Service 上面的 client 可以有多個。
client 可以通過 IBinder 接口獲取 Service 實例,實現 client 端調用 Service 中的方法以實現交互。
啟動的 Service 的生命周期與其綁定的 client 息息相關。當 client 銷毀時,會自動與 Service 解除綁定,當然,也可以調用 Context 的 `unbindService()` 方法手動與 Service 解除綁定。當沒有任何 client 與 Service 綁定時,Service 就會自行銷毀。
生命周期:
bindService:
沒啥特殊的,就是 bindService 時 Service 若沒創建會創建 Service 示例,并在綁定成功后收到 onBind()
回調。
unbindService:
unbindService 將組件與 Service 解綁后會收到 onUnbind()
回調,此時該 Service 若無任何組件與其綁定,則會自行銷毀。
通過上節介紹,可以了解到啟動 Service 有 startService()
和 bindService()
兩種方式。
該啟動方式,app
殺死、Activity
銷毀沒有任何影響,服務都不會銷毀停止運行,所以此方式適合后臺一直運行的任務,但無法調用 Service 中的方法進行交互。
比如:播放音樂、下載文件、進程保活…
停止方式:主動 stopService()
該啟動方式依賴于客戶端生命周期,當客戶端 Activity
銷毀時,即使沒有調用 unbindService()
方法,Service
也會銷毀停止運行。所以此方式適合短時使用同時與 Service 產生交互的任務。
比如:通過 Service 跨進程傳輸數據…
停止方式:Service 無任何綁定即會自動停止
該啟動方式 Service 可以在后臺一直運行,同時還可以與 Service 產生交互,調用它的方法。
比如:播放并控制音樂、下載文件并更新進度…
停止方式:需要解除綁定并 stopService()
既然提到 Service 可以執行后臺任務,那么也可以使用線程呀?那么看看有啥不同吧!
首先看看定義:
Thread
線程為程序執行的最小單元,Android 中的 UI 線程就是其中一種。
Service
安卓中的四大組件之一,為一種特殊的機制,其實是一種輕量級的 IPC
通信。
其次,需要了解一下 Thread 的局限性:
Thread
的運行是獨立于 Activity
的但依賴于進程,也就是說當應用進程被殺死或者線程體運行完畢時就會停止運行。
當 Activity
被 finish
后,是無法對其進行管理的。同時 Thread
運行過程中對其進行控制操作也非常麻煩,而且也無法通過它實現 IPC(跨進程)通信。
那么,就可以了解到 Service 是可以做到這些的。
默認情況下, Service 是運行在當前 app 進程的 UI 主線程中,可以在 AndroidManifest 文件中配置 android:process
指定它所在的進程。
因此,與 Activity 一樣,Service 是無法直接在其內部執行耗時任務的,需要開啟子線程去執行,否則就會產生 ANR。
在介紹 IntentService 之前先說明一下使用傳統的 Service 會有何問題:
無法直接處理耗時任務,需要內部開啟子線程
startService()
啟動之后需要手動去停止
那么 IntentService 就是為了解決這些問題而生的,看下該類的主要內容:
@Deprecated public abstract class IntentService extends Service { // 該 Handler 在子線程中創建 private final class ServiceHandler extends Handler { public ServiceHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { onHandleIntent((Intent)msg.obj); // 處理完任務后自動停止 stopSelf(msg.arg1); } } @Override public void onCreate() { super.onCreate(); HandlerThread thread = new HandlerThread("IntentService[" + mName + "]"); thread.start(); mServiceLooper = thread.getLooper(); mServiceHandler = new ServiceHandler(mServiceLooper); } @Override public void onStart(@Nullable Intent intent, int startId) { Message msg = mServiceHandler.obtainMessage(); msg.arg1 = startId; msg.obj = intent; // startService() 時將信息發送到 ServiceHandler 中 mServiceHandler.sendMessage(msg); } /** * 該方法的返回值為 Service 的標志位: * * @see Service.START_STICKY_COMPATIBILITY: START_STICKY 的兼容版本,但不保證服務被kill后一定能重啟 * * @see Service.START_STICKY: 粘性的,如果 service 進程被 kill 掉,保留 service 的狀態為開始狀態,但不保留傳送的 intent 對象。隨后系統會嘗試重新創建 service,創建后即會重新調用 onStartCommand(Intent,int,int) 方法。如果在此期間沒有任何啟動命令被傳遞到 service,那么參數 Intent 將為null * * @see Service.START_NOT_STICKY: 非粘性的,在執行完 onStartCommand 后,服務被異常 kill 掉,系統不會自動重啟該服務 * * @see Service.START_REDELIVER_INTENT: 重傳 Intent,在執行完 onStartCommand 后,服務被異常 kill 掉,系統會自動重啟該服務,并將 Intent 的值傳入 * */ @Override public int onStartCommand(@Nullable Intent intent, int flags, int startId) { onStart(intent, startId); return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY; } @Override public void onDestroy() { mServiceLooper.quit(); } @Override @Nullable public IBinder onBind(Intent intent) { return null; } // startService() -----> ServiceHandler 的 handleMessage() ------> onHandleIntent() @WorkerThread protected abstract void onHandleIntent(@Nullable Intent intent); }
從上述代碼中就可以看出它的主要特征:
會創建獨立的 worker
線程來處理所有的 Intent
請求,并最終執行復寫的 onHandleIntent()
方法;
請求任務處理完成后,IntentService
會自動停止,無需調用 stopSelf()
方法停止 Service
;
因此,對于那種需要后臺處理某些任務,處理完成即退出是非常適合使用 IntentService 的,如:下載文件。
上節中可以看到 IntentService 被標記廢棄了,這是由于 Android 8.0(API 26) 以后系統不允許后臺應用創建后臺服務,創建后臺服務需要使用 JobScheduler
來由系統進行調度任務的執行。那么怎樣應用會被認定為后臺呢?
如果滿足以下任意條件,應用將被視為處于前臺:
具有可見 Activity (不管該 Activity 已啟動還是已暫停);
具有前臺服務;
另一個前臺應用已關聯到該應用(不管是通過綁定到其中一個服務,還是通過使用其中一個內容提供程序);
IME;
壁紙服務;
通知偵聽器;
語音或文本服務。
如果以上條件均不滿足,應用將被視為處于后臺。
因此,Android 8.0 引入了一種全新的方法,即 Context.startForegroundService()
以在前臺啟動新服務。系統創建服務后應在五秒的時間內調用該服務的 startForeground()
方法以顯示新服務的用戶可見通知。如果未在此時間限制內未調用 startForeground()
方法,則系統將停止服務并聲明此應用為 ANR
。
但在一些特殊情況下,還是可以創建后臺服務的:
處理對用戶可見的任務時,后臺應用將被置于一個臨時白名單中并持續數分鐘。位于白名單中時,應用可以無限制地啟動服務,并且其后臺服務也可以運行。
處理一條高優先級 Firebase 云消息傳遞 (FCM) 消息;
接收廣播,例如短信/彩信消息;
從通知執行 PendingIntent。
bindService()
方法不受后臺限制。
使用步驟:
添加權限
創建一個前臺服務,首先需要請求前臺服務權限(Android 9 - API 級別 28 及以上 ),如下:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" ...> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <application ...> ... </application> </manifest>
這是一個正常的權限,系統會自動將其授予請求的應用程序。
創建前臺服務
創建一個前臺服務與創建一個正常服務沒太大區別,只是需要在 Service 創建之后調用 startForeground()
來啟動前臺服務,如下:
class TestService : Service() { override fun onCreate() { super.onCreate() // Android 8.0 調用 startForefround() 方法啟動服務 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { setForegroundService(); } } private fun setForegroundService() { // 創建通知渠道 val CHANNEL_ID = 1 val channelName = getString(R.string.channel_name) // 設置通知的優先級 val importance = NotificationManager.IMPORTANCE_LOW val channel = NotificationChannel(CHANNEL_ID, channelName, importance) val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.createNotificationChannel(channel) // 設置前臺服務通知的點擊事件 val pendingIntent: PendingIntent = Intent(this, ExampleActivity::class.java).let { notificationIntent -> PendingIntent.getActivity(this, 0, notificationIntent, 0) } // 創建通知 val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle(getText(R.string.notification_title)) .setContentText(getText(R.string.notification_message)) .setSmallIcon(R.drawable.icon) .setContentIntent(pendingIntent) .setTicker(getText(R.string.ticker_text)) .build() // Notification ID cannot be 0. startForeground(NOTIFICATION_ID, notification) } override fun onBind(intent: Intent): IBinder? { return null } }
在這里有兩個點需要注意一下:
Android 8.0 以上創建通知需要先創建通知渠道,再使用渠道 id 創建通知
前臺服務的通知優先級必須為 PRIORITY_LOW
或更高,通知優先級詳細見設置渠道的重要性級別
注冊服務
<manifest xmlns:android="http://schemas.android.com/apk/res/android" ...> <application ...> <service android:name="com.hl.myplugin.TestService" android:enabled="true" android:exported="false" /> </application> </manifest>
聲明前臺服務類型
如果在 Android 10(API 級別 29)或更高版本訪問前臺服務中的位置信息,則需要聲明 <service>
組件的前臺服務類型為 location;
如果在 Android 11(API 級別 30)或更高版本訪問前臺服務中的攝像頭或麥克風,則需要聲明 <service>
組件的前臺服務類型為 camera 或 microphone。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" ...> <application ...> <service ... android:foregroundServiceType="location|camera|microphone"/> </application> </manifest>
在運行時,如果前臺服務只需要訪問清單中聲明的類型的子集,則可以使用以下代碼片段中的邏輯來限制服務的訪問:
val notification: Notification = ... // 開啟前臺服務的重載方法 startForeground(NOTIFICATION_ID, notification, FOREGROUND_SERVICE_TYPE_LOCATION or FOREGROUND_SERVICE_TYPE_CAMERA)
但 Android 11(API 級別 30)為了幫助保護用戶隱私,對前臺服務何時可以訪問設備的位置、攝像頭或麥克風進行了限制。當應用程序在后臺運行啟動前臺服務時,前臺服務有以下限制:
除非用戶已授予應用程序 ACCESS_BACKGROUND_LOCATION
權限,否則 前臺服務無法訪問位置。
前臺服務無法訪問麥克風或攝像頭。
如果啟動的服務對位置、麥克風和攝像頭的訪問受到限制,那么在調試時 Logcat 中會顯示以下消息:
Foreground service started from background can not have location/camera/microphone access: service SERVICE_NAME
限制豁免:
在某些情況下,即使應用程序在后臺運行時啟動了前臺服務 ,它仍然可以在應用程序在前臺運行時(“使用中”)訪問位置、相機和麥克風信息。在這些情況下,如果服務聲明了一個 前臺服務類型 為 location
,并且服務由一個具有ACCESS_BACKGROUND_LOCATION
權限的應用程序啟動,那么該服務即使在后臺啟動但在前臺運行時仍可訪問位置信息。
以下包含這些情況:
該服務由系統組件啟動。
該服務通過與應用小部件交互啟動。
該服務通過與通知交互來啟動。
該服務作為PendingIntent
從不同的可見應用程序發送的啟動 。
該服務由在設備所有者模式下運行的設備策略控制器應用程序啟動。
該服務由提供VoiceInteractionService
.
該服務由具有START_ACTIVITIES_FROM_BACKGROUND
特權權限的應用程序啟動 。
啟動前臺服務
// Android 8.0使用 startForegroundService 在前臺啟動服務 if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){ startForegroundService(Intent(this,TestService::class.java) }else{ startService(Intent(this,TestService::class.java)) }
移除前臺服務
調用 stopForeground(removeNotification: Boolean)
方法即可移除前臺服務,參數代表是否刪除狀態欄通知。
注意: stopForeground()
并不會使服務停止運行,若想停止服務,仍需調用 stopService()
由于前臺服務在通知欄上會顯示該 Service
正在運行,這可能會帶來不好的用戶體驗。如果還是希望使用服務在后臺默默工作、通過使用服務開啟子進程等等,那么就可以使用 JobIntentService
。
JobIntentService
用于處理被加入到隊列的 job/service 任務。當運行在 Android O 或更高版本時,任務將作為 job
通過 JobScheduler.enqueue()
進行分發;當運行在較老版本的平臺上時,任務仍舊會使用 Context.startService()
執行。
使用步驟:
在 Manifest 中聲明 Permission
JobIntentService
處理了亮屏/鎖屏,因此需要在 AndroidManifest.xml
中添加 android.Manifest.permission.WAKE_LOCK
權限,如下:
<uses-permission android:name="android.permission.WAKE_LOCK" />
在 Manifest 中聲明 Service
JobIntentService
本質上也是一個 Service
,因此需要在 AndroidManifest.xml
聲明,以便系統與之交互,如下:
<service android:name="SimpleJobIntentService" android:permission="android.permission.BIND_JOB_SERVICE" > ... </service>
創建 JobIntentService 的實現類
與 IntentService 的 onHandleIntent()
方法相似,只需重寫 onHandleWork()
方法處理相應的邏輯即可:
class SimpleJobIntentService : JobIntentService() { companion object { private const val JOB_ID = 1 fun enqueueWork(context: Context, work: Intent) { enqueueWork(context, SimpleJobIntentService::class.java, JOB_ID, work) } } override fun onHandleWork(intent: Intent) { // 具體邏輯 } }
啟動服務
SimpleJobIntentService.enqueueWork(context, new Intent());
可以看出,它使用起來非常簡單,因為已經封裝了大量的內部邏輯,只需要調用 enqueueWork()
靜態方法就可以了。
Android 系統會盡可能長的延續一個應用程序進程,但在內存過低的時候,仍然會不可避免需要移除舊的進程。為了決定哪些進程留下,哪些進程被殺死,系統根據在進程中在運行的組件及組件的狀態,為每一個進程分配了一個優先級等級。優先級最低的進程首先被殺死。這個進程重要性的層次結構主要有五個等級。
主要分為:
前臺進程(Foreground process)
可見進程(Visible process)
服務進程 (Service process)
后臺進程 (Background process)
空進程
了解這些以后,你就能明白為啥不建議在 Activity 中開線程處理耗時任務?
主要原因如下:
Activity 中開線程做耗時操作,切到桌面會變成后臺進程
啟動 Service 新建線程處理耗時任務,這時會變為服務進程
因為服務進程的優先級比后臺進程的優先級高,所以此方式處理耗時任務更好。 同時,使用 Service 將會保證 app 至少有服務進程的優先級。
前臺進程是用戶當前做的事所必須的進程,如果滿足下面各種情況中的一種,一個進程被認為是在前臺:
進程持有一個正在與用戶交互的 Activity。
進程持有一個 Service,這個 Service 處于這幾種狀態:
Service 與用戶正在交互的 Activity 綁定。
Service 是在前臺運行的,即它調用了 startForeground()
。
Service 正在執行它的生命周期回調函數 —— onCreate()
、 onStart()
或 onDestroy()
。
進程持有一個 BroadcastReceiver,這個 BroadcastReceiver 正在執行它的 onReceive()
方法。
殺死前臺進程需要用戶交互,因為前臺進程的優先級是最高的。
如果一個進程不含有任何前臺的組件,但仍可被用戶在屏幕上所見。當滿足如下任一條件時,進程被認為是可見的:
進程持有一個 Activity,這個 Activity 不在前臺,但是仍然被用戶可見,即處于 onPause()
狀態。
進程持有一個 Service,這個 Service 和一個可見的 Activity 綁定。
可見的進程也被認為是很重要的,一般不會被銷毀,除非是為了保證所有前臺進程的運行而不得不殺死可見進程的時候。
如果一個進程中運行著一個 Service,這個 Service 是通過 startService()
開啟的,并且不屬于上面兩種較高優先級的情況(未進行任何綁定),這個進程就是一個服務進程。
盡管服務進程沒有和用戶可以看到的東西綁定,但是它們一般在做的事情是用戶關心的,比如后臺播放音樂,后臺下載數據等。所以系統會盡量維持它們的運行,除非系統內存不足以維持前臺進程和可見進程的運行需要。
如果進程不屬于上面三種情況,但是它持有一個用戶不可見的activity(Activity的 onStop()
被調用,但是 onDestroy()
沒有調用的狀態),就認為進程是一個后臺進程。
后臺進程不直接影響用戶體驗,系統會為了前臺進程、可見進程、服務進程而任意殺死后臺進程。
通常會有很多個后臺進程存在,它們會被保存在一個 LRU (least recently used) 列表中,這樣就可以確保用戶最近使用的 Activity 最后被銷毀,即最先銷毀時間最遠的 Activity。
如果一個進程不包含任何活躍的應用組件,則認為是空進程。
例如:一個進程當中已經沒有數據在運行,但是內存當中還為這個應用駐留了一個進程空間。
保存這種進程的唯一理由是為了緩存的需要,為了加快下次要啟動這個進程中的組件時的啟動時間。系統為了平衡進程緩存和底層內核緩存的資源,經常會殺死空進程。
設置最高優先級
<service android:name="com.dbjtech.acbxt.waiqin.UploadService" android:enabled="true" > <intent-filter android:priority="1000" > <action android:name="xxxx" /> </intent-filter> </service>
如上,Service 對于 intent-filter 可以通過 android:priority = “1000”
這個屬性設置最高優先級,1000是最高值,如果數字越小則優先級越低,同時適用于廣播。
使用前臺服務
Service 創建時通過 startForeground()
方法把 Service 提升為前臺進程級別,在 onDestroy()
里面要記得調用 stopForeground()
方法。
復寫onStartCommand()
方法,返回 START_STICKY
當 Service 因內存不足被 kill,當內存又有的時候,Service 就會被重新創建啟動。
注意:但是不能保證任何情況下都被重建,比如進程被干掉了….
onDestroy()
方法里發廣播重啟 Service
Service + Broadcast 方式,就是當 Service 走 onDestory()
的時候,發送一個自定義的廣播,當收到廣播的時候,重新啟動 Service。
注意:第三方應用或是在 setting 里-應用-強制停止時,APP 進程就直接被干掉了,onDestroy()
方法都進不來,所以無法保證會執行
監聽系統廣播判斷 Service 狀態
通過系統的一些廣播,比如:手機重啟、界面喚醒、應用狀態改變等等監聽并捕獲,然后判斷我們的 Service 是否還存活決定是否重新啟動。
Application 加上 Persistent 屬性
該屬性相當于將該進程設置為常駐內存進程,即系統應用。一般為安裝在/system/app下的 app,正常的三方應用安裝在 /data/app 下。
Android 接口定義語言 (AIDL),利用它定義客戶端與服務均認可的編程接口,以便二者使用進程間通信 (IPC) 進行相互通信。
跨進程通信 (IPC) 的方式很多,AIDL 是其中一種。還有
Binder
、文件共享、Messenger
、ContentProvider
和Socket
等進程間通信的方式。AIDL 是接口定義語言,只是一個工具。具體通信還是得用Binder 來進行。Binder 是 Android 獨有的跨進程通信方式,只需要一次拷貝,更快速和安全。官方推薦用
Messenger
來進行跨進程通信,但是Messenger
是以串行的方式來處理客戶端發來的消息,如果大量的消息同時發送到服務端,服務端仍然只能一個個處理。因此對于大量的并發請求,這種情況就得用 AIDL 。其實 Messenger 的底層也是 AIDL,只不過系統做了層封裝,簡化使用。
創建一個 Handler 對象,并實現 hanlemessage
方法,用于接收來自客戶端的消息,并作處理
創建一個 Messenger,封裝 Handler
messenger.getBinder()
方法獲取一個 IBinder 對象,通過 onBind
返回給客戶端
使用示例如下:
public class MessengerService extends Service { // 存儲客戶端發送的 Messenger 對象 ArrayList<Messenger> mClients = new ArrayList<Messenger>(); int mValue = 0; /** * 客戶端請求注冊 Messenger */ static final int MSG_REGISTER_CLIENT = 1; /** * 客戶端請求反注冊 Messenger */ static final int MSG_UNREGISTER_CLIENT = 2; /** * 客戶端請求設值,相當于請求其他命令 */ static final int MSG_SET_VALUE = 3; class IncomingHandler extends Handler { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_REGISTER_CLIENT: mClients.add(msg.replyTo); break; case MSG_UNREGISTER_CLIENT: mClients.remove(msg.replyTo); break; case MSG_SET_VALUE: mValue = msg.arg1; for (int i=mClients.size()-1; i>=0; i--) { try { // 取得客戶端傳送的 Messenger,發送消息回 Messenger 實現雙向通信 mClients.get(i).send(Message.obtain(null, MSG_SET_VALUE, mValue, 0)); } catch (RemoteException e) { // 客戶端有可能在此過程中死了產生異常,需要移除 mClients.remove(i); } } break; default: super.handleMessage(msg); } } } final Messenger mMessenger = new Messenger(new IncomingHandler()); @Override public IBinder onBind(Intent intent) { return mMessenger.getBinder(); } }
注意:該Service 在聲明時必須對外開放,即 android:exported="true"
在 Activity 中綁定服務
創建 ServiceConnection ,在其 onServiceConnected()
方法中通過參數 IBinder 將 Messenger 實例化
使用 Messenger 向服務端發送命令,或需要接收服務器端的返回信息,則還要創建一個 Messenger(handler)
,并將這個 Messenger 傳遞給服務端,在handler 中接收處理服務端的消息,這就實現了客戶端和服務端的雙向通信
使用示例如下:
public class MessengerServiceActivities extends Activity{ // 向服務端發送命令的 Messenger private Messenger mService = null; private boolean mIsBound; private TextView mCallbackText; private class IncomingHandler extends Handler { @Override public void handleMessage(Message msg) { switch (msg.what) { case MessengerService.MSG_SET_VALUE: mCallbackText.setText("Received from service: " + msg.arg1); break; default: super.handleMessage(msg); } } } // 接收服務端返回消息的 Messenger private final Messenger mMessenger = new Messenger(new IncomingHandler()); private ServiceConnection mConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { // 連接時獲取與服務端交互的 Messenger mService = new Messenger(service); mCallbackText.setText("Attached."); try { // 將需要接收服務端返回消息的 Messenger 發送在消息體中 Message msg = Message.obtain(null, MessengerService.MSG_REGISTER_CLIENT); msg.replyTo = mMessenger; mService.send(msg); // 向服務端發送設值命令 msg = Message.obtain(null, MessengerService.MSG_SET_VALUE, this.hashCode(), 0); mService.send(msg); } catch (RemoteException e) { // xxx } Toast.makeText(this, R.string.remote_service_connected, Toast.LENGTH_SHORT).show(); } public void onServiceDisconnected(ComponentName className) { mService = null; mCallbackText.setText("Disconnected."); Toast.makeText(this, R.string.remote_service_disconnected,Toast.LENGTH_SHORT).show(); } }; void doBindService() { bindService(new Intent(this, MessengerService.class), mConnection, Context.BIND_AUTO_CREATE); mIsBound = true; mCallbackText.setText("Binding."); } void doUnbindService() { if (mIsBound) { if (mService != null) { try { // 解綁時移除服務端中添加的 Messenger,取消消息接收 Message msg = Message.obtain(null, MessengerService.MSG_UNREGISTER_CLIENT); msg.replyTo = mMessenger; mService.send(msg); } catch (RemoteException e) { //xxx } } unbindService(mConnection); mIsBound = false; mCallbackText.setText("Unbinding."); } } }
步驟:
創建 .aidl 文件:
定義 AIDL 接口
實現接口:
Android SDK 工具會基于 .aidl 文件,使用 Java 編程語言生成繼承自 IInterface
接口的接口。生成的接口擁有一個繼承自 Binder 類名為 Stub 的內部抽象類,并聲明 AIDL 接口中的抽象方法。大概結構如下:
public interface IInterface{ public IBinder asBinder(); } public interface xxxInterface extends android.os.IInterface{ public static abstract class Stub extends android.os.Binder implements xxxInterface{ @Override public android.os.IBinder asBinder() { return this; } xxxx } // AIDL 中聲明的抽象方法 xxxx }
向客戶端公開接口:
實現 Service 并重寫 onBind(),從而返回 Stub 類的實現.
在 src/main
下面創建 aidl 目錄,然后新建 IPersonManager.aidl
文件,里面聲明方法用于客戶端調用,服務端實現。如下:
package com.xfhy.allinone.ipc.aidl; import com.xfhy.allinone.ipc.aidl.Person; interface IPersonManager { List<Person> getPersonList(); //in: 從客戶端流向服務端 boolean addPerson(in Person person); }
這個接口和平常我們定義接口時差別不是很大,需要注意的是即使 Person 和 PersonManager 在同一個包下面還是得導包,這是AIDL的規則。
AIDL 支持的數據類型
在 AIDL 文件中,不是所有數據類型都是可以使用的,支持的數據類型如下:
Java 編程語言中的所有原語類型(如 int、long、char、boolean 等)
String 和 CharSequence
List:只支持 ArrayList,里面每個元素都必須能夠被 AIDL 支持
Map:只支持HashMap,里面的每個元素都必須被 AIDL 支持,包括 key 和 value
Parcelable:所有實現了Parcelable接口的對象
AIDL:所有的AIDL接口本身也可以在 AIDL 文件中使用
定義傳輸的對象
在 kotlin 或 Java 這邊需要定義好這個需要傳輸的對象 Person,,或者定義在 aidl 目錄下, 但需要通過 sourceSet{}
將此目錄定義為 kotlin 或 java 源碼目錄,這里以在 kotlin 下為示例:
class Person(var name: String? = "") : Parcelable { constructor(parcel: Parcel) : this(parcel.readString()) override fun toString(): String { return "Person(name=$name) hashcode = ${hashCode()}" } override fun writeToParcel(parcel: Parcel, flags: Int) { parcel.writeString(name) } fun readFromParcel(parcel: Parcel) { this.name = parcel.readString() } override fun describeContents(): Int { return 0 } companion object CREATOR : Parcelable.Creator<Person> { override fun createFromParcel(parcel: Parcel): Person { return Person(parcel) } override fun newArray(size: Int): Array<Person?> { return arrayOfNulls(size) } }
然后得在 aidl 的相同目錄下也需要聲明一下這個 Person 對象,新建一個 Person.aidl:
package com.xfhy.allinone.ipc.aidl; parcelable Person;
注意:當需要傳遞對象時,則該對象必須實現 Parcelable 接口并且需要指示數據走向的方向標記
方向標記 | 意義 |
---|---|
in | 數據只能由客戶端流向服務端,服務端修改數據不會同步返回 |
out | 數據只能由服務端流向客戶端,客戶端會新創建一個無參對象傳遞到服務端,服務端修改數據會同步返回 |
inout | 數據可在服務端與客戶端之間雙向流通,服務端和客戶端同步共用一個對象 |
原語類型(基本類型)默認是 in,inout 開銷很大,因此慎用。調用 AIDL 生成接口的為客戶端,實現接口方為服務端。
都完成了之后,rebuild 一下,AS 會自動生成IPersonManager.java
接口文件。
定義一個 Service, 然后將其 process 設置成一個新的進程,與主進程區分開,模擬跨進程訪問,它里面需要實現 .aidl
生成的接口,如下:
class RemoteService : Service() { private val mPersonList = mutableListOf<Person?>() private val mBinder: Binder = object : IPersonManager.Stub() { override fun getPersonList(): MutableList<Person?> = mPersonList override fun addPerson(person: Person?): Boolean { return mPersonList.add(person) } } override fun onBind(intent: Intent?): IBinder? { return mBinder } override fun onCreate() { super.onCreate() mPersonList.add(Person("Garen")) mPersonList.add(Person("Darius")) } }
實現的 IPersonManager.Stub
是一個 Binder,需要通過 onBind()
返回,客戶端需要通過這個 Binder 來跨進程調用 Service 這邊的服務。
客戶端這邊需要通過 bindService()
來連接此 Service,進而實現通信。客戶端的 onServiceConnected()
回調會接收 Service 的 onBind()
方法所返回的 binder 實例。再調用 XxxInterface.Stub.asInterface(service)
就能轉換取得 XxxInterface 實例。如下:
class AidlActivity : TitleBarActivity() { companion object { const val TAG = "xfhy_aidl" } private var remoteServer: IPersonManager? = null private val serviceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName?, service: IBinder?) { log(TAG, "onServiceConnected") //在onServiceConnected調用IPersonManager.Stub.asInterface 獲取接口類型的實例 //通過這個實例調用服務端的服務 remoteServer = IPersonManager.Stub.asInterface(service) } override fun onServiceDisconnected(name: ComponentName?) { log(TAG, "onServiceDisconnected") } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_aidl) btnConnect.setOnClickListener { connectService() } btnGetPerson.setOnClickListener { getPerson() } btnAddPerson.setOnClickListener { addPerson() } } private fun connectService() { val intent = Intent() //action 和 package(app的包名) intent.action = "com.xfhy.aidl.Server.Action" intent.setPackage("com.xfhy.allinone") val bindServiceResult = bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE) log(TAG, "bindService $bindServiceResult") //如果targetSdk是30,那么需要處理Android 11中的程序包可見性 具體參見: https://developer.android.com/about/versions/11/privacy/package-visibility } private fun addPerson() { //客戶端調服務端方法時,需要捕獲以下幾個異常: //RemoteException 異常: //DeadObjectException 異常:連接中斷時會拋出異常; //SecurityException 異常:客戶端和服務端中定義的 AIDL 發生沖突時會拋出異常; try { val addPersonResult = remoteServer?.addPerson(Person("蓋倫")) log(TAG, "addPerson result = $addPersonResult") } catch (e: RemoteException) { e.printStackTrace() } catch (e: DeadObjectException) { e.printStackTrace() } catch (e: SecurityException) { e.printStackTrace() } } private fun getPerson() { val personList = remoteServer?.personList log(TAG, "person 列表 $personList") } override fun onDestroy() { super.onDestroy() //最后記得unbindService unbindService(serviceConnection) } }
測試時先 getPerson 再 addPerson 最后 getPerson,輸出日志:
2020-12-24 12:41:00.170 24785-24785/com.xfhy.allinone D/xfhy_aidl: bindService true 2020-12-24 12:41:00.906 24785-24785/com.xfhy.allinone D/xfhy_aidl: onServiceConnected 2020-12-24 12:41:04.253 24785-24785/com.xfhy.allinone D/xfhy_aidl: person 列表 [Person(name=Garen), Person(name=Darius)] 2020-12-24 12:41:05.952 24785-24785/com.xfhy.allinone D/xfhy_aidl: addPerson result = true 2020-12-24 12:41:09.022 24785-24785/com.xfhy.allinone D/xfhy_aidl: person 列表 [Person(name=Garen), Person(name=Darius), Person(name=蓋倫)]
注意:在客戶端調用這些遠程方法時是同步調用,在主線程調用可能會導致 ANR,應該在子線程去調用。
將 aidl 接口的方法前加上 oneway
關鍵字則這個方法就是異步調用,不會阻塞調用線程。當客戶端調用服務端的方法不需要知道返回結果時,使用異步調用可以提高客戶端的執行效率。
AIDL 的方法是在服務端的 Binder 線程池中執行的,所以多個客戶端同時進行連接且操作數據時可能存在多個線程同時訪問的情形。這時就需要在服務端 AIDL 方法中處理多線程同步問題。
先看下服務端的 AIDL 方法是在哪個線程中:
override fun addPerson(person: Person?): Boolean { log(TAG, "服務端 addPerson() 當前線程 : ${Thread.currentThread().name}") return mPersonList.add(person) } //日志輸出 服務端 addPerson() 當前線程 : Binder:3961_3
可以看到,確實是在非主線程中執行的,那確實會存在多線程安全問題。這就需要將 mPersonList 的類型修改為 CopyOnWriteArrayList,以確保線程安全:
//服務端 private val mPersonList = CopyOnWriteArrayList<Person?>() override fun getPersonList(): MutableList<Person?> = mPersonList //客戶端 private fun getPerson() { val personList = remoteServer?.personList personList?.let { log(TAG, "personList ${it::class.java}") } } //輸出日志 personList class java.util.ArrayList
另外還有 ConcurrentHashMap 也是同樣的道理,這里就不驗證了。
上面的案例中,只能在客戶端每次去調服務端的方法然后獲得結果。若想服務端數據有變動就通知一下客戶端,這就需要添加監聽器了。
因為這個監聽器 Listener 是需要跨進程的,這里首先就需要為這個 Listener 創建一個 aidl 的回調接口IPersonChangeListener.aidl
interface IPersonChangeListener { // 這里由服務端調用此接口,因此服務端其實充當 "Client",數據流通方向標記為 in 更合理 void onPersonDataChanged(in Person person); }
有了監聽器,還需要在 IPersonManager.aidl
中加上注冊/反注冊監聽的方法:
interface IPersonManager { ...... void registerListener(IPersonChangeListener listener); void unregisterListener(IPersonChangeListener listener); }
現在我們在服務端實現這個注冊/反注冊的方法,這還不簡單嗎? 搞一個 List<IPersonChangeListener>
來存放 Listener 集合,當數據變化的時候遍歷這個集合,通知一下這些Listener就行。
仔細想想這樣真的行嗎? 這個 IPersonChangeListener
是需要跨進程的,那么客戶端每次傳過來的對象是經過序列化與反序列化的,服務端這邊接收到的根本不是客戶端傳過來的那個對象。 雖然傳過來的 Listener 不同,但是用來通信的 Binder 是同一個,利用這個原理 Android 提供了一個 RemoteCallbackList
的東西,專門用于存放監聽接口的集合的。RemoteCallbackList
內部將數據存儲于一個 ArrayMap 中,key 就是用來傳輸的 binder,value 就是監聽接口的封裝。如下:
//RemoteCallbackList.java 源碼有刪減 public class RemoteCallbackList<E extends IInterface> { ArrayMap<IBinder, Callback> mCallbacks = new ArrayMap<IBinder, Callback>(); private final class Callback implements IBinder.DeathRecipient { final E mCallback; final Object mCookie; Callback(E callback, Object cookie) { mCallback = callback; mCookie = cookie; } } public boolean register(E callback, Object cookie) { synchronized (mCallbacks) { IBinder binder = callback.asBinder(); Callback cb = new Callback(callback, cookie); mCallbacks.put(binder, cb); return true; } } }
RemoteCallbackList
內部在操作數據的時候已經做了線程同步的操作,所以不需要單獨做額外的線程同步操作。現在來實現一下這個注冊/反注冊方法:
private val mListenerList = RemoteCallbackList<IPersonChangeListener?>() private val mBinder: Binder = object : IPersonManager.Stub() { ..... override fun registerListener(listener: IPersonChangeListener?) { mListenerList.register(listener) } override fun unregisterListener(listener: IPersonChangeListener?) { mListenerList.unregister(listener) } }
RemoteCallbackList
添加與刪除數據對應著 register()/unregister()
方法,然后我們模擬一下服務端數據更新的情況,開個線程每隔 5 秒添加一個 Person 數據,然后通知一下觀察者:
//死循環 每隔5秒添加一次person,通知觀察者 private val serviceWorker = Runnable { while (!Thread.currentThread().isInterrupted) { Thread.sleep(5000) val person = Person("name${Random().nextInt(10000)}") log(AidlActivity.TAG, "服務端 onDataChange() 生產的 person = $person}") mPersonList.add(person) onDataChange(person) } } private val mServiceListenerThread = Thread(serviceWorker) //數據變化->通知觀察者 private fun onDataChange(person: Person?) { //1. 使用RemoteCallbackList時,必須首先調用beginBroadcast(), 最后調用finishBroadcast(). 得成對出現 //這里拿到的是監聽器的數量 val callbackCount = mListenerList.beginBroadcast() for (i in 0 until callbackCount) { try { //這里try一下避免有異常時無法調用finishBroadcast() mListenerList.getBroadcastItem(i)?.onPersonDataChanged(person) } catch (e: RemoteException) { e.printStackTrace() } } //3. 最后調用finishBroadcast() 必不可少 mListenerList.finishBroadcast() } override fun onCreate() { ..... mServiceListenerThread.start() } override fun onDestroy() { super.onDestroy() mServiceListenerThread.interrupt() }
服務端實現好了,客戶端就比較好辦:
private val mPersonChangeListener = object : IPersonChangeListener.Stub() { override fun onPersonDataChanged(person: Person?) { log(TAG, "客戶端 onPersonDataChanged() person = $person}") } } private fun registerListener() { remoteServer?.registerListener(mPersonChangeListener) } private fun unregisterListener() { remoteServer?.asBinder()?.isBinderAlive?.let { remoteServer?.unregisterListener(mPersonChangeListener) } }
因為是需要跨進程通信的,所以需要繼承自 IPersonChangeListener.Stub 從而生成一個監聽器對象。最后輸出日志如下:
服務端 onDataChange() 生產的 person = Person(name=name9398) hashcode = 130037351} 客戶端 onPersonDataChanged() person = Person(name=name9398) hashcode = 217703225}
服務端進程可能隨時會被殺掉,這時需要在客戶端能夠被感知到 binder 已經死亡,從而做一些收尾清理工作或者進程重新連接。有如下 4 種方式能知道服務端是否已經掛掉:
調用 binder 的 pingBinder()
檢查,返回 false 則說明遠程服務失效
調用 binder 的 linkToDeath()
注冊監聽器,當遠程服務失效時,就會收到回調
綁定 Service 時用到的 ServiceConnection 有個 onServiceDisconnected()
回調在服務端斷開時也能收到回調
客戶端調用遠程方法時,拋出 DeadObjectException(RemoteException)
寫份代碼驗證一下,在客戶端修改為如下:
private val mDeathRecipient = object : IBinder.DeathRecipient { override fun binderDied() { //監聽 binder died log(TAG, "binder died") //移除死亡通知 mService?.unlinkToDeath(this, 0) mService = null //重新連接 connectService() } } private val serviceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName?, service: IBinder?) { this@AidlActivity.mService = service log(TAG, "onServiceConnected") //給binder設置一個死亡代理 service?.linkToDeath(mDeathRecipient, 0) mRemoteServer = IPersonManager.Stub.asInterface(service) } override fun onServiceDisconnected(name: ComponentName?) { log(TAG, "onServiceDisconnected") } }
綁定服務之后,將服務端進程殺掉,輸出日志如下:
//第一次連接 bindService true onServiceConnected, thread = main //殺掉服務端 binder died, thread = Binder:29391_3 onServiceDisconnected, thread = main //重連 bindService true onServiceConnected, thread = main
確實是監聽到服務端斷開連接的時刻,然后重新連接也是 ok 的。
注意:binderDied()
方法是運行在子線程的,onServiceDisconnected()
是運行在主線程的,如果要在這里更新UI,得注意一下。
有沒有注意到,目前的 Service 是完全暴露的,任何 app 都可以訪問這個 Service 并且遠程調用 Service 的服務,這樣不太安全。可以在清單文件中加入自定義權限,然后在 Service 中校驗一下客戶端有沒有這個權限即可。如下:
<permission android:name="com.xfhy.allinone.ipc.aidl.ACCESS_PERSON_SERVICE" android:protectionLevel="normal" />
客戶端需要在清單文件中聲明這個權限:
<uses-permission android:name="com.xfhy.allinone.ipc.aidl.ACCESS_PERSON_SERVICE"/>
服務端 Service 校驗權限:
override fun onBind(intent: Intent?): IBinder? { val check = checkCallingOrSelfPermission("com.xfhy.allinone.ipc.aidl.ACCESS_PERSON_SERVICE") if (check == PackageManager.PERMISSION_DENIED) { log(TAG,"沒有權限") return null } log(TAG,"有權限") return mBinder }
關于“在Android中Service和AIDL怎么使用”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識,可以關注億速云行業資訊頻道,小編每天都會為大家更新不同的知識點。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。