隨著App功能的不斷增多,Native層的程式碼規模也在迅速膨脹,為了Native層的程式碼結構清晰,會按照模組分別構建成獨立的so庫,使用一個JNI層so庫引用其他實現具體功能的功能實現so庫,Java層只載入這個JNI層so庫,來間接呼叫功能實現so庫。
so庫之間通過引用標頭檔案和執行時指定共享庫依賴的方式形成了依賴關係。但是這樣也會有一些問題。
- 我們常常會用到第三方的 so 庫,如果單個庫可能沒問題,如果多個第三方 so 庫檔案,同時載入可能會出現衝突,比如說騰訊的YSDK和BUGLY。
- 載入JNI層so庫的時候,即使這次JNI呼叫有些功能實現so庫裡面的資料結構或函式沒有被呼叫到,只要這個so庫被JNI層so庫宣告為執行時需要依賴的共享庫,也需要跟JNI層so庫一起被載入,這無形中增大了Native層的常駐記憶體。
這個時候就需要在Native層直接動態載入so庫,由JNI層so庫動態載入功能實現so庫。如下圖所示,會有一個統一介面so庫,在這個庫中定義好不可輕易修改的介面函式,呼叫方只需要知道這些介面即可,不需要依賴標頭檔案就能呼叫這些函式,這樣呼叫方和so庫之間就不存在直接的依賴,具體的工作就可以交給統一介面so庫完成,它通過動態呼叫再去執行功能so庫中的函式。
so庫動態載入的實現
在Native層的C/C++程式碼環境,so庫動態載入是使用dlopen()
、dlsym()
和dlclose()
這三個函式實現的。它們的作用分別是:dlopen()
開啟一個動態連結庫,返回一個動態連結庫的控制程式碼;dlsym()
根據動態連結庫控制程式碼和符號名,返回動態連結庫內的符號地址,這個地址既可以是變數指標,也可以是函式指標;dlclose()
關閉動態連結庫控制程式碼,並對動態連結庫的引用計數減1,當這個庫的引用計數為0,這個庫將會被系統解除安裝。
一般使用C/C++實現so庫動態載入的流程如下:
- 首先呼叫
dlopen()
函式,這個函式所需的引數,一個是so庫的路徑,一個是載入模式。一般使用的載入模式有兩個:RTLD_NOW
在返回前解析出所有未定義符號,如果解析不出來,dlopen()
返回NULL
;RTLD_LAZY
則只解析當前需要的符號(只對函式生效,變數定義仍然是全部解析)。顯然對於動態載入,載入方只需知道當前被載入的so庫裡面自己需要用的函式和變數定義,所以這裡選擇的是後者。如果這個呼叫成功將返回一個so庫的控制程式碼; - 在上一步得到so庫控制程式碼之後,這時就可以呼叫
dlsym()
函式,傳入so庫控制程式碼和所需的函式或變數名稱,返回相應的函式指標或變數指標;載入方這時就可以使用返回的指標呼叫被載入so庫之中定義的函式和資料結構; - 當so庫的呼叫結束,呼叫
dlclose()
函式關閉解除安裝so庫; - 如果在開啟關閉so庫,或者獲取so庫裡操作物件的指標出現錯誤的時候,可以呼叫
dlerror()
函式獲取具體的錯誤原因。
程式碼實現
比如,在硬體功能so庫中有一個int test_open(int port)
的函式,該如何最終呼叫到這個方法呢?
//1、宣告函式介面
typedef int (*Func_test_open)(int);
int open(int port){
//2、獲取so控制程式碼
void *handle = dlopen("libtest.so",RTLD_LAZY);
if(!handle ){
LOGE("%s",dlerror());
return -1;
}
//3、獲取函式指標
Func_test_open func_test_open = (Func_test_open) dlsym (handle,"test_open");
if(!func_test_open){
LOGE("%s",dlerror());
dlclose(handle);
return -1;
}
//4、呼叫函式
int ret = func_test_open(8080);
//5、關閉so
dlclose(handle);
return ret;
}
複製程式碼
這樣JNI層只需要去呼叫open(int port)
方法就可以呼叫到硬體功能so庫中的test_open(int port)
函式
總結
剛開始使用動態載入so庫的方案時,會比較擔心效能問題,但在實測時跟直接依賴對比,對效能並沒有明顯的影響,功能實現的so庫與JNI層完全解耦,有高度的獨立內聚性。同時支援動態載入解除安裝so庫,也一定程度上減少了Native層的常駐記憶體。