本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出
本篇基於上篇構建好的a靜態庫和so動態庫,若自己有a或so那麼可以直接看本篇了,若沒有那麼建議先去看上篇----如何將現有的cpp程式碼整合到專案中
- 初次使用CMake構建native專案
- 如何將現有的cpp程式碼整合到專案中
- 拷貝原始碼
- 編譯成庫檔案
- CMake連結a靜態庫以及so動態庫及動態庫和靜態庫的區別
準備工作
將so動態庫、a靜態庫、以及對應的標頭檔案,集中到一個資料夾中,本文因為是基於上篇的,那麼這些檔案就放在了,如下圖:
都放在了Project/export資料夾中,且在裡面將so和a分開,分別放在了,libajsoncpp和libsojsoncpp資料夾中,在每個資料夾中,又有include資料夾來放庫所需要的標頭檔案,lib中放so以及a庫檔案。
連結so動態庫
我們首先來連結我們較為熟悉的so動態庫,然後再來連結a靜態庫。
-
準備工作
-
將../app/src/main/cpp資料夾中的jsoncpp資料夾刪除,以防我們用的不是庫,而是…原始碼了(針對按著第二篇 將 原始碼拷貝到專案中的同學)。
-
將 ../app/src/main/cpp資料夾下的CMakeLists.txt內所有內容刪除,以防和本文的CMakeLists.txt中的配置不同。
-
將 ../app/build.gradle 修改如下:
apply plugin: 'com.android.application' android { ... defaultConfig { ... externalNativeBuild { cmake { arguments '-DANDROID_STL=c++_static' } } } buildTypes { ... } sourceSets { main { // 根據實際情況具體設定,由於本專案的lib放在 project/export/libsojsoncpp/lib 中 故如此設定 jniLibs.srcDirs = ['../export/libsojsoncpp/lib'] } } externalNativeBuild { cmake { path 'src/main/cpp/CMakeLists.txt' } } } ... 複製程式碼
-
-
寫app/src/main/cpp/CMakeLists.txt檔案
cmake_minimum_required(VERSION 3.4.1) # 設定變數 找到存放資源的目錄,".."代表上一級目錄 set(export_dir ${CMAKE_SOURCE_DIR}/../../../../export) # 新增.so動態庫(jsoncpp) add_library(lib_so_jsoncpp SHARED IMPORTED) # 連結 set_target_properties( # 庫名字 lib_so_jsoncpp # 庫所在的目錄 PROPERTIES IMPORTED_LOCATION ${export_dir}/libsojsoncpp/lib/${ANDROID_ABI}/libjsoncpp.so) add_library( native_hello SHARED native_hello.cpp ) # 連結標頭檔案 target_include_directories( native_hello PRIVATE # native_hello 需要的標頭檔案 ${export_dir}/libsojsoncpp/include ) # 連結專案中 target_link_libraries( native_hello android log # 連結 jsoncpp.so lib_so_jsoncpp ) 複製程式碼
嗯,這次看起來配置較多了,但是…,別慌 別慌 問題不大.jpg(自行腦部表情包) 我們來一條一條的看
-
cmake_minimum_required(VERSION 3.4.1) 這個就不用解釋了吧,就是設定下CMake的最小版本而已。
-
set(....) 因為考慮到用到export 資料夾的次數較多,而且都是絕對路徑,所以就來設定一個變數來簡化啦。export_dir 就是變數的名字,${CMAKE_SOURCE_DIR} 是獲取當前CMakeLists.txt 所在的路徑,然後 一路 "../"去找到 我們存放資原始檔的 export 資料夾。
-
add_library(lib_so_jsoncpp SHARED IMPORTED) 這個見名之意啦,就是新增庫檔案,後面的三個引數 "lib_so_jsoncpp" 庫名字;"SHARED" 因為我們要匯入 so 動態庫,所以就是 SHARED 了; "IMPORTED" 然後匯入;
-
set_target_properties 接下來就是這句了,後面的引數較多,較長,就不拷貝到這裡了。我們在 上句 已經新增庫了,但是…庫是空的呀(注意後面是 imported),什麼都沒有,只是一個名字 + 型別,所以接下來就得需要它來將名字和真實的庫連結起來,我已經在上面的CMakeLists.txt中寫上註釋了,這裡只說下在前面沒有提到過的"${ANDROID_ABI}",這是啥?上面的語句將此拼接到了裡面,但是我真實的路徑中沒有這個資料夾呀,去看下../libsojsoncpp/lib/下是啥,如下:
嗯啦,就是那一堆架構,所以…這個值就代表這些了(預設,全部型別)。
-
然後接下來就又是一個add_library 但是這個是帶資源的了。沒啥好說的了.
-
target_include_directories 我們有庫了,但是沒有對應的標頭檔案咋行,所以這句就是連結庫對應的標頭檔案了。
-
target_link_libraries 最後將所需的標頭檔案,連結到專案中就可以啦!
最後,Build/Make Module 'app'.
-
-
呼叫程式碼
-
cpp層的程式碼其實是不用改,直接用我們上次 拷貝 原始碼的方式就行,但是為了方便直接看本篇的同學,還是貼下 native_hello.cpp 內的程式碼如下:
// // Created by xong on 2018/9/28. // #include<jni.h> #include <string> #include "json/json.h" #define XONGFUNC(name)Java_com_xong_andcmake_jni_##name extern "C" JNIEXPORT jstring JNICALL XONGFUNC(NativeFun_outputJsonCode)(JNIEnv *env, jclass thiz, jstring jname, jstring jage, jstring jsex, jstring jtype) { Json::Value root; const char *name = env->GetStringUTFChars(jname, NULL); const char *age = env->GetStringUTFChars(jage, NULL); const char *sex = env->GetStringUTFChars(jsex, NULL); const char *type = env->GetStringUTFChars(jtype, NULL); root["name"] = name; root["age"] = age; root["sex"] = sex; root["type"] = type; env->ReleaseStringUTFChars(jname, name); env->ReleaseStringUTFChars(jage, age); env->ReleaseStringUTFChars(jsex, sex); env->ReleaseStringUTFChars(jtype, type); return env->NewStringUTF(root.toStyledString().c_str()); } extern "C" JNIEXPORT jstring JNICALL XONGFUNC(NativeFun_parseJsonCode)(JNIEnv *env, jclass thiz, jstring jjson) { const char *json_str = env->GetStringUTFChars(jjson, NULL); std::string out_str; Json::CharReaderBuilder b; Json::CharReader *reader(b.newCharReader()); Json::Value root; JSONCPP_STRING errs; bool ok = reader->parse(json_str, json_str + std::strlen(json_str), &root, &errs); if (ok && errs.size() == 0) { std::string name = root["name"].asString(); std::string age = root["age"].asString(); std::string sex = root["sex"].asString(); std::string type = root["type"].asString(); out_str = "name: " + name + "\nage: " + age + "\nsex:" + sex + "\ntype: " + type + "\n"; } env->ReleaseStringUTFChars(jjson, json_str); return env->NewStringUTF(out_str.c_str()); } 複製程式碼
-
對應的Java層程式碼如下:
package com.xong.andcmake.jni; /** * Create by xong on 2018/9/28 */ public class NativeFun { static { System.loadLibrary("native_hello"); } public static native String outputJsonCode(String name, String age, String sex, String type); public static native String parseJsonCode(String json_str); } 複製程式碼
-
呼叫程式碼:
package com.xong.andcmake; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.widget.TextView; import com.xong.andcmake.jni.NativeFun; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView tv_native_content = findViewById(R.id.tv_native_content); String outPutJson = NativeFun.outputJsonCode("xong", "21", "man", "so"); String parseJson = NativeFun.parseJsonCode(outPutJson); tv_native_content.setText("生成的Json:\n" + outPutJson + "\n解析:" + parseJson); } } 複製程式碼
-
結果:
嗯!整合成功,那麼下面我們來整合下a靜態庫。
-
連結a靜態庫
我們還是基於上面連結so動態庫的修改。
-
首先修改 ../app/build.gradle 檔案如下:
apply plugin: 'com.android.application' android { ... defaultConfig { ... externalNativeBuild { cmake { arguments '-DANDROID_STL=c++_static' } } } ... // 刪除 或註釋 // sourceSets { // main { // jniLibs.srcDirs = ['../export/libsojsoncpp/lib'] // } // } externalNativeBuild { cmake { path 'src/main/cpp/CMakeLists.txt' } } } 複製程式碼
只是將 整合 so時新增的 sourceSets 標籤刪除(或註釋啦!)。
-
其次修改 ../app/main/src/cpp/CMakeLists.txt 如下:
cmake_minimum_required(VERSION 3.4.1) # 設定變數 找到存放資源的目錄,".."代表上一級目錄 set(export_dir ${CMAKE_SOURCE_DIR}/../../../../export) # 新增.so動態庫(jsoncpp) # add_library(lib_so_jsoncpp SHARED IMPORTED) add_library(lib_a_jsoncpp STATIC IMPORTED) # 連結 #set_target_properties( # lib_so_jsoncpp # PROPERTIES IMPORTED_LOCATION ${export_dir}/libsojsoncpp/lib/${ANDROID_ABI}/libjsoncpp.so) set_target_properties( lib_a_jsoncpp PROPERTIES IMPORTED_LOCATION ${export_dir}/libajsoncpp/lib/${ANDROID_ABI}/libjsoncpp.a) add_library( native_hello SHARED native_hello.cpp ) # 連結標頭檔案 #target_include_directories( # native_hello # PRIVATE # # native_hello 需要的標頭檔案 # ${export_dir}/libsojsoncpp/include #) target_include_directories( native_hello PRIVATE # native_hello 需要的標頭檔案 ${export_dir}/libajsoncpp/include ) # 連結專案中 target_link_libraries( native_hello android log # 連結 jsoncpp.so # lib_so_jsoncpp lib_a_jsoncpp ) 複製程式碼
在上個整合so的配置上修改,如上,修改的地方都一 一 對應好了,基本上和整合so沒區別。
-
呼叫
Java層不需修改,呼叫時傳的引數如下:
package com.xong.andcmake; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.widget.TextView; import com.xong.andcmake.jni.NativeFun; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView tv_native_content = findViewById(R.id.tv_native_content); String outPutJson = NativeFun.outputJsonCode("xong", "21", "man", "a"); String parseJson = NativeFun.parseJsonCode(outPutJson); tv_native_content.setText("生成的Json:\n" + outPutJson + "\n解析:" + parseJson); } } 複製程式碼
-
結果:
可以看到type變成了 "a",這樣的話,靜態庫也就算整合成功了。
動態庫和靜態庫的區別
有的人會說了,你這都用的json,而且返回 type 是你傳進去的呀,你就是整合 so 傳 a 那麼就算整合a了?
另一個我們怎麼會知道打到apk中的是so動態庫,還是a靜態庫呢?不是都說了麼,Android中只支援呼叫so動態庫,不支援a靜態庫的,那麼這…整合a靜態庫這不是扯淡麼?
OK,接下來就來解釋這一系列的問題,首先我們要知道什麼是靜態庫什麼又是動態庫。
參考Linux下的庫
抽取出主要的:
-
靜態庫
連結時間: 靜態庫的程式碼是在編譯過程中被載入程式中;
連結方式: 目的碼用到庫內什麼函式,就去將該函式相關的資料整合進目的碼;
優點: 是在編譯後的執行程式不在需要外部的函式庫支援;
缺點: 如果所使用的靜態庫發生更新改變,程式必須重新編譯。
-
動態庫
連結時間: 動態庫在編譯的時候並沒有被編譯進目的碼,而是在執行時用到該庫中函式時才去呼叫;
連結方式: 動態連結,用到該庫內函式時就去載入該庫;
優點: 動態庫的改變並不影響程式,即不需要重新編譯;
缺點: 因為函式庫並沒有整合程式序,所以程式的執行環境必須依賴該庫檔案。
再精簡一點:
靜態庫是一堆cpp檔案,每次都需要編譯才能執行,在自己的程式碼中用到哪個,就從這一堆cpp中取出自己需要的進行編譯;
動態庫是將一堆cpp檔案都編譯好了,執行的時候不會對cpp進行重新編譯,當需要到庫中的某個函式時,就會將庫載入進來,不需要時就不用載入,前提,這個庫必須存在。
所以,就可以回答上述的疑問了,對,沒錯Android的確是只能呼叫so動態庫,我們的整合的a靜態庫,用到靜態庫中的函式時,就會去靜態庫中拿對應的後設資料,然後將這些資料再打入到打入到我們的最終要呼叫的so動態庫中,在上述中就是native_hello.so了。
然後我們在整合so動態庫時在../app/build.gradle 中加了一個標籤哈,如下:
sourceSets {
main {
jniLibs.srcDirs = ['../export/libsojsoncpp/lib']
}
}
複製程式碼
經過上面的解釋,再來理解這句就不難了,上面已經說過了:
當需要到庫中的某個函式時,就會將庫載入進來,不需要時就不用載入,前提,這個庫必須存在。
所以啦,native_hello.so依賴於jsoncpp.so,即jsoncpp.so必須存在,那麼加這個的意思就是,將jsoncpp.so打入apk中。我們可以將上面的整合so動態庫的apk用 jadx 檢視一下,如下:
上面的結論沒錯咯,裡面確實有兩個so,一個jsoncpp.so另一個就是我們自己的native_hello.so;
那麼我們再來看一下整合a靜態庫的吧!如下:
這就是區別啦!
結論
so方式,只要你程式碼中涉及了,那麼它就要存在,即使你只是呼叫,後續不使用它,它也存在。
a方式,只需要在編碼過程中,保持它存在就好,用幾個函式,就從a中取幾個函式。
question
jstring name = "xong";
const char* ccp_name = env->GetStringUTFChars(name, NULL);
env->ReleaseStringUTFChars(name, ccp_name);
複製程式碼
現象:
寫JNI時這兩個不陌生吧,很多人都會說,用了GetStringUTFChars 必須 呼叫 ReleaseStringUTFChars 講資源釋放掉,但 呼叫完ReleaseStringUTFChars會發現 ccp_name 還可以訪問,即並沒有釋放掉資源。
問題:
- 只呼叫GetStringUTFChars不呼叫ReleaseStringUTFChars會不會造成記憶體洩漏,從而導致崩潰;
- 呼叫完ReleaseStringUTFChars後是否應該繼續訪問ccp_name;
- 應該在什麼場合使用ReleaseStringUTFChars;
歡迎在下方的評論進行探討。
other
本來想將這一篇分成三篇來寫的,想了又想,Android開發嘛,沒必要對native很那麼瞭解,所以就壓縮壓縮,壓縮成一篇了。
在這三篇中,我們發現,寫CMakeLists.txt也不是那麼很麻煩,而且很多都是重複的,都是可以用指令碼來生成的,比如 add_library 中新增的資原始檔,當然其他的也一樣啦,那麼這個 CMakeLists.txt 是不是可以寫一個小指令碼呢?我感覺可以。
另一個,如何用Camke構建a靜態庫、so動態庫,以及如何整合,在Google的sample中都有的,再貼一下連結:android_ndk , 而且新增的時間也挺長的了,但是,百度到的文章還是 5年前的,真的是…不知道說啥了,還是多看些Google Github比較好。哈哈哈哈~
最後,上述三篇文章中涉及的原始碼均已上傳到GitHub,連結:UseCmakeBuildLib
END