抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

基础

介绍

NDK(Native Development Kit)是 Android 的一个工具集,允许开发者使用 C 和 C++ 语言编写 Android 应用程序中的一部分代码

NDK 工具包中提供了完整的一套将 c/c++ 代码编译成静态/动态库的工具,而 Android.mkApplication.mk 你可以认为是描述编译参数和一些配置的文件。比如指定使用 c++11 还是 c++14 编译,会引用哪些共享库,并描述关系等,还会指定编译的 abi。只有有了这些 NDK 中的编译工具才能准确的编译 c/c++ 代码。

ndk-build 文件是 Android NDK r4 中引入的一个 shell 脚本。其用途是调用正确的 NDK 构建脚本。其实最终还是会去调用 NDK 自己的编译工具。

CMake 又是什么呢。脱离 Android 开发来看,c/c++ 的编译文件在不同平台是不一样的。Unix 下会使用 makefile 文件编译,Windows 下会使用 project 文件编译。而 CMake 则是一个跨平台的编译工具,它并不会直接编译出对象,而是根据自定义的语言规则(CMakeLists.txt)生成 对应 makefileproject 文件,然后再调用底层的编译。

NDK、CMake、LLDB 的作用 https://developer.android.com/ndk/guides

JNI

JNI(Java Native Interface)是一个 Java 提供的编程框架,允许 Java 代码与其他语言(主要是 C 和 C++)编写的代码进行交互。JNI 使得 Java 应用程序能够调用本地(native)方法,实现与平台相关的功能,或者利用已有的 C/C++ 代码库。

从 Java 调用 Native 或从 Native 调用 Java 的成本很高,使用 JNI 时要限制跨越 JNI 边界的调用次数;

Java 的 native 方法和 JNI 函数是一一对应的映射关系,建立这种映射关系的注册方式有 2 种:

  • 方式 1 - 静态注册: 基于命名约定建立映射关系;
  • 方式 2 - 动态注册: 通过 JNINativeMethod 结构体建立映射关系。

ABI 与指令集

不同的 Android 设备使用不同的 CPU,而不同的 CPU 支持不同的指令集。CPU 与指令集的每种组合都有专属的应用二进制接口 (ABI)。ABI(Application binary interface)应用程序二进制接口。不同的 CPU 与指令集的每种组合都有定义的 ABI (应用程序二进制接口),一段程序只有遵循这个接口规范才能在该 CPU 上运行,所以同样的程序代码为了兼容多个不同的 CPU,需要为不同的 ABI 构建不同的库文件。当然对于 CPU 来说,不同的架构并不意味着一定互不兼容。

https://developer.android.com/ndk/guides/abis

工程

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
package com.example.myso;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.TextView;

import com.example.myso.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {

// Used to load the 'myso' library on application startup.
static {
System.loadLibrary("myso"); //创建的 so 文件名,并加载
}

private ActivityMainBinding binding;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());

// Example of a call to a native method
TextView tv = binding.sampleText;
tv.setText(stringFromJNI());
}

/**
* A native method that is implemented by the 'myso' native library,
* which is packaged with this application.
*/
public native String stringFromJNI(); //native 函数的声明
}

so 库需要在运行时调用 System.loadLibrary(…) 加载,一般有 2 种调用时机:

  • 1、在类静态初始化中: 如果只在一个类或者很少类中使用到该 so 库,则最常见的方式是在类的静态初始化块中调用;
  • 2、在 Application 初始化时调用: 如果有很多类需要使用到该 so 库,则可以考虑在 Application 初始化等场景中提前加载。

native-lib.cpp JNI 函数的静态注册规则

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

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

extern "C":指定 C++ 代码使用 C 风格的链接,以避免 C++ 名称修饰(name mangling),确保 Java 可以找到这个函数。

JNIEXPORTJNICALL:是宏定义,用于指示 JNI 的函数返回类型和调用约定。表示一个函数需要暴露给共享库外部使用时。 在 Window 和 Linux 上有不同的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
// Windows 平台 :
#define JNIEXPORT __declspec(dllexport)
#define JNIIMPORT __declspec(dllimport)

// Linux 平台:
#define JNIIMPORT
#define JNIEXPORT __attribute__ ((visibility ("default")))

// Windows 平台 :
#define JNICALL __stdcall // __stdcall 是一种函数调用参数的约定 , 表示函数的调用参数是从右往左。

// Linux 平台:
#define JNICALL

jstring:函数的返回类型,表示返回一个 Java 字符串。

Java_com_example_myso_MainActivity_stringFromJNI:这是 JNI 函数的名称,遵循特定的命名规则,以便 Java 能找到它。采用 JNI 函数静态注册约定的函数命名规则

JNIEnv* env:指向 JNI 环境的指针,用于访问 JNI 函数。

jobject /* this */:指向调用该方法的 Java 对象的引用。在这里我们没有使用它,所以用注释标记。 jobject 在 native 函数的声明改为 static 时,使用 jclass,因为静态方法可以通过类直接调用。

hello.c_str():将 std::string 转换为 C 风格的字符串(const char*)。

env->NewStringUTF(...):调用 JNI 提供的函数,将 C 风格字符串转换为 Java 字符串(jstring)。该方法会在 Java 堆上创建一个新的字符串对象,并返回其引用。

cmake 脚本。Java 的数据和 so 的数据不互通,如果 so 的数据最后要转到 java 层处理就需要 NewstringUTF,因此 NewstringUTF 可以成为一个 hook 点。

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
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html.
# For more examples on how to use CMake, see https://github.com/android/ndk-samples.
# Sets the minimum CMake version required for this project.
//这些注释提供了有关如何在 Android Studio 中使用 CMake 的文档链接和示例,供开发者参考。
cmake_minimum_required(VERSION 3.22.1)
//该行指定此项目所需的最低 CMake 版本。确保使用的 CMake 版本符合要求。
# Declares the project name. The project name can be accessed via ${ PROJECT_NAME},
# Since this is the top level CMakeLists.txt, the project name is also accessible
# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level
# build script scope).
project("myso")
//这里声明了项目的名称为 "myso",在整个 CMake 文件中,可以通过 ${PROJECT_NAME} 和 ${CMAKE_PROJECT_NAME} 访问这个名称。
# 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.
#
# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define
# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME}
# is preferred for the same purpose.
#
# In order to load a library into your app from Java/Kotlin, you must call
# System.loadLibrary() and pass the name of the library defined here;
# for GameActivity/NativeActivity derived applications, the same library name must be
# used in the AndroidManifest.xml file.
add_library(${CMAKE_PROJECT_NAME} SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt.
native-lib.cpp)
//add_library 创建一个名为 ${CMAKE_PROJECT_NAME} 的库(在这里是 " myso "),并指定为共享库(SHARED)。这表示生成的库可以被其他应用程序共享使用。
//native-lib.cpp 是要编译的源文件,使用相对路径引用。
# Specifies libraries CMake should link to your target library. You
# can link libraries from various origins, such as libraries defined in this
# build script, prebuilt third-party libraries, or Android system libraries.
target_link_libraries(${CMAKE_PROJECT_NAME}
# List libraries link to the target library
android
log)
//这一部分指定了要链接到目标库的其他库,包括 Android 系统库 android 和 log。这些库通常用于 Android 开发中的日志记录和其他系统功能。

CMakeLists.txt 中主要定义了哪些文件需要编译,以及和其他库的关系等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
cmake_minimum_required(VERSION 3.4.1)

# 编译出一个动态库 native-lib,源文件只有 src/main/cpp/native-lib.cpp
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 )

# 找到预编译库 log_lib 并link到我们的动态库 native-lib中
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( # Specifies the target library.
native-lib
# Links the target library to the log library
# included in the NDK.
${log-lib} )

grdle

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
45
46
47
48
plugins {
alias(libs.plugins.android.application)
}

android {
namespace 'com.example.myso'
compileSdk 34

defaultConfig {
applicationId "com.example.myso"
minSdk 26
targetSdk 34
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
externalNativeBuild {
cmake {
path file('src/main/cpp/CMakeLists.txt')
version '3.22.1'
}
}
buildFeatures {
viewBinding true
}
}

dependencies {

implementation libs.appcompat
implementation libs.material
implementation libs.constraintlayout
testImplementation libs.junit
androidTestImplementation libs.ext.junit
androidTestImplementation libs.espresso.core
}

JavaVM 和 JNIEnv

JavaVMJNIEnv 是定义在 jni.h 头文件中最关键的两个数据结构:

  • JavaVM: 代表 Java 虚拟机,每个 Java 进程有且仅有一个全局的 JavaVM 对象,JavaVM 可以跨线程共享;

  • JNIEnv: 代表 Java 运行环境,每个 Java 线程都有各自独立的 JNIEnv 对象,JNIEnv 不可以跨线程共享。

JavaVM 和 JNIEnv 的类型定义在 C 和 C++ 中略有不同,但本质上是相同的,内部由一系列指向虚拟机内部的函数指针组成。 类似于 Java 中的 Interface 概念,不同的虚拟机实现会从它们派生出不同的实现类,而向 JNI 层屏蔽了虚拟机内部实现

jni.h 中,JNIInvokeInterface*JNINativeInterface* 这两个结构体指针是 JavaVM 和 JNIEnv 的实体

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
45
46
47
48
49
50
51
52
53
struct _JNIEnv;
struct _JavaVM;

#if defined(__cplusplus)
// 如果定义了 __cplusplus 宏,则按照 C++ 编译
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
// 按照 C 编译
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif

/*
* C++ 版本的 _JavaVM,内部是对 JNIInvokeInterface* 的包装
*/
struct _JavaVM {
// 相当于 C 版本中的 JNIEnv
const struct JNIInvokeInterface* functions;

// 转发给 functions 代理
jint DestroyJavaVM()
{ return functions->DestroyJavaVM(this); }
jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)
{ return functions->AttachCurrentThread(this, p_env, thr_args); }
jint DetachCurrentThread()
{ return functions->DetachCurrentThread(this); }
jint GetEnv(void** env, jint version)
{ return functions->GetEnv(this, env, version); }
jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args)
{ return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }
#endif /*__cplusplus*/
};

/*
* C++ 版本的 JNIEnv,内部是对 JNINativeInterface* 的包装
*/
struct _JNIEnv {
// 相当于 C 版本的 JavaVM
const struct JNINativeInterface* functions;//在_JNIEnv 中定义了一个 functions 变量,这个变量是指向 JNINativeInterface 的指针。

// 转发给 functions 代理
jint GetVersion()
{ return functions->GetVersion(this); }

jclass DefineClass(const char *name, jobject loader, const jbyte* buf,
jsize bufLen)
{ return functions->DefineClass(this, name, loader, buf, bufLen); }

jclass FindClass(const char* name)
{ return functions->FindClass(this, name); }
...
};

结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
* JavaVM
*/
struct JNIInvokeInterface {
// 一系列函数指针
jint (*DestroyJavaVM)(JavaVM*);
jint (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
jint (*DetachCurrentThread)(JavaVM*);
jint (*GetEnv)(JavaVM*, void**, jint);
jint (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);
};

/*
* JNIEnv
*/
struct JNINativeInterface {
// 一系列函数指针
jint (*GetVersion)(JNIEnv *);
jclass (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*, jsize);
jclass (*FindClass)(JNIEnv*, const char*);
...
};

log 输出

1
2
3
4
5
6
// 不同的日志级别
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, __VA_ARGS__)

ANDROID_LOG_DEBUG: 调试信息

ANDROID_LOG_INFO: 一般信息

ANDROID_LOG_WARN: 警告信息

ANDROID_LOG_ERROR: 错误信息

ANDROID_LOG_FATAL: 致命错误

JNI 函数注册

静态注册 JNI 函数

静态注册采用的是基于「约定」的命名规则,通过 javah 可以自动生成 native 方法对应的函数声明(IDE 会智能生成,不需要手动执行命令)。名称格式为 Java_<包名>_<类名>_<方法名>

静态注册的命名规则分为「无重载」和「有重载」2 种情况:无重载时采用「短名称」规则,有重载时采用「长名称」规则。

  • 短名称规则(short name): Java_[类的全限定名 (带下划线)]_[方法名] ,其中类的全限定名中的 . 改为 _
  • 长名称规则(long name): 在短名称的基础上后追加两个下划线(__)和参数描述符,以区分函数重载。

系统会通过 dlopen 加载对应的 so,通过 dlsym 来获取指定名字的函数地址,然后调用静态注册的 jni 函数

动态注册 JNI 函数

静态注册是在首次调用 Java native 方法时搜索对应的 JNI 函数,而动态注册则是提前手动建立映射关系,并且不需要遵守静态注册的 JNI 函数命名规则。

动态注册使用方法

动态注册需要使用 RegisterNatives(...) 函数,其定义在 jni.h 文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct JNINativeInterface {
// 注册
// 参数二:Java Class 对象的表示
// 参数三:JNINativeMethod 结构体数组
// 参数四:JNINativeMethod 结构体数组长度
jint (*RegisterNatives)(JNIEnv*, jclass, const JNINativeMethod*, jint);
// 注销
// 参数二:Java Class 对象的表示
jint (*UnregisterNatives)(JNIEnv*, jclass);
};

typedef struct {
const char* name; // Java 方法名
const char* signature; // Java 方法描述符
void* fnPtr; // JNI 函数指针
} JNINativeMethod;

动态注册原理分析

RegisterNatives 方式的本质是直接通过结构体指定映射关系,而不是等到调用 native 方法时搜索 JNI 函数指针,因此动态注册的 native 方法调用效率更高。此外,还能减少生成 so 库文件中导出符号的数量,则能够优化 so 库文件的体积。

注册 JNI 函数的时机

注册时机 注册方式 描述
在第一次调用该 native 方法时 静态注册 虚拟机会在 JNI 函数库中搜索函数指针并记录下来,后续调用不需要重复搜索
加载 so 库时 动态注册 加载 so 库时会自动回调 JNI_OnLoad 函数,在其中调用 RegisterNatives 注册
提前注册 动态注册 在加载 so 库后,调用该 native 方法前,通过静态注册的 native 函数触发 RegisterNatives 注册。例如在 App 启动时,很多系统源码会提前做一次注册

so

加载

在 Android 添加 so 有两种方式,

一种是调用 load(String filename) 方法,传递进去的是路径;

另一种是调用 loadLibrary(String libname) 方式,传递进去的是 so 的名称

1
2
3
4
5
6
7
8
System.loadLibrary(libPath)
-> Runtime.load0(libPath)
-> nativeLoad(libPath)

System.loadLibrary(libName)
-> Runtime.loadLibrary0(libNane)
-> ClassLoader#findLibrary(libName)-> DexPathList#findLibrary(libName)
-> nativeLoad(libPath)

nativeLoad

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
45
//共享库列表
std::unique_ptr<Libraries> libraries_;

//已简化
bool JavaVMExt::LoadNativeLibrary(JNIEnv* env,
const std::string& path,
jobject class_loader,
std::string* error_msg) {
SharedLibrary* library;
Thread* self = Thread::Current();

//1、检查是否已经加载过
library = libraries_->Get(path);

//2、已经加载过,跳过
if (library != nullptr) {
...
return true;
}

//3、调用 dlopen 打开 so 库
void* handle = dlopen(path,RTLD_NOW);

//4、创建共享库
std::unique_ptr<SharedLibrary> new_library(
new SharedLibrary(env,
self,
path,
handle,
needs_native_bridge,
关注点:共享库中持有 ClassLoader(卸载 so 库时用到)
class_loader,
class_loader_allocator));

//5、将共享库记录到 libraries_ 表中
libraries_->Put(path, library);

// 6、调用 so 库中的 JNI_OnLoad 方法
void* sym = dlsym(library,"JNI_OnLoad");
typedef int (*JNI_OnLoadFn)(JavaVM*, void*);
JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym);
int version = (*jni_on_load)(this, nullptr);

return true
}

JNI_OnLoad

在使用 native 方法前都会先加载该 native 方法的 so 文件,通常在一个类的静态代码块中进行加载,当然也可以在构造函数,或者调用前加载。jvm 在加载 so 时都会先调用 so 中的 JNI_OnLoad 函数,如果你没有重写该方法,那么系统会给你自动生成一个。

1
2
3
4
5
6
7
8
9
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = nullptr;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
LOGD("GetEnv failed");
return -1;
}
return JNI_VERSION_1_6;
//如果我们的 *.so 中没有提供 JNI_OnLoad()函数,VM 会默认该*.so 档是使用最老的 JNI 1.1 版本。
}

1、一个 so 中可以不定义 JNI_OnLoad,一旦定义了 JNI_OnLoad,在 so 被加载的时候会自动执行,必须返回 JNI 版本 JNI_VERSION_1_6

卸载 so 库

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
已简化
void UnloadNativeLibraries(){
//1、遍历共享库列表 libraries_
for (auto it = libraries_.begin(); it != libraries_.end(); ) {
SharedLibrary* const library = it->second;

//2、检查关联的 ClassLoader 是否卸载(unload)
const jweak class_loader = library->GetClassLoader();
if (class_loader != nullptr && self->IsJWeakCleared(class_loader)) {

//3、记录需要卸载的共享库
unload_libraries.push_back(library);
it = libraries_.erase(it);
} else {
++it;
}
}
//4、遍历需要卸载的共享库,执行 JNI_OnUnloadFn()
typedef void (*JNI_OnUnloadFn)(JavaVM*, void*);
for (auto library : unload_libraries) {
void* const sym = dlsym(library, "JNI_OnUnload")
JNI_OnUnloadFn jni_on_unload = reinterpret_cast<JNI_OnUnloadFn>(sym);
jni_on_unload(self->GetJniEnv()->GetVm(), nullptr);

5、回收内存
delete library;
}
}

数据类型转换

Java 类型映射

基础数据类型: 会直接转换为 C/C++ 的基础数据类型,例如 int 类型映射为 jint 类型。由于 jint 是 C/C++ 类型,所以可以直接当作普通 C/C++ 变量使用,而不需要依赖 JNIEnv 环境对象;

引用数据类型: 对象只会转换为一个 C/C++ 指针,例如 Object 类型映射为 jobject 类型。由于指针指向 Java 虚拟机内部的数据结构,所以不可能直接在 C/C++ 代码中操作对象,而是需要依赖 JNIEnv 环境对象。另外,为了避免对象在使用时突然被回收,在本地方法返回前,虚拟机会固定(pin)对象,阻止其 GC。

基础数据类型在映射时是直接映射,而不会发生数据格式转换。例如,Java char 类型在映射为 jchar 后旧是保持 Java 层的样子,数据长度依旧是 2 个字节,而字符编码依旧是 UNT-16 编码。

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
typedef uint8_t  jboolean; /* unsigned 8 bits */
typedef int8_t jbyte; /* signed 8 bits */
typedef uint16_t jchar; /* unsigned 16 bits */ /* 注意:jchar 是 2 个字节 */
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 */
typedef jint jsize;

#ifdef __cplusplus
// 内部的数据结构由虚拟机实现,只能从虚拟机源码看
class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jobjectArray : public _jarray {};
class _jbooleanArray : public _jarray {};
...
// 说明我们接触到到 jobject、jclass 其实是一个指针
typedef _jobject* jobject;
typedef _jclass* jclass;
typedef _jstring* jstring;
typedef _jarray* jarray;
typedef _jobjectArray* jobjectArray;
typedef _jbooleanArray* jbooleanArray;
...
#else /* not __cplusplus */
...
#endif /* not __cplusplus */

字符串类型操作

Java 中的 java.lang.String 字符串类型也会映射为一个 jobject 指针。可能是因为字符串的使用频率实在是太高了,所以 JNI 规范还专门定义了一个 jobject 的派生类 jstring 来表示 Java String 类型,这个相对特殊。

由于 Java 与 C/C++ 默认使用不同的字符编码,因此在操作字符数据时,需要特别注意在 UTF-16 和 UTF-8 两种编码之间转换。

1、Java String 对象转换为 C/C++ 字符串: 调用 GetStringUTFChars 函数将一个 jstring 指针转换为一个 UTF-8 的 C/C++ 字符串,并在不再使用时调用 ReleaseStringChars 函数释放内存;

2、构造 Java String 对象: 调用 NewStringUTF 函数构造一个新的 Java String 字符串对象。

数组类型操作

与 jstring 的处理方式类似,JNI 规范将 Java 数组定义为 jobject 的派生类 jarray

  • 基础类型数组:定义为 jbooleanArrayjintArray 等;
  • 引用类型数组:定义为 jobjectArray

下面区分基础类型数组和引用类型数组两种情况:

操作基础类型数组(以 jintArray 为例):

  • 1、Java 基本类型数组转换为 C/C++ 数组: 调用 GetIntArrayElements 函数将一个 jintArray 指针转换为 C/C++ int 数组;
  • 2、修改 Java 基本类型数组: 调用 ReleaseIntArrayElements 函数并使用模式 0;
  • 3、构造 Java 基本类型数组: 调用 NewIntArray 函数构造 Java int 数组。

NDK 多线程

创建线程的方法

在 JNI 开发中,有两种创建线程的方式:

  • 方法 1 - 通过 Java API 创建: 使用我们熟悉的 Thread#start() 可以创建线程,优点是可以方便地设置线程名称和调试;
  • 方法 2 - 通过 C/C++ API 创建: 使用 pthread_create()std::thread 也可以创建线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void *thr_fn(void *arg) {
printids("new thread: ");
return NULL;
}

int main(void) {
pthread_t ntid;
//第一个是指向 pthread 的指针,也是线程 id,第二个是线程属性,第三个是线程执行的函数,第四个是函数参数
err = pthread_create(&ntid, NULL, thr_fn, NULL);
if (err != 0) {
printf("can't create thread: %s\n", strerror(err));
}
return 0;
}

线程引用

JNIEnv: JNIEnv 只在所在的线程有效,在不同线程中调用 JNI 函数时,必须使用该线程专门的 JNIEnv 指针,不能跨线程传递和使用。通过 AttachCurrentThread 函数将当前线程依附到 JavaVM 上,获得属于当前线程的 JNIEnv 指针。如果当前线程已经依附到 JavaVM,也可以直接使用 GetEnv 函数。

局部引用: 局部引用只在创建的线程和方法中有效,不能跨线程使用。可以将局部引用升级为全局引用后跨线程使用

1
2
3
4
5
6
7
8
// 局部引用
jclass localRefClz = env->FindClass("java/lang/String");
// 释放全局引用(非必须)
env->DeleteLocalRef(localRefClz);
// 局部引用升级为全局引用
jclass globalRefClz = env->NewGlobalRef(localRefClz);
// 释放全局引用(必须)
env->DeleteGlobalRef(globalRefClz);

默认的线程属性是 joinable 随着主线程结束而结束

1
2
pthread_create(&pthread, nullptr, reinterpret_cast<void *(*)(void *)>(myThread), nullptr);
//线程属性是 dettach,可以分离执行

JavaVM 的获取方式

1
2
3
4
5
6
7
8
9
10
11
12
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = nullptr;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
LOGD("GetEnv failed");
return -1;
}
LOGD("JavaVM %p",vm);
JavaVM** vm1;
env->GetJavaVM(vm1);
LOGD("env->GetJavaVM %p",*vm1);
return JNI_VERSION_1_6;
}

hook

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
function hook_native(){
var libnative_addr = Module.findBaseAddress('libnative-lib.so');//so 文件名
console.log("libnative_addr is => ",libnative_addr)
var stringfromJNI3 = libnative_addr.add(0xf454);//hook 的函数地址 baseadress+偏移
console.log("stringfromJNI3 address is =>",stringfromJNI3);

var stringfromJNI3_2 = Module.findExportByName('libnative-lib.so', "_Z14stringFromJNI3P7_JNIEnvP7_jclassP8_jstring")
console.log("stringfromJNI3_2 address is =>",stringfromJNI3_2);

Interceptor.attach(stringfromJNI3_2,{
onEnter:function(args){

console.log("jnienv pointer =>",args[0])
console.log("jobj pointer =>",args[1])
console.log("jstring pointer=>",Java.vm.getEnv().getStringUtfChars(args[2], null).readCString() )

},onLeave:function(retval){
console.log("retval is =>",Java.vm.getEnv().getStringUtfChars(retval, null).readCString())
console.log("=================")

}
})

}
function main(){
hook_native()
}
setImmediate(main)