關於在Android中使用CMake你所需要了解的一切(三)

xong發表於2018-09-30

本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出

本篇基於上篇構建好的a靜態庫和so動態庫,若自己有a或so那麼可以直接看本篇了,若沒有那麼建議先去看上篇----如何將現有的cpp程式碼整合到專案中


準備工作

將so動態庫、a靜態庫、以及對應的標頭檔案,集中到一個資料夾中,本文因為是基於上篇的,那麼這些檔案就放在了,如下圖:

關於在Android中使用CMake你所需要了解的一切(三)

都放在了Project/export資料夾中,且在裡面將so和a分開,分別放在了,libajsoncpp和libsojsoncpp資料夾中,在每個資料夾中,又有include資料夾來放庫所需要的標頭檔案,lib中放so以及a庫檔案。

連結so動態庫

我們首先來連結我們較為熟悉的so動態庫,然後再來連結a靜態庫。

  1. 準備工作

    1. 將../app/src/main/cpp資料夾中的jsoncpp資料夾刪除,以防我們用的不是庫,而是…原始碼了(針對按著第二篇 將 原始碼拷貝到專案中的同學)。

    2. 將 ../app/src/main/cpp資料夾下的CMakeLists.txt內所有內容刪除,以防和本文的CMakeLists.txt中的配置不同。

    3. 將 ../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'
              }
          }
      }
      ...
      複製程式碼
  2. 寫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(自行腦部表情包) 我們來一條一條的看

    1. cmake_minimum_required(VERSION 3.4.1) 這個就不用解釋了吧,就是設定下CMake的最小版本而已。

    2. set(....) 因為考慮到用到export 資料夾的次數較多,而且都是絕對路徑,所以就來設定一個變數來簡化啦。export_dir 就是變數的名字,${CMAKE_SOURCE_DIR} 是獲取當前CMakeLists.txt 所在的路徑,然後 一路 "../"去找到 我們存放資原始檔的 export 資料夾。

    3. add_library(lib_so_jsoncpp SHARED IMPORTED) 這個見名之意啦,就是新增庫檔案,後面的三個引數 "lib_so_jsoncpp" 庫名字;"SHARED" 因為我們要匯入 so 動態庫,所以就是 SHARED 了; "IMPORTED" 然後匯入;

    4. set_target_properties 接下來就是這句了,後面的引數較多,較長,就不拷貝到這裡了。我們在 上句 已經新增庫了,但是…庫是空的呀(注意後面是 imported),什麼都沒有,只是一個名字 + 型別,所以接下來就得需要它來將名字和真實的庫連結起來,我已經在上面的CMakeLists.txt中寫上註釋了,這裡只說下在前面沒有提到過的"${ANDROID_ABI}",這是啥?上面的語句將此拼接到了裡面,但是我真實的路徑中沒有這個資料夾呀,去看下../libsojsoncpp/lib/下是啥,如下:

      關於在Android中使用CMake你所需要了解的一切(三)

      嗯啦,就是那一堆架構,所以…這個值就代表這些了(預設,全部型別)。

    5. 然後接下來就又是一個add_library 但是這個是帶資源的了。沒啥好說的了.

    6. target_include_directories 我們有庫了,但是沒有對應的標頭檔案咋行,所以這句就是連結庫對應的標頭檔案了。

    7. target_link_libraries 最後將所需的標頭檔案,連結到專案中就可以啦!

    最後,Build/Make Module 'app'.

  3. 呼叫程式碼

    1. 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());
      }
      複製程式碼
    2. 對應的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);
      }
      
      複製程式碼
    3. 呼叫程式碼:

      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);
          }
      }
      
      複製程式碼
    4. 結果:

      關於在Android中使用CMake你所需要了解的一切(三)

      嗯!整合成功,那麼下面我們來整合下a靜態庫。

連結a靜態庫

我們還是基於上面連結so動態庫的修改。

  1. 首先修改 ../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 標籤刪除(或註釋啦!)。

  2. 其次修改 ../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沒區別。

  3. 呼叫

    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);
        }
    }
    
    複製程式碼
  4. 結果:

    關於在Android中使用CMake你所需要了解的一切(三)

    可以看到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 檢視一下,如下:

關於在Android中使用CMake你所需要了解的一切(三)

上面的結論沒錯咯,裡面確實有兩個so,一個jsoncpp.so另一個就是我們自己的native_hello.so;

那麼我們再來看一下整合a靜態庫的吧!如下:

關於在Android中使用CMake你所需要了解的一切(三)

這就是區別啦!

結論

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 還可以訪問,即並沒有釋放掉資源。

問題:

  1. 只呼叫GetStringUTFChars不呼叫ReleaseStringUTFChars會不會造成記憶體洩漏,從而導致崩潰;
  2. 呼叫完ReleaseStringUTFChars後是否應該繼續訪問ccp_name;
  3. 應該在什麼場合使用ReleaseStringUTFChars;

歡迎在下方的評論進行探討。


other

​ 本來想將這一篇分成三篇來寫的,想了又想,Android開發嘛,沒必要對native很那麼瞭解,所以就壓縮壓縮,壓縮成一篇了。

​ 在這三篇中,我們發現,寫CMakeLists.txt也不是那麼很麻煩,而且很多都是重複的,都是可以用指令碼來生成的,比如 add_library 中新增的資原始檔,當然其他的也一樣啦,那麼這個 CMakeLists.txt 是不是可以寫一個小指令碼呢?我感覺可以。

​ 另一個,如何用Camke構建a靜態庫、so動態庫,以及如何整合,在Google的sample中都有的,再貼一下連結:android_ndk , 而且新增的時間也挺長的了,但是,百度到的文章還是 5年前的,真的是…不知道說啥了,還是多看些Google Github比較好。哈哈哈哈~

​ 最後,上述三篇文章中涉及的原始碼均已上傳到GitHub,連結:UseCmakeBuildLib


END

相關文章