餓了麼開源專案Hermes:新穎巧妙易用的Android程式間通訊IPC框架

weixin_33797791發表於2018-06-11

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

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

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

  xiaofei.library

  hermes

  0.2

  pom

使用方法

接下來的部分將告訴你如何在其他程式呼叫主程式的函式。Hermes支援任意程式之間的函式呼叫,想要知道如何呼叫非主程式的函式,請看這裡

AndroidManifest.xml

在AndroidManifest.xml中加入如下宣告,你可以加上其他屬性。

初始化

經常地,一個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()打出錯誤日誌。你可以通過日誌定位問題。

相關文章