Everything You Need to Know About Using CMake in Android

Everything You Need to Know About Using CMake in Android

Today’s Tech News

Recently, Google held its hardware launch event in New York, themed “Made By Google,” unveiling a series of new hardware products including the Pixel phone, a 2-in-1 tablet, and speakers. Over the past month, the four major tech giants in the U.S., including Google, Microsoft, Apple, and Amazon, have successively released their latest hardware. Compared to the other three, Google’s range of hardware is the most comprehensive this time, showcasing more of its hardware ambitions.

Author’s Introduction

Happy Monday, everyone! Let’s move forward this new week!

This article is a reprint from xong, sharing everything you need to know about using CMake in Android. Let’s take a look! Hope you enjoy it.

xong‘s blog address:

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

Introduction

I’m sure everyone has encountered JNI during development to some extent, and every time we deal with JNI, we can’t help but take a deep breath—it’s so difficult!

It’s fine with just Java and C++ code. When creating a new project, just check the “Include C++ support” option, then click next and finally finish. A simple Android project with C++ code is completed. Then, looking at how to write the CMakeLists.txt, you can just follow the examples. However, in actual development, it’s not that simple, as the underlying (C++) code may come as either a .a static library or a .so dynamic library, and figuring out how to integrate it into the project is just confusing. The objective fact is that you can only search for answers on Baidu, but most results are about using Android.mk, but we are already in the Android Studio 3.1+ era, and still using mk? There’s very little information about CMake, and that’s why this article was created.

Last year, when I was still an intern at my company, my boss handed me a .a static library (at that time, I didn’t know it was a static library), and I was very curious and wanted to know how .a was built and what the difference between .a and .so was. In short, I had a lot of questions. After becoming a full-time employee, I also tried to build an .a static library, but unfortunately, all I could find on Baidu were mk-related articles. I found one about CMake, but it required compiling under Linux and some custom toolchain, which was a hassle.

Thus, this series was born. This series will not elaborate on what CMake, NDK, or JNI are, but will simply describe common issues encountered when using CMake in Android and their solutions.

This series covers:

  • First-time use of CMake to build native projects

  • How to integrate existing cpp code into the project

    • Copying source code

    • Compiling into library files

  • CMake linking a static library .a and a dynamic library .so, and the difference between static and dynamic libraries

Getting Started

First-time Use of CMake to Build Native Projects

This is quite simple. When creating a new project, just check the include C++ support option, but let’s do it ourselves without relying on the IDE to see if we can. Create a regular project named: AndCmake, and after creation, it looks like this:

Everything You Need to Know About Using CMake in Android

In the new project, there’s nothing. We all know that if you want to use CMake, you must have CMakeLists.txt (note that the name cannot be wrong). The second thing is the required cpp resources. This is simple; just create a new folder in the src/main directory for cleanliness, creating a cpp folder, and then create CMakeLists.txt and native_hello.cpp files inside it. After a simple configuration in CMakeLists.txt, it looks like this:

../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());
}

It should be noted here that Java_com_xong_andcmake_jni_ is used to find the class layer by layer in Java, for example, this is looking for it in com.xong.andcmake.jni. The latter part NativeFun_stringFromJNI is the class and method name. The function’s parameters must be like this; the second parameter can be written as “jobject”. Recently, when I wrote JNI, I was prompted to change it to “jclass”, but “jobject” is not wrong either; it is correct.

./src/main/cpp/CMakeLists.txt

# CMake minimum version
cmake_minimum_required(VERSION 3.4.1)
# Add resources to be packaged
add_library(
        # Library name
        native_hello
        # Library type
        SHARED
        # Included cpp
        native_hello.cpp
)
# Link to the project
 target_link_libraries(
        native_hello
        android
        log
)

This completes the C++ part. Modify ../app/build.gradle as follows:

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

Write the corresponding Java layer code, create a jni folder under the com.xong.andcmake package, and create the NativeFun class with the following code:

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();
}

Call NativeFun.stringFromJNI() to check if the correct information is output: this is quite simple. Just add a TextView in the Activity and display it. If correct, it should show as follows:

Everything You Need to Know About Using CMake in Android

OK, our first app with CPP code is complete. Now let’s consider the code in native_hello.cpp:

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

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

For us:

  1. Avoid writing duplicate code

  2. Code should be efficient

Regarding the function name Java_com_xong_andcmake_jni_, if we feel that the package in which the native class and method are located is not suitable and want to change the package name, but there are many native methods in this class, if we change the package, then the corresponding function names in the cpp need to be changed one by one. If there are 1000 functions, it becomes really tedious and easy to miss. So is there a way to minimize the workload? The class and package are relatively fixed, so can we extract the package name and represent it with an expression? Yes! That’s what macro definitions are for!

Regarding the return statement, why did I write it like this? Because when creating a project with C++ support in Android Studio, this is how it is written, and it feels natural to copy it. But let’s think, is it necessary to write it this way? We can see that in the final return, the string object is converted again into a char array (of course, in C++ it is a char* pointer, whether it’s const or not, we don’t need to know what const is; we just need to know that c_str() actually converts the string object back into a char[] array), so isn’t it a bit redundant, causing the computer to do more work? So the modified code is as follows:

// 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++");
}

In this case, if NativeFun is no longer in the jni package but in the jnia package, we only need to change the macro definition above. Since we are not operating on the string, we don’t need it, and we can return directly, which makes the computer less burdened. After making the project, what happens?

Those who have dealt with JNI should understand that adding cpp code to the project makes the apk much larger. Why is that? Let’s sort out what happens after making the project with cpp code. It is clear that .so will be generated. So… how many are generated by default?

Open the ../app/build/intermediates/cmake/debug/obj directory as follows:

Everything You Need to Know About Using CMake in Android

By default, 4 will be generated. v7a is the largest, and x86_64 is the smallest. By default, all 4 will be included in the apk (because it’s unclear which phone the apk will be installed on, so all resources must be included in the apk). To reduce the apk size, we need to include only the required so files in the apk. Now the question is, how can we generate the specified so library? Here’s the code:

apply plugin: 'com.android.application'

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

By adding the ndk tag in ../app/build.gradle and specifying the architecture names you want to generate, you can rebuild the project. As shown in the figure:

Everything You Need to Know About Using CMake in Android

Now only the v7a and x86 architecture so libraries are included in the apk!

In the next article, we will explain the various ways to integrate existing cpp code into the project and how to do it. How to integrate existing cpp code into the project?

This is quite common when writing JNI. For instance, the json library is not provided by C++ itself, and when writing JNI, we usually need to interact with the upper layer to exchange data, and a simple way is to use json. So let’s take json as an example. First, find a json library! First, clone the code. Integrate the source code into the project.

Copy the json folder from include to ../app/src/main/cpp directory. Copy the files in src/lib_json except for CMakeLists.txt to ../app/src/main/cpp directory. The final structure is as follows:

Everything You Need to Know About Using CMake in Android

Modify ../app/src/main/cpp/CMakeLists.txt as follows:

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
        # If the following cpp does not exist, create one based on the previous article
        native_hello.cpp
)
target_link_libraries(
        native_hello
        android
        log
)

Make build again.

Everything You Need to Know About Using CMake in Android

What are you doing!!! An error occurred, indicating that there was an error on line 10 of json_tool.h. Let’s take a look at that line:

Everything You Need to Know About Using CMake in Android

Oh, the include is wrong; it should be #include “json/config.h”. Let’s change that! We need to check every file that comes over (imagine if there are 10,000 files, what to do… After changing this, if we reference it elsewhere, we might forget that it was changed before, stop! Stop! My eyes hurt!).

Writing Test Code

Open ../app/src/main/cpp/native_hello.cpp and change it as follows:

// 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), &amp;root, &amp;errs);
    if (ok &amp;&amp; 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());
}

Modify the NativeFun class as follows:

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);
}

Testing

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("Generated Json:\n" + outPutJson + "\nParsed:" + parseJson);
    }
}

The result is shown in the figure below:

Everything You Need to Know About Using CMake in Android

Compiling into Library Files

OK, the integration is successful, but it’s too complex and troublesome. Every time I have to write so many configuration files, missing one will cause issues, and I have to go through and change includes, just thinking about it is scary. So, can we package this jsoncpp into a library? We need to consider the following:

  1. Must use CMake, not mk;

  2. Must work on any system, cannot switch to another system while compiling the library;

Well, based on these two points, I searched Baidu and found… GG, nothing fits. CMake requires compiling under Linux and building toolchains by yourself; otherwise, it’s mk… I searched Google again and found the same thing. Is there really nothing? I went to find in Google’s samples and surprisingly found something, link: hello-libs

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

Compiling so Dynamic Library

Modify the cpp directory as follows:

Everything You Need to Know About Using CMake in Android

Modify the CMakeLists.txt in ../cpp/jsoncpp as follows:

cmake_minimum_required(VERSION 3.4.1)

set(CMAKE_VERBOSE_MAKEFILE on)

add_library(
        # Library name
        jsoncpp
        # Library type
        SHARED
        # Resources included in the library
        src/json_tool.h
        src/json_reader.cpp
        src/json_valueiterator.inl
        src/json_value.cpp
        src/json_writer.cpp
        src/version.h.in)

# Export directory, the main directory is set to be in Project/export folder.
set(export_dir ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../export)

set_target_properties(
        # Library name
        jsoncpp
        # Set the output path for the .so dynamic library
        PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${export_dir}/libsojsoncpp/lib/${ANDROID_ABI}")

add_custom_command(
        # POST_BUILD has three optional values
        # They are:
        # PRE_BUILD: Execute before hello runs other rules
        # PRE_LINK: Execute after compiling source files but before linking other binary files or running static library manager or archiving tool
        # POST_BUILD: Execute last
        TARGET jsoncpp POST_BUILD

        # Copy command, copy ${CMAKE_CURRENT_SOURCE_DIR}/src/json/allocator.h to ${export_dir}/libsojsoncpp/include/json/ with the same name
        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"
        )

Modify ../cpp/CMakeLists.txt as follows:

cmake_minimum_required(VERSION 3.4.1)

set(CMAKE_VERBOSE_MAKEFILE on)

# Set the main directory for resources CMAKE_CURRENT_SOURCE_DIR represents the directory where the current CMakeLists.txt is located
set(lib_src_DIR ${CMAKE_CURRENT_SOURCE_DIR})

# Set the temporary directory for CMake compiled files
set(lib_build_DIR $ENV{HOME}/tmp)

# Create lib_build_DIR directory
file(MAKE_DIRECTORY ${lib_build_DIR})

# Add subdirectory
add_subdirectory(${lib_src_DIR}/jsoncpp ${lib_build_DIR}/jsoncpp)

Modify ../app/build.gradle as follows:

apply plugin: 'com.android.application'

android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                // It's best to have this name match the one set in ../cpp/jsoncpp/CMakeLists.txt
                targets 'jsoncpp'
            }
        ...
        }
    }
    ...
    externalNativeBuild {
        cmake {
            path 'src/main/cpp/CMakeLists.txt'
        }
    }
}

Note: Clicking Build/Make Project (or Make Module ‘app’) will create a new export folder in the project root directory, which will store the required headers and so dynamic libraries.

After building, it looks like this:

Everything You Need to Know About Using CMake in Android

Both so and header files are generated, but when writing the ../cpp/jsoncpp/CMakeLists.txt file, we also found that exporting the required header files to the specified directory was a bit cumbersome. If we were to write code, a for loop could handle it. So, can we achieve something similar in CMakeLists.txt? Of course, we can! The final modification is as follows:

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
        # Export all files in the src/json folder to ${export_dir}/libsojsoncpp/include/json/ folder
        COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/src/json" "${export_dir}/libsojsoncpp/include/json/")

Delete the previously generated export folder and rebuild to find that it works. OK, the dynamic library can compile successfully. Now, logically, the static library should be the same. Let’s try to compile the static library.

Compiling a Static Library

Based on the premise of compiling the above so dynamic library, modify ../cpp/jsoncpp/CMakeLists.txt as follows:

cmake_minimum_required(VERSION 3.4.1)

set(CMAKE_VERBOSE_MAKEFILE on)

add_library(
        jsoncpp
        # Change the library type from SHARED to 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
        # Change LIBRARY_OUTPUT_DIRECTORY to ARCHIVE_OUTPUT_DIRECTORY
        # for easier viewing of the generated a file directory, modify libsojsoncpp to 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/"
)

After completing the modifications, Build/Make Project (or Make Module ‘app’) will generate in the Project/export directory:

Everything You Need to Know About Using CMake in Android

This completes the compilation of .a static libraries and .so dynamic libraries.

Consolidate the so dynamic library, a static library, and the corresponding header files into one folder. Since this article is based on the previous one, these files are placed as shown below:

Everything You Need to Know About Using CMake in Android

They are placed in the Project/export folder, with so and a separated into their respective folders, and each folder has an include folder for the required header files, while the lib folder contains the so and a library files.

Linking so Dynamic Library

First, let’s link the familiar so dynamic library, and then we will link the a static library.

  • Preparation

  1. Delete the jsoncpp folder in ../app/src/main/cpp to prevent using source code instead of the library (for those who followed the second article and copied the source code into the project).

  2. Delete all contents in the CMakeLists.txt in ../app/src/main/cpp to prevent differences with the configuration in this article’s CMakeLists.txt.

  3. Modify ../app/build.gradle as follows:

apply plugin: 'com.android.application'

android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                arguments '-DANDROID_STL=c++_static'
            }
        }
    }
    buildTypes {
        ...
    }
    sourceSets {
        main {
            // Set according to actual situation. Since the lib of this project is placed in project/export/libsojsoncpp/lib, set it like this
            jniLibs.srcDirs = ['../export/libsojsoncpp/lib']
        }
    }
    externalNativeBuild {
        cmake {
            path 'src/main/cpp/CMakeLists.txt'
        }
    }
}
  • Write app/src/main/cpp/CMakeLists.txt file

cmake_minimum_required(VERSION 3.4.1)

# Set variable to find the resource directory, ".." represents the parent directory
set(export_dir ${CMAKE_SOURCE_DIR}/../../../../export)

# Add .so dynamic library (jsoncpp)
add_library(lib_so_jsoncpp SHARED IMPORTED)

# Link
set_target_properties(
        # Library name
        lib_so_jsoncpp
        # Library location
        PROPERTIES IMPORTED_LOCATION ${export_dir}/libsojsoncpp/lib/${ANDROID_ABI}/libjsoncpp.so)

add_library(
        native_hello
        SHARED
        native_hello.cpp
)

# Link header files
target_include_directories(
        native_hello
        PRIVATE
        # Required header files for native_hello
        ${export_dir}/libsojsoncpp/include
)

# Link to the project
target_link_libraries(
        native_hello
        android
        log
        # Link jsoncpp.so
        lib_so_jsoncpp
)

This time the configuration looks a bit more, but… don’t panic. Let’s take a look one by one. Finally, Build/Make Module ‘app’.

cmake_minimum_required(VERSION 3.4.1) doesn’t need explanation; it just sets the minimum version of CMake.

set(….) is set because we consider the number of times the export folder is used, and they are all absolute paths, so we set a variable to simplify it. export_dir is the variable name, ${CMAKE_SOURCE_DIR} gets the path where the current CMakeLists.txt is located, and then traverses up with “..” to find the export folder where we store resource files.

add_library(lib_so_jsoncpp SHARED IMPORTED) is self-explanatory; it adds the library file. The three parameters are “lib_so_jsoncpp” for the library name; “SHARED” because we are importing a so dynamic library, so it is SHARED; “IMPORTED” then imports;

set_target_properties is the next important line. There are many parameters, so I won’t copy them here. We have already added the library, but… the library is empty (note that the following is imported); it has nothing but a name and type, so we need to link it to the actual library in the next line. I have already commented in the previous CMakeLists.txt, and here I will only mention something that hasn’t been mentioned before: “${ANDROID_ABI}”. What is this? This statement concatenates it, but I don’t have this folder in the real path. Let’s check ../libsojsoncpp/lib, as shown below:

Everything You Need to Know About Using CMake in Android

Yes, that’s a bunch of architectures, so… this value represents these (by default, all types).

  1. Then there’s another add_library, but this one has resources. Nothing to say here.

  2. target_include_directories We have the library, but we need the corresponding header files, so this line links the header files for the library.

  3. target_link_libraries Finally, we link the required header files to the project!

  • Calling Code

The cpp layer code doesn’t need to change; just use the way we copied the source code last time, but for the convenience of those who directly read this article, I’ll paste the code in native_hello.cpp as follows:

// 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), &amp;root, &amp;errs);
    if (ok &amp;&amp; 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());
}

The corresponding Java layer code is as follows:

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);
}

Calling code

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("Generated Json:\n" + outPutJson + "\nParsed:" + parseJson);
    }
}
  • Results

Everything You Need to Know About Using CMake in Android

You can see that the type has changed to “a,” indicating that the static library has been successfully integrated.

Differences Between Dynamic and Static Libraries

Some may say that you are using json, and the returned type is what you passed in. You integrated so by passing a, so you also integrated a?

How can we tell if the apk contains so dynamic libraries or a static library? Didn’t we say that Android only supports calling so dynamic libraries and does not support a static libraries? Then… integrating a static library doesn’t make sense, does it?

OK, let’s explain this series of questions. First, we need to know what a static library and a dynamic library are.

Refer to the library under Linux, the address is as follows:

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

Extract the main points:

  • Static Library

Linking time: The code of the static library is loaded into the program during the compilation process;

Linking method: The target code uses whatever functions are in the library, and the related data is integrated into the target code;

Advantages: The compiled executable program does not require external function library support;

Disadvantages: If the static library used changes, the program must be recompiled.

  • Dynamic Library

Linking time: The dynamic library is not compiled into the target code during compilation but is called when the function from that library is used at runtime;

Linking method: Dynamic linking loads the library when a function from that library is called;

Advantages: Changes to the dynamic library do not affect the program, so recompilation is not needed;

Disadvantages: Since the function library is not integrated into the program, the program’s runtime environment must depend on the library file.

To simplify a bit:

A static library is a bunch of cpp files that need to be compiled every time to run. When you use it in your code, you take what you need from these cpp files for compilation;

A dynamic library is a bunch of cpp files that have already been compiled. When running, there is no need to recompile the cpp. When a function from the library is needed, the library is loaded in, and when it is not needed, it does not load, provided that the library must exist.

Thus, we can answer the previous questions. Yes, it is true that Android can only call so dynamic libraries. The a static library we integrated will take the corresponding metadata from the static library when we need to use functions from the static library, and then this data will be included in the final so dynamic library we will call, which is native_hello.so in this case.

Additionally, when we integrated the so dynamic library, we added a tag in ../app/build.gradle as follows:

sourceSets {
        main {
            jniLibs.srcDirs = ['../export/libsojsoncpp/lib']
        }
    }

After the explanation above, it’s not difficult to understand this line. As mentioned before:

When we need to use a function from the library, the library will be loaded. If it is not needed, it will not load, provided that the library must exist.

Therefore, native_hello.so depends on jsoncpp.so, which means jsoncpp.so must exist. So, adding this means that we are including jsoncpp.so in the apk. We can use jadx to check the apk built with the above so dynamic library integration, as shown:

Everything You Need to Know About Using CMake in Android

The conclusion is correct; there are indeed two so files inside, one is jsoncpp.so and the other is our own native_hello.so.

Now let’s take a look at the integration of the a static library:

Everything You Need to Know About Using CMake in Android

This is the difference!

Summary

With the so method, as long as your code involves it, it must exist, even if you only call it, it still exists.

With the a method, you just need to keep it present during the coding process, and if you use a few functions, you just take a few functions from a.

I originally wanted to split this article into three parts, but after thinking about it, Android development doesn’t require such a deep understanding of native code, so I compressed it into one article.In these three articles, we discovered that writing CMakeLists.txt is not that complicated, and much of it is repetitive and can be generated with scripts, such as the resource files added in add_library. Of course, the same goes for others, so can we write a small script for this CMakeLists.txt? I think we can.Another thing is how to use CMake to build a static library, dynamic library, and how to integrate them. Google’s samples have all of that; I’ll attach the link again: android_ndk

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

Moreover, the time it takes to add is quite long, but the articles found on Baidu are still from 5 years ago. I really… don’t know what to say. It’s better to look at Google’s GitHub more. Hahaha~

Finally, the source code involved in the above three articles has been uploaded to GitHub, link: UseCmakeBuildLib

https://github.com/lilinxiong/UseCmakeBuildLib

Welcome long press the image below -> recognize the QR code in the image

Or scan to follow my public account

Everything You Need to Know About Using CMake in Android

Everything You Need to Know About Using CMake in Android

Leave a Comment