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

4,794 阅读9分钟

本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

本篇基于上篇构建好的a静态库和so动态库,若自己有a或so那么可以直接看本篇了,若没有那么建议先去看上篇----如何将现有的cpp代码集成到项目中


准备工作

将so动态库、a静态库、以及对应的头文件,集中到一个文件夹中,本文因为是基于上篇的,那么这些文件就放在了,如下图:

都放在了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/下是啥,如下:

      嗯啦,就是那一堆架构,所以…这个值就代表这些了(默认,全部类型)。

    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