餓了麼開源項目Hermes:新穎巧妙易用的Android進程間通信IPC框架

版權所有。所有權利保留。

歡迎轉載,轉載時請註明出處:

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

特色

  1. 使得進程間通信像調用本地函數一樣方便簡單。

  2. 輕而易舉在本地進程創建其他進程類的對象,輕而易舉在本進程獲取其他進程的單例,輕而易舉在本進程使用其他進程的工具類。

  3. 支持進程間函數回調,調用其他進程函數的時候可以傳入回調函數,讓其他進程回調本進程的方法。

  4. 自帶內存優化,並且支持跨進程垃圾回收。

基本原理

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()。

  1. 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的實例。

  2. 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的實例。

  3. 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);
    

注意事項

  1. 事實上,如果兩個進程屬於兩個不同的app(分別叫App A和App B),App A想訪問App B的一個類,並且App A的接口和App B的對應類有相同的包名和類名,那麼就沒有必要在類和接口上加@ClassId註解。但是要注意使用ProGuard後類名和包名仍要保持一致。

  2. 如果接口和類裏面對應的方法的名字相同,那麼也沒有必要在方法上加上@MethodId註解,同樣注意ProGuard的使用後接口內的方法名字必須仍然和類內的對應方法名字相同。

  3. 如果進程A的一個類上面有一個@ClassId註解,這個類在進程B中對應的接口上有一個相同的@ClassId註解,那麼進程A在進程B訪問這個類之前必須註冊這個類。否則進程B使用Hermes.newInstance()、Hermes.getInstance()或Hermes.getUtilityClass()時,Hermes在進程A中找不到匹配的類。類可以在構造器或者Application.OnCreate()中註冊。

    但是,如果類和對應的接口上面沒有@ClassId註解,但有相同的包名和類名,那麼就不需要註冊類。Hermes通過包名和類名匹配類和接口。

    對於接口和類裏面的函數,上面的說法仍然適用。

  4. 如果你不想讓一個類或者函數被其他進程訪問,可以在上面加上@WithinProcess註解。

  5. 使用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(){});
    
  6. 如果被調用的函數的參數類型和返回值類型是int、double等基本類型或者String這樣的Java通用類型,上面的說法可以很好地解決問題。但如果類型是自定義的類,比如“注意事項”的第5點中的例子,並且兩個進程分別屬於兩個不同app,那麼你必須在兩個app中都定義這個類,且必須保證代碼混淆後,兩個類仍然有相同的包名和類名。不過你可以適用@ClassId和@MethodId註解,這樣包名和類名在混淆後不同也不要緊了。

  7. 如果被調用的函數有回調參數,那麼函數定義中這個參數必須是一個接口,不能是抽象類。請特別注意回調函數運行的線程。

    如果進程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);
    }
    
  8. 調用函數的時候,任何Context在另一個進程中都會變成對方進程的application context。

  9. 數據傳輸是基於Json的。

  10. 使用Hermes框架的時候,有任何的錯誤,都會使用android.util.Log.e()打出錯誤日誌。你可以通過日誌定位問題。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章