android之一篇史上最適合最全面的JNI入門教程

前言:

   一定要下載demo,動手動腦,結合本篇博客來跑demo,否則看了也還是不會;寫代碼還是要勤動手才能掌握,否則裏邊的坑也只是想當然

NDK的基礎知識,強烈推薦小楠總的NDK系列博客,先拜讀一遍,照着學習還是很厲害的


一.基礎知識

      JNI:是java和c/c++交互的橋樑;有必要去弄明白整個開發流程;jni的效率比java要快,所以一些好性能的都會通過走底層來調用java
    用途:用的比較多的是視頻、美顏、相機、地圖等涉及底層以及效率問題
    NDK:是一針對jni中的工具包,包括底層c庫、編譯工具等
              1. 編譯
  xxx.c ——> windows .obj ; Linux .o –》 語法檢查
 2. 鏈接:將函數之間的關係鏈接起來,生成一個靜態或動態庫文件(可執行文件)
                  .o —–> log.so .dll .exe
  靜態庫:靜態鏈接是指把要調用的函數或者過程鏈接到可執行文件中,成爲可執行文件的一部分,已經整合進去了
  動態庫:裝入過程中將所有動態鏈接庫載入內存。應用程序在運行時,將所有可能要運行到的模塊都全部裝入內存(共享內存),
  動態鏈接過程只是把需要調用的函數的路徑做個標誌(類似於頭文件聲明),直到運行用到函數時纔會從內存載入
  
  jvm 是虛擬機內存,C/C++ 是 native內存,並且這個 so庫 是放在 apk 的 lib 下面的
  當我們調用 java中native聲明的javaDiff()方法 的時候會到 Java虛擬機 的內存當中來處理找這個方法,
  而加了 native 關鍵字的時候他就會去到 C++/c 的堆棧空間找這個 C++/c 的實現。

二.JNI開發注意事項

 jni開發中如果不是作爲外部庫或.c文件的函數不對外,則可以不生成.h頭文件,頭文件的聲明只是想把當前.c/.cpp文件下的功能提供給外部文件使用,如:demo只有一個.c文件,函數定義  都是可以直接調用的,不需要聲明;但是要是作爲第三方庫且庫中的函數對外開放,則必須要有對應.c的頭文件.h,否則無法調用相應的函數;
即:不管動態還是靜態註冊,只要不是對外(.c文件之間以及庫和庫之間)提供的函數功能,則不需要生成頭文件;
demo將分開來說,只要按照以下說的步驟,就能清楚知道靜態註冊、動態註冊以及so庫對外的函數會在當前JNI函數中被調用的情況。
期間會涉及到修改cmakeList.txt腳本,所以每次修改後,都要同步gradle和重新build--->makeProject(先刪除build)

這裏也會分兩種方式說明:
一、不驗證so庫,直接gradle,build之後,運行:主要是驗證jni的方法是否正確,算是調試吧
二、驗證so庫
      驗證so庫的整個過程:
 想要只測試so庫,需要將驗證的so庫放到jniLibs指定的目錄,然後將build.gradle中相應的所有的cmake都註釋掉如下
 externalNativeBuild {
          cmake {
          }
     }
      然後刪除build以及.externalNative目錄,再者gradle同步,其次build-->makeproject,最後運行,能正確調用相應的jni  的函數

 在c、c++中叫函數 java中叫方法,實際上兩個是一樣的玩意,這裏爲了區分jni和native所以有兩個叫法

三.JNI開發分類

     1、靜態註冊

          1) .靜態註冊的流程
按照鏈接給的步驟,就可以創建一個JNIdemo,靜態註冊,不需要導入so庫,直接build->make project之後,點擊as的運行,安裝app 就可以正常使用native了
2).驗證so庫
  第一步build之後,在\app\build\intermediates\cmake目錄下就拿到.so庫
  1、將module下的build.gradle中的externalNativeBuild都註釋掉(防止執行cmakeLists.txt腳本),這樣就不會執行腳本生成so庫
  2、將\app\.externalNativeBuild和app\build目錄刪除,排除一切干擾
  3、將so庫放到app\src\main\jniLibs目錄下,如果沒有jniLibs目錄就自己創建一個
  4、gradle同步一下,運行成功就驗證so庫是可以的

      2、動態註冊

             大概就是當java中通System.loadLibrary(name);時會調用JNI_OnLoad()函數,所以在這函數中將java中native方法         聲明和jni函數實現進行註冊,也就是綁定(聲明-實現)
            生成動態so庫的鏈接:動態註冊
            代碼如下:
            
JNIEXPORT jstring JNICALL
string_from_JNI(
        JNIEnv *env,
        jobject instance/* this */) {
    /* 默認是以c++的方式
     * std::string hello = "Hello from C++";
      return env->NewStringUTF(hello.c_str());
     */
    const char *hello = "Hello from C";
    jstring content = (*env)->NewStringUTF(env, hello);
    (*env)->ReleaseStringChars(env, content, hello);
    return content;
}

/****
 *通過jni的方法訪問java中方法
 */
JNIEXPORT void JNICALL
access_method(JNIEnv *env, jobject instance,jstring methodName) {
    jclass mainClass = (*env)->GetObjectClass(env, instance);
    const char* method_n = (*env)->GetStringUTFChars(env,methodName,NULL);
    jmethodID rid = (*env)->GetMethodID(env, mainClass, method_n,
                                        "(I)I");//最後一個參數是方法簽名:即前一個I表示java方法的參數,最後一個I表示返回值
    jint rNum = (*env)->CallIntMethod(env, instance, rid, 20);
    printf("output from C : %d", rNum);
}

//}

/***
參數1:name是Java中方法名。
參數2:signature簽名,用字符串是描述了Java中函數的參數和返回值
參數3:fnPtr是函數指針,指向native函數。前面都要接 (void *) C/C++中對應函數的函數名(地址):即指針函數變量名必須和c/c++實現的函數名一樣

*/
const JNINativeMethod gMethods[] = {
    {"stringFromJNI1","()Ljava/lang/String;",(void*)string_from_JNI},
    {"accessMethod1","(Ljava/lang/String;)V",(void*)access_method}
};

int registerNatives(JNIEnv* engv) {
    LOGI("registerNatives begin");
    jclass  clazz;
    //這個是具體的類名,不能寫錯,寫錯就無法註冊成功,也就無法調用jni函數了
    clazz = (*engv) -> FindClass(engv, "com/jni/www/jnidemo/dif/JNIDynamicUtil");

    if (clazz == NULL) {
        LOGI("clazz is null");
        return JNI_FALSE;
    }

    if ((*engv) ->RegisterNatives(engv, clazz, gMethods, NELEM(gMethods)) < 0) {
        LOGI("RegisterNatives error");
        return JNI_FALSE;
    }

    return JNI_TRUE;
}

/***
 當java中通System.loadLibrary(name);時會調用此方法
*/
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved){

    LOGI("jni_OnLoad begin");

    JNIEnv* env = NULL;
    jint result = -1;

    if ((*vm)->GetEnv(vm,(void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        LOGI("ERROR: GetEnv failed\n");
        return -1;
    }
    assert(env != NULL);

    registerNatives(env);
    return JNI_VERSION_1_4;
}

      注意gMethod數組和registerNatives(env)函數,重點是在註釋;看懂流程纔是重點;
      這裏流程是System.loadLibrary()-->JNI_onLoad()-->registerNatives()--->RegisterNatives(...)
      一定要看註釋,這個流程中registerNatives(env)中,注意這個參數值,是java中的具體類的路徑名,只是將"."換成“/”
//這個是具體的類名,不能寫錯,寫錯就無法註冊成功,也就無法調用jni函數了
clazz = (*engv) -> FindClass(engv, "com/jni/www/jnidemo/dif/JNIDynamicUtil");
    生成並驗證so庫
    
   1.完成.c或.cpp文件編寫(參考鏈接)
   2.需要在cmakeLists.txt中增加和靜態庫一樣的三個方法,將靜態註冊中的.c/.cpp文件修改成當前編寫的動態註冊的.c/.cpp文件
   3.將\app\.externalNativeBuild和app\build目錄刪除,排除一切干擾
   4.gradle同步後,(要先刪除build目錄和extenalNativeBuild目錄)再build->make project;
   5.build之後,在\app\build\intermediates\cmake目錄下就拿到.so庫(如果還保留着靜態註冊的代碼,則會生成兩個so庫,參考文末demo)
   6.將so庫放到app\src\main\jniLibs目錄下,如果沒有jniLibs目錄就自己創建一個(注:確保so庫的路徑正確,如果存放在其它目錄下,需要配置gradle文件的jniLibs.dir具體可以百度)
   7.java的代碼中聲明native方法,通過static{System.loadLibrary(name);}加載so庫,就可以直接通過調動native聲明進而調用到jni函數的實現了
     注:雖然直接複製過來的so庫name會帶有前綴,直接將name中不要前綴lib 遵循linux生成so庫的標準,會加上前綴,但是在調用此方法時要將前綴lib去掉) 
   8.將module下的build.gradle中的externalNativeBuild都註釋掉(防止執行cmakeLists.txt腳本),這樣就不會執行腳本生成so庫
   9.點擊gralde同步
   10.如果步驟正確,直接運行應該就成功了 
動態註冊是需要JNIDynamicUtil類的具體路徑才能進行動態註冊的,所以如果要封裝成第三發so庫供給第三方使用,需要寫一個工具類統一管理native的註冊,然後將這個工具類做成jar包,將jar包和so供給第三方使用;(這只是個人看法,沒有具體實施,希望有知道怎麼做的,能不吝賜教)

注:不管是靜態註冊還是動態註冊:都必須遵循 java的類名包名方法名和jni的函數一一對應
如:
       靜態註冊:不管so庫是不是在不同的項目中使用,都必須保證java中使用聲明native的方法的類必須和JNI函數命名規則            保持一致:即java_包名_類名_方法名

       動態註冊:由於動態註冊也是要通過java類中的絕對路徑來找到類中的class,才能進行映射;如代碼中

   3.so庫鏈接

零碎筆記建議:一定要看,不然坑死你

若是要引用第三方so庫的話需要將第三方的頭文件和當前的so庫建立關係

as中寫下native方法後,ctrl+1或者alt+enter會在當前目錄下生成jni目錄,會自動生成.c文件

快速獲取頭文件的方式:http://blog.csdn.net/wang_zhi_hao/article/details/49126955
歸根結底都是通過命令 javah -d jni -jni -classpath class的路徑生成
如:

javah -d jni -jni -classpath  

C:\Users\Administrator\Desktop\JNIDemo\app\build\intermediates\classes\debug   com.jni.www.jnidemo.JNIUtil

會在當前目錄下創建jni目錄(沒有的話),並將自動生成.h頭文件在jni目錄下,
注意此時的頭文件是整個是:包名_類名.h 一定要改成和.c文件一樣的文件名;此外還要.h頭文件的函數聲明也要和.c對應上,尤其是動態註冊的函數一定一定要一一對應上,否則很容易報找不到方法

如果想要jni中調用第三方的so庫,那麼需要通過書寫cmakeLists.txt腳本關聯jni與第三方的so庫,編寫好之後build就可以是掉第三方so庫的方法了;

如:在cmakeList.txt中加入,其中jni是存放頭文件的目錄

set(distribution_DIR ${CMAKE_SOURCE_DIR}/jni)
#加入頭文件:第三方的so庫的頭文件加入編譯到native-lib.so中,native-lib.so中才能使用它裏邊的函數
#參數是頭文件所在的目錄
target_include_directories(native-lib PRIVATE ${distribution_DIR})

具體用法可以參考:
文檔地址:https://developer.android.com/ndk/guides/cmake.html

build.gradle腳本中配置externalNativeBuild{}中的信息可以查看: 

app\.externalNativeBuild\cmake\debug\arm64-v8a\cmake_build_command.txt,這裏有build之後的具體信息

比如:
查詢文檔可以知道 arguments 中 -DANDROID_PLATFORM 代表編譯的 android 平臺,
文檔建議直接設置 minSdkVersion 就行了,所以這個參數可忽略。
另一個參數 -DANDROID_TOOLCHAIN=clang,
CMake 一共有2種編譯工具鏈 - clang 和 gcc,gcc 已經廢棄,clang 是默認的。

    流程:
     
第三方的so庫和當前的so庫建立連接,還是要通過System.loadLibrary("native-dy-lib");加載各個so庫
1.獲取第三方的so庫提供給其它庫使用的函數的頭文件(也就是so庫對外的函數對應的頭文件)
2.將頭文件加到本地so庫以及和第三方so庫鏈接
3.gradle後,build(要先刪除build目錄和extenalNativeBuild目錄)-->make project獲取到本地so庫
4.將本地so庫和第三方的so庫導入項目。
5.需要使用的地方調用System.loadLibrary();加載so庫
一定要保證加載的第三方的so庫和放在jniLibs指定目錄中的第三方so庫是一個庫,否則很可能會報找不到對應的so的函數
   可能頻繁出現的異常
   
java.lang.UnsatisfiedLinkError: dlopen failed: could not load library "libnative-dy-lib.so" needed by "libnative-lib.so"; caused by library "libnative-dy-lib.so" not found
libnative-dy-lib這個庫也要放到jniLibs指定的目錄否則native-lib.so找不到
報錯:Fatal signal 11 (SIGSGV) at 0x00002820 (code=1),thread 23696 (xvdy.oa:vitamio) 這個問題都是調用的jni函數有問題,解決辦法就是一邊註釋代碼一邊運行看看是哪行代碼報錯,或者通過調試來定位,調試有機會在講講

cmakeLists.txt:
生成靜態庫和動態庫:區分於靜態註冊和動態註冊:庫是將函數打包成動態庫,註冊是函數註冊,兩者概念沒關係
  
 #靜態註冊的動態庫---so庫的cmake
add_library( # Sets the name of the library.也是.so庫的名,生成的so庫是在\app\build\intermediates\cmake
             native-lib


             # Sets the library as a shared library.SHARED:動態庫 STATIC:靜態庫
             SHARED


             # Provides a relative path to your source file(s).
             src/main/cpp/native-lib.c )
find_library( log-lib
              log )
target_link_libraries( native-lib
                       ${log-lib} )
#-----靜態註冊的動態庫結束-------


#----開始---動態註冊的庫---生成動態so庫的cmake
add_library( # Sets the name of the library.也是.so庫的名(可以自己修改,修改後一定要刪除build和externalNativeBuild目錄,重新build),生成的so庫是在\app\build\intermediates\cmake
           native-dy-lib1
           SHARED
            src/main/cpp/native-dynamic-lib.c )
find_library( log-lib
          log )
target_link_libraries( native-dy-lib1
                       ${log-lib} )
#------動態庫結束--------
#在 .externalNativeBuild/cmake/debug/{abi}/cmake_build_output.txt 中查看 log。
message(STATUS "execute CMakeLists")
set(CMAKE_VERBOSE_MAKEFILE on)

#當前cmakeList.txt的文件路徑
set(lib_src_DIR ${CMAKE_CURRENT_SOURCE_DIR})

set(lib_build_DIR ${lib_src_DIR}/tmp)
#創建目錄。父目錄不存在也會創建
file(MAKE_DIRECTORY ${lib_build_DIR})

#外層的 CMakeLists 裏面核心就是 add_subdirectory,查詢CMake 官方文檔可以知道這條命令的作用是爲構建添加一個子路徑。
#子路徑中的 CMakeLists.txt 也會被執行。即會去分別執行 gmath 、gperf、mtxxJni 中的 CMakeLists.txt
#參數指定了源碼路徑,參數2指定了當前cmakeList執行結果的輸出路徑
#add_subdirectory(${lib_src_DIR}/gmath ${lib_build_DIR}/gmath)
#add_subdirectory(${lib_src_DIR}/gperf ${lib_build_DIR}/gperf)
add_subdirectory(${lib_src_DIR}/mtxxJni)#不指定參數2,默認輸出路徑

#更改庫的輸出路徑爲${distribution_DIR}/gperf/lib/${ANDROID_ABI}
set_target_properties(gperf
                      PROPERTIES
                      LIBRARY_OUTPUT_DIRECTORY
                      "${distribution_DIR}/gperf/lib/${ANDROID_ABI}")
set(CMAKE_VERBOSE_MAKEFILE on)
#創建目錄
file(MAKE_DIRECTORY ${distribution_DIR}/gperf/include)
總結:
     
驗證so庫的整個過程:
想要只測試so庫,需要將驗證的so庫放到jniLibs指定的目錄,然後將build.gradle中相應的所有的cmake都註釋掉如下
externalNativeBuild {
         cmake {
         }
    }
然後刪除build以及.externalNative目錄,再者gradle同步,其次build-->makeproject,最後運行,能正確調用相應的jni的函數

demo

以下以下強烈推薦一篇博客:cmake的手冊文檔、ndk的手冊文檔在這篇博客都有講解,會更新cmake的講解
http://mp.weixin.qq.com/s/QTxEQg4s5ummtFNe8vRIvA

 CMake 的官方文檔使用。

https://cmake.org/documentation/

同時在這推薦一箇中文翻譯的簡易的 CMake手冊

https://www.zybuluo.com/khan-lau/note/254724

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