[TOC]
# JNI基础
上面一节主要描述了系统中Java层和Native层交互和实现,并没有对JNI的基础理论,流程进行分析
## JNI命名规则
JNI方法名规范 :
~~~
返回值 + Java前缀 + 全路径类名 + 方法名 + 参数① JNIEnv + 参数② jobject + 其它参数
~~~
简单的一个例子,返回一个字符串
~~~
extern "C" JNIEXPORT jstring JNICALL
Java_com_yeungeek_jnisample_NativeHelper_stringFromJNI(JNIEnv *env, jclass jclass1) {
LOGD("##### from c");
return env->NewStringUTF("Hello JNI");
}
~~~
* 返回值:jstring
* 全路径类名:com\_yeungeek\_jnisample\_NativeHelper
* 方法名:stringFromJNI
## JNI开发流程
* 在Java中先声明一个native方法
* 编译Java源文件javac得到.class文件
* 通过javah -jni命令导出JNI的.h头文件
* 使用Java需要交互的本地代码,实现在Java中声明的Native方法(如果Java需要与C++交互,那么就用C++实现Java的Native方法。)
* 将本地代码编译成动态库(Windows系统下是.dll文件,如果是Linux系统下是.so文件,如果是Mac系统下是.jnilib)
* 通过Java命令执行Java程序,最终实现Java调用本地代码。
## 数据类型
### 基本数据类型
| Signature | Java | Native |
| --- | --- | --- |
| B | byte | jbyte |
| C | char | jchar |
| D | double | jdouble |
| F | float | jfloat |
| I | int | jint |
| S | short | jshort |
| J | long | jlong |
| Z | boolean | jboolean |
| V | void | jvoid |
### 引用数据类型
| Signature | Java | Native |
| --- | --- | --- |
| L+classname +; | Object | jobject |
| Ljava/lang/String; | String | jstring |
| \[L+classname +; | Object\[\] | jobjectArray |
| Ljava.lang.Class; | Class | jclass |
| Ljava.lang.Throwable; | Throwable | jthrowable |
| \[B | byte\[\] | jbyteArray |
| \[C | char\[\] | jcharArray |
| \[D | double\[\] | jdoubleArray |
| \[F | float\[\] | jfloatArray |
| \[I | int\[\] | jintArray |
| \[S | short\[\] | jshortArray |
| \[J | long\[\] | jlongArray |
| \[Z | boolean\[\] | jbooleanArray |
## 方法签名
JNI的方法签名的格式:
~~~
(参数签名格式...)返回值签名格式
复制代码
~~~
demo的native 方法:
~~~
public static native java.lang.String stringFromJNI();
复制代码
~~~
可以通过javap命令生成方法签名``:
~~~
()Ljava/lang/String;
复制代码
~~~
# JNI原理
Java语言的执行环境是Java虚拟机(JVM),JVM其实是主机环境中的一个进程,每个JVM虚拟机都在本地环境中有一个JavaVM结构体,该结构体在创建Java虚拟机时被返回,在JNI环境中创建JVM的函数为JNI\_CreateJavaVM。
JNI 定义了两个关键数据结构,即“JavaVM”和“JNIEnv”,两者本质上都是指向函数表的二级指针。
## JavaVM
JavaVM是Java虚拟机在JNI层的代表,JavaVM 提供了“调用接口”函数,您可以利用此类函数创建和销毁 JavaVM。理论上,每个进程可以包含多个JavaVM,但AnAndroid只允许每个进程包含一个JavaVM。
## JNIEnv
JNIEnv是一个线程相关的结构体,该结构体代表了Java在本线程的执行环境。JNIEnv 提供了大多数 JNI 函数。您的原生函数均会接收 JNIEnv 作为第一个参数。
JNIEnv作用:
* 调用Java函数
* 操作Java代码
JNIEnv定义(jni.h): `libnativehelper/include/nativehelper/jni.h`
~~~
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif
复制代码
~~~
定义中可以看到JavaVM,Android中一个进程只会有一个JavaVM,一个JVM对应一个JavaVM结构,而一个JVM中可能创建多个Java线程,每个线程对应一个JNIEnv结构
![](https://img.kancloud.cn/18/93/1893602172709cde07c48970ba284cc5_834x449.png)
## 注册JNI函数
Java世界和Native世界的方法是如何关联的,就是通过JNI函数注册来实现。JNI函数注册有两种方式:
### 静态注册
这种方法就是通过函数名来找对应的JNI函数,可以通过`javah`命令行来生成JNI头文件
~~~
javah com.yeungeek.jnisample.NativeHelper
复制代码
~~~
生成对应的`com_yeungeek_jnisample_NativeHelper.h`文件,生成对应的JNI函数,然后在实现这个函数就可以了
~~~
/*
* Class: com_yeungeek_jnisample_NativeHelper
* Method: stringFromJNI
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_yeungeek_jnisample_NativeHelper_stringFromJNI
(JNIEnv *, jclass);
复制代码
~~~
静态注册方法中,Native是如何找到对应的JNI函数,在[JNI查找方式](#JNI%E6%9F%A5%E6%89%BE%E6%96%B9%E5%BC%8F "#JNI%E6%9F%A5%E6%89%BE%E6%96%B9%E5%BC%8F")中介绍系统的流程,并没有详细说明静态注册的查找。这里简单说明下这个过程(以上面的声明为例子s):
当Java调用native stringFromJNI函数时,会从对应JNI库中查找`Java_com_yeungeek_jnisample_NativeHelper_stringFromJNI`函数,如果没有找到,就会报错。
静态注册方法,就是根据函数名来关联Java函数和JNI函数,JNI函数需要遵循特定的格式,这其中就有一些缺点:
* 声明了native方法的Java类,需要通过`javah`来生成头文件
* JNI函数名称非常长
* 第一次调用native函数,需要通过函数名来搜索关联对应的JNI函数,效率比较低
如何解决这些问题,让native函数,提前知道JNI函数,就可以解决这个问题,这个过程就是动态注册。
### 动态注册
动态注册在前面的Camera例子中,已经有涉及到,JNI函数`classInit`的声明。
~~~
static const JNINativeMethod gCameraMetadataMethods[] = {
// static methods
{ "nativeClassInit",
"()V",
(void *)CameraMetadata_classInit }, //和Java层nativeClassInit()对应
......
}
复制代码
~~~
JNI中有一种结构用来记录Java的Native方法和JNI方法的关联关系,它就是JNINativeMethod,它在jni.h中被定义:
~~~
typedef struct {
const char* name; //Java层native函数名
const char* signature; //Java函数签名,记录参数类型和个数,以及返回值类型
void* fnPtr; //Native层对应的函数指针
} JNINativeMethod;
复制代码
~~~
在[JNI查找方式](#JNI%E6%9F%A5%E6%89%BE%E6%96%B9%E5%BC%8F "#JNI%E6%9F%A5%E6%89%BE%E6%96%B9%E5%BC%8F")说到,JNI注册的两种时间,第一种已经介绍过了,我们自定义的native函数,基本都是会使用`System.loadLibrary(“xxx”)`,来进行JNI函数的关联。
#### loadLibrary(Android7.0)
~~~
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}
复制代码
~~~
调用到Runtime(libcore/ojluni/src/main/java/java/lang/Runtime.java)的loadLibrary0方法:
~~~
synchronized void loadLibrary0(ClassLoader loader, String libname) {
......
String libraryName = libname;
if (loader != null) {
String filename = loader.findLibrary(libraryName);
if (filename == null) {
// It's not necessarily true that the ClassLoader used
// System.mapLibraryName, but the default setup does, and it's
// misleading to say we didn't find "libMyLibrary.so" when we
// actually searched for "liblibMyLibrary.so.so".
throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
System.mapLibraryName(libraryName) + "\"");
}
//doLoad
String error = doLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
//loader 为 null
......
for (String directory : getLibPaths()) {
String candidate = directory + filename;
candidates.add(candidate);
if (IoUtils.canOpenReadOnly(candidate)) {
String error = doLoad(candidate, loader);
if (error == null) {
return; // We successfully loaded the library. Job done.
}
lastError = error;
}
}
......
}
复制代码
~~~
#### doLoad
~~~
private String doLoad(String name, ClassLoader loader) {
//调用 native 方法
synchronized (this) {
return nativeLoad(name, loader, librarySearchPath);
}
}
复制代码
~~~
#### nativeLoad
进入到虚拟机代码`/libcore/ojluni/src/main/native/Runtime.c`
~~~
JNIEXPORT jstring JNICALL
Runtime_nativeLoad(JNIEnv* env, jclass ignored, jstring javaFilename,
jobject javaLoader, jstring javaLibrarySearchPath)
{
return JVM_NativeLoad(env, javaFilename, javaLoader, javaLibrarySearchPath);
}
复制代码
~~~
然后调用`JVM_NativeLoad`,JVM\_NativeLoad方法申明在jvm.h中,实现在`OpenjdkJvm.cc(/art/runtime/openjdkjvm/OpenjdkJvm.cc)`
~~~
JNIEXPORT jstring JVM_NativeLoad(JNIEnv* env,
jstring javaFilename,
jobject javaLoader,
jstring javaLibrarySearchPath) {
ScopedUtfChars filename(env, javaFilename);
if (filename.c_str() == NULL) {
return NULL;
}
std::string error_msg;
{
art::JavaVMExt* vm = art::Runtime::Current()->GetJavaVM();
bool success = vm->LoadNativeLibrary(env,
filename.c_str(),
javaLoader,
javaLibrarySearchPath,
&error_msg);
if (success) {
return nullptr;
}
}
// Don't let a pending exception from JNI_OnLoad cause a CheckJNI issue with NewStringUTF.
env->ExceptionClear();
return env->NewStringUTF(error_msg.c_str());
}
复制代码
~~~
#### LoadNativeLibrary
调用JavaVMExt的LoadNativeLibrary方法,方法在(art/runtime/java\_vm\_ext.cc)中,这个方法代码非常多,选取主要的部分进行分析
~~~
bool JavaVMExt::LoadNativeLibrary(JNIEnv* env,
const std::string& path,
jobject class_loader,
jstring library_path,
std::string* error_msg) {
......
bool was_successful = false;
//加载so库中查找JNI_OnLoad方法,如果没有系统就认为是静态注册方式进行的,直接返回true,代表so库加载成功,
//如果找到JNI_OnLoad就会调用JNI_OnLoad方法,JNI_OnLoad方法中一般存放的是方法注册的函数,
//所以如果采用动态注册就必须要实现JNI_OnLoad方法,否则调用java中申明的native方法时会抛出异常
void* sym = library->FindSymbol("JNI_OnLoad", nullptr);
if (sym == nullptr) {
VLOG(jni) << "[No JNI_OnLoad found in \"" << path << "\"]";
was_successful = true;
} else {
// Call JNI_OnLoad. We have to override the current class
// loader, which will always be "null" since the stuff at the
// top of the stack is around Runtime.loadLibrary(). (See
// the comments in the JNI FindClass function.)
ScopedLocalRef<jobject> old_class_loader(env, env->NewLocalRef(self->GetClassLoaderOverride()));
self->SetClassLoaderOverride(class_loader);
VLOG(jni) << "[Calling JNI_OnLoad in \"" << path << "\"]";
typedef int (*JNI_OnLoadFn)(JavaVM*, void*);
JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym);
//调用JNI_OnLoad方法
int version = (*jni_on_load)(this, nullptr);
if (runtime_->GetTargetSdkVersion() != 0 && runtime_->GetTargetSdkVersion() <= 21) {
// Make sure that sigchain owns SIGSEGV.
EnsureFrontOfChain(SIGSEGV);
}
self->SetClassLoaderOverride(old_class_loader.get());
}
......
}
复制代码
~~~
代码里的主要逻辑:
* 加载so库中查找JNI\_OnLoad方法,如果没有系统就认为是静态注册方式进行的,直接返回true,代表so库加载成功
* 如果找到JNI\_OnLoad就会调用JNI\_OnLoad方法,JNI\_OnLoad方法中一般存放的是方法注册的函数
* 所以如果采用动态注册就必须要实现`JNI_OnLoad`方法,否则调用Java中的native方法时会抛出异常
## jclass、jmethodID和jfieldID
如果要通过原生代码访问对象的字段,需要执行以下操作:
1. 使用 FindClass 获取类的类对象引用
2. 使用 GetFieldID 获取字段的字段 ID
3. 使用适当内容获取字段的内容,例如 GetIntField
具体的使用,放在第二篇文章中讲解
## JNI的引用
JNI规范中定义了三种引用:
* 局部引用(Local Reference)
* 全局引用(Global Reference)
* 弱全局引用(Weak Global Reference)
### 局部引用
也叫本地引用,在 JNI层函数使用的非全局引用对象都是Local Reference,最大的特点就是,JNI 函数返回后,这些声明的引用可能就会被垃圾回收
### 全局引用
这种声明的对象,不会主动释放资源,不会被垃圾回收
### 弱全局引用
一种特殊的全局引用,在运行过程中可能被回收,使用之前需要判断下是否为空
# 参考资料
[Android NDK-深入理解JNI](https://juejin.cn/post/6844903933375152136)
- Android
- 四大组件
- Activity
- Fragment
- Service
- 序列化
- Handler
- Hander介绍
- MessageQueue详细
- 启动流程
- 系统启动流程
- 应用启动流程
- Activity启动流程
- View
- view绘制
- view事件传递
- choreographer
- LayoutInflater
- UI渲染概念
- Binder
- Binder原理
- Binder最大数据
- Binder小结
- Android组件
- ListView原理
- RecyclerView原理
- SharePreferences
- AsyncTask
- Sqlite
- SQLCipher加密
- 迁移与修复
- Sqlite内核
- Sqlite优化v2
- sqlite索引
- sqlite之wal
- sqlite之锁机制
- 网络
- 基础
- TCP
- HTTP
- HTTP1.1
- HTTP2.0
- HTTPS
- HTTP3.0
- HTTP进化图
- HTTP小结
- 实践
- 网络优化
- Json
- ProtoBuffer
- 断点续传
- 性能
- 卡顿
- 卡顿监控
- ANR
- ANR监控
- 内存
- 内存问题与优化
- 图片内存优化
- 线下内存监控
- 线上内存监控
- 启动优化
- 死锁监控
- 崩溃监控
- 包体积优化
- UI渲染优化
- UI常规优化
- I/O监控
- 电量监控
- 第三方框架
- 网络框架
- Volley
- Okhttp
- 网络框架n问
- OkHttp原理N问
- 设计模式
- EventBus
- Rxjava
- 图片
- ImageWoker
- Gilde的优化
- APT
- 依赖注入
- APT
- ARouter
- ButterKnife
- MMKV
- Jetpack
- 协程
- MVI
- Startup
- DataBinder
- 黑科技
- hook
- 运行期Java-hook技术
- 编译期hook
- ASM
- Transform增量编译
- 运行期Native-hook技术
- 热修复
- 插件化
- AAB
- Shadow
- 虚拟机
- 其他
- UI自动化
- JavaParser
- Android Line
- 编译
- 疑难杂症
- Android11滑动异常
- 方案
- 工业化
- 模块化
- 隐私合规
- 动态化
- 项目管理
- 业务启动优化
- 业务架构设计
- 性能优化case
- 性能优化-排查思路
- 性能优化-现有方案
- 登录
- 搜索
- C++
- NDK入门
- 跨平台
- H5
- Flutter
- Flutter 性能优化
- 数据跨平台