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

本篇文章已受權微信公衆號 guolin_blog (郭霖)獨家發佈

本篇基於上篇構建好的a靜態庫和so動態庫,若本身有a或so那麼能夠直接看本篇了,若沒有那麼建議先去看上篇----如何將現有的cpp代碼集成到項目中java


準備工做

將so動態庫、a靜態庫、以及對應的頭文件,集中到一個文件夾中,本文由於是基於上篇的,那麼這些文件就放在了,以下圖:android

都放在了Project/export文件夾中,且在裏面將so和a分開,分別放在了,libajsoncpp和libsojsoncpp文件夾中,在每一個文件夾中,又有include文件夾來放庫所須要的頭文件,lib中放so以及a庫文件。c++

連接so動態庫

咱們首先來連接咱們較爲熟悉的so動態庫,而後再來連接a靜態庫。git

  1. 準備工做github

    1. 將../app/src/main/cpp文件夾中的jsoncpp文件夾刪除,以防咱們用的不是庫,而是…源碼了(針對按着第二篇 將 源碼拷貝到項目中的同窗)。json

    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文件app

    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(自行腦部表情包) 咱們來一條一條的看ide

    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/下是啥,以下:

      嗯啦,就是那一堆架構,因此…這個值就表明這些了(默認,所有類型)。

    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. 結果:

      嗯!集成成功,那麼下面咱們來集成下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. 結果:

    能夠看到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 還能夠訪問,即並無釋放掉資源。

問題:

  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

相關文章
相關標籤/搜索