NDK避坑指南(2)cmake和ndk-build实现NDK

2,924 阅读13分钟

我们在前面的文章中大致了解了NDK的概念以及NDK的大致组成部分

现在该让我们亲手实现一下NDK了

本文中我将使用Cmake 和 ndk-build来分别实现NDK

一. 实现NDK的大致步骤

实现NDK的完整步骤如下:

  1. 创建Android ndk project,使用Native关键字定义Java方法(即需要调用的native方法)
  2. 使用javac编译上述 Java源文件 (即 .java文件)最终得到 .class文件
  3. 通过 javah 命令编译.class文件,最终导出JNI的头文件(.h文件)
  4. C++实现在 Java中声明的Native方法
  5. Activity加载so库,使用native方法
  6. 生成.so库文件
  7. 执行代码

我们可以发现实际上实现NDK的步骤和实现JNI的步骤基本是完全一样的

3,4步骤在cmakendk-build中自动为我们做好了,我们没必要手动完成,所以可以省去3,4步骤

生成.so文件的操作也是自动完成的,我们也可以省去

所以改变后我们需要手动完成的步骤如下

  1. 创建Android ndk project,使用Native关键字声明native方法
  2. C++实现Native方法
  3. Activity中写加载so库,调用native方法的代码
  4. 执行代码

下面我将按照上面的步骤一步步的实现NDK

实现NDK的前提是我们得先配置NDK环境

但是因为环境变量的配置网上很多,所以这里我省略了环境变量的配置的阐述

二.实现NDK的相关配置

1. 使用Android studio自动创建一个Android NDK 项目

Android studio很贴心的为我们提供了自动化创建一个Android NDK 项目的傻瓜式操作

我们只要点击Android studio中的

File -> new -> new project

然后选择下图中的nativeC++,然后和普通的Android project进行一样的操作就行了

image-20200527211046058«

2. 手动创建 Android NDK Project

虽然Android studio 提供了自动创建 NDK 项目的功能,可以自动进行NDK所需的配置

但是为了更好的了解 NDK ,也为了需要修改NDK的相关文件时知道怎么修改, 我们还是有必要了解怎么进行手动配置

因为我们已经知道了 NDK 的编译工具分为 cmakendk-build ,所以下面我将分别说明三类配置

  1. 通用配置
  2. cmake 配置
  3. ndk-build 配置

注意: 通用配置是一定要配置的,cmake和ndk-build配置只要选择一个就够了

2.1 通用配置

  • 设置 NDK 的路径

    在项目的 local.properties 中进行如下的设置

    注意:这里的 ndk.dir 要设置成你自己的ndk路径

ndk.dir=/Users/fczhao/Library/Android/sdk/ndk/20.0.5594570
  • 设置是否支持以前的版本

    因为我们要操作的Android ndk project 中有许多有悠久的历史,所以使用的还是好多年前的ndk包,为了让 gradle 支持以前的 ndk ,我们可以对gradle进行设置

    当然如果你的NDK使用的是现在的版本,可以不进行配置

    在项目的 gradle.properties 中进行如下的设置

android.useDeprecatedNdk=true 

2.2 cmake 配置

完成cmake配置时,不要忘了配置通用配置

在使用cmake时,我们必须要配置四个文件,在下图中我圈出来了即是这四个文件

同时我也将在下面分别说明他们进行了什么改变

注意:下面我分析的代码是Android studio自动生成的代码

image-20200529172759406

  1. 在使用 NDK 的 **module**的 build.gradle 中进行如下的配置

    注意

    这里我们的module叫 app,所以在**app**里面进行配置

    同时是在module中配置,而不是在 project 中的 build.gradle 中配置

android {
		......
    defaultConfig {
				......
        externalNativeBuild {
            cmake {
                cppFlags ""
            }
        }
    }
        //extenalNativeBuild 是 gradle 的一个属性
        //可以指定 NDK 的配置,里面主要有 cmake 和 ndk-build两个属性
        //cmake 和 ndk-build 中的属性可以指定 cmake 和 ndk-build 的版本,路径等等
    externalNativeBuild {
        cmake {
        		//指定了 CmakeLists.txt 文件的路径
          	//指定了 cmake的版本
            path "src/main/cpp/CMakeLists.txt"
            version "3.10.2"
        }
    }
}

main 目录下面创建 cpp 目录 , 里面创建两个文件**CmakeLists.txtxxx.cpp**

  1. CMakeLists.txt

    这个文件主要配置 cmake ,上面代码中的path指向的就是这个文件的路径

下面的是最基础的配置,即如果要使用cmake,那么一定要在 cmakeList.txt 进行配置的

# 注意:native-lib是Android studio自动生成的,要是喜欢,可以改成其他的名字

# 设置cmake的最小版本
cmake_minimum_required(VERSION 3.4.1)


# add_library
# 可以创建一个so库,同时可以进行下面几个配置
# 1.设置编译后 so文件的名字
# 2.把它设为 静态库(Static) 或 共享库/动态库(shared)
# 3.提供这个库的源码所在的 相对路径

# 也可以定义多个库,Gradle会自动将共享库与APK打包在一起

add_library( 
						 # 设置编译后 so 库 的名字,这里名字设置为 native-lib
						 # .so 文件可以和 .cpp 文件的名字不同,即我这里可以不用 native-lib 这个名字
             native-lib

             # 设置生成的文件为共享库,即 so 文件,这里生成 native-lib.so
             # NDK中的library分为 静态库(static)和共享库(shared)
             # 如果要生成静态库,这里设置为 STATIC ,生成后文件为 native-lib.a
             SHARED
    
             # 源文件的相对路径,即要被编译的文件路径
             # 有这个,我们其实可以把 xxx.cpp 放在任何位置,但是一般我们都放在 main/cpp下面
             # 这个cpp文件的路径可以有多个,即多个cpp文件编译到成一个so文件
             native-lib.cpp )


# find_library 
# 从系统库中搜索指定的依赖并设置它的别名
# 这里我们从系统查找 log 库,并把他的别名设置为 log-lib

# 由于默认情况下CMake在搜索路径中包含系统库,因此只需要指定要添加的依赖库的名称即可
# 在完成编译之前,CMake会检查该库是否存在

find_library( 
							# 设置该依赖库的别名
              log-lib

              # 指定您要CMake查找的的依赖库的名称
              log )

# target_link_libraries
# 配置库的依赖关系
# 可以依赖多个库,比如可以依赖下面的几个
# 1. 自己在项目中定义的库
# 2. 依赖的第三方库
# 3. 系统库

target_link_libraries( # 指定需要这些依赖的源文件
                       native-lib

                       # 依赖库,可以是多个
                       ${log-lib} )
  1. xxx.cpp 这个文件就是我们要进行调用的 C++ 代码

    这里是 native-lib.cpp , 里面的大致代码如下

#include <jni.h>
#include <string>
//导入C++的头文件

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_ndk_1test_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

xxx.cpp 中的一些注意事项

​ extern "C" JNIEXPORT jstring JNICALL Java_com_example_ndk_1test_MainActivity_stringFromJNI

​ 看起来很长,实际上我们可以把它分成三部分

  1. extern "C"

    这个表示使用C语言的方式来进行编译

    每个方法都需要这个来定义,但是如果我们有多个方法时,可以使用**extern "C"{...} **来定义

    extern "C"{
    JNIEXPORT jstring JNICALL
    Java_com_example_myapplication_MainActivity_stringFromJNI(...) {...}
    
    JNIEXPORT jstring
    JNICALL Java_com_example_myapplication_MainActivity_sayHelloWorld(...) {...}
    }
    
  2. JNIEXPORT jstring

    这个声明了该方法的返回类型,这里的返回类型为 string

    JNI中的返回类型和Java,C++中的不太一样,具体的可以去网上查一下

  3. JNICALL Java_com_example_ndk_1test_MainActivity_stringFromJNI

    这里实际上是标志了我们要JNI要调用方法(或者说我们要用C++覆写的native方法)的具体路径

    这个实际上也是由三个部分组成

    a. Java 标识了这个是Java方法,J一定要大写

    b. 包名,com_example_ndk_1test_MainActivity是我们的包名

    c. 方法名 stringFromJNI是我们要覆写的方法名

如果仔细观察,会发现路径中有 __1这两个东西,那这两个东西到底是什么呢,实际上在Java包名转换成这个的过程中有下面两个规则

a. 包中的 .会替换为_

b. 包中的_会替换为_1

​ 所以我们原来的包名是com.example.ndk_test.MainActivity.stringFromJNI转换后变成了com_example_ndk_1test_MainActivity_stringFromJNI

2.3 ndk-build 配置

在完成ndk-build时,不要忘记配置通用配置

在ndk-build中,我们要修改的文件如图所示:

image-20200602171959505

  • 红色部分是我们要进行修改的
  • 黄色部分是我们要删除的,因为黄色部分是Android studio自动生成的关于cmake实现的部分,我们这里要使用ndk-build,拿来没用,所以要删除

下面我将依次分析红色部分的内容:

  • module中的build.gradle

    在这里我们声明我们要使用ndk-build

    代码如下:

    android {
    		...
        defaultConfig {
          ...
    //				这里注释掉,因为这里是Android studio 自动生成的关于cmake的代码        
    //        externalNativeBuild {
    //            cmake {
    //                cppFlags ""
    //            }
    //        }
        }
    		...
    
        externalNativeBuild {
          
    //        cmake {
    //            path "src/main/cpp/CMakeLists.txt"
    //            version "3.10.2"
    //        }
          
    //			ndkBuild path是 Android.mk文件的路径      
            ndkBuild{
                path 'src/main/jni/Android.mk'
            }
        }
    }
    
  • 创建jni文件夹,里面创建Android.mk,Application.mk,xxx.cpp

  • Android.mk

      # 这个是源文件的路径,call my-dir表示了返回当前Android.mk所在的目录
      LOCAL_PATH := $(call my-dir)
      
      # 清除许多 LOCAL_XXX 变量
      # 注意:不会清除 LOCAL_PATH 
      include $(CLEAR_VARS)
      
      # LOCAL_MODULE 变量存储要构建的模块的名称
      # 这里最终会生成叫 libnative-lib.so 的文件
      LOCAL_MODULE := native-lib
      
      # 源文件名字
      # 可以有多个源文件,使用空格隔开
      LOCAL_SRC_FILES := native-lib.cpp
      
      # 指定编译出什么二进制文件
      # 这里编译出共享库,即:.so文件
      # 编译出静态库可以使用: BUILD_STATIC_LIBRARY
      include $(BUILD_SHARED_LIBRARY)
    
  • Application.mk

    # 定义生成的二进制文件要生成的CPU架构
    # 这里指定生成 arm64-v8a 可以用的二进制文件
    APP_ABI := arm64-v8a
    
    # 定义可以使用该二进制文件的Android版本
    # 注意:比 android:minSdkVersion 值大时,会有警告
    APP_PLATFORM := android-21
    
    # 默认情况下,ndk-build 假定 Android.mk 文件位于项目根目录的相对路径 jni/Android.mk 中。
    # 要从其他位置加载 Android.mk 文件,将 APP_BUILD_SCRIPT 设置为 Android.mk 文件的绝对路径。
    APP_BUILD_SCRIPT
    
    # 没有这个会出现error: no template named 'pair'的问题
    APP_STL := c++_shared
    
  • native-lib.cpp

    这里的cpp实际上和上面cmake中的是完全一样的,这里我也特地把他命名为cmake中的,具体要注意的事情参考上面cmake中的即可

    到了这一步(完成Android.mk, Application.mk , xxx.cpp的配置后),我们就可以生产.so文件了

    命令行进入jni目录下,输入ndk-build即可

    可能输入后,会有一些问题,关于这些问题,我将在后面的博客中进行阐述

  • MainActivity

    MainActivity中的设置也是和cmake中的一样的,ndk-build 中我也特地把生成.so文件命名为cmake中的

三. 使用cmake来实现ndk

现在我们已经知道了cmake实现NDK的一些东西了,下面让我们尝试自己动手实现一下

cmake为我们减少了很多的复杂操作,所以我们只需要三步就可以实现NDK

  1. 声明Native方法
  2. 实现Native方法,经过一系列操作,最终生成.so 文件
  3. 加载.so文件,调用Native方法

下面我将具体的讲述每一个步骤是怎么实现的:

1 在Activity中定义一个native方法

public class MainActivity extends AppCompatActivity {
  
		//定义Native方法
    public native String stringFromJNI();
}

2. 用C++实现Native方法

注意:是在**cmakeLists.txtadd_library指定的位置实现**

如在上面我们指定了 native-lib.cpp会被我们编译

那么我们只能在native-lib.cpp中实现Native方法,否则我们写的 C++ 代码没有任何作用

   add_library( 
   					......
            native-lib.cpp )

并且注意覆写的方法名是否正确,如上文的

extern "C" JNIEXPORT jstring JNICALL Java_com_example_myapplication_MainActivity_stringFromJNI

但是我们可以发现这个方法名太长了,所以Android studio 也提供了很简便的方法来自动生成方法名

我们在Activity中的Native方法,按下option + enter

image-20200529115115402

然后就会自动生成下面的代码

extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_myapplication_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {
    // TODO: implement stringFromJNI()
}

然后我们在里面写我们需要实现的C++代码即可

这里我们返回一个 "Hello Wrold ,this is from C++ "

extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_myapplication_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {
     std::string hello = "Hello Wrold ,this is from C++  ";
    return env->NewStringUTF(hello.c_str());
}

3. 在Activity中调用 相应的库(共享库或者静态库)

//加载so文件
  static {
        System.loadLibrary("native-lib");
    }

4. 调用Native方法

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    TextView tv = findViewById(R.id.sample_text);

  	//调用这个方法
    tv.setText(stringFromJNI());
}

最后运行这个Android project

因为我们在上面的Activity中把stringFromJNI的值设置给了TextView,所以可以显示Hello Wrold ,this is from C++

5. .so文件

这个过程看起来挺简单的,但是如果我们看了上面的步骤:

  1. 声明Native方法

  2. 实现Native方法,经过一系列操作,最终生成.so 文件

  3. 加载.so文件

  4. 调用Native方法

    会发现这样一个问题,我们在这个过程中的需要的 .so文件去哪里了呢?

    实际上,如果我们仔细寻找,可以在**app/build/intermediates/cmake**目录下找到,如下图:

    因为我是在app这个module中使用了ndk,所以这里是app,app改成你使用了ndk的module就行了

    image-20200529203427537

5.1 验证Activity调用的是 .so文件

但是为了更好的了解NDK的实现过程,以及确定Activity的确是调用 .so 库,而不是直接调用 C++ 代码

所以我们可以使用这样的思路来验证:

使编译的.cpp文件与生成的.so文件不同名,Activity加载.cpp名字的文件,看是否可以运行

我们使用下面的步骤来进行验证

  1. 修改cmakeLists.txt

    我们上面已经知道,cmakeLists.txt决定了cmake的编译

    所以我们修改生成.so文件的相关代码,实现.cpp.so不同名

    add_library( 
    						 # 设置编译后 so 库 的名字,这里名字设置为 native-test
    						 # 最后会生成 libnative-test.so
    						 # 这里我们让 .cpp 与 .so 不同名
                 native-test
                 
                 native-lib.cpp
    )
    
    target_link_libraries( 
    											 # 这里必须与 .so 文件同名
                           native-test
                           ...	)
    

    然后我们到cmake目录下去找,看到生成了libnative-test.so

    image-20200530170251403

    然后Activity还是上面的代码,加载 native-lib文件

    static {
        System.loadLibrary("native-lib");
    }
    

    运行代码,会发现有这样的报错:

     java.lang.UnsatisfiedLinkError: 
     ......
     couldn't find "libnative-lib.so"
    

    这里我们就可以完全明白了 Activity中调用的是 .so文件,而不是直接调用C++代码

    我们把代码改成 native-test后,运行成功

    static {
        System.loadLibrary("native-test");
    }
    

四. 使用ndk-build实现NDK

这里我将使用ndk-build来实现NDK,ndk-build中的很多注意事项,实际上在上面的配置和cmake实现NDK过程中已经很清楚的说明了,所以已经说过的提示,这里将不赘述

1. 使用Android studio创建project

因为Android studio中现在默认使用cmake,所以自动创建的是已经配置好的cmake环境,我们这里使用ndk-build,需要对project进行一些改变,如下:

  • 删除cpp目录下所有文件
  • 建立jni目录
  • jni目录下建立三个文件,Android.mk,Application.mk,xxx.cpp,这里的xxx.cpp为了方便,也为了保留native-lib.cpp中的JNI语句方便以后我们参考,我们可以直接把cpp目录下的native-lib.cpp弄过来
  • 其他的配置将在下面说明

2. gradle.properties

因为ndk-build常在历史很久的项目中使用,所以一般使用的都是几年前的NDK,为了让Android studio 支持以前的NDK,我们必须写这个

android.useDeprecatedNdk = true

3. module中的build.gradle

android {
		...
    defaultConfig {
      ...
//				这里注释掉,因为这里是Android studio 自动生成的关于cmake的代码        
//        externalNativeBuild {
//            cmake {
//                cppFlags ""
//            }
//        }
    }
		...
    externalNativeBuild {
      
//        cmake {
//            path "src/main/cpp/CMakeLists.txt"
//            version "3.10.2"
//        }
//			ndkBuild path是 Android.mk文件的路径      
        ndkBuild{
            path 'src/main/jni/Android.mk'
        }
    }
}

4. jni目录下的配置

  • Android.mk
 # 这个是源文件的路径,call my-dir表示了返回当前Android.mk所在的目录
  LOCAL_PATH := $(call my-dir)
  
  # 清除许多 LOCAL_XXX 变量
  # 注意:不会清除 LOCAL_PATH 
  include $(CLEAR_VARS)
  
  # LOCAL_MODULE 变量存储要构建的模块的名称
  # 这里最终会生成叫 libnative-lib.so 的文件
  LOCAL_MODULE := native-lib
  
  # 源文件名字
  # 可以有多个源文件,使用空格隔开
  LOCAL_SRC_FILES := native-lib.cpp
  
  # 指定编译出什么二进制文件
  # 这里编译出共享库,即:.so文件
  # 编译出静态库可以使用: BUILD_STATIC_LIBRARY
  include $(BUILD_SHARED_LIBRARY)
  • jni目录下的Application.mk
# 定义生成的二进制文件要生成的CPU架构
# 这里指定生成使用CPU架构都可以用的二进制文件
APP_ABI := all

# 定义可以使用该二进制文件的Android版本
# 注意:比 android:minSdkVersion 值大时,会有警告
APP_PLATFORM := android-21

# 默认情况下,ndk-build 假定 Android.mk 文件位于项目根目录的相对路径 jni/Android.mk 中。
# 要从其他位置加载 Android.mk 文件,将 APP_BUILD_SCRIPT 设置为 Android.mk 文件的绝对路径。
APP_BUILD_SCRIPT

# 没有这个会出现error: no template named 'pair'的问题
APP_STL := c++_shared
  • jni目录下的native-lib.cpp
#include <jni.h>
#include <string>

//因为只声明了这个方法,所以这里只实现了stringFromJNI方法
//native方法的实现按照这个的格式即可
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapplication_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello , this is from C11 ";
    return env->NewStringUTF(hello.c_str());
}

JNI语法中的一些注意点在上面的cmake部分已经强调了,这里不再赘述

5. 加载so库 调用native方法

public class MainActivity extends Activity {

    // 加载 so 库
  	// 这里的名字一定和 Android.mk 中 LOCAL_MODULE 相同
	  // 如:LOCAL_MODULE := native-lib
    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 调用native方法
        TextView tv = findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());
    }
  	//声明native方法
    public native String stringFromJNI();
}

6. so

命令行进入jni目录,输入ndk-build即可

 fczhao@iMac  ~/SelfCode/Android/MyApplication/app/src/main/jni  ndk-build                                                                                                                                                          ✔  509  13:09:44
Android NDK: WARNING: APP_PLATFORM android-28 is higher than android:minSdkVersion 1 in /Users/fczhao/SelfCode/Android/MyApplication/app/src/main/AndroidManifest.xml. NDK binaries will *not* be compatible with devices older than android-28. See https://android.googlesource.com/platform/ndk/+/master/docs/user/common_problems.md for more information.    
[arm64-v8a] Compile++      : native-lib <= native-lib.cpp
[arm64-v8a] SharedLibrary  : libnative-lib.so
[arm64-v8a] Install        : libnative-lib.so => libs/arm64-v8a/libnative-lib.so
[arm64-v8a] Install        : libc++_shared.so => libs/arm64-v8a/libc++_shared.so
[x86_64] Compile++      : native-lib <= native-lib.cpp
[x86_64] SharedLibrary  : libnative-lib.so
[x86_64] Install        : libnative-lib.so => libs/x86_64/libnative-lib.so
[x86_64] Install        : libc++_shared.so => libs/x86_64/libc++_shared.so
[armeabi-v7a] Compile++ thumb: native-lib <= native-lib.cpp
[armeabi-v7a] SharedLibrary  : libnative-lib.so
[armeabi-v7a] Install        : libnative-lib.so => libs/armeabi-v7a/libnative-lib.so
[armeabi-v7a] Install        : libc++_shared.so => libs/armeabi-v7a/libc++_shared.so
[x86] Compile++      : native-lib <= native-lib.cpp
[x86] SharedLibrary  : libnative-lib.so
[x86] Install        : libnative-lib.so => libs/x86/libnative-lib.so
[x86] Install        : libc++_shared.so => libs/x86/libc++_shared.so

我们可以看到他们生成了:x86,x86_64,arm64-v8a,armeabi-v7a的``so`文件

image-20200602131259569

可以看见创建了libs , obj,两个目录来存放生成的so文件

最后运行Android studio project即可