多程式的優點
系統為每個應用分配一定大小的記憶體,從之前的 16M 到 32M、48M,甚至更高。但畢竟有限。
程式是資源分配的基本單位。也就是說,一個應用有對個程式,那這個應用可以獲得更多的記憶體。
所以,開啟多程式可以分擔主程式的記憶體消耗,常見音樂類 APP 的後臺播放,應用的推送服務等。
多程式的不足
1、資料共享問題
Android 系統為每個程式分配獨立的虛擬機器,不同的虛擬機器之間資料不能共享,即使是靜態成員還是單例模式。
2、執行緒同步機制失效
不同程式鎖的不是同一個物件,無法保證執行緒同步了。
3、SharedPreferences 可靠性下降
SharedPreferences 還沒有增加對多程式的支援。
4、Application 多次建立
當一個元件跑在新的程式中,系統要在建立程式的同時為其分配獨立的虛擬機器,自然就會建立新的 Application。這就導致了 application 的 onCreate方法重複執行全部的初始化程式碼。因此,可以根據程式需要進行最小的業務初始化。
多程式注意事項
- 靜態成員和單例模式會失效
- 執行緒同步機制失效
- SharePreference 穩定性不能保證,使用 ContentProvider 封裝,對外提供資料服務
- Application 會多次建立,需要注意多程式間使用的物件是否初始化 在 web 程式中呼叫主程式功能都需要注意 Context 和資料的讀取,否則會出現空指標的問題.
Application 多次建立
不同程式共同的初始化業務邏輯 :
public class AppInitialization {
/**
* 不同程式共同的初始化程式碼
* @param application
*/
public void onAppCreate(Application application) {
// init
}
複製程式碼
簡單工廠模式 :
根據程式名進行對應程式的初始化邏輯。
public class AppInitFactory {
public static AppInitialization getAppInitialization(String processName) {
AppInitialization appInitialization;
if (processName.endsWith(":second")) {
appInitialization = new SecondApplication();
} else if (processName.endsWith(":third")) {
appInitialization = new ThirdApplication();
} else {
appInitialization = new AppInitialization();
}
return appInitialization;
}
static class SecondApplication extends AppInitialization {
@Override
public void onAppCreate(Application application) {
super.onAppCreate(application);
// init
}
}
static class ThirdApplication extends AppInitialization {
@Override
public void onAppCreate(Application application) {
super.onAppCreate(application);
// init
}
}
複製程式碼
具體呼叫程式碼 :
public class MyApplication extends Application {
private static final String TAG = "MyApplication";
@Override
public void onCreate() {
super.onCreate();
String currentProcessName = getCurrentProcessName();
Log.e(TAG, "currentProcessName : " + currentProcessName );
AppInitialization appInitialization = AppInitFactory.getAppInitialization(currentProcessName);
if (appInitialization != null) {
appInitialization.onAppCreate(this);
}
}
/**
* 獲取當前程式名稱
* @return
*/
private String getCurrentProcessName() {
String currentProcessName = "";
int pid = android.os.Process.myPid();
ActivityManager manager = (ActivityManager) this.getSystemService(Context.ACTIVITY_SERVICE);
for (ActivityManager.RunningAppProcessInfo processInfo : manager.getRunningAppProcesses()) {
if (processInfo.pid == pid) {
currentProcessName = processInfo.processName;
break;
}
}
return currentProcessName;
}
}
複製程式碼
Web 獨立程式
WebView 拆分為獨立程式執行,從而減輕主程式記憶體壓力很有必要,當記憶體緊張時,系統則會自動殺死 web 程式.拆分為多程式後,主要問題在於程式間通訊與主程式保活.
拆分 Web 程式
獨立程式分為兩種模式,私有獨立程式 和 全域性獨立程式 兩種模式,開始方式也很簡單.
<!-- 私有獨立程式:與主程式同ShareUID,共享data目錄、元件資訊、共享記憶體資料 -->
<activity
android:name=".WebActivity"
android:process=":web"/>
<!-- 全域性獨立程式:與主程式不同ShareUID -->
<activity
android:name=".WebActivity"
android:process=".web"/>
複製程式碼
如果只與本應用通訊,不需要為全域性獨立程式.
當開啟網頁時,傳送請求應該帶使用者資訊,原來的資訊儲存方式是SharePreference,但是這種方式對於多程式呼叫時,容易出現不穩定的情況,並且它的多程式呼叫方式已經被標記為廢棄,所以為了保證穩定性,使用ContentProvider將其封裝,供不同的程式呼叫.
/**
* 在 web 程式裡只是獲取使用者資訊,當web裡返回要登入資訊時,跳轉至主程式裡的登入頁面,所以只實現了 query 方法,更新 SharePreference 依舊使用了單程式讀寫模式.
*/
public class UserProvider extends ContentProvider {
private static String sAuthoriry = BuildConfig.APPLICATION_ID + ".UserProvider";
@Override
public boolean onCreate() {
return true;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
//這個 name 獲取的就是 xml 的檔名,預設取 uri 的 path 欄位的第一個
if (!sAuthoriry.equals(uri.getAuthority())) {
return null;
}
Bundle bundle = new Bundle();
if (getContext() != null) {
bundle.putString("user", SharedPreferUtil.get(getContext(), Constants.EXTRA_USER_CACHE, ""));
}
return new BundleCursor(bundle);
}
private static final class BundleCursor extends MatrixCursor {
private Bundle mBundle;
public BundleCursor(Bundle extras) {
super(new String[]{}, 0);
mBundle = extras;
}
@Override
public Bundle getExtras() {
return mBundle;
}
@Override
public Bundle respond(Bundle extras) {
mBundle = extras;
return mBundle;
}
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
throw new UnsupportedOperationException("No external call");
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
throw new UnsupportedOperationException("No external call");
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
throw new UnsupportedOperationException("No external call");
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
throw new UnsupportedOperationException("No external call");
}
}
複製程式碼
AndroidManifest.xml 配置
<!-- 由於只與本應用通訊所以就沒有配置許可權 -->
<provider
android:name=".provider.UserProvider"
android:authorities="${applicationId}.UserProvider"
android:exported="true" />
複製程式碼
這樣就配置好了多程式讀取 SharePreference 的 ContentProvider,凡是讀取使用者資訊的地方都需替換為如下方式
@Nullable
public static User getUser() {
String authority = "content://" + BuildConfig.APPLICATION_ID + ".UserProvider";
Uri uri = Uri.parse(authority);
//由於多程式模式,所以 Application 會多次初始化,MyApplication.getInstance() 的初始化不能寫在某個程式裡,如果這樣則其他程式獲取不到例項,導致這裡會出現NPE
Cursor cursor = MyApplication.getInstance().getContentResolver().query(uri, null, null, null, null);
if (cursor != null) {
Bundle args = cursor.getExtras();
cursor.close();
if (args != null) {
return User.stringToUser(args.getString("user"));
}
}
return null;
}
複製程式碼
通過以上配置,就可以在不同程式裡獲取User物件.
我們業務中有個邏輯是分享網頁,當使用者點選網頁分享按鈕,調起分享頁面,然後分享至微信,分享成功返回後呼叫 js,所以需要在微信回撥中通知網頁,使用BroadcastReceiver通知網頁執行 js 指令碼.
主程式保活
在完成程式拆分後測試中發現,當主程式佔用一百多 MB 時紅米 Note3 機器開啟網頁程式,再消耗一百多 MB時,系統會自動殺死主程式,導致返回到主程式會再次載入,為了避免這種問題發生,只能在網頁程式啟動後,將主程式置為前臺程式. 程式保活話題如果要展開談,可以寫好多東西,這裡只介紹我們應用的方法.邏輯很簡單就是在主程式中啟動一個前臺 Service,然後再啟動一個相同 ID 的 Service,最後停止一個 Service,這樣通知欄裡便不會出現通知,而應用在前臺 oom_adj 值較高,程式不會被殺死.
public class KeepLiveService extends Service {
public static final int NOTIFICATION_ID = 0x11;
public KeepLiveService() {
}
@Override
public IBinder onBind(Intent intent) {
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public void onCreate() {
super.onCreate();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
//API 18 以下,直接傳送 Notification 並將其置為前臺
startForeground(NOTIFICATION_ID, new Notification());
} else {
//API 18 以上,傳送 Notification 並將其置為前臺後,啟動 InnerService
Notification.Builder builder = new Notification.Builder(this);
builder.setSmallIcon(R.drawable.push);
startForeground(NOTIFICATION_ID, builder.build());
ContextCompat.startForegroundService(getApplicationContext(), new Intent(this, InnerService.class));
}
}
public static class InnerService extends Service {
public InnerService() {
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
//傳送與 KeepLiveService 中I D 相同的 Notification,然後取消自己的前臺顯示
Notification.Builder builder = new Notification.Builder(this);
builder.setSmallIcon(R.drawable.push);
startForeground(NOTIFICATION_ID, builder.build());
stopSelf();
}
}
}
複製程式碼
當然 Service 也必須在 AndroidManifest 中註冊.
- compleVerison 27 targetVersion 26 如果再為更高的版本則通知攔會顯示出應用正在後臺執行,給使用者造成不好的體驗
- 為了在使用者體驗和記憶體消耗間平衡,在 Application 的 onTrimMemory中,當 level 值大於等於 TRIM_MEMORY_MODERATE 且 Web 程式在後臺後,主動殺死 web 程式.
跨程式通訊
程式間的通訊方式:
- 四大元件間傳遞Bundle
- 使用檔案共享方式,多程式讀寫一個相同的檔案,獲取檔案內容進行互動
- AIDL
- 使用Messenger,一種輕量級的跨程式通訊方案,底層使用AIDL實現
- 使用ContentProvider,常用於多程式共享資料,比如系統的相簿,音樂等,我們也可以通過ContentProvider訪問到
- Socket
Android的程式與程式之間通訊,有些不需要我們額外編寫通訊程式碼,例如:把選擇圖片模組放到獨立的程式,我們仍可以使用startActivityForResult方法,將選中的圖片放到Bundle中,使用Intent傳遞即可。
但是對於把“訊息推送Service”放到獨立的程式,這個業務就稍微複雜點了,這個時候可能會發生Activity跟Service傳遞物件,呼叫Service方法等一系列複雜操作。
由於各個程式執行在相對獨立的記憶體空間,所以它們是不能直接通訊的,因為程式裡的變數、物件等初始化後都是具有記憶體地址的,舉個簡單的例子,讀取一個變數的值,本質是找到變數的記憶體地址,取出存放的值。不同的程式,執行在相互獨立的記憶體(其實就可以理解為兩個不同的應用程式),顯然不能直接得知對方變數、物件的記憶體地址,這樣的話也自然不能訪問對方的變數,物件等。此時兩個程式進行互動,就需要使用跨程式通訊的方式去實現。簡單說,跨程式通訊就是一種讓程式與程式之間可以進行互動的技術。
AIDL是Android提供給我們的標準跨程式通訊API。Messenger也是使用AIDL實現的一種跨程式方式,Messenger顧名思義,就像是一種序列的訊息機制,它是一種輕量級的IPC方案,可以在不同程式中傳遞Message物件,我們在Message中放入需要傳遞的資料即可輕鬆實現程式間通訊。但是當我們需要呼叫服務端方法,或者存在併發請求,那麼Messenger就不合適了。而四大元件傳遞Bundle,這個就不需要解釋了,把需要傳遞的資料,用Intent封裝起來傳遞即可。
AIDL實現一個多程式訊息推送
像圖片選擇這樣的多程式需求,可能並不需要我們額外編寫程式通訊的程式碼,使用四大元件傳輸Bundle就行了,但是像推送服務這種需求,程式與程式之間需要高度的互動,此時就繞不過程式通訊這一步了。 下面我們就用即時聊天軟體為例,手動去實現一個多程式的推送例子,具體需求如下:
- UI和訊息推送的Service分兩個程式;
- UI程式用於展示具體的訊息資料,把使用者傳送的訊息,傳遞到訊息Service,然後傳送到遠端伺服器;
- Service負責收發訊息,並和遠端伺服器保持長連線,UI程式可通過Service傳送訊息到遠端伺服器,Service收到遠端伺服器訊息通知UI程式;
- 即使UI程式退出了,Service仍需要保持執行,收取伺服器訊息。
實現思路
- 建立UI程式(下文統稱為客戶端);
- 建立訊息Service(下文統稱為服務端);
- 把服務端配置到獨立的程式(AndroidManifest.xml中指定process標籤);
- 客戶端和服務端進行繫結(bindService);
- 讓客戶端和服務端具備互動的能力。(AIDL使用)
Step0. AIDL呼叫流程概覽
開始之前,我們先來概括一下使用AIDL進行多程式呼叫的整個流程:
- 客戶端使用bindService方法繫結服務端;
- 服務端在onBind方法返回Binder物件;
- 客戶端拿到服務端返回的Binder物件進行跨程式方法呼叫;
Step1.客戶端使用bindService方法繫結服務端
建立客戶端和服務端,把服務端配置到另外的程式
- 建立客戶端 -> MainActivity;
- 建立服務端 -> MessageService;
- 把服務端配置到另外的程式 -> android:process=”:remote”
上面描述的客戶端、服務端、以及把服務端配置到另外程式,體現在AndroidManifest.xml中,如下所示:
<manifest ...>
<application ...>
<activity android:name=".ui.MainActivity"/>
<service
android:name=".service.MessageService"
android:enabled="true"
android:exported="true"
android:process=":remote" />
</application>
</manifest>
複製程式碼
繫結MessageService到MainActivity
建立MessageService 此時的MessageService就是剛建立的模樣,onBind中返回了null,下一步中我們將返回一個可操作的物件給客戶端。
public class MessageService extends Service {
public MessageService() {
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
複製程式碼
客戶端MainActivity呼叫bindService方法繫結MessageService
這一步其實是屬於Service元件相關的知識,在這裡就比較簡單地說一下,啟動服務可以通過以下兩種方式:
- 使用bindService方法 -> bindService(Intent service, ServiceConnection conn, int flags);
- 使用startService方法 -> startService(Intent service);
bindService & startService區別:
使用bindService方式,多個Client可以同時bind一個Service,但是當所有Client unbind後,Service會退出,通常情況下,如果希望和Service互動,一般使用bindService方法,使用onServiceConnected中的IBinder物件可以和Service進行互動,不需要和Service互動的情況下,使用startService方法即可。
正如上面所說,我們是要和Service互動的,所以我們需要使用bindService方法,但是我們希望unbind後Service仍保持執行,這樣的情況下,可以同時呼叫bindService和startService(比如像本例子中的訊息服務,退出UI程式,Service仍需要接收到訊息),程式碼如下:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setupService();
}
/**
* unbindService
*/
@Override
protected void onDestroy() {
unbindService(serviceConnection);
super.onDestroy();
}
/**
* bindService & startService
*/
private void setupService() {
Intent intent = new Intent(this, MessageService.class);
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
startService(intent);
}
ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d(TAG, "onServiceConnected");
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.d(TAG, "onServiceDisconnected");
}
};
}
複製程式碼
Stpe2.服務端在onBind方法返回Binder物件
首先,什麼是Binder?
要說Binder,首先要說一下IBinder這個介面,IBinder是遠端物件的基礎介面,輕量級的遠端過程呼叫機制的核心部分,該介面描述了與遠端物件互動的抽象協議,而Binder實現了IBinder介面,簡單說,Binder就是Android SDK中內建的一個多程式通訊實現類,在使用的時候,我們不用也不要去實現IBinder,而是繼承Binder這個類即可實現多程式通訊。
其次,這個需要在onBind方法返回的Binder物件從何而來?
在這裡就要引出本文中的主題了——AIDL 多程式中使用的Binder物件,一般通過我們定義好的 .adil 介面檔案自動生成,當然你可以走野路子,直接手動編寫這個跨程式通訊所需的Binder類,其本質無非就是一個繼承了Binder的類,鑑於野路子走起來麻煩,而且都是重複步驟的工作,Google提供了 AIDL 介面來幫我們自動生成Binder這條正路。
定義AIDL介面
很明顯,接下來我們需要搞一波上面說的Binder,讓客戶端可以呼叫到服務端的方法,而這個Binder又是通過AIDL介面自動生成,那我們就先從AIDL搞起,搞之前先看看注意事項,以免出事故:
AIDL支援的資料型別:
Java 程式語言中的所有基本資料型別(如 int、long、char、boolean 等等) String和CharSequence Parcelable:實現了Parcelable介面的物件 List:其中的元素需要被AIDL支援,另一端實際接收的具體類始終是 ArrayList,但生成的方法使用的是 List 介面 Map:其中的元素需要被AIDL支援,包括 key 和 value,另一端實際接收的具體類始終是 HashMap,但生成的方法使用的是 Map 介面
其他注意事項:
在AIDL中傳遞的物件,必須實現Parcelable序列化介面; 在AIDL中傳遞的物件,需要在類檔案相同路徑下,建立同名、但是字尾為.aidl的檔案,並在檔案中使用parcelable關鍵字宣告這個類; 跟普通介面的區別:只能宣告方法,不能宣告變數; 所有非基礎資料型別引數都需要標出資料走向的方向標記。可以是 in、out 或 inout,基礎資料型別預設只能是 in,不能是其他方向。
建立一個AIDL介面,介面中提供傳送訊息的方法(Android Studio建立AIDL:專案右鍵 -> New -> AIDL -> AIDL File),程式碼如下:
package com.example.aidl;
import com.example.aidl.data.MessageModel;
interface MessageSender {
void sendMessage(in MessageModel messageModel);
}
複製程式碼
被“in”標記的引數,就是接收實際資料的引數,這個跟我們普通引數傳遞一樣的含義。在AIDL中,“out” 指定了一個僅用於輸出的引數,換而言之,這個引數不關心呼叫方傳遞了什麼資料過來,但是這個引數的值可以在方法被呼叫後填充(無論呼叫方傳遞了什麼值過來,在方法執行的時候,這個引數的初始值總是空的),這就是“out”的含義,僅用於輸出。而“inout”顯然就是“in”和“out”的合體了,輸入和輸出的引數。區分“in”、“out”有什麼用?這是非常重要的,因為每個引數的內容必須編組(序列化,傳輸,接收和反序列化)。in/out標籤允許Binder跳過編組步驟以獲得更好的效能。
上述的MessageModel為訊息的實體類,該類在AIDL中傳遞,實現了Parcelable序列化介面,程式碼如下:
public class MessageModel implements Parcelable {
private String from;
private String to;
private String content;
...
Setter & Getter
...
@Override
public int describeContents() {
return 0;
}
//...
序列化相關程式碼
//...
}
複製程式碼
手動實現Parcelable介面比較麻煩,安利一款AS自動生成外掛android-parcelable-intellij-plugin 建立完MessageModel這個實體類,別忘了還有一件事要做:”在AIDL中傳遞的物件,需要在類檔案相同路徑下,建立同名、但是字尾為.aidl的檔案,並在檔案中使用parcelable關鍵字宣告這個類“。程式碼如下:
package com.example.aidl.data;
parcelable MessageModel;
複製程式碼
我們剛剛新增的3個檔案:
- MessageSender.aidl -> 定義了傳送訊息的方法,會自動生成名為MessageSender.Stub的Binder類,在服務端實現,返回給客戶端呼叫
- MessageModel.java -> 訊息實體類,由客戶端傳遞到服務端,實現了Parcelable序列化
- MessageModel.aidl -> 宣告瞭MessageModel可在AIDL中傳遞,放在跟MessageModel.java相同的包路徑下
在服務端建立MessageSender.aidl這個AIDL介面自動生成的Binder物件,並返回給客戶端呼叫,服務端MessageService程式碼如下:
public class MessageService extends Service {
private static final String TAG = "MessageService";
public MessageService() {
}
IBinder messageSender = new MessageSender.Stub() {
@Override
public void sendMessage(MessageModel messageModel) throws RemoteException {
Log.d(TAG, "messageModel: " + messageModel.toString());
}
};
@Override
public IBinder onBind(Intent intent) {
return messageSender;
}
}
複製程式碼
MessageSender.Stub是Android Studio根據我們MessageSender.aidl檔案自動生成的Binder物件(至於是怎樣生成的,下文會有答案),我們需要把這個Binder物件返回給客戶端。
客戶端拿到Binder物件後呼叫遠端方法
呼叫步驟如下:
- 在客戶端的onServiceConnected方法中,拿到服務端返回的Binder物件;
- 使用MessageSender.Stub.asInterface方法,取得MessageSender.aidl對應的操作介面;
- 取得MessageSender物件後,像普通介面一樣呼叫方法即可。
public class MainActivity extends AppCompatActivity {
private MessageSender messageSender;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setupService();
}
//...
private void setupService() {
Intent intent = new Intent(this, MessageService.class);
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
startService(intent);
}
ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//使用asInterface方法取得AIDL對應的操作介面
messageSender = MessageSender.Stub.asInterface(service);
//生成訊息實體物件
MessageModel messageModel = new MessageModel();
messageModel.setFrom("client user id");
messageModel.setTo("receiver user id");
messageModel.setContent("This is message content");
//呼叫遠端Service的sendMessage方法,並傳遞訊息實體物件
try {
messageSender.sendMessage(messageModel);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
}
複製程式碼
在客戶端中我們呼叫了MessageSender的sendMessage方法,向服務端傳送了一條訊息,並把生成的MessageModel物件作為引數傳遞到了服務端,最終服務端列印的結果如下:
客戶端與服務端通訊過程
我們先來回顧一下從客戶端發起的呼叫流程:
- MessageSender messageSender = MessageSender.Stub.asInterface(service);
- messageSender.sendMessage(messageModel);
拋開其它無關程式碼,客戶端調跨程式方法就這兩個步驟,而這兩個步驟都封裝在 MessageSender.aidl 最終生成的 MessageSender.java 原始碼
public interface MessageSender extends android.os.IInterface {
public static abstract class Stub extends android.os.Binder implements com.example.aidl.MessageSender {
private static final java.lang.String DESCRIPTOR = "com.example.aidl.MessageSender";
/**
* 把IBinder物件轉換為 com.example.aidl.MessageSender 介面
* 判斷IBinder是否處於相同程式,相同程式返回Stub實現的com.example.aidl.MessageSender介面
* 不同程式,則返回Stub.Proxy實現的com.example.aidl.MessageSender介面
*/
public static com.example.aidl.MessageSender asInterface(android.os.IBinder obj) {
if ((obj == null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin != null) && (iin instanceof com.example.aidl.MessageSender))) {
return ((com.example.aidl.MessageSender) iin);
}
return new com.example.aidl.MessageSender.Stub.Proxy(obj);
}
/**
* 同一程式時,不會觸發
*
* 不同程式時,asInterface會返回Stub.Proxy,客戶端呼叫 messageSender.sendMessage(messageModel)
* 實質是呼叫了 Stub.Proxy 的 sendMessage 方法,從而觸發跨程式資料傳遞,
* 最終Binder底層將處理好的資料回撥到此方法,並呼叫我們真正的sendMessage方法
*/
@Override
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
switch (code) {
case INTERFACE_TRANSACTION: {
reply.writeString(DESCRIPTOR);
return true;
}
case TRANSACTION_sendMessage: {
data.enforceInterface(DESCRIPTOR);
com.example.aidl.data.MessageModel _arg0;
if ((0 != data.readInt())) {
_arg0 = com.example.aidl.data.MessageModel.CREATOR.createFromParcel(data);
} else {
_arg0 = null;
}
this.sendMessage(_arg0);
reply.writeNoException();
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
private static class Proxy implements com.example.aidl.MessageSender {
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote) {
mRemote = remote;
}
/**
* Proxy中的sendMessage方法,並不是直接呼叫我們定義的sendMessage方法,而是經過一頓的Parcel讀寫,
* 然後呼叫mRemote.transact方法,把資料交給Binder處理,transact處理完畢後會呼叫上方的onTransact方法,
* onTransact拿到最終得到的引數資料,呼叫由我們真正的sendMessage方法
*/
@Override
public void sendMessage(com.example.aidl.data.MessageModel messageModel) throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
if ((messageModel != null)) {
_data.writeInt(1);
messageModel.writeToParcel(_data, 0);
} else {
_data.writeInt(0);
}
//呼叫Binder的transact方法進行多程式資料傳輸,處理完畢後呼叫上方的onTransact方法
mRemote.transact(Stub.TRANSACTION_sendMessage, _data, _reply, 0);
_reply.readException();
} finally {
_reply.recycle();
_data.recycle();
}
}
}
static final int TRANSACTION_sendMessage = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
}
public void sendMessage(com.example.aidl.data.MessageModel messageModel) throws android.os.RemoteException;
}
複製程式碼
從客戶端的sendMessage開始,整個AIDL的呼叫過程如上圖所示,asInterface方法,將會判斷onBind方法返回的Binder是否存處於同一程式,在同一程式中,則進行常規的方法呼叫,若處於不同程式,整個資料傳遞的過程則需要通過Binder底層去進行編組(序列化,傳輸,接收和反序列化),得到最終的資料後再進行常規的方法呼叫。
敲黑板:物件跨程式傳輸的本質就是 序列化,傳輸,接收和反序列化 這樣一個過程,這也是為什麼跨程式傳輸的物件必須實現Parcelable介面
跨程式的回撥介面
在上面我們已經實現了從客戶端傳送訊息到跨程式服務端的功能,接下來我們還需要將服務端接收到的遠端伺服器訊息,傳遞到客戶端。有同學估計會說:“這不就是一個回撥介面的事情嘛”,設定回撥介面思路是對的,但是在這裡使用的回撥介面有點不一樣,在AIDL中傳遞的介面,不能是普通的介面,只能是AIDL介面,所以我們需要新建一個AIDL介面傳到服務端,作為回撥介面。
新建訊息收取的AIDL介面MessageReceiver.aidl:
package com.example.aidl;
import com.example.aidl.data.MessageModel;
interface MessageReceiver {
void onMessageReceived(in MessageModel receivedMessage);
}
複製程式碼
接下來我們把回撥介面註冊到服務端去,修改我們的MessageSender.aidl:
package com.example.aidl;
import com.example.aidl.data.MessageModel;
import com.example.aidl.MessageReceiver;
interface MessageSender {
void sendMessage(in MessageModel messageModel);
void registerReceiveListener(MessageReceiver messageReceiver);
void unregisterReceiveListener(MessageReceiver messageReceiver);
}
複製程式碼
以上就是我們最終修改好的aidl介面,接下來我們需要做出對應的變更:
- 在服務端中增加MessageSender的註冊和反註冊介面的方法;
- 在客戶端中實現MessageReceiver介面,並通過MessageSender註冊到服務端。
public class MainActivity extends AppCompatActivity {
private MessageSender messageSender;
@Override
protected void onCreate(Bundle savedInstanceState) {
//...
}
/**
* 1.unregisterListener
* 2.unbindService
*/
@Override
protected void onDestroy() {
//解除訊息監聽介面
if (messageSender != null && messageSender.asBinder().isBinderAlive()) {
try {
messageSender.unregisterReceiveListener(messageReceiver);
} catch (RemoteException e) {
e.printStackTrace();
}
}
unbindService(serviceConnection);
super.onDestroy();
}
//訊息監聽回撥介面
private MessageReceiver messageReceiver = new MessageReceiver.Stub() {
@Override
public void onMessageReceived(MessageModel receivedMessage) throws RemoteException {
Log.d(TAG, "onMessageReceived: " + receivedMessage.toString());
}
};
ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//使用asInterface方法取得AIDL對應的操作介面
messageSender = MessageSender.Stub.asInterface(service);
//生成訊息實體物件
MessageModel messageModel = new MessageModel();
//...
try {
//把接收訊息的回撥介面註冊到服務端
messageSender.registerReceiveListener(messageReceiver);
//呼叫遠端Service的sendMessage方法,並傳遞訊息實體物件
messageSender.sendMessage(messageModel);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
}
複製程式碼
客戶端主要有3個變更:
- 增加了messageReceiver物件,用於監聽服務端的訊息通知;
- onServiceConnected方法中,把messageReceiver註冊到Service中去;
- onDestroy時候解除messageReceiver的註冊。
服務端MessageServie進行變更:
public class MessageService extends Service {
private static final String TAG = "MessageService";
private AtomicBoolean serviceStop = new AtomicBoolean(false);
//RemoteCallbackList專門用來管理多程式回撥介面
private RemoteCallbackList<MessageReceiver> listenerList = new RemoteCallbackList<>();
public MessageService() {
}
IBinder messageSender = new MessageSender.Stub() {
@Override
public void sendMessage(MessageModel messageModel) throws RemoteException {
Log.e(TAG, "messageModel: " + messageModel.toString());
}
@Override
public void registerReceiveListener(MessageReceiver messageReceiver) throws RemoteException {
listenerList.register(messageReceiver);
}
@Override
public void unregisterReceiveListener(MessageReceiver messageReceiver) throws RemoteException {
listenerList.unregister(messageReceiver);
}
};
@Override
public IBinder onBind(Intent intent) {
return messageSender;
}
@Override
public void onCreate() {
super.onCreate();
new Thread(new FakeTCPTask()).start();
}
@Override
public void onDestroy() {
serviceStop.set(true);
super.onDestroy();
}
//模擬長連線,通知客戶端有新訊息到達
private class FakeTCPTask implements Runnable {
@Override
public void run() {
while (!serviceStop.get()) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
MessageModel messageModel = new MessageModel();
messageModel.setFrom("Service");
messageModel.setTo("Client");
messageModel.setContent(String.valueOf(System.currentTimeMillis()));
/**
* RemoteCallbackList的遍歷方式
* beginBroadcast和finishBroadcast一定要配對使用
*/
final int listenerCount = listenerList.beginBroadcast();
Log.d(TAG, "listenerCount == " + listenerCount);
for (int i = 0; i < listenerCount; i++) {
MessageReceiver messageReceiver = listenerList.getBroadcastItem(i);
if (messageReceiver != null) {
try {
messageReceiver.onMessageReceived(messageModel);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
listenerList.finishBroadcast();
}
}
}
}
複製程式碼
服務端主要變更:
- MessageSender.Stub實現了註冊和反註冊回撥介面的方法;
- 增加了RemoteCallbackList來管理AIDL遠端介面;
- FakeTCPTask模擬了長連線通知客戶端有新訊息到達。(這裡的長連線可以是XMPP,Mina,Mars,Netty等,這裡弄個假的意思意思,有時間的話我們開個帖子聊聊XMPP)
這裡還有一個需要講一下的,就是RemoteCallbackList,為什麼要用RemoteCallbackList,普通ArrayList不行嗎?當然不行,不然幹嘛又整一個RemoteCallbackList,registerReceiveListener 和 unregisterReceiveListener在客戶端傳輸過來的物件,經過Binder處理,在服務端接收到的時候其實是一個新的物件,這樣導致在 unregisterReceiveListener 的時候,普通的ArrayList是無法找到在 registerReceiveListener 時候新增到List的那個物件的,但是它們底層使用的Binder物件是同一個,RemoteCallbackList利用這個特性做到了可以找到同一個物件,這樣我們就可以順利反註冊客戶端傳遞過來的介面物件了。RemoteCallbackList在客戶端程式終止後,它能自動移除客戶端所註冊的listener,它內部還實現了執行緒同步,所以我們在註冊和反註冊都不需要考慮執行緒同步,的確是個666的類。
DeathRecipient
不知道你有沒有感覺到,兩個程式互動總是覺得缺乏那麼一點安全感…比如說服務端程式Crash了,而客戶端程式想要呼叫服務端方法,這樣就呼叫不到了。此時我們可以給Binder設定一個DeathRecipient物件,當Binder意外掛了的時候,我們可以在DeathRecipient介面的回撥方法中收到通知,並作出相應的操作,比如重連服務等等。
DeathRecipient的使用如下:
- 宣告DeathRecipient物件,實現其binderDied方法,當binder死亡時,會回撥binderDied方法;
- 給Binder物件設定DeathRecipient物件。
在客戶端MainActivity宣告DeathRecipient:
/**
* Binder可能會意外死忙(比如Service Crash),Client監聽到Binder死忙後可以進行重連服務等操作
*/
IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
Log.d(TAG, "binderDied");
if (messageSender != null) {
messageSender.asBinder().unlinkToDeath(this, 0);
messageSender = null;
}
//// TODO: 2017/2/28 重連服務或其他操作
setupService();
}
};
ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//...
try {
//設定Binder死亡監聽
messageSender.asBinder().linkToDeath(deathRecipient, 0);
} catch (RemoteException e) {
e.printStackTrace();
}
}
//...
};
複製程式碼
Binder中兩個重要方法:
- linkToDeath -> 設定死亡代理 DeathRecipient 物件;
- unlinkToDeath -> Binder死亡的情況下,解除該代理。
此外,Binder中的isBinderAlive也可以判斷Binder是否死亡。
許可權驗證
介紹兩種常用驗證方法:
- 在服務端的onBind中校驗自定義permission,如果通過了我們的校驗,正常返回Binder物件,校驗不通過返回null,返回null的情況下客戶端無法繫結到我們的服務;
- 在服務端的onTransact方法校驗客戶端包名,不通過校驗直接return false,校驗通過執行正常的流程。
自定義permission,在Androidmanifest.xml中增加自定義的許可權:
<permission
android:name="com.example.aidl.permission.REMOTE_SERVICE_PERMISSION"
android:protectionLevel="normal" />
<uses-permission android:name="com.example.aidl.permission.REMOTE_SERVICE_PERMISSION" />
複製程式碼
服務端檢查許可權的方法:
IBinder messageSender = new MessageSender.Stub() {
//...
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
/**
* 包名驗證方式
*/
String packageName = null;
String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
if (packages != null && packages.length > 0) {
packageName = packages[0];
}
if (packageName == null || !packageName.startsWith("com.example.aidl")) {
Log.d("onTransact", "拒絕呼叫:" + packageName);
return false;
}
return super.onTransact(code, data, reply, flags);
}
};
@Override
public IBinder onBind(Intent intent) {
//自定義permission方式檢查許可權
if (checkCallingOrSelfPermission("com.example.aidl.permission.REMOTE_SERVICE_PERMISSION") == PackageManager.PERMISSION_DENIED) {
return null;
}
return messageSender;
}
複製程式碼