Android JNI介紹(二)- 第一個JNI工程的詳細分析

上一篇文章中,咱們已經介紹了一個JNI工程的大體結構,接下來本文將對這個工程中的一些細節進行介紹。java

1、運行流程介紹

對於通常的JNI工程,編譯、安裝、運行的流程大體以下:linux

編譯:android

  1. 配置工程
  2. 編譯動態庫
  3. 將動態庫打包進apk

安裝:windows

  1. 系統將根據設備支持的ABI,首選主ABI的動態庫進行安裝,若是以主ABI找動態庫找不到,就會繼續以次ABI進行安裝,若是仍是找不到,就不拿動態庫了,這一點在Android開發者官網中也有說明

運行:bash

  1. 在運行時,須要先加載動態庫,咱們通常會在一個類的靜態代碼塊中使用System.loadLibrary進行加載動態庫,解析其中的符號
  2. 若找不到庫,則會提示java.lang.UnsatisfiedLinkError,並在日誌中打印相關信息
  3. 若加載成功,會進行函數解析,首先會使用dlsym函數檢查是否重寫了JNI_OnLoad函數,若是重寫了該函數,則執行該函數
  4. 在運行時,對於已經在JNI_OnLoad函數中進行動態註冊的函數,則能夠直接找到對應的函數運行;對於未進行動態註冊的函數,會按照Java_包名_類名_函數名的規則去尋找native函數,固然了,函數的參數、回傳值也是要進行驗證的

以上大體就是使用的一個流程,接下來回到工程,對這個默認的工程的一些細節進行解釋。app

2、工程細節說明

  • Java部分
    Java部分主要作了兩件事情:ide

    1. 加載動態庫
      由於動態庫只須要加載一次,因此通常咱們會在類的靜態代碼塊中進行加載,這樣還有個好處就是早出錯,早發現
      static {
          System.loadLibrary("native-lib");
      }
      複製代碼
    2. native函數聲明
      如下聲明表示這個函數是native函數,什麼參數也不傳,回傳一個String
      public native String stringFromJNI();
      複製代碼
  • native部分
    native函數的實現:
    函數標識:extern "C" JNIEXPORT
    回傳值類型:jstring
    參數類型:自動添加了JNIEnv*jobject
    函數

    extern "C" JNIEXPORT jstring JNICALL Java_com_wsy_jnidemo_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */){
        std::string hello = "Hello from C++";
        return env->NewStringUTF(hello.c_str());
    }
    複製代碼
  • CMakeLists部分工具

    # 聲明最低的cmake版本
    cmake_minimum_required(VERSION 3.4.1)
    # 添加一個名稱叫native-lib的動態庫,該庫的源文件爲src/main/native-lib.cpp
    add_library( native-lib # 庫的名稱
                 SHARED # SHARED:動態庫、STATIC:靜態庫
                 src/main/native-lib.cpp # 源文件,能夠是多個
                 )
                 
    # 尋找系統中的log庫,保存在log-lib變量中
    find_library( log-lib 
                  log )
                  
    # native-lib這個庫會去依賴log-lib這個庫
    target_link_libraries( native-lib
                           ${log-lib} )
    複製代碼

3、native函數聲明詳解

咱們仔細看一下這個native函數,雖然它的實現很簡單,可是這些亂七八糟的符號是什麼鬼?
oop

1. extern "C"

  • extern "C"說明

    在進行靜態註冊時,是要加上extern "C"的,它的做用主要是爲了讓編譯器以C的方式去編譯它,而不是C++,咱們知道,C++是一門面向對象的語言,是支持函數重載的,以下:

    而對於C語言,它是不支持函數重載的,若進行重載,在Android Studio下編譯器就會報錯,提示Duplicate declaration of function xxx

  • 那若是去掉extern "C",效果會怎樣?

    首先,編譯器會很友好地提示你:

    並建議你

  • 若是不接受建議,效果會怎樣?

    那就是crash

    Process: com.wsy.jnidemo, PID: 27921 
        java.lang.UnsatisfiedLinkError: No implementation found for java.lang.String com.wsy.jnidemo.MainActivity.stringFromJNI()
    (tried Java_com_wsy_jnidemo_MainActivity_stringFromJNI and
    Java_com_wsy_jnidemo_MainActivity_stringFromJNI__)
        at com.wsy.jnidemo.MainActivity.stringFromJNI(Native Method)
        at com.wsy.jnidemo.MainActivity.onCreate(MainActivity.java:22)
        at android.app.Activity.performCreate(Activity.java:7815)
        at android.app.Activity.performCreate(Activity.java:7804)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1318)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3349)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3513)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2109)
        at android.os.Handler.dispatchMessage(Handler.java:107)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7682)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:516)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:950)
    複製代碼

    這個提示是在運行後點擊按鈕出現的,也就是說,動態庫加載成功了,那隻能說明函數名被改了。

  • 函數名被修改爲了什麼?

    • 工具路徑

      咱們能夠使用NDK中提供的nm進行分析,假如咱們的動態庫是armeabi-v7a的,那麼咱們選擇的nm工具最好是toolchains\arm-linux-androideabi-4.9\prebuilt\windows-x86_64\bin下的arm-linux-androideabi-nm.exe,而不要選擇其餘目錄下的,雖然可能會存在兼容,可是有些是不兼容的。

    • 工具使用方式

      nm -D 動態庫文件

    • 分析加extern 'C'和不加extern 'C'的兩個動態庫

      執行gradlew externalNativeBuildRelease獲取動態庫,進行解析

      • extern 'C'的動態庫
      • 不加extern 'C'的動態庫

對於靜態註冊的函數而言,函數名發生了變動,按照原有的規則找不到對應的函數,所以就會報java.lang.UnsatisfiedLinkError

2. JNIEXPORT

定義以下

#define JNIEXPORT __attribute__ ((visibility ("default")))
複製代碼

JNIEXPORT描述了其可見性爲default,所以在不進行native代碼混淆的狀況下,其實咱們去掉也沒有問題,可是若是咱們把visibility修改成hidden效果又如何?

咱們來試試,將函數聲明修改成:

extern "C" __attribute__ ((visibility ("hidden"))) jstring JNICALL Java_com_wsy_jnidemo_MainActivity_stringFromJNI
複製代碼

運行:

能夠看到,運行直接崩潰,由於函數名被隱藏了,因此找不到native函數。

3. JNICALL

這個宏的定義以下

#define JNICALL
複製代碼

沒錯是空的,這個可用於標記,編譯時標記函數用了這個宏,至於做用,至今未發現,有了解的朋友麻煩告知下。

4、native函數參數及返回值說明

函數聲明:

extern "C" JNIEXPORT jstring JNICALL
Java_com_wsy_jnidemo_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
複製代碼

extern "C"JNIEXPORTJNICALL都已在上述進行說明,接下來看下這個函數的函數名和返回值。

1. 函數名

JNI提供了一套規則來實現靜態註冊時的函數名查找,方式就是:

Java_包名_類名_函數名

順便一提,在咱們未進行動態註冊時,函數的註冊是在首次調用這個函數時進行的,所以在動態庫加載完成時,一個native函數的首次調用耗時會高於後續的調用耗時。咱們能夠驗證一下:

  • 調用方式
for (int i = 0; i < 100; i++) {
        long start = System.nanoTime();
        stringFromJNI();
        long end = System.nanoTime();
        Log.i(TAG, "onCreate: " + (end - start));
    }
複製代碼
  • 日誌

2. 參數

JNI函數會自動添加兩個參數:JNIEnv *jobject

第一個參數是JNIEnv *env,咱們平時的JNI操做也基本都依賴這個變量來實現;
第二個參數在Java函數是靜態函數時是jclass,是成員函數時是jobject,其中jclassjobject的子類。

3. 返回值

對於Java函數而言,它須要的返回值是一個java.lang.String對象,與之對應的是native的jstring對象。

以上就是對一個JNI工程的介紹。

相關文章
相關標籤/搜索