AndroidStuio中的NDK开发初探

前段时间由于做比赛的事,一直都没时间写博客,现在终于可以补上一篇了,一直想学习一点NDK开发的知识,但是迟迟没有动手,正好有一个NDK相关的项目机会,便查阅了一些资料,遂将学习的一些心得方法记录于此。

其实写这篇博客还有一个目的,在我搜寻NDK相关学习资料的过程中,大部分都是基于eclipse开发的,所以有些过时,而现在Google推荐使用AndroidStudio+CMake的方式进行NDK开发,所以想更新一下有些知识,便于大家学习参考。

首先说说这次的开发工具及版本

1
2
3
4
AndroidStudio 2.3.3
NDK 15.1.4
CMake 3.6.4
Genymotion 模拟器

一、相关概念介绍

1 . 什么是NDK

NDK是一个让开发人员在android应用中嵌入使用本地代码编写的组件的工具集。 Android应用运行在Dalvik虚拟机中。NDK允许开发人员使用本地代码语言(例如C和C++)实现应用的部分功能。

上面是比较官方的介绍,通俗点来讲,就是帮助我们可以在Android应用中使用C/C++来完成特定功能的一套工具。

2 . NDK的应用场景

不是说什么场景下我们都要使用NDK来开发Android的功能,由于NDK开发在一定程度上加大了项目的开发难度,我们应该综合考虑各种因素和条件,在特定场景下选用NDK来开发Android的特定功能,下面就是一些NDK适用的场景。

1
2
3
4
5
6
7
1 . 重要核心代码保护。由于java层代码很容易反编译,而C/C++代码反汇编难度很大,所以对于重要的代码,可以使用C/C++来编写,Android去调用即可。

2 . Android中需要用到第三方的C/C++库。由于很多优秀的第三方库(比如FFmpeg)都是使用C/C++来编写的,我们想要使用它们,就必须通过NDK的方式来操作。

3 . 便于代码的移植。比如我们对于一些核心的公共组件(比如微信开源的的Mars),可能需要写一套代码在多个平台上运行(比如在Android和iOS上共用一个库),那么就需要选用NDK的方式。

4 . 对于音视频处理、图像处理这种计算量比较大追求性能的场景,也需要使用到NDK。

3 . 什么是交叉编译

交叉编译通俗一点讲,就是在一个平台上生产在另一个平台上可执行的代码。比如我们在电脑上为一些硬件开发驱动,最终编译出的代码需要在硬件上使用。还有我们在电脑上将C/C++代码编译成相应的库,然后在ARM、x86、mips等平台上使用。NDK中就我们提供了交叉编译的工具,帮助我们可以将我们编写的C/C++代码生成各个平台需要的库。

4 . 什么是jni

JNI的全称是Java Native Interface,它允许Java语言可以按照一定的规则去调用其他语言,与其进行交互。

jni的实现流程如下:

编写Java代码(*.java) —————> 编译生成字节码文件(*.class) —————> 产生C头文件(*.h) —————> 编写jni实现代码(*.c) —————> 编译成链接库(*.so)

5 . 什么是链接库

链接库可以简单理解为函数库,就是我们的C/C++代码编译生成的产物,供我们的java进行调用,同时,它又分为动态链接库和静态链接库。

动态链接库 : 在程序运行时才载入所需要的库,所以控制比较灵活,整个可执行文件的体积较小。

静态链接库 : 在程序的链接阶段,将其引用的代码也一并打包在了最终的可执行文件中,这样做的好处是可以不再依赖与环境,移植方便,但是这样做会使可执行文件体积较大。在Android中的静态链接库是.a文件。

6 . 什么是CMake

CMake是一款开源的跨平台自动化构建系统,它通过CMakeLists.txt来声明构建的行为,控制整个编译流程,我们在接下来的NDK开发中将会使用它配合Gradle来进行相关开发。

二、配置NDK开发环境

俗话说 工欲善其事必先利其器,接下来,我们先配置一下我们在开发NDK过程中要使用到的一些工具。

1 . 安装NDK

打开AndroidStudio,在如图所示的地方找到 SDK Tools, 勾选 NDK、LLDB、CMake,然后点击 Apply ,等待其下载安装完成,便配置好了基本的开发环境。

安装NDK

1
安装的工具中NDK和CMake上面已经介绍过了,LLDB是一款在开发NDK过程中的调试器,这篇博客中将不会介绍。

做完了上面的步骤我们就可以开始我们的第一个NDK程序了。

三、创建第一个NDK程序

下面我将以图示加序号的方式来说明新建步骤。

1 . 新建一个项目,填写基本信息,记得勾选Include C++ support,便于AndroidStudio为我们生成一些默认的配置。

新建项目1

新建项目2
2 . 接下来的几个步骤就选择默认设置

3 . 到最后一步如图,C++ Standard 选择 Toolchain Default,其它不变即可。

新建项目3

说明:

(a) C++ Standard是让我们选择C++标准,我们使用默认的CMake的设置

(b) Exceptions Support是添加C++中对于异常的处理,如果选中,Android Studio会
将 -fexceptions标志添加到模块级build.gradle文件的cppFlags中,Gradle会将其传递到CMake。

(c) Runtime Type Information Support是启用支持RTTI,请选中此复选框。如果选中,Android Studio会将-frtti标志添加到模块级build.gradle文件的cppFlags中,Gradle会将其传递到 CMake。

新建好的项目如图

新建好的项目

下面我们看看这个默认的项目中AndroidStudio都为我们做了哪些事 :

(1) 在app 模块中新建了一个cpp文件夹用来放置我们的C/C++文件,此处默认的文件为native-lib.cpp

native-lib.cpp文件内容:

1
2
3
4
5
6
7
8
9
10
11
#include <jni.h>
#include <string>

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

上面的代码中先是引入了固定的头文件jni.h,然后是引入了代码中需要用到的头文件,至于后面的返回字符串,我们在后面的时候将会讲到,现在只需要知道它就是返回了Hello from C++这个字符串即可。

上面的extern "C" 是告诉编译器按照C语言的规则来编译我们下面的代码

(2) 在app 模块下新建了一个CMakeLists.txt文件用于定义一些构建行为

CMakeLists.txt文件内容 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.4.1)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

add_library( # Sets the name of the library.
native-lib

# Sets the library as a shared library.
SHARED

# Provides a relative path to your source file(s).
src/main/cpp/native-lib.cpp )

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

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 )

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
native-lib

# Links the target library to the log library
# included in the NDK.
${log-lib} )

上面的完成的有注释的内容,但其中最核心的也就几句,下面分别做介绍:

cmake_minimum_required(VERSION 3.4.1) 用来设置在编译本地库时我们需要的最小的cmake版本,AndroidStudio自动生成,我们几乎不需要自己管。

1
2
3
4
5
6
7
8
add_library( # Sets the name of the library.
native-lib

# Sets the library as a shared library.
SHARED

# Provides a relative path to your source file(s).
src/main/cpp/native-lib.cpp )

add_library用来设置编译生成的本地库的名字为native-libSHARED表示编译生成的是动态链接库(这个概念前面已经提到过了),src/main/cpp/native-lib.cpp表示参与编译的文件的路径,这里面可以写多个文件的路径。

find_library 是用来添加一些我们在编译我们的本地库的时候需要依赖的一些库,由于cmake已经知道系统库的路径,所以我们这里只是指定使用log库,然后给log库起别名为log-lib便于我们后面引用,此处的log库是我们后面调试时需要用来打log日志的库,是NDK为我们提供的。

target_link_libraries 是为了关联我们自己的库和一些第三方库或者系统库,这里把我们把自己的库native-lib库和log库关联起来。

(3)在 app 模块对应的build.gradle文件中增加了一些配置,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
apply plugin: 'com.android.application'

android {
compileSdkVersion 25
buildToolsVersion "25.0.3"
defaultConfig {
applicationId "com.codekong.ndkdemo"
minSdkVersion 15
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:25.3.1'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
testCompile 'junit:junit:4.12'
}

主要的变化就两点:
(a) 在 android 的大括号内增加了 externalNativeBuild标签

1
2
3
4
5
externalNativeBuild {
cmake {
cppFlags ""
}
}

这里的cppFlags里面的内容为空,这里其实就是配置了我们在新建项目的时候的第(3)步中讲到的,如果我们勾选了异常支持和RTTI支持,这里就会有相关的配置信息。

(b) 使用 externalNativeBuild 来指定 CMakeLists.txt文件的路径,由于build.gradle文件和CMakeLists.txt文件在同一目录下,所以此处就直接写文件名啦。

1
2
3
4
5
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}

(4) 最终在MainActivity.java 文件中我们看到了函数的调用过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MainActivity extends AppCompatActivity {

// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}

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

// Example of a call to a native method
TextView tv = (TextView) findViewById(R.id.sample_text);
tv.setText(stringFromJNI());
}

/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
}

我们看到其实这里就主要做了三步操作:
(a)使用 native 关键字声明了一个本地方法 stringFromJNI()

(b)使用loadLibrary()方法载入我们编译生成的动态链接库,这里要注意,虽然我们生成的动态链接库名称为libnative-lib.so,但是此处我们只需要写 native-lib,即就是我们在CMakeLists.txt文件中指定的名称,其中的lib前缀和.so后缀是系统为我们添加的。

(c)我们在布局文件中放了一个TextView,然后将函数返回的字符串放到了TextView中。

我们对比一下我们声明的native方法和最终我们的ndk帮我们生成的c++代码的函数名:

1
2
3
4
5
6
//我们声明的native方法名
public native String stringFromJNI();

//ndk帮我们生成的c++方法名
JNIEXPORT jstring JNICALL
Java_com_codekong_ndkdemo_MainActivity_stringFromJNI(JNIEnv *env, jobject /* this */)

我们看到ndk生成的方法名是以 Java_包名_类名_方法名 的形式,其实这个方法名是javah帮助我们生成的。

注:我们对于新创建的项目可以点击菜单栏的Build——> Make Project来先编译项目,然后在 <项目目录>\app\build\intermediates\cmake\debug\obj\armeabi 下面就可以看到生成的动态链接库。由于我们没有指定我们需要生成什么平台的so库,所以系统帮我们生成了各个平台的库,分别放在对应的文件夹下面。

好了,以上就是我们使用AndroidStudio创建的第一个项目的分析,了解了上面这些,我们就基本了解了NDK开发的的一般步骤。

四、NDK开发中常用的函数

上面我们只是看了AndroidStudio为我们生成的代码,还没有自己动手写一行代码,下面我们就开始动手写代码啦。下面我们就自己新建一个项目,主要学习一下NDK里面的字符串操作和数组的操作。

1 . 新建项目,这个过程,我们在上一步的 三、创建第一个NDK程序 中已经讲到了,这里不再赘述。

2 . 删除项目为我们自动生成的native-lib.cpp文件,然后在cpp目录下新建一个hello-lib.c的文件,这时候AndroidStudio就会提醒我们这个文件没有在CMakeLists.txt文件中进行配置,所以我们去改动一下该文件,改动如下:

1
2
3
4
5
6
7
8
9
10
11
cmake_minimum_required(VERSION 3.4.1)

add_library(hello-lib
SHARED
src/main/cpp/hello-lib.c )

find_library(log-lib
log )

target_link_libraries(hello-lib
${log-lib} )

这里我们把我们新建的hello-lib.c的路径加入到了CMakeLists.txt文件中,而且也将log库与我们的库关联了起来,其他的具体信息前面已经讲过了。

3 . 我们在MainActivity.java文件对应的布局文件中放入一个TextView,并且在MainActivity.java中获取它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.codekong.ndkdemo;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

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

4 . 接着我们在MainActivity.java文件中写一个native函数sayHelloWorld(),并将其返回的字符串设置给TextView,然后使用loadLibrary载入我们的自定义库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.codekong.ndkdemo;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("hello-lib");
}

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

TextView tv = (TextView) findViewById(R.id.sample_text);
//将返回值设置给TextView
tv.setText(sayHelloWorld());
}

//自定义的native函数
public native String sayHelloWorld();
}

5 . 见证AndroidStudio强大的地方到了,我们在我们声明的sayHelloWorld()函数上按住Alt+Enter,就会自动生成C++代码,但是,这里存在一个问题,初次生成,AndroidStudio会创建一个jni文件夹,然后在里面创建hello-lib.c文件,并且自动生成对应的C代码,但是,由于我们在CMakeLists.txt中指定的路径为src/main/cpp/hello-lib.c,所以我们这里直接将我们的src/main/jni/hello-lib.c中的代码拷贝到src/main/cpp/hello-lib.c中,并将jni目录删除即可。hello-lib.c中的内容如下:

1
2
3
4
5
6
7
#include <jni.h>

JNIEXPORT jstring JNICALL
Java_com_codekong_ndkdemo_MainActivity_sayHelloWorld(JNIEnv *env, jobject instance) {

return (*env)->NewStringUTF(env, "Hello World");
}

上面的代码中,我们拿到了jni环境指针,然后调用其NewStringUTF()方法,传入env指针和我们需要的字符串,便可以了。

运行程序,便可以看到界面上显示Hello World

下面我们开始看看java中的类型和native类型的对应关系:
基本数据类型

引用类型

数组类型

可以看出上面的类型对应关系还是十分清楚的,其实我们在jni.h文件中就可以看到上述的定义。

下面我们主要说说字符串的使用和数组的使用

(1)字符串的使用

其实上面新建的项目就已经演示了返回字符串的例子,使用(*env)->NewStringUTF(env, "Hello World");即可返回字符串结果,下面在看看如何处理java传入的字符串。通过jni将Java传入的字符串写入文件。

(a) 在Mainactivity中添加如下代码

1
public native void writeFile(String filePath);

(b) 在hello-lib.c中生成如下代码

1
2
3
4
5
JNIEXPORT void JNICALL
Java_com_codekong_ndkdemo_MainActivity_writeFile(JNIEnv *env, jobject instance, jstring filePath_) {
const char *filePath = (*env)->GetStringUTFChars(env, filePath_, 0);
(*env)->ReleaseStringUTFChars(env, filePath_, filePath);
}

上面是AndroidStudio生成的代码,可以看出它主要用到了 (*env)->GetStringUTFChars(env, filePath_, 0); 来将java传入的字符串转化为C语言的char指针,最后又使用(*env)->ReleaseStringUTFChars(env, filePath_, filePath);将我们的指针指向的空间释放。

(c)我们可以在这个基础上写一个写入文件的小例子,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
JNIEXPORT void JNICALL
Java_com_codekong_ndkdemo_MainActivity_writeFile(JNIEnv *env, jobject instance, jstring filePath_) {
const char *filePath = (*env)->GetStringUTFChars(env, filePath_, 0);

FILE *file = fopen(filePath, "a+");

char data[] = "I am a boy";
int count = fwrite(data, strlen(data), 1, file);
if (file != NULL) {
fclose(file);
}
(*env)->ReleaseStringUTFChars(env, filePath_, filePath);
}

以上代码记得加头文件

1
2
3
#include <jni.h>
#include <stdio.h>
#include <string.h>

(d)还要记得在AndroidMainfest.xml文件中添加文件读写权限,然后在MainActivity.java中调用native方法

1
2
3
4
5
6
7
8
9
10
11
12
13
static {
System.loadLibrary("hello-lib");
}

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

String filePath = "/mnt/sdcard/boys.txt";
Toast.makeText(MainActivity.this, filePath, Toast.LENGTH_SHORT).show();
updateFile(filePath);
}

注意:由于我这里使用的是Genymotion模拟器,所以那样写文件路径就表示文件管理器根目录。

运行上面的程序,就可以在文件管理器根目录下发现boys.txt,并在其中发现我们写入的字符串。

(2) 数组的使用

现在我们看看我们如何在jni中使用数组。

数组的操作主要有以下两种方式(我们这里仍然用我们刚才的hello-lib.c文件测试):

(a) 直接操作数组指针。

我们现在看看在MainActivity.javahello-lib.c文件中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";

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

int[] testData = new int[]{1, 2, 3, 4, 5};
for (int i = 0; i < testData.length; i++) {
Log.d(TAG, "testData: origin " + testData[i]);
}
//测试
operationArray(testData);

for (int i = 0; i < testData.length; i++) {
Log.d(TAG, "testData: after " + testData[i]);
}
//声明方法
public native void operationArray(int[] args);
static {
//载入库
System.loadLibrary("hello-lib");
}
}

上面的代码写完,我们仍然使用Alt+Enter快捷键生成我们c语言的代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
JNIEXPORT void JNICALL
Java_com_codekong_ndkdemo_MainActivity_operationArray(JNIEnv *env, jobject instance,
jintArray args_) {
//获得数组指针
jint *args = (*env)->GetIntArrayElements(env, args_, NULL);
//获得数组长度
jint len = (*env)->GetArrayLength(env, args_);
int i = 0;
for (; i < len; ++i) {
++args[i];
}
//释放
(*env)->ReleaseIntArrayElements(env, args_, args, 0);
}

最终结果: 数组中的每个元素都被加1

上面其实还是很好理解的,大家可以查看注释。

(b) 将传入的数组先拷贝一份,操作完以后再将数据拷贝回原数组

这次还是像上面一样,只是我们在C++中换了一种操作数组的方式

1
2
3
4
5
6
7
8
9
10
11
12
//声明我们的本地方法,其余代码与上面一致
public native void operationArray2(int[] args);

int[] testData2 = new int[]{1, 2, 3, 4, 5};
for (int i = 0; i < testData2.length; i++) {
Log.d(TAG, "testData2: origin " + testData2[i]);
}

operationArray2(testData2);
for (int i = 0; i < testData2.length; i++) {
Log.d(TAG, "testData2: afetr " + testData2[i]);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
JNIEXPORT void JNICALL
Java_com_codekong_ndkdemo_MainActivity_operationArray2(JNIEnv *env, jobject instance,
jintArray args_) {
//声明一个native层的数组,用于拷贝原数组
jint nativeArray[5];
//将传入的jintArray数组拷贝到nativeArray
(*env)->GetIntArrayRegion(env, args_, 0, 5, nativeArray);
int i = 0;
for (; i < 5; ++i) {
//给每个元素加5
nativeArray[i] += 5;
}

//将操作完成的结果拷贝回jintArray
(*env)->SetIntArrayRegion(env, args_, 0, 5, nativeArray);
}

最终结果:数组中每个元素都加5

注意: 我们上面的两种方式返回值都是void,也就是说我们对数组的改变都是最终改变了原来数组的值。

五、NDK自定义配置

下面我们说一下NDK里面最常见的几点配置方法,这里也是记录方便自己以后查阅

1 . 添加多个参与编译的C/C++文件

首先,我们发现我们上面的例子都是涉及到一个C++文件,那么我们实际的项目不可能只有一个C++文件,所以我们首先要改变CMakeLists.txt文件,如下 :

1
2
3
4
add_library( HelloNDK
SHARED
src/main/cpp/HelloNDK.c
src/main/cpp/HelloJNI.c)

简单吧,简单明了,但是这里要注意的是,你在写路径的时候一定要注意当前的CMakeLists.txt在项目中的位置,上面的路径是相对于CMakeLists.txt 写的。

2 . 我们想编译出多个so库

大家会发现,我们上面这样写,由于只有一个CMakeLists.txt文件,所以我们会把所有的C/C++文件编译成一个so库,这是很不合适的,这里我们就试着学学怎么编译出多个so库。

先放上我的项目文件夹结构图:

文件夹结构

然后看看我们每个CMakeLists.txt文件是怎么写的:

one文件夹内的CMakeLists.txt文件的内容:

1
2
3
ADD_LIBRARY(one-lib SHARED one-lib.c)

target_link_libraries(one-lib log)

two文件夹内的CMakeLists.txt文件的内容:

1
2
3
ADD_LIBRARY(two-lib SHARED two-lib.c)

target_link_libraries(two-lib log)

app目录下的CMakeLists.txt文件的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.4.1)

add_library( HelloNDK
SHARED
src/main/cpp/HelloNDK.c
src/main/cpp/HelloJNI.c)
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(HelloNDK log)
ADD_SUBDIRECTORY(src/main/cpp/one)
ADD_SUBDIRECTORY(src/main/cpp/two)

通过以上的配置我们可以看出CMakeLists.txt 文件的配置是支持继承的,所以我们在子配置文件中只是写了不同的特殊配置项的配置,最后在最上层的文件中配置子配置文件的路径即可,现在编译项目,我们会在 <项目目录>\app\build\intermediates\cmake\debug\obj\armeabi 下面就可以看到生成的动态链接库。而且是三个动态链接库

3 . 更改动态链接库生成的目录

我们是不是发现上面的so库的路径太深了,不好找,没事,可以配置,我们只需要在顶层的CMakeLists.txt文件中加入下面这句就可以了

1
2
#设置生成的so动态库最后输出的路径
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI})

然后我们就可以在app/src/main下看到jniLibs目录,在其中看到我们的动态链接库的文件夹和文件(这里直接配置到了系统默认的路径,如果配置到其他路径需要在gradle文件中使用jinLibs.srcDirs = ['newDir']进行指定)。

六、NDK错误调试

在开发的过程中,难免会遇到bug,那怎么办,打log啊,下面我们就谈谈打log和看log的姿势。

1 . 在C/C++文件中打log

(1) 在C/C++文件中添加头文件

1
#include <android/log.h>

上面是打印日志的头文件,必须添加

(2) 添加打印日志的宏定义和TAG

1
2
3
4
5
6
7
//log定义
#define LOG "JNILOG" // 这个是自定义的LOG的TAG
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG,__VA_ARGS__) // 定义LOGD类型
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG,__VA_ARGS__) // 定义LOGI类型
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,LOG,__VA_ARGS__) // 定义LOGW类型
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG,__VA_ARGS__) // 定义LOGE类型
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,LOG,__VA_ARGS__) // 定义LOGF类型

上面的日志级别和Android中的log是对应的。

(3) 经过上面两步,我们就可以打印日志啦

1
2
int len = 5;
LOGE("我是log %d", len);

现在我们就可以在logcat中看到我们打印的日志啦。

2 . 查看报错信息

首先我们先手动写一个错误,我们在上面的C文件中找一个函数,里面写入如下代码:

1
2
int * p = NULL;
*p = 100;

上面是一个空指针异常,我们运行程序,发现崩溃了,然后查看控制台,只有下面一行信息:

1
libc: Fatal signal 11 (SIGSEGV), code 1, fault addr 0x0 in tid 17481

完全看不懂上面的信息好吧,这个也太不明显了,下面我们就学习一下如何将上面的信息变得清楚明了

我们需要用到是ndk-stack工具,它在我们的ndk根目录下,它可以帮助我们把上面的信息转化为更为易懂更详细的报错信息,下面看看怎么做:

(1) 打开AndroidStudio中的命令行,输入adb logcat > log.txt

上面这句我们是使用adb命令捕获log日志并写入log.txt文件,然后我们就可以在项目根目录下看到log.txt文件

(2) 将log.txt打开看到报错信息,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
F/libc    (17481): Fatal signal 11 (SIGSEGV), code 1, fault addr 0x0 in tid 17481 (dekong.ndkdemo1)

I/DEBUG ( 67): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***

I/DEBUG ( 67): Build fingerprint: 'generic/vbox86p/vbox86p:5.0/LRX21M/genymotion08251046:userdebug/test-keys'

I/DEBUG ( 67): Revision: '0'

I/DEBUG ( 67): ABI: 'x86'

I/DEBUG ( 67): pid: 17481, tid: 17481, name: dekong.ndkdemo1 >>> com.codekong.ndkdemo1 <<<

I/DEBUG ( 67): signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0

I/DEBUG ( 67): eax 00000000 ebx f3494fcc ecx ffa881a0 edx 00000000

I/DEBUG ( 67): esi f434e2b0 edi 00000000

I/DEBUG ( 67): xcs 00000023 xds 0000002b xes 0000002b xfs 00000007 xss 0000002b

I/DEBUG ( 67): eip f3492a06 ebp ffa88318 esp ffa88280 flags 00210246

I/DEBUG ( 67):

I/DEBUG ( 67): backtrace:

I/DEBUG ( 67): #00 pc 00000a06 /data/app/com.codekong.ndkdemo1-2/lib/x86/libHelloNDK.so (Java_com_codekong_ndkdemo1_MainActivity_updateFile+150)

I/DEBUG ( 67): #01 pc 0026e27b /data/dalvik-cache/x86/data@app@com.codekong.ndkdemo1-2@base.apk@classes.dex

I/DEBUG ( 67): #02 pc 9770ee7d <unknown>

I/DEBUG ( 67): #03 pc a4016838 <unknown>

I/DEBUG ( 67):

I/DEBUG ( 67): Tombstone written to: /data/tombstones/tombstone_05

现在的报错信息还是看不懂,所以我们需要使用ndk-stack转化一下:

(3) 继续在AndroidStudio中的命令行中输入如下命令(在这之前,我们必须要将ndk-stack的路径添加到环境变量,以便于我们在命令行中直接使用它)

1
ndk-stack -sym app/build/intermediates/cmake/debug/obj/x86 -dump ./log.txt

上面的-sym后面的参数为你的对应平台(我是Genymotion模拟器,x86平台)的路径,如果你按照上面的步骤改了路径,那就需要写改过的路径,-dump后面的参数就是我们上一步得出的log.txt文件,执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
********** Crash dump: **********
Build fingerprint: 'generic/vbox86p/vbox86p:5.0/LRX21M/genymotion08251046:userdebug/test-keys'
pid: 17481, tid: 17481, name: dekong.ndkdemo1 >>> com.codekong.ndkdemo1 <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
Stack frame I/DEBUG ( 67): #00 pc 00000a06 /data/app/com.codekong.ndkdemo1-2/lib/x86/libHelloNDK.so (Java_com_codekon
g_ndkdemo1_MainActivity_updateFile+150): Routine Java_com_codekong_ndkdemo1_MainActivity_updateFile at F:\AndroidFirstCode\NDK
Demo1\app\src\main\cpp/HelloJNI.c:32
Stack frame I/DEBUG ( 67): #01 pc 0026e27b /data/dalvik-cache/x86/data@app@com.codekong.ndkdemo1-2@base.apk@classes.d
ex
Stack frame I/DEBUG ( 67): #02 pc 9770ee7d <unknown>: Unable to open symbol file app/build/intermediates/cmake/debug/
obj/x86/<unknown>. Error (22): Invalid argument
Stack frame I/DEBUG ( 67): #03 pc a4016838 <unknown>: Unable to open symbol file app/build/intermediates/cmake/debug/
obj/x86/<unknown>. Error (22): Invalid argument
Crash dump is completed

尤其是上面的一句:

1
2
g_ndkdemo1_MainActivity_updateFile+150): Routine Java_com_codekong_ndkdemo1_MainActivity_updateFile at F:\AndroidFirstCode\NDK
Demo1\app\src\main\cpp/HelloJNI.c:32

准确指出了发生错误的行数,便于我们定位错误。

好了,上面就是简单介绍的调试技巧。

七、后记

终于,写完了,这一次的内容有点多,但都是一些简单的入门的知识,我也是刚接触不久,希望通过总结加深理解,写出来帮助有需要的人,真心希望可以帮助到他人,大神勿喷,错误之处,多多指点。

如果博客对您有帮助,不妨请我喝杯咖啡...