[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結構

## 注冊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 性能優化
- 數據跨平臺