Android Robolectric 加載運行本地 So 動態庫

原文發表於:http://rocko.xyz/2016/11/27/Android-Robolectric-加載運行本地-so-動態庫/html

前言

Robolectric 是 Android 的單元測試框架,運行無需 Android 真機環境直接運行在 JVM 之上,因此在 test case 運行速度效率上有了很大提高,接近於 Java JUnit test(JUnit test > Robolectric ≫ androidTest)。不過框架自己並不支持 so 本地庫的加載使用,加載時會直接報錯,由於實際上運行環境是電腦機器,而咱們打出的 so 文件是給手機上用的因此固然會報錯。雖然在 GitHub 上不少人問過關於使用 so 的問題但基本都建議說不要在單元測試中去加載本地庫,這在原則上是要這麼作,但可能有些項目中作起來就有些困難了,好比在代碼結構不夠好、依賴耦合較大或者自己就對 so 庫依賴很大的狀況下。因此下面說說在項目中 Robolectric 要怎麼解決須要加載運行本地 so 庫這個問題。java

動態庫

動態庫又稱動態連接庫(Dynamic-link library 縮寫 DLL),是一個包含可由多個程序同時使用的代碼和數據的庫,DLL 不是可執行文件。動態連接提供了一種方法,使進程能夠調用不屬於其可執行代碼的函數。函數的可執行代碼位於一個 DLL 中,該 DLL 包含一個或多個已被編譯、連接並與使用它們的進程分開存儲的函數。DLL 還有助於共享數據和資源。多個應用程序可同時訪問內存中單個DLL 副本的內容。DLL 是一個包含可由多個程序同時使用的代碼和數據的庫。Windows下動態庫爲 .dll 後綴(通常爲 PE 格式),在 Linux 在爲 .so 後綴(通常爲 ELF 格式),macOS下爲 .dylib 後綴(通常爲 Mach-O 格式)。因爲 CPU 架構和動態庫文件格式的不一樣於是在不一樣平臺下不能通用。其它細節的東西就不展開了由於也不會 :-)linux

而 Android 自己是 Linux 系統,因此用的動態庫也是 .so 的文件,於是運行與 JVM 的 Robolectric 是不能直接加載使用的(Linux 某些狀況下可用,下面提到)。android

Robolectric 中使用動態庫

咱們知道動態庫通常都是打給特定平臺、特定 CPU 架構用的,因此要解決在 Robolectric 下加載運行 so 動態庫的問題的思路就是在不一樣 Robolectric 運行平臺下去處理加載不一樣的動態庫,因此你要在 Ronbolectriv 中使用的 so 動態庫最好要有源碼否則在 macOS 和 Windows 下就不就好處理了。c++

Note: 注意動態庫名稱已 lib 開頭。git

Linux 下 Robolectric 中使用動態庫

Android 與 Linux 同氣連枝,因此底層的東西不少是通用的,動態庫也同樣。咱們 Android 使用 so 時通常也要對不一樣 CPU 架構的手機下使用不一樣的 so 文件,譬如:armeabi-v7amipsx86。而咱們使用的 LInux 發行版通常都是 64 位的,因此原理上咱們使用 x86-64 的動態庫是能夠的,不過可能須要處理依賴庫問題若是你的本地代碼裏有 include 其它依賴的話。若是沒加進來 Robolectric 運行就會報以下的錯誤:github

java.lang.UnsatisfiedLinkError: xxx/xxx.so xxx 動態庫找不到。

xxx.so 就是你所使用 so 的依賴,好比把新浪微博 SDK 的 x86-64 的 libweibosdkcore.so 加載進來的話就會報 liblog.so 等找不到,由於 libweibosdkcore 中有對 Android liblog 等 so 庫的依賴。那這個問題怎麼解決呢。咱們想一想打包 so 庫時用的是 ndk,須要使用 ndk-bundle 工具,咱們想一想,跟編譯 apk 差很少,apk 打包須要 sdk 工具,compileSdk 裏就是咱們編譯的依賴,裏面有 android.jar。因此咱們能夠到 ndk-bundle 裏找找,最後咱們發現不一樣 CPU 架構下的 so 依賴庫都是有的,像咱們通常的電腦 64 位 CPU 便可使用 arch-x86_64 下的 so 動態庫,因此咱們只須要在加載咱們程序的 so 庫以前加載這些必須的依賴便可。處理代碼後面貼出。bash

ndk-bundle依賴庫

注意 ndk-bundle 裏的 so 也是隻能在 Linux 下用的,若是用於其它平臺會報錯,緣由前面已說明。架構

java.lang.UnsatisfiedLinkError: xxx.so: unknown file type, first eight bytes: 0x7F 0x45 0x4C 0x46 0x02 0x01 0x01 0x00

macOS 下 Robolectric 中使用動態庫

前面已提到,不一樣平臺下動態連接庫是不通用的,因此必須對源碼從新編譯打包以移植到不一樣平臺下,若是你的 so 沒有源碼的話那在 macOS 和 Windows 下就行不通了。從新打包咱們能夠按以下兩步進行:app

# 先生成 .o ,-I 後加進 Java jni 的編譯依賴

cc -c -I/System/Library/Frameworks/JavaVM.framework/Headers *.cpp

# 打包成 .dylib

g++ -dynamiclib -undefined suppress -flat_namespace *.o -o something.dylib

某些依賴庫能夠到 /usr/lib 下找找,好比 libclibstdc++

Windows 下 Robolectric 中使用動態庫

本人沒有在 Windows 下開發因此這部分就略過了,思路是同樣的。

Sample

下面是簡單的處理代碼示例。首先新建一個包含 jni 的工程,裏面寫個基本的本地庫,以下:

正常流程

// native-lib.cpp

#include <jni.h>
#include <string>

extern "C"
jstring
Java_xyz_rocko_rsnl_nativeinterface_NativeSample_stringFromJNI(
       JNIEnv *env,
       jobject /* this */) {

   // 簡單返回個字符串
   std::string hello = "Hello from Native.";
   return env->NewStringUTF(hello.c_str());
}

而後在 Application 啓動時會加載這個本地庫:

// NativeLibsApplication.java

public class NativeLibsApplication extends Application {

 // Used to load the 'native-lib' library on application startup.
 static {
   System.loadLibrary("native-lib");
 }
}

此時運行 Robolectric 的 test case 就發生以下報錯:

java.lang.UnsatisfiedLinkError: no native-lib in java.library.path

正常運行 Test case

處理後的流程

首先流程應該在咱們的代碼裏避免能夠直接加載 so 動態庫,而後 Robolectric 在啓動時本身去加載須要的動態庫。

// NativeLibsApplication.java

public class NativeLibsApplication extends Application {

 @Override public void onCreate() {
   super.onCreate();
   loadNativeLibraries();
 }

 /**
  * 簡單讓子類可本身實現
  */
 protected void loadNativeLibraries() {

    // 代碼裏真正加載本地庫的地方,固然你本身的能夠處理地更解耦一點。
   NativeLibrariesManager.loadNativeLibraries();
 }
}

而後咱們的 Robolectric 裏自定義本身的 Application,裏面根據須要在不一樣運行平臺下本身加載須要的本地動態庫,首先複製咱們給 Robolectric 用的本地庫到 test 的 libs 文件夾裏,按不一樣平臺分類,以下圖:

test 內 libs 文件

Linux 下的咱們從 ndk-bundle 裏複製咱們須要的 .so,而後咱們本身的本地庫打一個 x86-64 的便可,注意 compileSdkVersion 選上高一點支持 x86-64 的版本。

而後從新移植打出 macOS 下的動態庫,簡單寫個打包腳本以下:

// make_macOS_dylib.sh

#!/usr/bin/env bash

OUTPUT=../../../build/intermediates/dylibs
mkdir -p ${OUTPUT}

# .o file
cc -c -I/System/Library/Frameworks/JavaVM.framework/Headers *.cpp -o ${OUTPUT}/libnative-lib.o

# .dylib file
g++ -dynamiclib -undefined suppress -flat_namespace ${OUTPUT}/*.o -o ${OUTPUT}/libnative-lib.dylib

libnative-lib.dylib 就是咱們要的。

而後咱們自定義 Application 處理加載這些動態庫:

// RobolectricApplication.java

public class RobolectricApplication extends NativeLibsApplication {

 static {
   ShadowLog.stream = System.out; //Android logcat output.
 }

 @Override protected void loadNativeLibraries() {
   //Disable super class load so file.
   //super.loadNativeLibraries();
   Log.d(TAG, "=====>> Robolectric start native libraries.");

   String libsBasePath =
       new File(new File("").getAbsolutePath() + "/src/test/libs").getAbsolutePath();
   String os = System.getProperty("os.name");
   os = !TextUtils.isEmpty(os) ? os : "";
   List<File> soFileList = new ArrayList<>();
   String systemArchPath = libsBasePath + "/framework/";
   //!!! 64 位機器下處理
   if (os.contains("Mac")) {
     //load system library if need
     String macSysSoBasePath = systemArchPath + "macOS/";
     soFileList.addAll(addLibs(macSysSoBasePath));
     // App so...
     String macAppSoPath = libsBasePath + "/macOS_x86-64/";
     // mac下so要使用macOS專用庫
     soFileList.addAll(addLibs(macAppSoPath));
   } else if (os.contains("Linux")) {
     //load system library if need
     String linuxSysSoBasePath = systemArchPath + "arch_x86-64/";
     soFileList.addAll(addLibs(linuxSysSoBasePath));
     // App so...
     String linuxAppSoPath = libsBasePath + "/linux_x86-64/";
     soFileList.addAll(addLibs(linuxAppSoPath));
   } else if (os.contains("Windows")) {
     // ignore
   }

   for (File soFie : soFileList) {
     System.load(soFie.getAbsolutePath());
   }
 }

 private List<File> addLibs(@NonNull String path) {
   File[] basePathFiles = new File(path).listFiles();
   List<File> pathFilesList = new ArrayList<>();
   if (basePathFiles != null && basePathFiles.length > 0) {
     pathFilesList.addAll(Arrays.asList(basePathFiles));
   }
   return pathFilesList;
 }
}

如今就能夠加載了,運行以下 test case,結果以下圖,成功了。

@Test public void testLoadNativeLibrariesSuccess() throws Exception {
      String nativeExcepted = "Hello from Native.";
      String result = NativeSample.stringFromJNI();
      Log.d(TAG, "result: " + result);
      assertEquals(nativeExcepted, result);
}

運行加載本地庫 Test case 成功

End

Linux 下使用最快速方便,只須要打包程序的 so 時順便打包出 x86-64 的 so ,而後複製 ndk-bundle 的 so 加上須要的依賴便可。macOS 和 Windows 下就須要本身打包出各自平臺下的動態庫纔可以使用,若是代碼裏有 Android 自帶 so 依賴的話那就須要本身去從新移植編譯打包 ndk-bundle 裏的動態庫了。

項目實例源碼:RobolectricSupportNativeLibs

參考

Core Java APIs and the Java Runtime on OS X

相關文章
相關標籤/搜索