Android程序員進階必知架構源碼:手把手講解IPC框架(2)

做者:享學課堂終身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

4、框架核心思想講解

咱們不使用IPC框架時,有兩件事很是噁心:post

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集合中。而,保存這些 Class, Method,則是爲了 反射執行指定的業務 Method作準備。此處有幾個精妙設計:

一、利用自定義註解 @ServiceId 對業務接口和實現類,都造成約束,這樣業務實現類就有了進行惟一性約束,由於在 Registry類中,一個 ServiceId只針對一種業務,若是用 Registry類註冊一個沒有 @ServiceId註解的業務類,就會拋出異常。

二、利用註解 @ServiceIdvalue做爲 key,保存全部的業務實現類的 Class , 以及該 Class的全部 publicMethodmap集合中,經過日誌打印,很容易看出當前服務端有哪些 業務類,業務類有哪些可供外界調用的方法。(·這裏須要注意,保存方法時,必須連同方法的參數類型一塊兒做爲 key,由於存在同名方法重載的狀況·) 當你運行 Demo,啓動服務端的時候,過濾一下日誌,就能看到:

3 、若是再發生業務擴展的狀況,咱們只須要直接改動加了 @ServiceId註解的業務類便可,並無其餘多餘的動做。若是我在 IUserBusiness接口中,增長一個 logout方法,而且在實現類中去實現它。那麼,再次啓動 服務端app,上圖的日誌中就會多出一個 logout方法.

四、提供一個 Map集合,專門用來保存每個 ServiceId對應的 Object,並提供 getObjectputObject方法,以便反射執行 Method時所需。

OK,一切準備萬全。業務類的每一個部分基本上都保存到了服務端進程內存中,反射執行Method,隨時能夠取用。

2)自定義通訊協議

跨進程通訊,咱們本質上仍是使用 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字符串表示接口執行的結果isSuccesstrue,接口執行成功, 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

它就是一個 extendsandroid.app.Service 的一個普通 Service,它在服務端啓動,而後與客戶端發生通訊。它必須在服務端 appmanifest文件中註冊。同時,當客戶端與它鏈接成功時,它必須返回一個 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,咱們利用 RegistryputObject把它保存起來。而,普通 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 完成通訊。

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個參數 @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通訊執行服務端過程拿到返回值。這個設計確實精妙。

5、 寫在最後的話

  1. 本案例提供的兩個Demo,都只是做爲演示效果做用的,代碼不夠精緻,請各位不要在乎這些細節。

  2. 此框架並不是本人原創,課題內容來自享學課堂Lance老師,本文只作學習交流之用,轉載請務必註明出處,謝謝合做。

  3. 第二個DemoIPC通訊框架實現RPC),個人原裝代碼中只實現了服務端1個服務2個客戶端同時調用,可是這個框架是支持服務端多個服務多個客戶端同時調用的,因此,能夠嘗試在個人代碼基礎上擴展出服務端N個業務接口實現類多個客戶端混合調用的場景。應該不會有bug。

  4. 建議讀者去嘗試擴展一下服務端和客戶端的代碼,由於這樣能夠最直觀地感覺到框架的給咱們開發帶來的便利。

結語

生活不止眼前的苟且...還要學會用大局觀思考....

框架思想,若是咱們可以理解,甚至創造本身的框架,那麼咱們就已經脫離了低級趣味,在走向進階了。然而,進階之路漫漫長。我昨天看了高手的一篇文章,或者一個視頻,感受學了點乾貨,那我想要吸取知識爲己所用,就不能真的把知識當成乾貨儲存起來,我要想辦法找點水把乾貨嚥下去,消化吸取,纔是我本身的東西。

喜歡的話記得點個贊,關注我,還有更多技術乾貨分享~

相關文章
相關標籤/搜索