前言
手把手講解系列文章,是我寫給各位看官,也是寫給我自己的。
文章可能過分詳細,但是這是為了幫助到盡量多的人,畢竟工作5,6年,不能老吸血,也到了回饋開源的時候.
這個系列的文章:
1、用通俗易懂的講解方式,講解一門技術的實用價值
2、詳細書寫源碼的追蹤,源碼截圖,繪制類的結構圖,盡量詳細地解釋原理的探索過程
3、提供Github 的 可運行的Demo工程,但是我所提供代碼,更多是提供思路,拋磚引玉,請酌情cv
4、集合整理原理探索過程中的一些坑,或者demo的運行過程中的注意事項
5、用gif圖,最直觀地展示demo運行效果如果覺得細節太細,直接跳過看結論即可。
本人能力有限,如若發現描述不當之處,歡迎留言批評指正。
學到老活到老,路漫漫其修遠兮。與眾君共勉 !
正文大綱
一、 概念QA以及前置技能
二、傳統方式IPC通信寫法
與使用IPC框架進行RPC通信
的對比
三、Demo展示
四、框架核心思想講解
五、 寫在最后的話
正文
一、 概念QA以及前置技能
Q:什么時候會用到多進程通信?
A: 常見的多進程app
一般是大型公司的 app組,像是騰訊系的QQ 微信 QQ空間,QQ郵箱
等等,有可能 在QQ郵箱
登錄時,可以直接調用QQ的登錄服務
,另外,騰訊阿里都有小程序,作為一個第三方開發的小程序應用,在微信客戶端運行
,如果和微信放在同一個進程運行,一旦崩潰
,微信也跟著玩完,明明是小程序開發者的鍋
,硬是讓騰訊給背
了,不合適。
而小型公司,emmmmm,連多進程開發都用的很少,就不要說通信了。但是,如果沒有一顆進大廠的心,就學不到高階技能,有些東西學了,總比一無所知要好。
Q:使用多進程有什么好處?
A:
1)進程隔離,子app
崩潰,不會影響其他進程。
2)系統運行期間,對每個進程的內存劃分是有一個上限的,具體多少,視具體設備而定,利用多進程開發,可以提高程序的可運行內存限制。
3)如果系統運行期間內存吃緊,可以殺死子進程,減少系統壓力。殺死進程的方式,往往比優化單個app的內存更加直接有效
Q:什么叫
RPC
?
A:從客戶端上通過參數傳遞的方式調用服務器上的一個函數并得到返回的結果,隱藏底層的通訊細節。在使用形式上像調用本地函數
一樣去調用遠程函數
。
Q:我們自己定義一個
RPC
進程間通信框架,有什么實際用處?
A:定義框架的作用,都是 把臟活,累活,別人不愿意重復干的活,都放到框架里面去,讓使用者用最干凈的方式使用業務接口。定義一個RPC
進程間通信框架,可以把C/S兩端那些惡心人的AIDL
編碼都集中放到框架module
中,這是最直觀的好處,另外,客戶端原本還需要手動去bindService
,定義ServiceConnection
,取得Binder
,再去通信,使用RPC
框架,這些內容都可以放到框架module
中. 而C/S兩端的代碼,就只剩下了S端
的服務注冊,C端
的RPC
接口調用,代碼外觀上非常簡潔(可能這里文字描述不夠直觀,后面有圖)
前置技能
要理解本文的核心代碼,還是需要一些基礎的,大致如下:
四大組件之一Service
使用方法,
androidAIDL
通信機制,
java注解,java反射,java 泛型
二、傳統方式IPC通信寫法
與 使用IPC框架進行RPC通信
的對比
見github : https://github.com/18598925736/MyRpcFramework , 運行 aidl_client
和 aidl_service
先展示效果
aidl.gif
圖中的查找用戶
,是從服務端
讀取的數據,觀察一下核心代碼:這是我優化之后的
IPC
項目結構(如果不優化,那么客戶端 服務端都需要編寫一樣的AIDL代碼,還要有一個包括包名在內神馬都要一模一樣的JavaBean,實在是丑陋
):
image.png
服務端
核心代碼:
public class ServiceActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
startService(new Intent(this, MyService.class));//服務端,app啟動之后,自動啟動服務
}
}
public class MyService extends Service {
ConcurrentMap<String, UserInfoBean> map;
@Nullable
@Override
public IBinder onBind(Intent intent) {
map = new ConcurrentHashMap<>();
for (int i = 0; i < 100; i++) {
map.put("name" + i, new UserInfoBean("name" + i, "accountNo" + i, i));
}
return new IUserInfo.Stub() {//數據接收器 Stub
@Override
public UserInfoBean getInfo(String name) {
return map.get(name);
}
};
}
@Override
public void onCreate() {
super.onCreate();
Log.e("MyService", "onCreate: success");
}
}
客戶端
核心代碼 :
public class ClientActivity extends AppCompatActivity {
private TextView resultView;
private String TAG = "clientLog";
private int i = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
private void initView() {
resultView = findViewById(R.id.resultView);
findViewById(R.id.connect).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
bindService();
}
});
findViewById(R.id.disconnect).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
unbindService(connection);
resultView.setText("嘗試釋放");
} catch (IllegalArgumentException e) {
resultView.setText("已經釋放了");
}
}
});
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (iUserInfo != null) {
try {
((Button) v).setText("查找name為:name" + ((i++) + 1) + "的UserInfoBean");
UserInfoBean bean = iUserInfo.getInfo("name" + i);
if (bean != null)
resultView.setText(bean.toString());
else
resultView.setText("沒找到呀");
} catch (RemoteException e) {
e.printStackTrace();
}
} else {
resultView.setText("沒有連接上service");
}
}
});
}
//作為IPC的客戶端,我們需要 建立起和Service的連接
private IUserInfo iUserInfo;//操作句柄,可以通過它向service發送數據
private void bindService() {
if (iUserInfo == null) {
Intent intent = new Intent();
intent.setComponent(new ComponentName(
"study.hank.com.aidl_service",
"study.hank.com.aidl_service.MyService"));
bindService(intent, connection, Context.BIND_AUTO_CREATE);
resultView.setText("嘗試連接");
} else {
resultView.setText("已經連接上service" + iUserInfo);
}
}
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
iUserInfo = IUserInfo.Stub.asInterface(service);
resultView.setText("連接成功");
Log.d(TAG, "connection:" + "連接成功");
}
@Override
public void onServiceDisconnected(ComponentName name) {
iUserInfo = null;
resultView.setText("連接 已經斷開");
Log.d(TAG, "connection:" + "已經斷開");
}
};
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(connection);
}
}
很容易發現,服務端的代碼量尚可,不是很復雜,但是客戶端這邊,要處理 connection
,要手動去綁定以及解綁Service
,所有參與通信的javabean
還必須實現序列化接口parcelable
Demo
中只有一個客戶端,還不是很明顯,但是如果有N
個客戶端Activity
都需要與service
發生通信,意味著每一個Activity
都必須寫類似的代碼. 不但累贅
,而且丑陋
.
前方高能
不使用RPC框架時,CS兩端的代碼的結構,已經有了大致的印象,下面是 使用IPC框架時
客戶端、服務端 的核心代碼
客戶端
客戶端核心代碼.png
之前的bindService
呢?沒了。客戶端使用此框架來進行 進程通信,不用去關心AIDL
怎么寫了,不用關注bindService,ServiceConnection
,省了很多事。
服務端
服務端核心代碼.png
對比 使用框架前后,我們的核心代碼的變化
有什么變化?顯而易見,極大縮減了
客戶端
的編碼量,而且,一勞永逸,除非需求大改,不然這個框架,一次編寫,終身使用。除此之外,使用框架還可以極大地節省客戶端
代碼量,減少人為編碼時產生的可能疏漏(比如忘記釋放連接造成泄漏等). 試想一下,如果你是一個團隊leader
,團隊成員的水平很有可能參差不齊
,那么如何保證項目開發中出錯概率最小
-------使用框架
, 用框架來簡化團隊成員的編碼量
和編碼難度
,讓他們傻瓜式
地寫代碼.
三、Demo展示
github地址:https://github.com/18598925736/MyRpc
ipc.gif
以上Demo,模擬的場景是:
服務端:開啟一個登錄服務
,啟動服務之后,保存一個可以登錄
的用戶名和密碼
客戶端1:RPC
調用登錄服務
,用戶名和密碼 和服務端的一樣
,可以登錄成功
客戶端2:RPC
調用登錄服務
,用戶名和密碼 和服務端的不一樣
,登錄失敗
Demo
工程代碼結構圖
客戶端.png
服務端.png
框架層.png
注:客戶端和服務端必須同時依賴框架層module implementation project(":ipc")
四、框架核心思想講解
我們不使用IPC框架
時,有兩件事
非常惡心:
1. 隨著業務的擴展,我們需要頻繁(
因為要新增業務接口
)改動AIDL
文件,而且AIDL
修改起來沒有任何代碼提示,只有到了編譯之后,編譯器才會告訴我哪里錯了,而且 直接引用到的JavaBean
還必須手動再聲明一次。實在是不想在這個上面浪費時間。
2. 所有客戶端Activity
,只要想進行進程間binder
通信,就不可避免要去手動bindService
,隨后去處理Binder
連接,重寫ServiceConnection
,還要在適當的時候釋放連接
,這種業務不相關而且重復性很大的代碼,要盡量少寫。
IPC框架將會著重解決這兩個問題。下面開始講解核心設計思想
注:
1.搭建框架牽涉的知識面會很廣,我不能每個細節都講得很細致,一些基礎部分一筆帶過的,如有疑問,希望能留言討論。
2.設計思路都是環環相扣的,閱讀時最好是從上往下依次理解.
框架思想四部曲:
1)業務注冊
上文說到,直接使用
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
做準備。
此處有幾個精妙設計:
1, 利用自定義注解@ServiceId
對業務接口和實現類,都形成約束,這樣業務實現類就有了進行唯一性約束,因為在Registry
類中,一個ServiceId
只針對一種業務,如果用Registry
類注冊一個沒有@ServiceId
注解的業務類,就會拋出異常。image.png
2, 利用注解
@ServiceId
的value
作為key
,保存所有的業務實現類的Class
, 以及該Class
的所有public
的Method
到map
集合中,通過日志打印,很容易看出當前服務端有哪些 業務類,業務類有哪些可供外界調用的方法。(·這里需要注意,保存方法時,必須連同方法的參數類型一起作為key
,因為存在同名方法重載的情況·)
當你運行Demo
,啟動服務端的時候,過濾一下日志,就能看到:image.png
3 ,如果再發生 業務擴展的情況,我們只需要直接改動加了@ServiceId
注解的業務類即可,并沒有其他多余的動作。
如果我在IUserBusiness
接口中,增加一個logout
方法,并且在實現類中去實現它。那么,再次啟動服務端
app,上圖的日志中就會多出一個logout
方法.image.png
4,提供一個Map
集合,專門用來保存每一個ServiceId
對應的Object
,并提供getObject
和putObject
方法,以便反射執行Method
時所需。image.png
OK,一切準備萬全。業務類的每個部分基本上都保存到了服務端進程
的內存
中,反射執行Method
,隨時可以取用。
2)自定義通信協議
跨進程通信,我們本質上還是使用Binder AIDL這一套,所以AIDL代碼還是要寫的,但是,是寫在框架層中,一旦確定了通信協議,那這一套AIDL就不會隨著業務的變動去改動它,因為它是框架層代碼,不會隨意去動。 要定自己的通信協議,其實沒那么復雜。想一想,通信,無非就是客戶端向服務端發送消息,并且取得回應的過程,那么,核心方法就確定為 send:
image.png
入參是Request
,返回值是Response
,有沒有覺得很像HTTP
協議。
request和response
都是我們自定義的,注意,要參與跨進程通信的javaBean
,必須實現Parcelable
接口,它們的屬性類型也必須實現Parcelable
接口。image.png
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個類講解完了,那么下一步應該是把這個協議使用起來。
3)binder連接封裝
參照Demo
源碼,這一個步驟中的兩個核心類:IpcService
, Channel
先說
IpcService.java
它就是一個
extends android.app.Service
的一個普通Service
,它在服務端啟動,然后與客戶端發生通信。它必須在服務端app
的manifest
文件中注冊。同時,當客戶端與它連接成功時,它必須返回一個Binder
對象,所以我們要做兩件事:
1 服務端的manifest
中對它進行注冊
image.png
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
通信即可。(我是這么理解的,如有異議,歡迎留言)
2 重寫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;
}
};
}
這里有一些細節需要總結一下:
1 從request
中拿到的 參數列表是Parameter[]
類型的,而我們反射執行某個方法,要的是Object[]
,那怎么辦?反序列化咯,先前是用gson
去序列化的,這里同樣使用gson
去反序列化, 我定義了一個名為:restoreParams
的方法去反序列化成Object[]
.
2 之前在request
中,定義了一個type
,用來區分靜態的getInstance
方法,和 普通的業務method
,這里要根據request
中的type
值,區分對待。getInstance
方法,會得到一個業務實現類的Object
,我們利用Registry
的putObject
把它保存起來。 而,普通method
,再從Registry
中將剛才業務實現類的Object
取出來,反射執行method
3 靜態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類代碼,兩個關鍵:
1bindService+ServiceConnection
供客戶端調用,綁定服務,并且將連接成功之后的binder保存起來image.png
2 提供一個send
方法,傳入request
,且 返回response
,使用serviceId
對應的binder
完成通信。
4)動態代理實現RPC
終于到了最后一步,前面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
個參數
@param service
要訪問哪一個遠程service,因為不同的service會返回不同的Binder
@param classType
要訪問哪一個業務類,注意,這里的業務類完全是客戶端自己定義的,包名不必和服務端一樣,但是一定要有一個和服務端對應類一樣的注解。注解相同,框架就會認為你在訪問相同的業務。
@param getInstanceMethodName
我們的業務類都是設計成單例的,但是并不是所有獲取單例對象的方法都叫做getInstance,我們框架要允許其他的方法名
@param params
參數列表,類型為Object[]
重中之重,實現RPC的最后一個步驟,如圖:
image.png
如果服務端的單例對象
創建成功,那么說明 服務端的注冊表
中已經存在了一個業務實現類的對象
,進而,我可以通過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。
結語
生活不止眼前的茍且...還要學會用大局觀思考。。。
框架思想,如果我們能夠理解,甚至創造自己的框架,那么我們就已經脫離了低級趣味,在走向進階了。
然而,進階之路漫漫長。我昨天看了高手的一篇文章,或者一個視頻,感覺學了點干貨,那我想要吸收知識為己所用,就不能真的把知識當成干貨儲存起來,我要想辦法找點水把干貨咽下去,消化吸收,才是我自己的東西。