版權所有。所有權利保留。
歡迎轉載,轉載時請註明出處:
http://blog.csdn.net/xiaofei_it/article/details/51464518
Android進程間通信IPC是比較高級的話題,很多Android程序員碰到IPC就覺得頭疼,尤其是AIDL這類東西。
公司最近在研究DroidPlugin插件開發,DroidPlugin把每個子app都變成一個進程。這樣的話子app和主app如果需要共享數據,就需要IPC。所以我開發了Hermes框架,讓IPC變得非常簡單優雅。
項目地址:
https://github.com/Xiaofei-it/Hermes
這個框架開發難度很大,涉及到AIDL、binder、反射、註解、進程間垃圾回收、動態代理等很多技術。我以後會對源碼進行解析。
本來我寫的文檔是英文的,後來爲了便於讀者查閱,特意翻譯成了中文文檔。希望大家持續關注,可以給個star。
中文文檔鏈接:
https://github.com/Xiaofei-it/Hermes/blob/master/README-ZH-CN.md
Hermes是一套新穎巧妙易用的Android進程間通信IPC框架。這個框架使得你不用瞭解IPC機制就可以進行進程間通信,像調用本地函數一樣調用其他進程的函數。
你們知道把英文文檔翻譯成中文有多麼蛋疼嗎???還不給我star一下 o(╥﹏╥)o
特色
-
使得進程間通信像調用本地函數一樣方便簡單。
-
輕而易舉在本地進程創建其他進程類的對象,輕而易舉在本進程獲取其他進程的單例,輕而易舉在本進程使用其他進程的工具類。
-
支持進程間函數回調,調用其他進程函數的時候可以傳入回調函數,讓其他進程回調本進程的方法。
-
自帶內存優化,並且支持跨進程垃圾回收。
基本原理
IPC的主要目的是調用其他進程的函數,Hermes讓你方便地調用其他進程函數,調用語句和本地進程函數調用一模一樣。
比如,單例模式經常在Android App中使用。假設有一個app有兩個進程,它們共享如下單例:
@ClassId(“Singleton”)
public class Singleton {
private static Singleton sInstance = null;
private volatile String mData;
private Singleton() {
mData = new String();
}
public static synchronized Singleton getInstance() {
if (sInstance == null) {
sInstance = new Singleton();
}
return sInstance;
}
@MethodId(“setData”)
public void setData(String data) {
mData = data;
}
@MethodId(“getData”)
public String getData() {
return mData;
}
}
如果不使用Hermes,單例是無法共享的。
假設單例在進程A中,進程B想訪問這個單例。那麼你寫如下接口:
@ClassId(“Singleton”)
public interface ISingleton {
@MethodId(“setData”)
void setData(String data);
@MethodId(“getData”)
String getData();
}
進程B使用單例的時候,代碼如下:
//obtain the instance of Singleton
ISingleton singleton = Hermes.getInstance(ISingleton.class);
//Set a data
singleton.setData(“Hello, Hermes!”);
//Get the data
Log.v(TAG, singleton.getData());
是不是很神奇?
只要給Hermes.getInstance()傳入這樣的接口,Hermes.getInstance()便會返回和進程A中實例一模一樣的實例。之後你在進程B中調用這個實例的方法時,進程A的同一個實例的方法也被調用。
但是,怎麼寫這種接口呢?很簡單。比如,進程A有一個類Foo,你想在進程B中訪問使用這個類。那麼你寫如下接口IFoo,加入同樣的方法,再在類Foo和接口IFoo上加上同樣的@ClassId註解,相同的方法上加上同樣的@MethodId註解。之後你就可以在進程B使用Hermes.getInstance(IFoo.class)獲取進程A的Foo實例。
Gradle
dependencies {
compile 'xiaofei.library:hermes:0.2'
}
Maven
<dependency>
<groupId>xiaofei.library</groupId>
<artifactId>hermes</artifactId>
<version>0.2</version>
<type>pom</type>
</dependency>
使用方法
接下來的部分將告訴你如何在其他進程調用主進程的函數。Hermes支持任意進程之間的函數調用,想要知道如何調用非主進程的函數,請看這裏。
AndroidManifest.xml
在AndroidManifest.xml中加入如下聲明,你可以加上其他屬性。
<service android:name="xiaofei.library.hermes.HermesService$HermesService0">
</service>
初始化
經常地,一個app有一個主進程。給這個主進程命名爲進程A。
假設有一個進程B,想要調用進程A的函數。那麼進程B應該初始化Hermes。
你可以在進程B的Application.OnCreate()或者Activity.OnCreate()中對Hermes初始化。相應的API是Hermes.connect(Context)。
Hermes.connect(getApplicationContext());
你可以調用Hermes.isConnected()來查看通信的進程是否還活着。
設置Context
在給其他進程提供函數的進程中,可以使用Hermes.setContext(Context)來設置context。
函數調用時,如果參數有Context,這個參數便會被轉換成之前設置的Context。具體見“注意事項”的第8點。
註冊
進程A中,被進程B調用的類需要事先註冊。有兩種註冊類的API:Hermes.register(Class<?>)和Hermes.register(Object)。Hermes.register(object)等價於Hermes.register(object.getClass())。
但是如果類上面沒有加上註解,那麼註冊就不是必須的,Hermes會通過類名進行反射查找相應的類。詳見“注意事項”的第3點。
創建實例
進程B中,創建進程A中的實例有三種方法:Hermes.newInstance()、Hermes.getInstance()和Hermes.getUtilityClass()。
-
Hermes.newInstance(Class, Object...)
這個函數在進程A中創建指定類的實例,並將引用返回給進程B。函數的第二個參數將傳給指定類的對應的構造器。
@ClassId(“LoadingTask”) public class LoadingTask { public LoadingTask(String path, boolean showImmediately) { //... } @MethodId(“start”) public void start() { //... } } @ClassId(“LoadingTask”) public class ILoadingTask { @MethodId(“start”) void start(); }
在進程B中,調用Hermes.newInstance(ILoadingTask.class, “files/image.png”, true)便得到了LoadingTask的實例。
-
Hermes.getInstance(Class, Object...)
這個函數在進程A中通過指定類的getInstance方法創建實例,並將引用返回給進程B。第二個參數將傳給對應的getInstance方法。
這個函數特別適合獲取單例,這樣進程A和進程B就使用同一個單例。
@ClassId(“BitmapWrapper”) public class BitmapWrapper { @GetInstance public static BitmapWrapper getInstance(String path) { //... } @GetInstance public static BitmapWrapper getInstance(int label) { //... } @MethodId(“show”) public void show() { //... } } @ClassId(“BitmapWrapper”) public class IBitmapWrapper { @MethodId(“show”) void show(); }
進程B中,調用Hermes.getInstance(IBitmapWrapper.class, “files/image.png”)或Hermes.getInstance(IBitmapWrapper.class, 1001)將得到BitmapWrapper的實例。
-
Hermes.getUtilityClass(Class)
這個函數獲取進程A的工具類。
這種做法在插件開發中很有用。插件開發的時候,通常主app和插件app存在不同的進程中。爲了維護方便,應該使用統一的工具類。這時插件app可以通過這個方法獲取主app的工具類。
@ClassId(“Maths”) public class Maths { @MethodId(“plus”) public static int plus(int a, int b) { //... } @MethodId(“minus”) public static int minus(int a, int b) { //... } } @ClassId(“Maths”) public class IMaths { @MethodId(“plus”) int plus(int a, int b); @MethodId(“minus”) int minus(int a, int b); }
進程B中,使用下面代碼使用進程A的工具類。
IMaths maths = Hermes.getUtilityClass(IMaths.class); int sum = maths.plus(3, 5); int diff = maths.minus(3, 5);
注意事項
-
事實上,如果兩個進程屬於兩個不同的app(分別叫App A和App B),App A想訪問App B的一個類,並且App A的接口和App B的對應類有相同的包名和類名,那麼就沒有必要在類和接口上加@ClassId註解。但是要注意使用ProGuard後類名和包名仍要保持一致。
-
如果接口和類裏面對應的方法的名字相同,那麼也沒有必要在方法上加上@MethodId註解,同樣注意ProGuard的使用後接口內的方法名字必須仍然和類內的對應方法名字相同。
-
如果進程A的一個類上面有一個@ClassId註解,這個類在進程B中對應的接口上有一個相同的@ClassId註解,那麼進程A在進程B訪問這個類之前必須註冊這個類。否則進程B使用Hermes.newInstance()、Hermes.getInstance()或Hermes.getUtilityClass()時,Hermes在進程A中找不到匹配的類。類可以在構造器或者Application.OnCreate()中註冊。
但是,如果類和對應的接口上面沒有@ClassId註解,但有相同的包名和類名,那麼就不需要註冊類。Hermes通過包名和類名匹配類和接口。
對於接口和類裏面的函數,上面的說法仍然適用。
-
如果你不想讓一個類或者函數被其他進程訪問,可以在上面加上@WithinProcess註解。
-
使用Hermes跨進程調用函數的時候,傳入參數的類型可以是原參數類型的子類,但不可以是匿名類和局部類。但是回調函數例外,關於回調函數詳見“注意事項”的第7點。
public class A {} public class B extends A {}
進程A中有下面這個類:
@ClassId(“Foo”) public class Foo { public static A f(A a) { } }
進程B的對應接口如下:
@ClassId(“Foo”) public interface IFoo { A f(A a); }
進程B中可以寫如下代碼:
IFoo foo = Hermes.getUtilityClass(IFoo.class); B b = new B(); A a = foo.f(b);
但你不能寫如下代碼:
A a = foo.f(new A(){});
-
如果被調用的函數的參數類型和返回值類型是int、double等基本類型或者String這樣的Java通用類型,上面的說法可以很好地解決問題。但如果類型是自定義的類,比如“注意事項”的第5點中的例子,並且兩個進程分別屬於兩個不同app,那麼你必須在兩個app中都定義這個類,且必須保證代碼混淆後,兩個類仍然有相同的包名和類名。不過你可以適用@ClassId和@MethodId註解,這樣包名和類名在混淆後不同也不要緊了。
-
如果被調用的函數有回調參數,那麼函數定義中這個參數必須是一個接口,不能是抽象類。請特別注意回調函數運行的線程。
如果進程A調用進程B的函數,並且傳入一個回調函數供進程B在進程A進行回調操作,那麼默認這個回調函數將運行在進程A的主線程(UI線程)。如果你不想讓回調函數運行在主線程,那麼在接口聲明的函數的對應的回調參數之前加上@Background註解。
如果回調函數有返回值,那麼你應該讓它運行在後臺線程。如果運行在主線程,那麼返回值始終爲null。
默認情況下,Hermes框架持有回調函數的強引用,這個可能會導致內存泄漏。你可以在接口聲明的對應回調參數前加上@WeakRef註解,這樣Hermes持有的就是回調函數的弱引用。如果進程的回調函數被回收了,而對方進程還在調用這個函數(對方進程並不會知道回調函數被回收),這個不會有任何影響,也不會造成崩潰。如果回調函數有返回值,那麼就返回null。
如果你使用了@Background和@WeakRef註解,你必須在接口中對應的函數參數前進行添加。如果加在其他地方,並不會有任何作用。
@ClassId(“Foo”) public class Foo { public static void f(int i, Callback callback) { } } @ClassId(“callback”) public interface Callback { void callback(); } @ClassId(“Foo”) public interface IFoo { void f(int i, @WeakRef @Background Callback callback); }
-
調用函數的時候,任何Context在另一個進程中都會變成對方進程的application context。
-
數據傳輸是基於Json的。
-
使用Hermes框架的時候,有任何的錯誤,都會使用android.util.Log.e()打出錯誤日誌。你可以通過日誌定位問題。