AndroidStudio中的NDK开发初探

前段时间由于做比赛的事,一直都没时间写博客,现在终于可以补上一篇了,一直想学习一点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
安装的工具中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文件内容:

#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-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文件中增加了一些配置,如下:

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.javahello-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

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

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

七、后记

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容