前段时间由于做比赛的事,一直都没时间写博客,现在终于可以补上一篇了,一直想学习一点NDK开发的知识,但是迟迟没有动手,正好有一个NDK相关的项目机会,便查阅了一些资料,遂将学习的一些心得方法记录于此。
其实写这篇博客还有一个目的,在我搜寻NDK相关学习资料的过程中,大部分都是基于eclipse开发的,所以有些过时,而现在Google推荐使用AndroidStudio+CMake的方式进行NDK开发,所以想更新一下有些知识,便于大家学习参考。
首先说说这次的开发工具及版本
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 . 重要核心代码保护。由于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和CMake上面已经介绍过了,LLDB是一款在开发NDK过程中的调试器,这篇博客中将不会介绍。
做完了上面的步骤我们就可以开始我们的第一个NDK程序了。
三、创建第一个NDK程序
下面我将以图示加序号的方式来说明新建步骤。
1 . 新建一个项目,填写基本信息,记得勾选Include C++ support,便于AndroidStudio为我们生成一些默认的配置。
2 . 接下来的几个步骤就选择默认设置
3 . 到最后一步如图,C++ Standard 选择 Toolchain Default,其它不变即可。
说明:
(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文件内容:
#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文件内容 :
# 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自动生成,我们几乎不需要自己管。
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-lib
,SHARED
表示编译生成的是动态链接库
(这个概念前面已经提到过了),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
文件中增加了一些配置,如下:
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
标签
externalNativeBuild {
cmake {
cppFlags ""
}
}
这里的cppFlags
里面的内容为空,这里其实就是配置了我们在新建项目的时候的第(3)步中讲到的,如果我们勾选了异常支持和RTTI支持,这里就会有相关的配置信息。
(b) 使用 externalNativeBuild
来指定 CMakeLists.txt
文件的路径,由于build.gradle文件和CMakeLists.txt
文件在同一目录下,所以此处就直接写文件名啦。
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
(4) 最终在MainActivity.java
文件中我们看到了函数的调用过程如下:
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++代码的函数名:
//我们声明的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
文件中进行配置,所以我们去改动一下该文件,改动如下:
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
中获取它。
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
载入我们的自定义库。
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中的内容如下:
#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中添加如下代码
public native void writeFile(String filePath);
(b) 在hello-lib.c
中生成如下代码
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)我们可以在这个基础上写一个写入文件的小例子,代码如下:
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);
}
以上代码记得加头文件
#include <jni.h>
#include <stdio.h>
#include <string.h>
(d)还要记得在AndroidMainfest.xml文件中添加文件读写权限,然后在MainActivity.java中调用native方法
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.java
和 hello-lib.c
文件中的代码
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语言的代码,如下:
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++中换了一种操作数组的方式
//声明我们的本地方法,其余代码与上面一致
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]);
}
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
文件,如下 :
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
文件的内容:
ADD_LIBRARY(one-lib SHARED one-lib.c)
target_link_libraries(one-lib log)
two文件夹内的CMakeLists.txt
文件的内容:
ADD_LIBRARY(two-lib SHARED two-lib.c)
target_link_libraries(two-lib log)
app目录下的CMakeLists.txt
文件的内容
# 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
文件中加入下面这句就可以了
#设置生成的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++文件中添加头文件
#include <android/log.h>
上面是打印日志的头文件,必须添加
(2) 添加打印日志的宏定义和TAG
//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) 经过上面两步,我们就可以打印日志啦
int len = 5;
LOGE("我是log %d", len);
现在我们就可以在logcat中看到我们打印的日志啦。
2 . 查看报错信息
首先我们先手动写一个错误,我们在上面的C文件中找一个函数,里面写入如下代码:
int * p = NULL;
*p = 100;
上面是一个空指针异常,我们运行程序,发现崩溃了,然后查看控制台,只有下面一行信息:
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打开看到报错信息,如下:
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的路径添加到环境变量,以便于我们在命令行中直接使用它)
ndk-stack -sym app/build/intermediates/cmake/debug/obj/x86 -dump ./log.txt
上面的-sym
后面的参数为你的对应平台(我是Genymotion模拟器,x86平台)的路径,如果你按照上面的步骤改了路径,那就需要写改过的路径,-dump
后面的参数就是我们上一步得出的log.txt文件,执行结果如下:
********** 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
尤其是上面的一句:
g_ndkdemo1_MainActivity_updateFile+150): Routine Java_com_codekong_ndkdemo1_MainActivity_updateFile at F:\AndroidFirstCode\NDK
Demo1\app\src\main\cpp/HelloJNI.c:32
准确指出了发生错误的行数,便于我们定位错误。
好了,上面就是简单介绍的调试技巧。
七、后记
终于,写完了,这一次的内容有点多,但都是一些简单的入门的知识,我也是刚接触不久,希望通过总结加深理解,写出来帮助有需要的人,真心希望可以帮助到他人,大神勿喷,错误之处,多多指点。