Android NDK开发基础

前言

NDK即Native Development Kit,是Android上用来开发c/c++的开发工具包。
安装步骤:https://developer.android.com/studio/projects/install-ndk

一、基础用法

  1. 在local.properties中配置ndk目录:
ndk.dir=/Users/bc/android-ndk-r17c
sdk.dir=/Users/bc/Library/Android/sdk
  1. 新建cpp目录,编写native代码:
    0673bd44559649e6a0d8b2110617f758~tplv-k3u1fbpfcp-watermark.image.png


    其中,cpp文件是c++代码文件,CMakeLists.txt是CMake构建脚本(后面详细介绍)。
  2. 在build.gradle中配置ndk编译选项:
android {
    defaultConfig {
        ndk {
            // 指定编译的abi架构
            abiFilters "armeabi-v7a", "arm64-v8a"
        }
        externalNativeBuild {
            // cmake配置
            cmake {
                //Sets optional flags for the C++ compiler.
                cppFlags "-std=c++11 -frtti -fexceptions"
                // Passes optional arguments to CMake.
                arguments "-DANDROID_STL=c++_shared"
                arguments "-DANDROID_TOOLCHAIN=gcc"
            }
        }
    }

    // Use this block to link Gradle to your CMake or ndk-build script
    // 将gralde关联到cmake构建脚本
    externalNativeBuild {
        cmake {
            // 构建脚本路径
            path "src/main/cpp/CMakeLists.txt"
            version "3.10.2"
        }
    }
}
  1. 执行编译,Gradle会根据外部构建脚本CMakeLists.txt将c++代码生成so,并打包到APK中,位于/lib/{abi}/目录下。
  2. 在java代码中加载so:
public class NativeDemo {
    private String libPath = "/data/data/com.bc.sample/files/download/nativeLib2.so";
    
    public void initNativeLib() {
        // 1.对于APK内的so文件,使用loadLibrary来加载
        System.loadLibrary("nativeLib");
        // 2.对于文件系统中的so文件,使用load()来加载
        System.load(libPath);
    }
}

二、NDK编译基础

NDK支持的编译方式有两种:

(1)CMake:NDK的默认构建工具,可在CMakeLists.txt 构建脚本中配置编译选项,CMake的C++运行时默认值为c++_static,CMake和独立工具链默认启用C++异常,默认启用 RTTI。

(2)ndk-build:可在Android.mk 和 Application.mk文件中配置编译选项,ndk-build的C++运行时默认值为none,ndk-build中默认停用C++异常,默认停用RTTI。

2.1 CMake基础

CMake官网介绍如下,翻译过来就是,CMake是开源的、跨平台的编译工具,可以根据所指定的编译环境生成对应的makefile文件。

CMake is an open-source, cross-platform family of tools 
designed to build, test and package software. 
CMake is used to control the software compilation process
using simple platform and compiler independent configuration
files, and generate native makefiles and workspaces that can
be used in the compiler environment of your choice. 

CMake使用工具链来执行编译、链接等任务,对不同语言需要使用不同的工具链;

NDK的工具链文件位于 NDK目录中的 {NDK_root}/build/cmake/android.toolchain.cmake 内,并可以在build.gradle中的android.defaultConfig.externalNativeBuild.cmake.arguments闭包下给CMake工具链传递参数。

2.1.1 CMake构建脚本

CMakeLists.txt是CMake的构建脚本,在CMakeLists.txt中可以使用cmake的一些命令来自定义构建过程,以下列举了一些常用的cmake命令:

# Sets the minimum version of CMake required to build your native library.
# This ensures that a certain set of CMake features is available to
# your build.
# cmake最低版本号
cmake_minimum_required(VERSION 3.4.1)

# set:给变量赋值
set(libs "${CMAKE_SOURCE_DIR}/../jniLibs")
set(SOURCES src/main/cpp/native-lib.cpp src/main/cpp/native-lib2.cpp)

# message:打印信息
message("jniLibs folder: " ${libs})

# option:定义宏
option(IS_ANDROID "编译ANDROID则定义为'ON',其他定义为'OFF'" ON)

# if-endif:cmake的逻辑控制
if(IS_ANDROID)
    message("Building for Android")
    add_definitions(-DIS_ANDROID)
else()
    message("Building for others")
endif(IS_ANDROID)

# include_directories:Add the given directories to those the compiler uses to search for include files.把指定目录添加到编译器查找include头文件的列表中
include_directories(Thirdparty/opencv)
        
# find_library:This command is used to find a library.查找NDK库并将其路径存储为一个变量
find_library( # Sets the name of the path variable.
        # 也可以直接用log,像下面的jnigraphics一样
        log-lib

        # Specifies the name of the NDK library that
        # you want CMake to locate.
        log)

# Specifies a library name, specifies whether the library is STATIC or
# SHARED, and provides relative paths to the source code. You can
# define multiple libraries by adding multiple add_library() commands,
# and CMake builds them for you. When you build your app, Gradle
# automatically packages shared libraries with your APK.
# add_library:把一个library添加到工程
add_library( # Specifies the name of the library.
             native-lib

             # Sets the library as a shared library.
             # 动态库用SHARED 静态库用STATIC
             SHARED

             # Provides a relative path to your source file(s).
             # 把所有需要用到的cpp都添加进来;或者用前面set的SOURCES变量
             src/main/cpp/native-lib.cpp
             src/main/cpp/native-lib2.cpp)

# 添加一个已构建的库,使用IMPORTED
add_library(opencv_java3 SHARED IMPORTED)
# 对应已构建的库,需要指定库的路径
set_target_properties(opencv_java3 
        # Specifies the target library.
        PROPERTIES IMPORTED_LOCATION
        # Provides the path to the library you want to import.
        "${libs}/${ANDROID_ABI}/libopencv_java3.so")
             
# target_link_libraries(<target> ... <item>... ...) 关联target和item
# Links your native library against one or more other native libraries.
# 首个参数是target,后面的参数是item;target必须先用add_library()创建过;
target_link_libraries( # Specifies the target library.
            native-lib
            opencv_java3
            jnigraphics
            ${log-lib} )

2.1.2 gralde关联cmake

在build.gradle中配置cmake,即可将二者关联起来,关联后在gralde构建的过程中就会构建native代码:

android {
    defaultConfig {
        ndk {
            // 指定编译的abi架构
            abiFilters "armeabi-v7a", "arm64-v8a"
        }
        externalNativeBuild {
            // cmake配置
            cmake {
                //Sets optional flags for the C++ compiler.
                cppFlags "-std=c++11 -frtti -fexceptions"
                // Passes optional arguments to CMake toolchain.
                arguments "-DANDROID_STL=c++_shared"
                arguments "-DANDROID_TOOLCHAIN=gcc"
            }
        }
    }

    // Use this block to link Gradle to your CMake or ndk-build script
    // 将gralde关联到cmake构建脚本
    externalNativeBuild {
        cmake {
            // 构建脚本路径
            path "src/main/cpp/CMakeLists.txt"
            version "3.10.2"
        }
    }
}

2.1.3 多个CMake项目

如果项目中包含多个CMake项目,可以使用一个 CMakeLists.txt 文件作为顶级 CMake 构建脚本,并添加其他 CMake 项目作为此构建脚本的依赖项。

例如,项目中需要构建native-lib1和native-lib2两个so,则可以新建一个顶层 CMakeLists.txt,并将顶层 CMakeLists.txt配置到build.gralde中,然后在顶层 CMakeLists.txt中添加native-lib1和native-lib2:

cmake_minimum_required(VERSION 3.4.1)

# add_subdirectory:将另一个 CMakeLists.txt 文件指定为构建依赖项,然后关联其输出;
add_subdirectory(
    # Specifies the directory of the CMakeLists.txt file.
    ./src/main/cpp/native-lib1
    # Specifies the directory for the build outputs.
    ./src/main/cpp/native-lib1/outputs
    )
add_subdirectory(
    # Specifies the directory of the CMakeLists.txt file.
    ./src/main/cpp/native-lib2
    # Specifies the directory for the build outputs.
    ./src/main/cpp/native-lib2/outputs
    )

2.2 ndk-build基础

详细介绍见https://developer.android.com/ndk/guides/ndk-build (本文不重点介绍),在build.gradle中配置如下:

externalNativeBuild {
    ndkBuild {
        path file('src/main/jni/Android.mk')
    } 
}

2.3 native api

在常用的cmake命令中介绍过,cmake可以使用find_library命令找到 NDK native api库并将其路径存储为一个变量;或者也可以在target_link_libraries直接使用NDK native api。

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)

下面主要列举一些常用的NDK native api:

  • c库:<stdlib.h> 和 <stdio.h> 等标准 C11 库头文件;无需显示链接;
  • c++库:提供 C++17 支持;
  • log:<android/log.h> 用于输出log到logcat的API。从API级别3开始提供;
  • bitmap:libjnigraphics 库用来访问 Java Bitmap 对象的像素缓冲区;
  • OpenGL ES 1.0 - 3.2:开放式图形库;
  • EGL:用于分配和管理 OpenGL ES 上下文和 Surface;
  • Vulkan:用于高性能三维图形渲染的低开销、跨平台 API。
    更多NDK native api请参考官方文档https://developer.android.com/ndk/reference 。

三、NDK构建产物

native代码在构建后,有两种产物,生成哪一种产物是由编译配置决定的:

(1)native shared library动态库,即.so文件,CMakeList.txt中配置如下:

add_library(native-lib 
            SHARED
            src/main/cpp/native-lib.cpp)

(2)native static library静态库,即.a文件,CMakeList.txt中配置如下:

add_library(native-lib 
            STATIC
            src/main/cpp/native-lib.cpp)

二者的区别:.so文件可以在运行过程中由java代码调用加载,.a文件不能在运行过程中直接由java代码加载;so文件在运行中可以去加载其他的so文件或者a文件;

四、JNI基础

JNI即java native interface,是java和native代码进行交互的接口;

4.1 基础用法

在java中使用native关键字声明jni方法:

public class NativeDemo {
    public native String stringFromJNI();
    
    public static native void invole();
}

在kotlin中使用externl关键字声明jni方法:

class NativeDemo {
    external fun stringFromJNI(): String;
}

有两种方式可以实现对应的native代码:

(1)静态注册

在cpp目录下,新建native_lib.cpp,添加对应的native实现:

#include <jni.h>
#include <string>

// JNIEXPORT JNICALL、参数里的前两个参数JNIEnv* env,jobject obj等是固定格式;固定参数中的jobject obj表示this
extern "C" 
JNIEXPORT jstring JNICALL
Java_com_bc_sample_NativeDemo_stringFromJNI(
        JNIEnv* env,
        jobject obj) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
    
// static的native方法,参数默认是jclass
extern "C" 
JNIEXPORT void JNICALL
Java_com_bc_sample_NativeDemo_stringFromJNI(
        JNIEnv* env,
        jclass clazz) {
        
}

(2)动态注册

在cpp目录下,新建native_lib.cpp,在JNI_OnLoad时调用env->RegisterNatives进行注册(JNI_OnLoad是在动态库被加载时由系统进行调用):

// 需要注册jni方法所在的类
static const char *jniClassName = "com/bc/sample/NativeDemo";

// 需要注册的jni方法,分别表示java方法名、方法签名、native函数指针
// 方法签名可以用javap命令查看:
// javap -s /Users/bc/Demo/app/build/intermediates/javac/debug/classes/com/bc/sample/NativeDemo.class
static JNINativeMethod methods[] = {{"stringFromJNI", "()Ljava/lang/String", (jstring *) native_stringFromJNI}};

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env = NULL;
    jint result = -1;

    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK)
        return JNI_ERR;

    if (!registerNatives(env))
        return JNI_ERR;

    result = JNI_VERSION_1_6;
    return result;
}

static int registerNatives(JNIEnv *env) {
    jclass clazz = env->FindClass(jniClassName);
    if (clazz == NULL)
        return JNI_FALSE;

    jint methodSize = sizeof(methods) / sizeof(methods[0]);
    if (env->RegisterNatives(clazz, methods, methodSize) < 0)
        return JNI_FALSE;

    return JNI_TRUE;
}

// native实现
jstring native_stringFromJNI(JNIEnv *env, jobject obj) {
    return env->NewStringUTF("hello");
}

4.1 JavaVM 和 JNIEnv

JNI 定义了两个关键数据结构“JavaVM”和“JNIEnv”,两者本质上都是指向函数表的二级指针。

Android中每个进程只允许有一个JavaVM。JNIEnv作用域为单个线程,可通过JavaVM的getEnv来获得当前线程的JNIEnv,JNIEnv可通过GetJavaVM来获得JavaVM。

 // 保存JavaVM,方便在子线程中获取
static JavaVM *_my_jvm;

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
    env->GetJavaVM(&_my_jvm)
    JNIEnv *env = NULL;
    jint result = -1;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK)
        return JNI_ERR;
    return result;
}

4.2 线程与JavaVM

在java代码中,可以通过Thread.start()启动一个线程;

对于在native代码中通过pthread_create() 或 std::thread 启动的线程,是没有JNIEnv的,也就无法调用JNI,可以使用 AttachCurrentThread() 或 AttachCurrentThreadAsDaemon() 函数将JavaVM附加到线程,附加后的线程可以调用JNI代码:

 // 保存JavaVM,方便在子线程中获取
static JavaVM *_my_jvm;
// 保存java层obj,方便在子线程获取后回调结果
static java_obj;

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
    env->GetJavaVM(&_my_jvm)
    JNIEnv *env = NULL;
    jint result = -1;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK)
        return JNI_ERR;
    return result;
}

jstring native_stringFromJNI(JNIEnv *env, jobject obj) {
    java_obj = env->NewGlobalRef(obj);
}

void native_callback() {
    JNIEnv *env = NULL;
    _my_jvm->AttachCurrentThread(&env, nullptr);   
    jmethodId method_call_back = env->GetMethodId(user_class, "callback", "()V");
    env->CallVoidMethod(java_obj, method_call_back);
    // 使用完后需要detach
    _my_jvm->DetachCurrentThread(); 
}

4.3 JNI数据类型

4.3.1 基础数据类型

/* jni.h源码 */

/* Primitive types that match up with Java equivalents. */
/* jni基础数据类型与java一一对应,二者可直接转换 */
typedef uint8_t  jboolean; /* unsigned 8 bits */
typedef int8_t   jbyte;    /* signed 8 bits */
typedef uint16_t jchar;    /* unsigned 16 bits */
typedef int16_t  jshort;   /* signed 16 bits */
typedef int32_t  jint;     /* signed 32 bits */
typedef int64_t  jlong;    /* signed 64 bits */
typedef float    jfloat;   /* 32-bit IEEE 754 */
typedef double   jdouble;  /* 64-bit IEEE 754 */

/* "cardinal indices and sizes" */
typedef jint     jsize;

#ifdef __cplusplus
/*
 * Reference types, in C++
 */
class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jobjectArray : public _jarray {};
class _jbooleanArray : public _jarray {};
class _jbyteArray : public _jarray {};
class _jcharArray : public _jarray {};
class _jshortArray : public _jarray {};
class _jintArray : public _jarray {};
class _jlongArray : public _jarray {};
class _jfloatArray : public _jarray {};
class _jdoubleArray : public _jarray {};
class _jthrowable : public _jobject {};

typedef _jobject*       jobject;
typedef _jclass*        jclass;
typedef _jstring*       jstring;
typedef _jarray*        jarray;
typedef _jobjectArray*  jobjectArray;
typedef _jbooleanArray* jbooleanArray;
typedef _jbyteArray*    jbyteArray;
typedef _jcharArray*    jcharArray;
typedef _jshortArray*   jshortArray;
typedef _jintArray*     jintArray;
typedef _jlongArray*    jlongArray;
typedef _jfloatArray*   jfloatArray;
typedef _jdoubleArray*  jdoubleArray;
typedef _jthrowable*    jthrowable;
typedef _jobject*       jweak;

#else /* not __cplusplus */

/*
 * Reference types, in C.
 */
typedef void*           jobject;
typedef jobject         jclass;
typedef jobject         jstring;
typedef jobject         jarray;
typedef jarray          jobjectArray;
typedef jarray          jbooleanArray;
typedef jarray          jbyteArray;
typedef jarray          jcharArray;
typedef jarray          jshortArray;
typedef jarray          jintArray;
typedef jarray          jlongArray;
typedef jarray          jfloatArray;
typedef jarray          jdoubleArray;
typedef jobject         jthrowable;
typedef jobject         jweak;

#endif /* not __cplusplus */

4.3.2 string类型

extern "C" 
JNIEXPORT jstring JNICALL
Java_com_bc_sample_NativeDemo_stringFromJNI(
        JNIEnv* env,
        jobject obj, jstring str) {
    jstring loacl_str = env->NewStringUTF("local_str");
    const char * c  = env->GetStringUTFChars(str, false);
    // 删除避免内存泄漏
    env->ReleaseStringUTFChars(str, c);
    return env->NewStringUTF(hello.c_str());
}

4.3.3 引用类型

例如:java中定义了一个User类如下:

package com.bc.sample;

class User {
    public String userName;
    public static String TAG;
    
    public void setName() {
    }
    
    public static void show() {
    }
}

在jni中调用User相关方法的方式如下:

extern "C" 
JNIEXPORT jobject JNICALL
Java_com_bc_sample_NativeDemo_objectFromJNI(
        JNIEnv* env,
        jobject obj, jobject user) {
    jclass user_class = env->GetObjectClass(user);
    // 1.1 变量
    jfieldID user_name_id = env->GetFiledId(user_class, "userName", "Ljava/lang/String");
    jstring name = env->NewStringUTF("jack");
    env->SetObjectFiled(user, user_name_id, name);
    // 1.2 静态变量
    env->GetStaticFieldID(user_class, "TAG", "Ljava/lang/String");
    
    // 2.1 方法
    jmethodId method_set_name = env->GetMethodId(user_class, "setName", "()V");
    env->CallVoidMethod(user, method_set_name);
    // 2.2 静态方法
    jmethodId method_show = env->GetStaticMethodId(user_class, "show", "()V");
    env->CallStaticObjectMethod(user_class, method_show);
    
    // 3 构造函数,用<init>表示
    jclass usr_class = env->FindClass("com/bc/sample/User");
    jmethodId method_init = env->GetMethodId(user_class, "<init>", "()V");
    jobject usr = env->NewObject(usr_class, method_init);
    jobjectArray usr_array = env->NewObjectArray(5, usr_class, nullptr);
    
    // 局部引用超过512会报错local reference table overflow (max=512);所以用完后要释放
    env->DeleteLocalRef(user_class);
}

4.3.4 数组类型

不管是基础类型的数组jintArray、jbooleanArray还是引用类型的数组jobjectArray,都是继承了jarray,是一个引用类型。

extern "C" 
JNIEXPORT jobject JNICALL
Java_com_bc_sample_NativeDemo_objectFromJNI(
        JNIEnv* env,
        jobject obj, jintArray int_array,
        jstringArray str_array,
        jobjectArray user_array) {
    int len = env->GetArrayLength(user_array);
    // 1 基本数据类型数组
    jint n = env->GetIntArrayElement(int_array, 0);
    // 2 string数据类型数组
    jstring str = static_cast<jstring>(env->GetObjectArrayElement(str_array, 0));
    const char * c  = env->GetStringUTFChars(str, false);
    env->ReleaseStringUTFChars(str, c);
    
    // 3 引用类型数组;获得jobject后可按4.3.3中获得user数据
    jobject user = env->GetObjectArrayElement(user_array, 0);
    env->DeleteLocalRef(user);
}

4.4 java签名

可以使用javap命令生成类的方法及参数签名:

>javap -s java.lang.String

java类型及签名对应关系如下:

# 基本类型
V	 void	一般用于表示方法的返回值
Z	 boolean	
B	 byte	
C	 char	
S	 short	
I	 int	
J	 long	
F	 float	
D	 double	

# 引用类型前要加L
String   Ljava/lang/String
Class    Ljava/lang/Class

# 数组前要加[
int[]    [I
Object[] [Ljava/lang/Object

# 方法签名:括号内表示参数,括号后表示返回类型(引用类型后要用;分隔)
String fun()                 () Ljava/lang/String;
int fun( int i, String str)   (ILjava/lang/String;)I

4.5 JNI全局引用 局部引用 弱引用

static java_obj;

jstring native_stringFromJNI(JNIEnv *env, jobject obj) {
    // 局部引用
    jclass str_clazz = env->FindClass("Ljava/lang/String");
    env->DeleteLocalRef(str_clazz);
    // 全局引用,手动调用DeleteGlobalRef后才释放
    java_obj = env->NewGlobalRef(obj);
    env->DeleteGlobalRef(obj);
    // 弱引用,使用前需要判断是否为null
    java_obj = env->NewWeakGlobalRef(obj);
    jboolean is_null = env->IsSameObject(java_obj, nullptr);
}

4.6 JNI异常捕获

假设java中的user类中定义如下

class User {
    public void setName() throws RuntimeException {
    }
    
    public native void stringFromJNI();
}

在native层调用java代码发生异常时,可按如下方式捕获:

jstring native_stringFromJNI(JNIEnv *env, jobject obj) {
   jclass user_class = env->GetObjectClass(user);
   jmethodId method_set_name = env->GetMethodId(user_class, "setName", "()V");
    env->CallVoidMethod(user, method_set_name);
    // 1.如果调用java方法setName时发生异常;可使用如下方法判断是否发生crash并捕获;否则会直接crash
    jthrowable throwable_occurred = env->ExceptionOccurred();
    if (throwable_occurred) {
        env->ExceptionDescribe();
        env->ExceptionClear();
    }
    
    // 2.或者当异常发生时,native层可以向java层抛出一个异常,在java层完成try-catch
    jclass throwable_clazz = env->FindClass("java/lang/RuntimeException");
    env->ThrowNew(throwable_clazz, "exception occurred");
}

4.7 JNI线程

native开发常用的线程库:

  1. Posix API:<pthread.h>;
  2. c++11支持的<thread.h>(本文不介绍);
#include<pthread.h>
// 线程互斥锁
pthread_mutex_t mutex;
// 线程条件变量
pthread_cond_t cont;

// 需要在子线程中执行的方法
void *sayHello(void * arg) {
    pthread_mutex_lock(&mutex);
    jstring local_arg = static_cast<jstring > (arg);
    LOG(local_arg);
    pthread_mutex_unlock(&mutex);
    return nullptr;
}

jstring native_stringFromJNI(JNIEnv *env, jobject obj) {
    // 1 创建线程
    pthread_t handle;
    jstring loacl_str = env->NewStringUTF("local_str");
    int result = pthread_create(&handle, nullptr, sayHello, loacl_str);
    
    // 2 线程同步常用方法 
    // pthread_mutex_lock() pthread_mutex_unlock()
    pthread_mutex_init(&mutex, nullptr);
    pthread_mutex_destroy(&mutex);
    // pthread_cond_wait() pthread_cond_signal()
    pthread_cond_init(&cond, nullptr);
    pthread_cond_destroy(&cond);
}

五、ABI简介

不同的 Android 设备使用不同的 CPU,而不同的 CPU 支持不同的指令集。CPU 与指令集的每种组合都有专属的应用二进制接口 (ABI)。CPU 与指令集的每种组合都有专属的应用二进制接口 (ABI)。

默认情况下,Gradle会针对所有非弃用ABI进行构建。要限制应用支持的ABI集,可以在build.gradle中设置以下配置:

android {
    defaultConfig {
        ndk {
            // 限定编译的abi
            abiFilters "armeabi", "arm64-v8a"
        }
    }
}

cpu架构及支持的abi如下:

cpu \ abiarmeabiarmeabi-v7aarm64-v8ax86x86_64mipsmips64
ARMv5支持
ARMv7支持支持
ARMv8支持支持支持
x86支持支持支持
x86_64支持支持支持
MIPS支持
MIPS64支持支持

目前市场上主流CPU架构是ARM v7;mips/mips64、x86 / x86_64市场占有率很少,可以忽略;

六、crash堆栈分析

常见native crash:https://source.android.com/devices/tech/debug/native-crash

当native层发生crash时,crash堆栈如下所示:

A/libc: Fatal signal 11 (SIGSEGV), code 1, fault addr 0x0 in tid 8427 (com.bc.sample), pid 8427 (com.bc.sample)
A/DEBUG: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
A/DEBUG: Build fingerprint: 'Meizu/meizu_16th_CN/16th:8.1.0/OPM1.171019.026/1554749207:user/release-keys'
A/DEBUG: Revision: '0'
A/DEBUG: ABI: 'arm64'
A/DEBUG: pid: 8427, tid: 8427, name: com.bc.sample  >>> com.bc.sample <<<
A/DEBUG: signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
A/DEBUG: Cause: null pointer dereference
A/DEBUG:     x0   0000000000000000  x1   0000000000000000  x2   000000000000000e  x3   0000007feb60313f
A/DEBUG:     x4   0000000000000000  x5   0080000000000000  x6   7266206f6c6c6548  x7   2b2b43206d6f7266
A/DEBUG:     x8   0101010101010101  x9   406a834e06fb5ba3  x10  0000000000000000  x11  000000000000000e
A/DEBUG:     x12  0000000000000001  x13  0000000000000004  x14  00000074aa8bca40  x15  00000074a9b7a8a8
A/DEBUG:     x16  000000740b3bae98  x17  00000074a9aa1890  x18  0000007425800080  x19  00000074256c2a00
A/DEBUG:     x20  0000007425288220  x21  00000074256c2a00  x22  0000007feb60340c  x23  000000740b68273a
A/DEBUG:     x24  0000000000000004  x25  00000074aa8bca40  x26  00000074256c2aa0  x27  0000000000000001
A/DEBUG:     x28  0000000000000002  x29  0000007feb603070  x30  000000740b3957f4
A/DEBUG:     sp   0000007feb603060  pc   00000074a9aa18a0  pstate 0000000080000000
A/DEBUG: backtrace:
A/DEBUG:     #00 pc 000000000001d8a0  /system/lib64/libc.so (strlen+16)
A/DEBUG:     #01 pc 000000000000f7f0  /data/app/com.bc.sample-2gV8CATI0EfKhLhzjuP6sA==/lib/arm64/libnative-lib.so (std::__ndk1::char_traits<char>::length(char const*)+20)
A/DEBUG:     #02 pc 000000000000fdf8  /data/app/com.bc.sample-2gV8CATI0EfKhLhzjuP6sA==/lib/arm64/libnative-lib.so (std::__ndk1::basic_string<char, std::__ndk1::char_traits<char>, std::__ndk1::allocator<char>>::assign(char const*)+44)
A/DEBUG:     #03 pc 000000000000f2f4  /data/app/com.bc.sample-2gV8CATI0EfKhLhzjuP6sA==/lib/arm64/libnative-lib.so
A/DEBUG:     #04 pc 000000000000f1f8  /data/app/com.bc.sample-2gV8CATI0EfKhLhzjuP6sA==/lib/arm64/libnative-lib.so (Java_com_bc_sample_MainActivity_stringFromJNI+76)
A/DEBUG:     #05 pc 000000000000909c  /data/app/com.bc.sample-2gV8CATI0EfKhLhzjuP6sA==/oat/arm64/base.odex (offset 0x9000)

可以看到,crash信息描述了发生crash的abi架构、crash的原因以及堆栈backtrace;

一般有两种方式可用来分析native层crash的backtrace:

6.1 ndk-stack命令

ndk-stack命令位于{NDK_root}/ndk-stack,使用时需要将crash的log复制到一个txt文件中,这个命令会从txt中的

A/DEBUG: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***

后面开始分析,使用方法如下:

# 用法:ndk-stack -sym ${SO_PARENT_DIR} -dump crash_log.txt
# -sys so所在目录,即${SO_PARENT_DIR},注意必须是目录名,不是so路径;
# -dump 发生crash的log信息,即crash_log.txt;

# 示例:
>ndk-stack -sym /Users/bc/demo/MyApplication2/app/build/intermediates/merged_native_libs/debug/out/lib/arm64-v8a -dump /Users/bc/crash.txt 
# 结果如下:
********** Crash dump: **********
Build fingerprint: 'Meizu/meizu_16th_CN/16th:8.1.0/OPM1.171019.026/1554749207:user/release-keys'
pid: 8427, tid: 8427, name: com.bc.sample  >>> com.bc.sample <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
Stack frame #00 pc 000000000001d8a0  /system/lib64/libc.so (strlen+16)
Stack frame #01 pc 000000000000f7f0  /data/app/com.bc.sample-2gV8CATI0EfKhLhzjuP6sA==/lib/arm64/libnative-lib.so (std::__ndk1::char_traits<char>::length(char const*)+20): Routine std::__ndk1::char_traits<char>::length(char const*) at /Users/bc/Library/Android/sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/include/c++/v1/__string:217
Stack frame #02 pc 000000000000fdf8  /data/app/com.bc.sample-2gV8CATI0EfKhLhzjuP6sA==/lib/arm64/libnative-lib.so (std::__ndk1::basic_string<char, std::__ndk1::char_traits<char>, std::__ndk1::allocator<char>>::assign(char const*)+44): Routine std::__ndk1::basic_string<char, std::__ndk1::char_traits<char>, std::__ndk1::allocator<char> >::assign(char const*) at /Users/bc/Library/Android/sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/include/c++/v1/string:2385
Stack frame #03 pc 000000000000f2f4  /data/app/com.bc.sample-2gV8CATI0EfKhLhzjuP6sA==/lib/arm64/libnative-lib.so: Routine std::__ndk1::basic_string<char, std::__ndk1::char_traits<char>, std::__ndk1::allocator<char> >::operator=(char const*) at /Users/bc/Library/Android/sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/include/c++/v1/string:890
Stack frame #04 pc 000000000000f1f8  /data/app/com.bc.sample-2gV8CATI0EfKhLhzjuP6sA==/lib/arm64/libnative-lib.so (Java_com_bc_sample_MainActivity_stringFromJNI+76): Routine Java_com_bc_sample_MainActivity_stringFromJNI at /Users/bc/Demo/app/src/main/cpp/native-lib.cpp:9
Stack frame #05 pc 000000000000909c  /data/app/com.bc.sample-2gV8CATI0EfKhLhzjuP6sA==/oat/arm64/base.odex (offset 0x9000)

6.2 addr2line命令

addr2line命令位于{NDK_root}/toolchains/${ABI}/prebuilt/darwin-x86_64/bin/x86_64-linux-android-addr2line,使用方法如下:

# 用法:arm-linux-androideabi-addr2line -C -f -e ${SOPATH} ${Address}
# -C -f 打印错误行数所在的函数名称
# -e 打印错误地址的对应路径及行数
# ${SOPATH} so路径
# ${Address} crash堆栈地址
# 示例如下:
>arm-linux-androideabi-addr2line -C -f -e libnative-lib.so 000000000000f1f8
# 结果如下,可以得到发生crash的行号:
>Java_com_bc_sample_MainActivity_stringFromJNI
/Users/bc/demo/MyApplication2/app/src/main/cpp/native-lib.cpp:9

The End

欢迎关注我,一起解锁更多技能:BC的掘金主页~💐 BC的CSDN主页~💐💐
请添加图片描述

参考文档:

cmake指南:https://developer.android.com/ndk/guides/cmake

配置cmake:https://developer.android.com/studio/projects/configure-cmake

cmake命令文档:https://cmake.org/cmake/help/latest/manual/cmake-commands.7.html

cmake-toolchain文档:https://cmake.org/cmake/help/latest/manual/cmake-toolchains.7.html?highlight=ndk#cross-compiling-for-android-with-the-ndk

NDK native api简介:https://developer.android.com/ndk/guides/stable_apis

NDK native api:https://developer.android.com/ndk/reference

JNI 官方文档:https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html

  • 5
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值