The purpose of this article is to introduce what NDK is in Android and focus on the use of the latest Android Studio build tool CMake

1 introduction of the NDK

Before I introduce the NDK, I will introduce the official NDK documentation for Android. portal

The official documentation describes the NDK in the following aspects

  1. NDKBasic concepts of
  2. How to compileNDKproject
  3. ABIWhat is it and what are supported by different CPU instruction setsABI
  4. How do you use your own and other pre-built libraries

This section will summarize and supplement the document. So it’s recommended to either browse through the documentation or go back to it after reading this article.

1.1 Basic Concepts of NDK

First of all, explain JNI, NDK, and Android development, C/C ++ development with simple words. Mk, application. mk, NDK-build, CMake and CMakeList are common nouns that will be eliminated during the explanation.

Java Native Interface (JNI) : Indicates a Java Native Interface. Is a layer of interface (also a standard) that Java encapsulates to make it easy to call native code such as C and C ++. As you all know, Java has the advantage of being cross-platform, but it also has the disadvantage of programming when interacting locally. Due to the cross-platform feature of Java, the ability of local interaction is not strong enough. Some features related to the operating system cannot be completed in Java. Therefore, Java provides JNI specifically for interacting with local code, which enhances the local interaction ability of Java language. Some of the above text is excerpted from Ren Yugang’s introduction to Java JNI

NDK (Native Development Kit) : A series of tools for developing Native code, including but not limited to compilation tools, some common libraries, and Development ides.

The NDK toolkit provides a complete set of tools for compiling C/C ++ code into static/dynamic libraries, while Android.mk and application. mk you can think of as files that describe compilation parameters and some configuration. Such as specifying whether to compile with c++11 or c++14, which shared libraries to reference, describing relationships, and so on, and specifying the compiled abi. Only with these tools in the NDK can you compile C/C ++ code accurately.

The NdK-build file is a shell script introduced in Android NDK R4. The purpose is to call the correct NDK build script. In fact, the NDK will eventually call its own compilation tools.

So what is CMake. Apart from Android development, c/ C ++ compilation files are different for different platforms. It is compiled using a makefile file on Unix and a project file on Windows. CMake, on the other hand, is a cross-platform compilation tool that does not directly compile objects. Instead, it generates corresponding Makefiles or project files according to custom language rules (cmakelists.txt) and then invokes the underlying compilation.

With CMake support added to the tools after Android Studio 2.2, you can assume that you have two options for compiling your C/C ++ code after Android Studio 2.2. One is the combination of NDK-build + Android.mk + application. mk and the other is the combination of CMake + cmakelists.txt. These two combinations have nothing to do with Android code and C/C ++ code, just different build scripts and build commands. This article will mainly describe the latter combination. (Which Android is now pushing)

1.2 What is the ABI

Application Binary Interface (ABI) Application binary interface. Each combination of different cpus and instruction sets has a defined ABI (Application binary Interface). A program can only run on that CPU if it complies with this interface specification, so the same program code needs to build different libraries for different ABIs to be compatible with multiple cpus. Of course, different architectures do not necessarily mean incompatible cpus.

  • Armeabi devices are compatible only with Armeabi;
  • Armeabi-v7a devices are compatible with ArmeabI-V7A and ArmeABI;
  • Arm64-v8a devices compatible with ARM64-V8A, ArMEABI-V7A, ArmeABI;
  • X86 devices are compatible with X86 and Armeabi;
  • X86_64 devices are compatible with X86_64, X86, and Armeabi;
  • Mips64 devices are compatible with MIPS64 and MIPS.
  • MIPS is only compatible with MIPS;

Specific compatibility issues can be found in this article. Android SO file compatibility and adaptation

When we were developing Android apps, Java code was running on virtual machines, so we never really cared about this. But when we develop or use native code, we need to learn about the different ABIs and choose libraries that plug into them for our programs. (The more libraries, the bigger the package, so be selective)

Let’s take a look at what ABI’s there are and the corresponding instruction set

ABI

2 Use of CMake

This section focuses on the rules and use of CMake, and how to use CMake to compile your own and other pre-built libraries.

2.1 Hello world

We understand CMake through a Hello World project

Start by creating a new project that contains native code. In New Project, check Include C++ support

New Project

Once the project is created, we can see four differences from normal Android projects.

  1. mainI’m going to addcppDirectory, where c/ C ++ code is placed
  2. The module – levelbuild.gradleThere are changes
  3. increasedCMakeLists.txtfile
  4. One more.externalNativeBuilddirectory
Difference

build.gradle

android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                cppFlags "-frtti -fexceptions"
                arguments "-DANDROID_ARM_NEON=TRUE"
            }
        }
    }
    buildTypes {
        ...
    }
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
}
...Copy the code

Since the CMake command is integrated into gradle-externalNativeBuild, there are two places to configure CMake in Gradle.

Externalnativebuild-cmake outside of defaultConfig specifies the path to cmakelist. TXT. Set externalNativebuild-cmake in defaultConfig to the cmake command parameters. Finally by the arguments of the parameters is converted into an executable CMake command, can be in the externalNativeBuild/CMake/debug / {} abi/cmake_build_command in TXT. The following

cmake command

For more command parameters and meanings, see the Android NDk-cmake documentation

CMakeLists.txt

Cmakelists. TXT mainly defines which files need to be compiled and the relationship with other libraries.

Take a look at cmakelists.txt in the new project

Cmake_minimum_required (VERSION 3.4.1) # compile a dynamic library native-lib, CPP add_library(# Sets the name of the library. Native lib # Sets the library as a shared file Library.shared # Provides a relative path to your source file(s).src /main/ CPP /native-lib.cpp Find_library (# Sets the name of the path variable. log-lib # Specifies the name of the NDK library that # you want CMake to locate. log ) target_link_libraries( # Specifies the target library. native-lib # Links  the target library to the log library # included in the NDK. ${log-lib} )Copy the code

This is actually a basic cmakelists. TXT, in fact, cmakelists. TXT can be very powerful, such as custom commands, find files, header files include, set variables and so on. It is recommended to use it in conjunction with the official documentation of CMake. At the same time here is recommended a Chinese translation of a simple CMake manual

2.2 CMake uses its own and other pre-built libraries

Consider how you can use your own and other pre-built libraries in CMake when you need to import existing static/dynamic libraries (FFMpeg) or compile the core and provide it yourself.

The Android NDK official website uses the documentation for using the existing library, which uses the combination of NDK-build + Android.mk + application. mk. (Most of the official documentation does not use CMake.)

Fortunately, the official Github example includes a project hello-libs that implements how to create static/dynamic libraries and reference them. Now let’s pull down the code and see how it works.

hello-libs

Let’s take a look at Github’s README introduction:

  • App – from$project/distribution/Use a static library and a dynamic library in
  • Gen – Libs – Generate a dynamic library and a static library and copy to$project/distribution/Directory, you do not need to compile the library, the binaries are already saved in the project. Of course, you can compile your own source code if necessary, just remove itsetting.gradleapp/build.gradle, then execute it once, and then comment it back again to prevent it from being affected during the build process.

We analyze modules in a bottom-up way, so let’s look at thisgen-libsThe module.

gen-libs/build.gradle

android { ... defaultConfig { ... externalNativeBuild { cmake { arguments '-DANDROID_PLATFORM=android-9', '-DANDROID_TOOLCHAIN=clang' // explicitly build libs targets 'gmath', 'gperf' } } } ... }...Copy the code

Arguments -dandroid_platform stands for compiled Android platform. It is recommended to set minSdkVersion to minSdkVersion. The other argument is -dandroid_toolchain =clang. CMake has two different compilation toolchains – clang and GCC. GCC is deprecated and clang is the default.

Targets ‘gmath’, ‘gperf’ indicates which items to compile. (Do not fill or compile)

cpp/CMakeLists.txt

Cmake_minimum_required (VERSION 3.4.1) set(CMAKE_VERBOSE_MAKEFILE on) set(lib_src_DIR ${CMAKE_CURRENT_SOURCE_DIR}) set(lib_build_DIR $ENV{HOME}/tmp) file(MAKE_DIRECTORY ${lib_build_DIR}) add_subdirectory(${lib_src_DIR}/gmath ${lib_build_DIR}/gmath) add_subdirectory(${lib_src_DIR}/gperf ${lib_build_DIR}/gperf)Copy the code

Add_subdirectory is the core of the CMakeLists command. Check the CMake official documentation to see that this command is used to add a subpath to the build. Cmakelists. TXT in the subpath will also be executed. Cmakelists.txt in gmath and Gperf, respectively

cpp/gmath/CMakeLists.txt

Cmake_minimum_required (VERSION 3.4.1) set(CMAKE_VERBOSE_MAKEFILE on) add_library(gmath STATIC SRC /gmath.c) # copy out the lib binary... need to leave the static lib around to pass gradle check set(distribution_DIR ${CMAKE_CURRENT_SOURCE_DIR}/.. /.. /.. /.. /.. /distribution) set_target_properties(gmath PROPERTIES ARCHIVE_OUTPUT_DIRECTORY "${distribution_DIR}/gmath/lib/${ANDROID_ABI}") # copy out lib header file... add_custom_command(TARGET gmath POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy "${CMAKE_CURRENT_SOURCE_DIR}/src/gmath.h" "${distribution_DIR}/gmath/include/gmath.h" # **** the following 2 lines are for potential future debug purpose **** # COMMAND "${CMAKE_COMMAND}" -E # remove_directory "${CMAKE_CURRENT_BINARY_DIR}" COMMENT "Copying gmath to output directory")Copy the code

This is the cmakelists.txt of one of the static libraries, and the other one looks a lot like him. We just changed STATIC to SHARED.

Add_library (gmath STATIC SRC /gmath.c) is used to compile a STATIC library. The source file is SRC /gmath.c

The set_target_properties command means to set some properties of the target to change the way they are built. The ARCHIVE_OUTPUT_DIRECTORY property of gmath is set in this command. So you change the output path.

The add_custom_command command is a custom command. The header file in the distribution_DIR command is also copied to the distribution_DIR file.

This is how a static/dynamic library is compiled. Sum up the following three points

  1. Compile static/dynamic libraries
  2. Modifying the Output Path
  3. Copy the exposed header file

And then, let’s seeappHow modules use pre-built static/dynamic libraries.

app/src/main/cpp/CMakeLists.txt

Cmake_minimum_required (VERSION 3.4.1) # configure import libs set(distribution_DIR ${CMAKE_SOURCE_DIR}/.. /.. /.. /.. Add_library (lib_gmath STATIC IMPORTED) set_target_properties(lib_gmath PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/gmath/lib/${ANDROID_ABI}/libgmath.a Libgperf. so add_library(lib_gperf SHARED IMPORTED) set_target_properties(lib_gperf PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/gperf/lib/${ANDROID_ABI}/libgperf.so) # build application's shared lib set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} -std=gnu++11") # create library hello-libs add_library(hello-libs SHARED hello-libs.cpp target_include_directories(hello-libs PRIVATE ${distribution_DIR}/gmath/include ${distribution_DIR}/gperf/include) # Hello-libs lib_gmath lib_gperf target_link_libraries(hello-libs android lib_gmath lib_gperf log)Copy the code

I put the explanation in the comments. As you can see, it’s basically broken down into four steps:

  1. Create static/dynamic libraries, respectively, referencing existing.a files or.so files
  2. Create your own application libraryhello-libs
  3. Expose header files before joining
  4. Link to static/dynamic libraries

It makes sense. Once edited and synced, you can see that the C/C ++ code in hello-libs can call internal methods referring to the exposed header file.

3. Data and literature

The first Android NDK official documentation, although many are incomplete, but is definitely something to read.

When I first started NDK development and thought the new Hello World project was too easy. I suggest pulling down the Googlesamples – Android – NDK project. There are several examples of reference, much more complete than the official document.

Google Samples

If you find that some of the NDK configurations in the sample do not meet your requirements, you will need to check the official CMake documentation for the full supported functions, and there is also a simple Chinese translation of the CMake manual.

The above documents are only for solving the compilation and configuration problems during NDK development. The specific C/C ++ logic writing and JNI are not included in this category.

eggs

At the end of the article, there’s a bunch of Easter eggs and a Q&A list of the pitfalls and tricks you’ve encountered during CMake or NDK development. Continuously updated

Q1: how do you specify the C++ standard?

A: In build_gradle, configure cppflags-std

externalNativeBuild {
  cmake {
    cppFlags "-frtti -fexceptions -std=c++14"
    arguments '-DANDROID_STL=c++_shared'
  }
}Copy the code

Q2: How does add_library compile all the source files in a directory?

A: Use the aux_source_directory method to put the entire list of paths into A variable.

${CMAKE_HOME_DIRECTORY}/ SRC/API SRC_LIST aux_source_directory(${CMAKE_HOME_DIRECTORY}/src/core CORE_SRC_LIST) list(APPEND SRC_LIST ${CORE_SRC_LIST}) add_library(native-lib SHARED ${SRC_LIST})Copy the code

Q3: How to debug code in cmakelists.txt?

A: Use the message method

Cmake_minimum_required (VERSION 3.4.1) message(STATUS "execute CMakeLists")...Copy the code

Then after the operation in the externalNativeBuild/cmake/debug / {} abi/cmake_build_output TXT view log.

Q4: When will cmakelists. TXT be executed?

A: I’ve tested it and it looks like it will be executed in sync. Something like a file cache that generates a makefile after execution is placed in externalNativeBuild. So if there is no modification in cmakelists.txt, it seems that the resynchronization will not be performed again. (Or delete the.externalNativeBuild directory)

The actual compile appears to be just reading the parsed makefile in the. ExternalNativeBuild directory and compiling it. Cmakelists.txt will no longer be executed