前言:
NDK的基礎知識,強烈推薦小楠總的NDK系列博客,先拜讀一遍,照着學習還是很厲害的
一.基礎知識
xxx.c ——> windows .obj ; Linux .o –》 語法檢查
2. 鏈接:將函數之間的關係鏈接起來,生成一個靜態或動態庫文件(可執行文件)
.o —–> log.so .dll .exe
靜態庫:靜態鏈接是指把要調用的函數或者過程鏈接到可執行文件中,成爲可執行文件的一部分,已經整合進去了
動態庫:裝入過程中將所有動態鏈接庫載入內存。應用程序在運行時,將所有可能要運行到的模塊都全部裝入內存(共享內存),
動態鏈接過程只是把需要調用的函數的路徑做個標誌(類似於頭文件聲明),直到運行用到函數時纔會從內存載入
當我們調用 java中native聲明的javaDiff()方法 的時候會到 Java虛擬機 的內存當中來處理找這個方法,
而加了 native 關鍵字的時候他就會去到 C++/c 的堆棧空間找這個 C++/c 的實現。
二.JNI開發注意事項
想要只測試so庫,需要將驗證的so庫放到jniLibs指定的目錄,然後將build.gradle中相應的所有的cmake都註釋掉如下
externalNativeBuild {
cmake {
}
在c、c++中叫函數 java中叫方法,實際上兩個是一樣的玩意,這裏爲了區分jni和native所以有兩個叫法
三.JNI開發分類
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、動態註冊
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;
}
//這個是具體的類名,不能寫錯,寫錯就無法註冊成功,也就無法調用jni函數了 clazz = (*engv) -> FindClass(engv, "com/jni/www/jnidemo/dif/JNIDynamicUtil");
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.如果步驟正確,直接運行應該就成功了
如:
動態註冊:由於動態註冊也是要通過java類中的絕對路徑來找到類中的class,才能進行映射;如代碼中
3.so庫鏈接
零碎筆記建議:一定要看,不然坑死你
若是要引用第三方so庫的話需要將第三方的頭文件和當前的so庫建立關係
as中寫下native方法後,ctrl+1或者alt+enter會在當前目錄下生成jni目錄,會自動生成.c文件
歸根結底都是通過命令 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函數有問題,解決辦法就是一邊註釋代碼一邊運行看看是哪行代碼報錯,或者通過調試來定位,調試有機會在講講
#靜態註冊的動態庫---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
http://mp.weixin.qq.com/s/QTxEQg4s5ummtFNe8vRIvA
CMake 的官方文檔使用。
https://cmake.org/documentation/
同時在這推薦一箇中文翻譯的簡易的 CMake手冊