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

640?wx_fmt=png


今日科技快讯


近日,谷歌在纽约如期举行了主题为“谷歌制造”(Made By Google)的硬件发布会,推出了Pixel手机、二合一平板,以及音箱等一系列新品硬件。一个月以来,包含谷歌、微软、苹果、亚马逊在内的美国四大科技巨头都相继发布了旗下最新硬件。而相比其他三家,谷歌本次的硬件品类最为全面,也彰显出更多的硬件野心。


作者简介


大家周一好,新的一周,勇往直前!

本篇转自 xong的文章,分享了关于关于在Android中使用CMake你所需要了解的一切的相关内容,一起来看看!希望大家喜欢。

xong的博客地址:

https://juejin.im/user/58c382750ce4630054690774


前言


相信大家在开发的过程中,都或多或少的接触过JNI,然后每次要接触JNI的时候,倒吸一口冷气,太难啦!

只有Java代码和C++代码 还好,在新建项目的时候把那个 "Include C++ support"勾选上,然后一路next,最后finish,一个简单的带有C++代码的Android项目就算完成了,然后在看下CMakeLists.txt怎么写的,照猫画虎就可以了,但是实际开发中,并不简单,因为底层(C++)那里给过来的有可能是.a静态库,也有可能是.so动态库,然后如何集成进项目里就是:一脸懵逼,二脸茫然,三脸不知所措。由于客观事实只能去百度,然后百度到的全是用Android.mk实现的居多,然而现在都Android Studio 3.1+时代了,还mk?关于CMake的资料又很少,所以就有了本文。

在去年在公司还是实习的时候,老大丢给我一个.a静态库(当时并不知道这是一个静态库),很是好奇,很想知道.a是怎么构建出来的,a和so的区别又是什么,总之一大堆疑问,在转正后,也尝试过要构建一个.a静态库,无奈!百度到的都是用mk的咯,找到一篇CMake的,但竟然要去Linux下面编译,还要什么自定义工具链,总之,麻烦的一批。

所以就有了这一系列,本系列不阐述什么是CMake,什么是NDK,什么是JNI,只是写一下在Android中使用CMake常遇的问题,解决方法是什么。

本系列涉及到的有:

  • 初次使用CMake构建native项目

  • 如何将现有的cpp代码集成到项目中

    • 拷贝源码

    • 编译成库文件

  • CMake链接a静态库以及so动态库及动态库和静态库的区别


开始


初次使用CMake构建native项目

这个就很简单啦,新建项目的时候把 include C++ support 勾选上就可以了,但,我们还是自己动手来一遍,不依靠IDE,看看可不可以,新建普通项目,名字为:AndCmake,新建完成后如下:

640?wx_fmt=other

新项目,什么都没有,我们都知道用CMake的话,必须要有CMakeLists.txt(注意名字不能写错),第二个就是需要的cpp资源啦,这就很简单了,在src/main目录下新建就好了,为了整洁在main目录下新建cpp文件夹,且在里面新建CMakeLists.txt和native_hello.cpp文件,然后在去CMakeLists.txt简单的配置下就好了,完成后,如下:

../src/main/cpp/native_hello.cpp

//
// Created by xong on 2018/9/28.
//
#include<jni.h>
#include <string>

extern "C" JNIEXPORT
jstring JNICALL
Java_com_xong_andcmake_jni_NativeFun_stringFromJNI(JNIEnv *env, jclass thiz)
{
    std::string hello = "Hello,I from C++";
    return env->NewStringUTF(hello.c_str());
}

这里需要说明以下,Java_com_xong_andcmake_jni_是去java的一层一层的包下面去寻找,比如这个就是去com.xong.andcmake.jni下面找,后面的NativeFun_stringFromJNI就是类和方法名了。函数的形参两个必须是这样的,第二个形参可以写成"jobject",我最近写JNI的时候,提示改成"jclass"但是,"jobject"也不能算错,也是对的。

./src/main/cpp/CMakeLists.txt

# CMake最低版本
cmake_minimum_required(VERSION 3.4.1)
# 将需要打包的资源添加进来
add_library(
        # 库名字
        native_hello
        # 库类型
        SHARED
        # 包含的cpp
        native_hello.cpp
)
# 链接到项目中
target_link_libraries(
        native_hello
        android
        log
)

这就把C++部分写好了。修改../app/build.gradle,修改后如下:

android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                arguments '-DANDROID_STL=c++_static'
            }
        }
        ...
    }
    ...
    externalNativeBuild {
        cmake {
            path 'src/main/cpp/CMakeLists.txt'
        }
    }
    ...
}

写对应的Java层代码,在com.xong.andcmake包下新建jni,然后新建NativeFun类,代码如下:

package com.xong.andcmake.jni;

/**
 * Create by xong on 2018/9/28
 */

public class NativeFun {

    static {
        System.loadLibrary("native_hello");
    }

    public static native String stringFromJNI();
}

调用NativeFun.stringFromJNI(),查看是否有正确的信息输出:这就很简单啦,在Activity中添加一个TextView然后显示下就好了,正确的话,应该会有如下显示:

640?wx_fmt=other

OK,这样我们的第一个带有CPP代码的APP就算完成了,然后我们来思考一下,先来看native_hello.cpp的代码:

 
  

//
// Created by xong on 2018/9/28.
//
#include<jni.h>
#include <string>

extern "C" JNIEXPORT
jstring JNICALL
// 这里可不可以优化一下?
Java_com_xong_andcmake_jni_NativeFun_stringFromJNI(JNIEnv *env, jclass thiz)
{
    std::string hello = "Hello,I from C++";
    // 这里呢?
    return env->NewStringUTF(hello.c_str());
}

对于我们来讲:

  1. 尽量不写重复代码

  2. 代码要高效

对于上面的函数名来讲,Java_com_xong_andcmake_jni_,假如我们在Java中对应的native类和方法,感觉在这个包中不合适,要换下包名,但是我这个类中有很多native方法,只要一换包,那么对应的cpp中的函数名就要挨个改,函数少还好办,假如有1000个,改起来真的是不要太爽,而且容易丢,那么有没有办法来尽量的减少工作量呢?类和包是相对固定的,那么我们能不能把包名抽取出来用一个表达式来表示呢?没错!就是宏定义啦!

对于下面返回的那一句,我为啥要这样写呢,因为用Android Studio创建一个带有cpp项目的时候,里面就是这样写的,然后就很自然的抄来了,但是我们想一下,有这样写的必要吗?可以看到的是在最后返回的时候,string对象又经过了一层转换点进去后可以发现,是将 string对象又转成了char数组(当然在C++中是char*指针还是const的,这里不需要知道const是啥,只需要知道,c_str()其实是又将string对象转成了char[]数组),然后我们这样写是不是就…多此一举,让计算机多做了一些事情呢?所以改完后的代码如下:

//
// Created by xong on 2018/9/28.
//
#include<jni.h>
#define XONGFUNC(name)Java_com_xong_andcmake_jni_##name

extern "C" JNIEXPORT
jstring JNICALL
XONGFUNC(NativeFun_stringFromJNI)(JNIEnv *env, jclass thiz)
{
    return env->NewStringUTF("Hello,I from C++");
}

这样的话,假如NativeFun不在jni包下了,而是在jnia包下,那么我们只需要将上面的宏定义改下就好了;因为我们没有对string做操作,那么我们就不需要它啦,直接返回就好了,这样计算机就不那么累了。make project后,发生了什么?

接触过JNI的同学应该了解,项目里面添加了cpp代码,apk会变的很大,那么原因嘞,那么我就来捋一捋,cpp的代码再make project后都发生了什么,都很清楚会生成so,那么…在默认的情况下会生成几个?

打开 ../app/build/intermediates/cmake/debug/obj 目录如下:

640?wx_fmt=other

在默认的情况下会生成4个,v7a的是最大的,x86_64是最小的,默认情况下,这4个是都会打到apk里面的(因为不清楚到底是往那台手机上安装呀,只能将所有的资源都打到apk里面了),要想缩小apk,那么就把需要的so打入apk就好了。然后,问题来了,怎么才能生成规定的so库呢?上代码,如下:

apply plugin: 'com.android.application'

android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                arguments '-DANDROID_STL=c++_static'
            }
            ndk {
                abiFilters "armeabi-v7a""x86"
            }
        }
        ...
    }
    ...
}
...

在../app/build.gradle中添加ndk标签,且在里面写上需要生成的架构名称就可以了,重新build下项目,如下图:

640?wx_fmt=other

这样打入apk中的就只有v7a和x86两种架构的so库啦! 下篇我们来讲解一下 现有的cpp代码集成到项目中有几种方式,以及如何操作。如何将现有的cpp代码集成到项目中?

这个在写JNI的时候就很常见了,比如json库,C++自己是没有提供json库的,然后我们在写JNI的时候,通常需要和上层交互数据,较简单的就是json了,那么就拿json来做讲解吧。首先来找一个json库啦!首先把代码clone下来。源码集成进项目中

将include里的json文件夹拷贝到../app/src/main/cpp目录下。将src/lib_json里面的文件除去 CMakeLists.txt拷贝到../app/src/main/cpp目录下。最终如下:

640?wx_fmt=other

修改../app/src/main/cpp/CMakeLists.txt如下:

cmake_minimum_required(VERSION 3.4.1)
add_library(
        native_hello
        SHARED
        json_tool.h
        json_reader.cpp
        json_valueiterator.inl
        json_value.cpp
        json_writer.cpp
        version.h.in
        # 下面的cpp若是没有则新建一个,本文基于上篇文章
        native_hello.cpp
)
target_link_libraries(
        native_hello
        android
        log
)

make build一下

640?wx_fmt=other

What Are you!!!出错了,告诉在json_tool.h的第10行出错了,那行吧,点进去瞅一眼,如下:

640?wx_fmt=other

哦,include错了,应该改为 #include “json/config.h” 那就改吧!cv过来的所有文件都得去查看一下(试想一下,若是cv过来的文件有1W个,咋办…,改完这个,将来别的地方在引用,又忘了那个曾经是改过的了,停!stop,我眼疼!)。

编写测试代码

打开../app/src/main/cpp/native_hello.cpp 更改如下:

//
// Created by xong on 2018/9/28.
//
#include<jni.h>
#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());
}

修改NativeFun类如下

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""code");
        String parseJson = NativeFun.parseJsonCode(outPutJson);
        tv_native_content.setText("生成的Json:\n" + outPutJson + "\n解析:" + parseJson);
    }
}

结果如下图

640?wx_fmt=other

编译成库文件

OK,集成成功,但是太复杂,太麻烦了,每次要写那么一堆配置文件,少一个都不行,还要挨个的去改 include 想想都可怕,那么可不可以把这个jsoncpp打成库呢?那么我们就要考虑如下:

  1. 必须使用CMake,不使用编写mk的方式;

  2. 在任何系统上都可以,不可在编译库的时候切换到其他系统;

好吧,基于以上两点,百度搜了一波,发现…GG,没有符合的哎,用CMake就得去Linux下面,且需要自己构建工具链,要不然就是…mk…。再去Google搜一搜,发现还是这样的,难道,不存在?再去GitHub搜,发现没有相关的,无可奈何,去Google提供的sample中找一找吧,哈!还真有发现,链接:hello-libs

https://github.com/googlesamples/android-ndk/tree/master/hello-libs

编译so动态库

修改cpp目录为

640?wx_fmt=other

修改../cpp/jsoncpp/目录中的CMakeLists.txt如下:

cmake_minimum_required(VERSION 3.4.1)

set(CMAKE_VERBOSE_MAKEFILE on)

add_library(
        # 库名字
        jsoncpp
        # 库类型
        SHARED
        # 库包含的资源
        src/json_tool.h
        src/json_reader.cpp
        src/json_valueiterator.inl
        src/json_value.cpp
        src/json_writer.cpp
        src/version.h.in)

# 导出目录 此处的设置 导出主目录在 Project/export文件夹内。
set(export_dir ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../export)

set_target_properties(
        # 库名字
        jsoncpp
        # 设置输出.so动态库的路径 
        PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${export_dir}/libsojsoncpp/lib/${ANDROID_ABI}")

add_custom_command(
        # POST_BUILD 处 有三个值可选
        # 分别是:
        # PRE_BUILD:在 hello 运行其他规则前执行
        # PRE_LINK:在编译源文件之后但在 链接其他二进制文件 或 运行静态库的库管理器 或 归档工具 之前执行
        # POST_BUILD:最后执行
        TARGET jsoncpp POST_BUILD

        # 拷贝命令 将 ${CMAKE_CURRENT_SOURCE_DIR}/src/json/allocator.h 文件拷贝到 ${export_dir}/libsojsoncpp/include/json/ 文件夹内 且名字和之前的相同
        COMMAND "${CMAKE_COMMAND}" -E copy "${CMAKE_CURRENT_SOURCE_DIR}/src/json/allocator.h" "${export_dir}/libsojsoncpp/include/json/allocator.h"

        COMMAND "${CMAKE_COMMAND}" -E copy "${CMAKE_CURRENT_SOURCE_DIR}/src/json/config.h" "${export_dir}/libsojsoncpp/include/json/config.h"

        COMMAND "${CMAKE_COMMAND}" -E copy "${CMAKE_CURRENT_SOURCE_DIR}/src/json/forwards.h" "${export_dir}/libsojsoncpp/include/json/forwards.h"

        COMMAND "${CMAKE_COMMAND}" -E copy "${CMAKE_CURRENT_SOURCE_DIR}/src/json/features.h" "${export_dir}/libsojsoncpp/include/json/features.h"

        COMMAND "${CMAKE_COMMAND}" -E copy "${CMAKE_CURRENT_SOURCE_DIR}/src/json/value.h" "${export_dir}/libsojsoncpp/include/json/value.h"

        COMMAND "${CMAKE_COMMAND}" -E copy "${CMAKE_CURRENT_SOURCE_DIR}/src/json/reader.h" "${export_dir}/libsojsoncpp/include/json/reader.h"

        COMMAND "${CMAKE_COMMAND}" -E copy "${CMAKE_CURRENT_SOURCE_DIR}/src/json/writer.h" "${export_dir}/libsojsoncpp/include/json/writer.h"

        COMMAND "${CMAKE_COMMAND}" -E copy "${CMAKE_CURRENT_SOURCE_DIR}/src/json/assertions.h" "${export_dir}/libsojsoncpp/include/json/assertions.h"

        COMMAND "${CMAKE_COMMAND}" -E copy "${CMAKE_CURRENT_SOURCE_DIR}/src/json/autolink.h"  "${export_dir}/libsojsoncpp/include/json/autolink.h"

        COMMAND "${CMAKE_COMMAND}" -E copy "${CMAKE_CURRENT_SOURCE_DIR}/src/json/json.h"  "${export_dir}/libsojsoncpp/include/json/json.h"

        COMMAND "${CMAKE_COMMAND}" -E copy "${CMAKE_CURRENT_SOURCE_DIR}/src/json/version.h"  "${export_dir}/libsojsoncpp/include/json/version.h"

        )

修改 ../cpp/CMakeLists.txt如下

cmake_minimum_required(VERSION 3.4.1)

set(CMAKE_VERBOSE_MAKEFILE on)

# 设置资源主目录 CMAKE_CURRENT_SOURCE_DIR 代表当前CMakeLists.txt 所在的目录
set(lib_src_DIR ${CMAKE_CURRENT_SOURCE_DIR})

# 设置CMake编译后文件的存放的临时目录
set(lib_build_DIR $ENV{HOME}/tmp)

# 得到 lib_build_DIR 文件夹内文件
file(MAKE_DIRECTORY ${lib_build_DIR})

# 添加子目录
add_subdirectory(${lib_src_DIR}/jsoncpp ${lib_build_DIR}/jsoncpp)

修改 ../app/build.gradle如下

apply plugin: 'com.android.application'

android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                // 这里的名字最好和 ../cpp/jsoncpp/CMakeLists.txt 中设置的名字相同
                targets 'jsoncpp'
            }
        ...
        }
    }
    ...
    externalNativeBuild {
        cmake {
            path 'src/main/cpp/CMakeLists.txt'
        }
    }
}

说明:点击Build/Make Project(或者 Make Module 'app') 会在 项目根目录下 新建 export 文件夹 在里面会存放 库所需的头文件和so动态库。

编译后如下

640?wx_fmt=other

so和头文件都生成了,但是我们在写../cpp/jsoncpp/CMakeLists.txt 文件时,也发现了,将所需的头文件导出到指定目录时有点儿费劲,只是名字换了下,若是用代码来写的话,一个for循环就可以了,那么在CMakeLists.txt中,可不可以实现类似的呢?哈哈,那当然是肯定的了,最终修改如下:

cmake_minimum_required(VERSION 3.4.1)

set(CMAKE_VERBOSE_MAKEFILE on)

add_library(
        jsoncpp 
        SHARED
        src/json_tool.h
        src/json_reader.cpp
        src/json_valueiterator.inl
        src/json_value.cpp
        src/json_writer.cpp
        src/version.h.in)

set(export_dir ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../export)

set_target_properties(
        jsoncpp
        PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${export_dir}/libsojsoncpp/lib/${ANDROID_ABI}")

add_custom_command(
        TARGET jsoncpp POST_BUILD
        # 将 ${CMAKE_CURRENT_SOURCE_DIR}/src/json 文件夹下的文件 导出到 ${export_dir}/libajsoncpp/include/json/ 文件夹内
        COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/src/json" "${export_dir}/libsojsoncpp/include/json/")

将之前生成的export文件夹删除,重新build发现是可以的。OK,动态库可以编译成功,那么讲道理,静态库也是一样的,来尝试下编译静态库库。

编译a静态库

在以上编译so动态库的前提下,修改../cpp/jsoncpp/CMakeLists.txt如下:

cmake_minimum_required(VERSION 3.4.1)

set(CMAKE_VERBOSE_MAKEFILE on)

add_library(
        jsoncpp
        # 将 库 类型 由 SHARED 修改为 STATIC
        STATIC
        src/json_tool.h
        src/json_reader.cpp
        src/json_valueiterator.inl
        src/json_value.cpp
        src/json_writer.cpp
        src/version.h.in)

set(export_dir ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../export)

set_target_properties(
        jsoncpp
        # 将 LIBRARY_OUTPUT_DIRECTORY 修改为 ARCHIVE_OUTPUT_DIRECTORY
        # 方便查看 生成的a文件目录修改一下 即 将 libsojsoncpp 修改为 libajsoncpp
        PROPERTIES ARCHIVE_OUTPUT_DIRECTORY "${export_dir}/libajsoncpp/lib/${ANDROID_ABI}")

add_custom_command(
        TARGET jsoncpp POST_BUILD
        COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/src/json" "${export_dir}/libajsoncpp/include/json/"
)

修改完成后,Build/Make Project(或者 Make Module 'app') 会在 Project/export目录下生成:

640?wx_fmt=other

这样编译.a静态库和.so动态库就完成了。

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

640?wx_fmt=other

都放在了Project/export文件夹中,且在里面将so和a分开,分别放在了,libajsoncpp和libsojsoncpp文件夹中,在每个文件夹中,又有include文件夹来放库所需要的头文件,lib中放so以及a库文件。

链接so动态库

我们首先来链接我们较为熟悉的so动态库,然后再来链接a静态库。

  • 准备工作

  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'
        }
    }
}
...
  • 写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(自行脑部表情包) 我们来一条一条的看。最后,Build/Make Module 'app'.

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

640?wx_fmt=other

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

  1. 然后接下来就又是一个add_library 但是这个是带资源的了。没啥好说的了.

  2. target_include_directories 我们有库了,但是没有对应的头文件咋行,所以这句就是链接库对应的头文件了。

  3. target_link_libraries 最后将所需的头文件,链接到项目中就可以啦!

  • 调用代码

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);
    }
}
  • 结果

640?wx_fmt=other

嗯!集成成功,那么下面我们来集成下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);
    }
}
  • 结果

640?wx_fmt=other

可以看到type变成了 "a",这样的话,静态库也就算集成成功了。

动态库和静态库的区别

有的人会说了,你这都用的json,而且返回 type 是你传进去的呀,你就是集成 so 传 a 那么就算集成a了?

另一个我们怎么会知道打到apk中的是so动态库,还是a静态库呢?不是都说了么,Android中只支持调用so动态库,不支持a静态库的,那么这…集成a静态库这不是扯淡么?

OK,接下来就来解释这一系列的问题,首先我们要知道什么是静态库什么又是动态库。

参考Linux下的库,地址如下所示:

https://blog.csdn.net/llzk_/article/details/55519242

抽取出主要的:

  • 静态库

链接时间: 静态库的代码是在编译过程中被载入程序中;

链接方式: 目标代码用到库内什么函数,就去将该函数相关的数据整合进目标代码;

优点: 是在编译后的执行程序不在需要外部的函数库支持;

缺点: 如果所使用的静态库发生更新改变,程序必须重新编译。

  • 动态库

链接时间: 动态库在编译的时候并没有被编译进目标代码,而是在运行时用到该库中函数时才去调用;

链接方式: 动态链接,用到该库内函数时就去加载该库;

优点: 动态库的改变并不影响程序,即不需要重新编译;

缺点: 因为函数库并没有整合进程序,所以程序的运行环境必须依赖该库文件。

再精简一点:

静态库是一堆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 查看一下,如下:

640?wx_fmt=other

上面的结论没错咯,里面确实有两个so,一个jsoncpp.so另一个就是我们自己的native_hello.so;

那么我们再来看一下集成a静态库的吧!如下:

640?wx_fmt=other

这就是区别啦!


总结


so方式,只要你代码中涉及了,那么它就要存在,即使你只是调用,后续不使用它,它也存在。

a方式,只需要在编码过程中,保持它存在就好,用几个函数,就从a中取几个函数。

本来想将这一篇分成三篇来写的,想了又想,Android开发嘛,没必要对native很那么了解,所以就压缩压缩,压缩成一篇了。在这三篇中,我们发现,写CMakeLists.txt也不是那么很麻烦,而且很多都是重复的,都是可以用脚本来生成的,比如 add_library 中添加的资源文件,当然其他的也一样啦,那么这个 CMakeLists.txt 是不是可以写一个小脚本呢?我感觉可以。另一个,如何用Camke构建a静态库、so动态库,以及如何集成,在Google的sample中都有的,再贴一下链接:android_ndk

https://github.com/googlesamples/android-ndk

而且添加的时间也挺长的了,但是,百度到的文章还是 5年前的,真的是…不知道说啥了,还是多看些Google Github比较好。哈哈哈哈~

最后,上述三篇文章中涉及的源码均已上传到GitHub,链接:UseCmakeBuildLib

https://github.com/lilinxiong/UseCmakeBuildLib


欢迎长按下图 -> 识别图中二维码

或者 扫一扫 关注我的公众号

640.png?

640?wx_fmt=jpeg

猜你喜欢

转载自blog.csdn.net/c10WTiybQ1Ye3/article/details/83053625