做者:享學課堂終身VIP週週java
轉載請聲明出處!android
上一期《手把手講解IPC框架》享學課堂週週同窗分享了概念QA以及前置技能、傳統方式IPC通訊寫法與使用IPC框架進行RPC通訊的對比以及Demo展現三個部分。這一期他將繼續爲你們帶來手把手講解IPC框架分享。編程
1、概念QA以及前置技能json
2、傳統方式IPC通訊寫法與使用IPC框架進行RPC通訊的對比數組
3、Demo展現bash
4、框架核心思想講解app
5、寫在最後的話框架
接上一篇:明星學員做品:手把手講解IPC框架(1)ide
咱們不使用IPC框架時,有兩件事很是噁心:post
1. 隨着業務的擴展,咱們須要頻繁(由於要新增業務接口)改動
AIDL
文件,並且AIDL
修改起來沒有任何代碼提示,只有到了編譯以後,編譯器纔會告訴我哪裏錯了,並且 直接引用到的JavaBean
還必須手動再聲明一次。實在是不想在這個上面浪費時間。2. 全部客戶端
Activity
,只要想進行進程間binder
通訊,就不可避免要去手動bindService
,隨後去處理Binder
鏈接,重寫ServiceConnection
,還要在適當的時候釋放鏈接,這種業務不相關並且重複性很大的代碼,要儘可能少寫。
IPC框架將會着重解決這兩個問題。下面開始講解核心設計思想
注:1.搭建框架牽涉的知識面會很廣,我不能每一個細節都講得很細緻,一些基礎部分一筆帶過的,若有疑問,但願能留言討論。
2.設計思路都是環環相扣的,閱讀時最好是從上往下依次理解.
上文說到,直接使用
AIDL
通訊,當業務擴展時,咱們須要對AIDL
文件進行改動,而改起來比較費勁,且容易出錯。怎麼辦?利用業務註冊
的方式,將業務類
的class
對象,保存到服務端 內存中。進入Demo代碼Registry.java
:
public class Ipc {
/**
* @param business
*/
public static void register(Class<?> business) {
//註冊是一個單獨過程,因此單獨提取出來,放在一個類裏面去
Registry.getInstance().register(business);
//註冊機是一個單例,啓動服務端,
// 就會存在一個註冊機對象,惟一,不會隨着服務的綁定解綁而受影響
}
...省略無關代碼
}
複製代碼
/**
* 業務註冊機
*/
public class Registry {
...省略不關鍵代碼
/**
* 業務表
*/
private ConcurrentHashMap<String, Class<?>> mBusinessMap
= new ConcurrentHashMap<>();
/**
* 業務方法表, 二維map,key爲serviceId字符串值,value爲 一個方法map - key,方法名;value
*/
private ConcurrentHashMap<String, ConcurrentHashMap<String, Method>> mMethodMap
= new ConcurrentHashMap<>();
/**
* 業務類的實例,要反射執行方法,若是不是靜態方法的話,仍是須要一個實例的,因此在這裏把實例也保存起來
*/
private ConcurrentHashMap<String, Object> mObjectMap = new ConcurrentHashMap<>();
/**
* 業務註冊
* 將業務class的class和method對象都保存起來,以便後面反射執行須要的method
*/
public void register(Class<?> business) {
//這裏有個設計,使用註解,標記所使用的業務類是屬於哪個業務ID,在本類中,ID惟一
ServiceId serviceId = business.getAnnotation(ServiceId.class);
//獲取那個類頭上的註解
if (serviceId == null) {
throw new RuntimeException("業務類必須使用ServiceId註解");
}
String value = serviceId.value();
mBusinessMap.put(value, business);
//把業務類的class對象用 value做爲key,保存到map中
//而後要保存這個business類的全部method對象
ConcurrentHashMap<String, Method> tempMethodMap = mMethodMap.get(value);
//先看看方法表中是否已經存在整個業務對應的方法表
if (tempMethodMap == null) {
tempMethodMap = new ConcurrentHashMap<>();
//不存在,則new
mMethodMap.put(value, tempMethodMap);
// 而且將它存進去
}
for (Method method : business.getMethods()) {
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
String methodMapKey = getMethodMapKeyWithClzArr(methodName, parameterTypes);
tempMethodMap.put(methodMapKey, method);
}
...省略不關鍵代碼
}
...省略不關鍵代碼
/**
* 如何尋找到一個Method?
* 參照上面的構建過程,
*
* @param serviceId
* @param methodName
* @param paras
* @return
*/
public Method findMethod(String serviceId, String methodName, Object[] paras) {
ConcurrentHashMap<String, Method> map = mMethodMap.get(serviceId);
String methodMapKey = getMethodMapKeyWithObjArr(methodName, paras);
//一樣的方式,構建一個StringBuilder
return map.get(methodMapKey);
}
/**
* 放入一個實例
*
* @param serviceId
* @param object
*/
public void putObject(String serviceId, Object object) {
mObjectMap.put(serviceId, object);
}
/**
* 取出一個實例
*
* @param serviceId
*/
public Object getObject(String serviceId) {
return mObjectMap.get(serviceId);
}
}
複製代碼
/**
* 自定義註解,用於註冊業務類的
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ServiceId {
String value();
}
複製代碼
我利用一個單例的
Registry
類,將當前這個業務class
對象,拆解出每個Method
,保存到map
集合中。而,保存這些Clas
s,Method
,則是爲了 反射執行指定的業務Method
作準備。此處有幾個精妙設計:一、利用自定義註解
@ServiceId
對業務接口和實現類,都造成約束,這樣業務實現類就有了進行惟一性約束,由於在Registry
類中,一個ServiceId
只針對一種業務,若是用Registry
類註冊一個沒有@ServiceId
註解的業務類,就會拋出異常。![]()
二、利用註解
@ServiceId
的value
做爲key
,保存全部的業務實現類的Class
, 以及該Class
的全部public
的Method
到map
集合中,經過日誌打印,很容易看出當前服務端有哪些 業務類,業務類有哪些可供外界調用的方法。(·這裏須要注意,保存方法時,必須連同方法的參數類型一塊兒做爲key
,由於存在同名方法重載的狀況·) 當你運行Demo
,啓動服務端的時候,過濾一下日誌,就能看到:![]()
3 、若是再發生業務擴展的狀況,咱們只須要直接改動加了
@ServiceId
註解的業務類便可,並無其餘多餘的動做。若是我在IUserBusiness
接口中,增長一個logout
方法,而且在實現類中去實現它。那麼,再次啓動服務端
app,上圖的日誌中就會多出一個logout
方法.![]()
四、提供一個
Map
集合,專門用來保存每個ServiceId
對應的Object
,並提供getObject
和putObject
方法,以便反射執行Method
時所需。![]()
OK,一切準備萬全。業務類的每一個部分基本上都保存到了服務端進程的內存中,反射執行
Method
,隨時能夠取用。
跨進程通訊,咱們本質上仍是使用
BinderAIDL
這一套,因此AIDL
代碼仍是要寫的,可是,是寫在框架層中,一旦肯定了通訊協議,那這一套AIDL
就不會隨着業務的變更去改動它,由於它是框架層代碼,不會隨意去動。要定本身的通訊協議,其實沒那麼複雜。想想,通訊,無非就是客戶端向服務端發送消息,而且取得迴應的過程,那麼,核心方法就肯定爲send
:![]()
入參是
Request
,返回值是Response
,有沒有以爲很像HTTP
協議。request和response
都是咱們自定義的,注意,要參與跨進程通訊的javaBean
,必須實現Parcelable
接口,它們的屬性類型也必須實現Parcelable
接口。![]()
Request
中的重要元素包括:serviceId
客戶端告訴服務端要調用哪個業務methodName
要調用哪個方法parameters
調這個方法要傳什麼參數 這3
個元素,足矣涵蓋客戶端的任何行爲。可是,因爲個人業務實現類定義 爲了單例,因此它有一個靜態的getInstance
方法。靜態方法和普通方法的反射調用不太同樣,因此,加上一個type
屬性,加以區分。
public class Request implements Parcelable {
private int type;
/**
* 建立業務類實例,而且保存到註冊表中
*/
public final static int TYPE_CREATE_INSTANCE = 0;
/**
* 執行普通業務方法
*/
public final static int TYPE_BUSINESS_METHOD = 1;
public int getType() {
return type;
}
private String serviceId;
//客戶端告訴服務端要調用哪個業務
private String methodName;
//要調用哪個方法
private Parameter[] parameters;
//調這個方法要傳什麼參數
...省略無關代碼
}
複製代碼
Response
中的重要元素有:result
字符串類型,用json
字符串表示接口執行的結果isSuccess
爲true
,接口執行成功,false
執行失敗。
public class Response implements Parcelable {
private String result;
//結果json串
private Boolean isSuccess;
//是否成功
}
複製代碼
最後,Request引用的Parameter類:type表示,參數類型(若是是String類型,那麼這個值就是java.long.String)value 表示,參數值,Gson序列化以後獲得的字符串。
public class Parameter implements Parcelable {
private String value;
//參數值序列化以後的json
private String type;
//參數類型 obj.getClass
}
複製代碼
爲何設計這麼一個Parameter?爲何不直接使用Object?由於,Request 中須要客戶端給的參數列表,但是若是直接使用客戶端給的Object[] ,你並不能保證數組中的全部參數都實現了 Parcelable
,一旦有沒有實現的,通訊就會失敗( binder
AIDL
通訊,全部參與通訊的對象,都必須實現 Parcelable
,這是基礎),因此,直接用 gson
將Object[] 轉化成Parameter[],再傳給Request,是不錯的選擇,當須要反射執行的時候,再把Parameter[] 反序列化成爲 Object[] 便可。
OK,通訊協議的3個類講解完了,那麼下一步應該是把這個協議使用起來。
參照 Demo
源碼,這一個步驟中的兩個核心類:IpcService
, Channel
先說
IpcService.java
它就是一個
extendsandroid.app.Service
的一個普通Service
,它在服務端啓動,而後與客戶端發生通訊。它必須在服務端app
的manifest
文件中註冊。同時,當客戶端與它鏈接成功時,它必須返回一個Binder
對象,因此咱們要作兩件事:一、服務端的
manifest
中對它進行註冊![]()
ps: 這裏確定有人注意到了,上面
service
註冊時,其實使用了多個IpcService
的內部靜態子類,設計多個內部子類的意義是,考慮到服務端存在多個業務接口的存在,讓每個業務接口的實現類都由一個專門的IpcService
服務區負責通訊。舉個例子:上圖中存在兩個IpcService
的子類,我讓IpcService0
負責用戶業務UserBusiness
,讓IpcService1
負責DownloadBusiness
, 當客戶端須要使用UserBusiness
時,就鏈接到IpcService0
,當須要使用DownloadBusiness
時,就鏈接到IpcService1
. 可是這個並非硬性規定,而只是良好的編程習慣,一個業務接口A
,對應一個IpcService子類A
,客戶端要訪問業務接口A
,就直接和IpcService子類A
通訊便可。同理,一個業務接口B
,對應一個IpcService子類B
,客戶端要訪問業務接口B
,就直接和IpcService子類B
通訊便可。(我是這麼理解的,若有異議,歡迎留言)
二、重寫 onBind
方法,返回一個Binder對象:咱們要明確返回的這個Binder對象的做用是什麼。它是給客戶端去使用的,客戶端用它來調用遠程方法用的,因此,咱們前面兩個大步驟準備的註冊機Registry,和通訊協議request,response,就是在這裏大顯身手了 。
public IBinder onBind(Intent intent) {
return new IIpcService.Stub() {
//返回一個binder對象,讓客戶端能夠binder對象來調用服務端的方法
@Override
public Response send(Request request) throws RemoteException {
//當客戶端調用了send以後
//IPC框架層應該要 反射執行服務端業務類的指定方法,而且視狀況返回不一樣的迴應
//客戶端會告訴框架,我要執行哪一個類的哪一個方法,我傳什麼參數
String serviceId = request.getServiceId();
String methodName = request.getMethodName();
Object[] paramObjs = restoreParams(request.getParameters());
//全部準備就緒,能夠開始反射調用了?
//先獲取Method
Method method = Registry.getInstance().findMethod(serviceId, methodName, paramObjs);
switch (request.getType()) {
case Request.TYPE_CREATE_INSTANCE:
try {
Object instance = method.invoke(null, paramObjs);
Registry.getInstance().putObject(serviceId, instance);
return new Response("業務類對象生成成功", true);
}
catch (Exception e) {
e.printStackTrace();
return new Response("業務類對象生成失敗", false);
}
case Request.TYPE_BUSINESS_METHOD:
Object o = Registry.getInstance().getObject(serviceId);
if (o != null) {
try {
Log.d(TAG, "1:methodName:" + method.getName());
for (int i = 0; i < paramObjs.length; i++) {
Log.d(TAG, "1:paramObjs " + paramObjs[i]);
}
Object res = method.invoke(o, paramObjs);
Log.d(TAG, "2");
return new Response(gson.toJson(res), true);
}
catch (Exception e) {
return new Response("業務方法執行失敗" + e.getMessage(), false);
}
}
Log.d(TAG, "3");
break;
}
return null;
}
}
;
}
複製代碼
這裏有一些細節須要總結一下: 一、從
request
中拿到的 參數列表是Parameter[]
類型的,而咱們反射執行某個方法,要的是Object[]
,那怎麼辦?反序列化咯,先前是用gson
去序列化的,這裏一樣使用gson
去反序列化, 我定義了一個名爲:restoreParams
的方法去反序列化成Object[]
.二、以前在
request
中,定義了一個type
,用來區分靜態的getInstance
方法,和 普通的業務method
,這裏要根據request
中的type
值,區分對待。getInstance
方法,會獲得一個業務實現類的Object
,咱們利用Registry
的putObject
把它保存起來。而,普通method
,再從Registry
中將剛纔業務實現類的Object
取出來,反射執行method
三、靜態
getInstance
的執行結果,不須要告知客戶端,因此沒有返回Response
對象,而 普通Method
,則有可能存在返回值,因此必須將返回值gson
序列化以後,封裝到Response
中,return
出去。
再來說 Channel
類:
以前抱怨過,不喜歡重複寫
bindService,ServiceConnection,unbindService
。可是其實仍是要寫的,寫在IPC框架層,只寫一次就夠了。
public class Channel {
String TAG = "ChannelTag";
private static final Channel ourInstance = new Channel();
/**
* 考慮到多重鏈接的狀況,把獲取到的binder對象保存到map中,每個服務一個binder
*/
private ConcurrentHashMap<Class<? extends IpcService>, IIpcService> binders = new ConcurrentHashMap<>();
public static Channel getInstance() {
return ourInstance;
}
private Channel() {
}
/**
* 考慮app內外的調用,由於外部的調用須要傳入包名
*/
public void bind(Context context, String packageName, Class<? extends IpcService> service) {
Intent intent;
if (!TextUtils.isEmpty(packageName)) {
intent = new Intent();
Log.d(TAG, "bind:" + packageName + "-" + service.getName());
intent.setClassName(packageName, service.getName());
} else {
intent = new Intent(context, service);
}
Log.d(TAG, "bind:" + service);
context.bindService(intent, new IpcConnection(service), Context.BIND_AUTO_CREATE);
}
private class IpcConnection implements ServiceConnection {
private final Class<? extends IpcService> mService;
public IpcConnection(Class<? extends IpcService> service) {
this.mService = service;
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
IIpcService binder = IIpcService.Stub.asInterface(service);
binders.put(mService, binder);
//給不一樣的客戶端進程預留不一樣的binder對象
Log.d(TAG, "onServiceConnected:" + mService + ";bindersSize=" + binders.size());
}
@Override
public void onServiceDisconnected(ComponentName name) {
binders.remove(mService);
Log.d(TAG, "onServiceDisconnected:" + mService + ";bindersSize=" + binders.size());
}
}
public Response send(int type, Class<? extends IpcService> service, String serviceId, String methodName, Object[] params) {
Response response;
Request request = new Request(type, serviceId, methodName, makeParams(params));
Log.d(TAG, ";bindersSize=" + binders.size());
IIpcService iIpcService = binders.get(service);
try {
response = iIpcService.send(request);
Log.d(TAG, "1 " + response.isSuccess() + "-" + response.getResult());
}
catch (RemoteException e) {
e.printStackTrace();
response = new Response(null, false);
Log.d(TAG, "2");
}
catch (NullPointerException e) {
response = new Response("沒有找到binder", false);
Log.d(TAG, "3");
}
return response;
}
...省略不關鍵代碼
}
複製代碼
上面的代碼是Channel類代碼,兩個關鍵:
一、
bindService+ServiceConnection
供客戶端調用,綁定服務,而且將鏈接成功以後的binder保存起來![]()
二、 提供一個
send
方法,傳入request
,且 返回response
,使用serviceId
對應的binder
完成通訊。
終於到了最後一步,前面3個步驟,爲進程間通訊作好了全部的準備工做,只差最後一步了------ 客戶端調用服務。重申一下RPC的定義:讓客戶端像使用本地方法同樣調用遠程過程。
像 使用本地方法同樣?咱們平時是怎麼使用本地方法的呢?
A a = new A();
a.xxx();
複製代碼
相似上面這樣。可是咱們的客戶端和服務端是兩個隔離的進程,內存並不能共享,也就是說服務端存在的類對象,不能直接被客戶端使用,那怎麼辦?泛型+動態代理!咱們須要構建一個處在客戶端進程內的業務代理類對象,它能夠執行和服務端的業務類同樣的方法,可是它確實不是服務端進程的那個對象,如何實現這種效果?
public class Ipc {
...省略無關代碼
/**
* @param service
* @param classType
* @param getInstanceMethodName
* @param params
* @param <T> 泛型,
* @return
*/
public static <T> T getInstanceWithName(Class<? extends IpcService> service,
Class<T> classType, String getInstanceMethodName, Object... params) {
//這裏以前不是建立了一個binder麼,用binder去調用遠程方法,在服務端建立業務類對象並保存起來
if (!classType.isInterface()) {
throw new RuntimeException("getInstanceWithName方法 此處必須傳接口的class");
}
ServiceId serviceId = classType.getAnnotation(ServiceId.class);
if (serviceId == null) {
throw new RuntimeException("接口沒有使用指定ServiceId註解");
}
Response response = Channel.getInstance().send(Request.TYPE_CREATE_INSTANCE, service, serviceId.value(), getInstanceMethodName, params);
if (response.isSuccess()) {
//若是服務端的業務類對象建立成功,那麼咱們就構建一個代理對象,實現RPC
return (T) Proxy.newProxyInstance(
classType.getClassLoader(), new Class[]{
classType
}
,
new IpcInvocationHandler(service, serviceId.value()));
}
return null;
}
}
複製代碼
上面的 getInstanceWithName
,會返回一個動態代理的 業務類對象(處在客戶端進程), 它的行爲 和 真正的業務類(服務端進程)如出一轍。這個方法有 4
個參數 @paramservice
要訪問哪個遠程service,由於不一樣的service會返回不一樣的Binder @paramclassType
要訪問哪個業務類,注意,這裏的業務類徹底是客戶端本身定義的,包名沒必要和服務端同樣,可是必定要有一個和服務端對應類同樣的註解。註解相同,框架就會認爲你在訪問相同的業務。@paramgetInstanceMethodName
咱們的業務類都是設計成單例的,可是並非全部獲取單例對象的方法都叫作getInstance,咱們框架要容許其餘的方法名 @paramparams
參數列表,類型爲 Object[]
。
重中之重,實現RPC的最後一個步驟,如圖:
![]()
若是服務端的單例對象建立成功,那麼說明 服務端的註冊表中已經存在了一個業務實現類的對象,進而,我能夠經過binder通訊來 使用這個對象 執行我要的業務方法,而且拿到方法返回值,最後 把返回值反序列化成爲
Object
,做爲動態代理業務類的方法的執行結果。
關鍵代碼 IpcInvocationHandler
:
/**
* RPC調用 執行遠程過程的回調
*/
public class IpcInvocationHandler implements InvocationHandler {
private Class<? extends IpcService> service;
private String serviceId;
private static Gson gson = new Gson();
IpcInvocationHandler(Class<? extends IpcService> service, String serviceId) {
this.service = service;
this.serviceId = serviceId;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//當,調用代理接口的方法時,就會執行到這裏,執行真正的過程
//而你真正的過程是遠程通訊
Log.d("IpcInvocationHandler", "類:" + serviceId + " 方法名" + method.getName());
for (int i = 0; i < args.length; i++) {
Log.d("IpcInvocationHandler", "參數:" + args.getClass().getName() + "/" + args[i].toString());
}
Response response = Channel.getInstance().send(Request.TYPE_BUSINESS_METHOD, service, serviceId, method.getName(), args);
if (response.isSuccess()) {
//若是此時執行的方法有返回值
Class<?> returnType = method.getReturnType();
if (returnType != void.class && returnType != Void.class) {
//既然有返回值,那就必須將序列化的返回值 反序列化成對象
String resStr = response.getResult();
return gson.fromJson(resStr, returnType);
}
}
return null;
}
}
複製代碼
ok,收工以前總結一下,最後 RPC
的實現,藉助了 Proxy
動態代理+ Binder
通訊。用動態代理產生一個本進程中的對象,而後在重寫 invoke
時,使用 binder
通訊執行服務端過程拿到返回值。這個設計確實精妙。
本案例提供的兩個Demo,都只是做爲演示效果做用的,代碼不夠精緻,請各位不要在乎這些細節。
此框架並不是本人原創,課題內容來自享學課堂Lance老師,本文只作學習交流之用,轉載請務必註明出處,謝謝合做。
第二個Demo(IPC通訊框架實現RPC),個人原裝代碼中只實現了服務端1個服務,2個客戶端同時調用,可是這個框架是支持服務端多個服務,多個客戶端同時調用的,因此,能夠嘗試在個人代碼基礎上擴展出服務端N個業務接口和實現類,多個客戶端混合調用的場景。應該不會有bug。
建議讀者去嘗試擴展一下服務端和客戶端的代碼,由於這樣能夠最直觀地感覺到框架的給咱們開發帶來的便利。
生活不止眼前的苟且...還要學會用大局觀思考....
框架思想,若是咱們可以理解,甚至創造本身的框架,那麼咱們就已經脫離了低級趣味,在走向進階了。然而,進階之路漫漫長。我昨天看了高手的一篇文章,或者一個視頻,感受學了點乾貨,那我想要吸取知識爲己所用,就不能真的把知識當成乾貨儲存起來,我要想辦法找點水把乾貨嚥下去,消化吸取,纔是我本身的東西。
喜歡的話記得點個贊,關注我,還有更多技術乾貨分享~