JNI用法
根据JNI的定义可以知道,Java层想要调用JNI层函数,就需要JNI来实现。接下来看看JNI的使用方法:
JNI的使用主要有以下几个步骤:
1. Java 层
在Java层,主要完成两件事:
1.1 加载JNI库
使用System.loadLibrary()加载so.
例如,导入名为"hello_jni"的库。
static {
System.loadLibrary("hello_jni");
}
加载的库的名字为“hello_jni”,这个库在不同的平台的拓展名不一样:
- linux:在linux上会拓展为
xxx.so。例子中的库会被拓展为hello_jni.so - windows:在windows上会拓展为
xxx.dll。例子中的库会被拓展为hello_jni.dll
原则上,JNI库只要在调用定义的native函数之前加载就行,一般放在static中进行加载。
1.2 定义满足业务需求的函数
例如,定义一个函数,打印一个字符串。
public static native String sayHello();
其中native关键字表示这个函数将由JNI层实现。
完整的Java例子代码如下:
package com.example.jnidemo;
public class JniExample {
static {
System.loadLibrary("hello_jni");
}
public static void javaSayHello(){
System.out.println("java say hello");
}
// 使用native关键字定义一个函数
public static native String sayHello();
}
2. 编译生成头文件
2.1 编译生成class文件
将在Java层完成的代码编译程.class文件。在例子中就是将JniExample.java编译成JniExample.class。
有两个方法:
- 使用Android studio的
build->build/rebuild project
使用Android Studio编译生成的.class文件在app/build/intermediates/javac/debug/classes下面。
- 使用javac指令编译
javac xxx.java
例如:javac JniExample.java
使用指令生成的.class文件在对应的Java类的目录下。当然也可以修改生成的文件的路径。
2.2 反编译生成.h头文件
使用javah指令反编译2.1中生成的.class字节码文件
javah -o <targetjniname> <classpath>
例如 javah -o hello_jni.h com.example.jnidemo.JniExample
其中 -o 表示输出文件,-o之后的参数最好使用System.``loadLibrary导入的库的名字。如果反编译javac指令 编译出的.class出现问题,可以使用cd到buil生成的文件下使用javah反编译。
反编译完成的.h文件如下所示:
hello.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_jnidemo_JniExample */
#ifndef _Included_com_example_jnidemo_JniExample
#define _Included_com_example_jnidemo_JniExample
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_jnidemo_JniExample
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT jstring JNICALL Java_com_example_jnidemo_JniExample_sayHello
(JNIEnv *env, jclass);
#ifdef __cplusplus
}
#endif
#endif
3. 环境配置
在实现反编译生成的头文件之前,需要配置环境。
3.1 配置ndk
- 下载NDK
下载NDK主要有两种方法,任选其一即可:
- 在Android Studio中通过
File -> Settings -> Android SDK -> SDK Tools,勾选Show Package Details,选择一个合适的NDK版本,点击Apply等待下载,然后点击OK。 - 在Android Studio的右上角点击
SDK Manager,选择版本下载。
- 配置项目的NDK
配置NDK也有两种方法,任选其一:
- 在Android Studio 中点击
Project Structure -> SDK Location -> Android NDK location选择对应目录下的ndk。然后点击Apply -> OK
- 点击Project下的
local.properties,手动添加ndk位置。
3.2 配置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.10.2)
# Declares and names the project.
// 项目名称
project("jnidemo")
# 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.
// 库名称
hello_jni
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
// 实现头文件的.cpp文件
hello_jni.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.
// jni名称
hello_jni
# Links the target library to the log library
# included in the NDK.
${log-lib} )
4. 实现函数
新建.cpp文件,实现头文件中定义的方法
如:新建hello_jni.cpp,实现hello_jni.h中定义的方法。
#include "hello_jni.h"
#include <string>
extern "C" JNIEXPORT jstring JNICALL Java_com_example_jnidemo_JniExample_sayHello
(JNIEnv *env, jclass)
{
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
5. 在Java层调用
以上打通了Java层到Native之间的隔阂,现在只需要在需要的地方调用Native层的函数就可以了。
例如:
package com.example.jnidemo;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
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);
JniExample.javaSayHello();
Log.d(TAG, "onCreate: "+JniExample.sayHello());
}
}
项目最终结构如下所示:
run一下
JNI原理
在上面的hello_jni的例子中,函数的调用逻辑是这样的
让人好奇的是,JniExample.java中声明的函数是sayHello(),JNI层定义和实现的函数是:
Java_com_example_jnidemo_JniExample_sayHello(JNIEnv *env, jclass),这明显和Java层定义的函数不完全一样。从项目最终结构可以看到JniExample.java的位置在java.com``.example.jnidemo目录下。这似乎和JNI层实现的函数类似。仔细看看不难发现,JNI层定义和实现的Java_com_example_jnidemo_JniExample_sayHello(JNIEnv *env, jclass),就是JniExample.sayHello()前面加上JniExample.java所在的位置,并且把所有的 " . " 换成了 “ _ ”。
在Java层调用sayHello()的时候,会从对应的JNI库中寻找Java_com_example_jnidemo_JniExample_sayHello(JNIEnv *env, jclass),如果找不到,则会报错;
例如:
java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader:xxxxx couldn't find "libhello_jni.so);
如果找的到,则会为sayHello()和Java_com_example_jnidemo_JniExample_sayHello(JNIEnv *env, jclass)建立联系。其实就是保存JNI层函数的指针。以后再调用sayHello()的时候,虚拟机可以直接使用这个指针。
静态注册
通过函数名建立起Java层函数和Native层函数之间的联系,JNI层函数遵守特定的格式,这种方法就是静态注册。上述详细描述JNI用法的示例,就是静态注册。可以看到静态注册有以下特点:
- 需要先编译声明Native函数的Java类,生成二进制文件
xxx.class,然后通过javah反编译,生成一个.h头文件。如果有多个声明了Native函数的Java类,都需要反编译生成对应的.h头文件。 - 经过反编译生成的头文件中定义的函数有固定的命名方式,一般使用这个固定命名方式的函数名都会很长(因为包含包名)。
- 第一次调用/建立联系的时候,需要通过函数名来检索字符串,最终建立联系。而函数名通常很长,String类占用内存多,检索速度也比较慢,因此第一次调用的时候比较很低。
既然Java和Native函数之间的联系是通过JNI指针建立的,那么如果能够让Native层的函数知道JNI层对应函数的函数指针,似乎就可以避免效率低等问题了。猜想一下,如果有这样一个结构体,应该记录以下信息:
- Java层函数名
- JNI层函数指针
动态注册
动态注册没使用过,只能使用AOSP中的代码来看看了。
JNI层的函数指针由一个结构体来记录:
libnativehelper/include_jni/jni.h
jni.h
typedef struct {
// java层函数名,例如JniExample.java中定义的sayHello()
const char* name;
// signature?签名?
const char* signature;
// JNI层对应函数的函数指针
void* fnPtr;
} JNINativeMethod;
结构体JNINativeMethod中主要记录了三个信息:
- Java层定义的函数名
- 签名
- JNI层对应的函数指针
从MediaScanner的进程看看这个结构体的使用方法
frameworks/base/media/jni/android_media_MediaScanner.cpp
android_media_MediaScanner.cpp
static const JNINativeMethod gMethods[] = {
{
// Java层函数名
"processDirectory",
// 签名
"(Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
// JNI层函数指针
(void *)android_media_MediaScanner_processDirectory
},
{
"processFile",
"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)Z",
(void *)android_media_MediaScanner_processFile
},
{
"setLocale",
"(Ljava/lang/String;)V",
(void *)android_media_MediaScanner_setLocale
},
{
"extractAlbumArt",
"(Ljava/io/FileDescriptor;)[B",
(void *)android_media_MediaScanner_extractAlbumArt
},
{
"native_init",
"()V",
(void *)android_media_MediaScanner_native_init
},
{
"native_setup",
"()V",
(void *)android_media_MediaScanner_native_setup
},
{
"native_finalize",
"()V",
(void *)android_media_MediaScanner_native_finalize
},
};
int register_android_media_MediaScanner(JNIEnv *env)
{
// 调用AndroidRuntime. registerNativeMethods()完成注册。
return AndroidRuntime::registerNativeMethods(env,
kClassMediaScanner, gMethods, NELEM(gMethods));
}
gMethods[]数组中记录的结构体就是JNINativeMethod。然后调用register_android_media_MediaScanner``()完成动态注册。
接下来看看AndroidRuntime::registerNativeMethods``()
AndroidRuntime.cpp
/*
* Register native methods using JNI.
*//*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env,
const char* className, const JNINativeMethod* gMethods, int numMethods)
{
return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}
调用jniRegisterNativeMethods``(),传入参数为刚才构建的JNINativeMethod数据等信息。
接下来看看jniRegisterNativeMethods``()。
libnativehelper/JNIHelp.c
JNIHelp.c
int jniRegisterNativeMethods(JNIEnv* env, const char* className,
const JNINativeMethod* methods, int numMethods)
{
ALOGV("Registering %s's %d native methods...", className, numMethods);
// find calss
jclass clazz = (*env)->FindClass(env, className);
ALOG_ALWAYS_FATAL_IF(clazz == NULL,
"Native registration unable to find class '%s'; aborting...",
className);
// 调用JNIEnv的RegisterNatives()完成注册
int result = (*env)->RegisterNatives(env, clazz, methods, numMethods);
(*env)->DeleteLocalRef(env, clazz);
if (result == 0) {
return 0;
}
// Failure to register natives is fatal. Try to report the corresponding exception,
// otherwise abort with generic failure message.
jthrowable thrown = (*env)->ExceptionOccurred(env);
if (thrown != NULL) {
struct ExpandableString summary;
ExpandableStringInitialize(&summary);
if (GetExceptionSummary(env, thrown, &summary)) {
ALOGF("%s", summary.data);
}
ExpandableStringRelease(&summary);
(*env)->DeleteLocalRef(env, thrown);
}
ALOGF("RegisterNatives failed for '%s'; aborting...", className);
return result;
}
也就是说最终是在JNIEnv中依次使用FindClass()和registerNatives()完成的注册。
动态函数的注册方式也就明了了:
jclass clazz = (*env)->FindClass(env, className);
(*env)->RegisterNatives(env, clazz, methods, numMethods);
所以,在自己的JNI层代码中,只需要使用以上两个方法就可以完成动态注册了。
在Java层通过System.loadLibrary()加载完JNI库之后,会查找该库的JNI_OnLoad函数。如果有,经通过JNI_OnLoad()函数完成动态注册。
例如,在android_media_MediaPlayer.cpp中,实现了JNI_OnLoad()
frameworks/base/media/jni/android_media_MediaPlayer.cpp
android_media_MediaPlayer.cpp
jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
{
JNIEnv* env = NULL;
jint result = -1;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
ALOGE("ERROR: GetEnv failed\n");
goto bail;
}
assert(env != NULL);
if (register_android_media_ImageWriter(env) != JNI_OK) {
ALOGE("ERROR: ImageWriter native registration failed");
goto bail;
}
if (register_android_media_ImageReader(env) < 0) {
ALOGE("ERROR: ImageReader native registration failed");
goto bail;
}
if (register_android_media_MediaPlayer(env) < 0) {
ALOGE("ERROR: MediaPlayer native registration failed\n");
goto bail;
}
if (register_android_media_MediaRecorder(env) < 0) {
ALOGE("ERROR: MediaRecorder native registration failed\n");
goto bail;
}
// 注册mediascanner
if (register_android_media_MediaScanner(env) < 0) {
ALOGE("ERROR: MediaScanner native registration failed\n");
goto bail;
}
if (register_android_media_MediaMetadataRetriever(env) < 0) {
ALOGE("ERROR: MediaMetadataRetriever native registration failed\n");
goto bail;
}
if (register_android_media_ResampleInputStream(env) < 0) {
ALOGE("ERROR: ResampleInputStream native registration failed\n");
goto bail;
}
if (register_android_media_MediaProfiles(env) < 0) {
ALOGE("ERROR: MediaProfiles native registration failed");
goto bail;
}
if (register_android_mtp_MtpDatabase(env) < 0) {
ALOGE("ERROR: MtpDatabase native registration failed");
goto bail;
}
if (register_android_mtp_MtpDevice(env) < 0) {
ALOGE("ERROR: MtpDevice native registration failed");
goto bail;
}
if (register_android_mtp_MtpServer(env) < 0) {
ALOGE("ERROR: MtpServer native registration failed");
goto bail;
}
if (register_android_media_MediaCodec(env) < 0) {
ALOGE("ERROR: MediaCodec native registration failed");
goto bail;
}
if (register_android_media_MediaSync(env) < 0) {
ALOGE("ERROR: MediaSync native registration failed");
goto bail;
}
if (register_android_media_MediaExtractor(env) < 0) {
ALOGE("ERROR: MediaCodec native registration failed");
goto bail;
}
if (register_android_media_MediaMuxer(env) < 0) {
ALOGE("ERROR: MediaMuxer native registration failed");
goto bail;
}
if (register_android_media_MediaCodecList(env) < 0) {
ALOGE("ERROR: MediaCodec native registration failed");
goto bail;
}
if (register_android_media_Crypto(env) < 0) {
ALOGE("ERROR: MediaCodec native registration failed");
goto bail;
}
if (register_android_media_Drm(env) < 0) {
ALOGE("ERROR: MediaDrm native registration failed");
goto bail;
}
if (register_android_media_Descrambler(env) < 0) {
ALOGE("ERROR: MediaDescrambler native registration failed");
goto bail;
}
if (register_android_media_MediaHTTPConnection(env) < 0) {
ALOGE("ERROR: MediaHTTPConnection native registration failed");
goto bail;
}
/* success -- return valid version number */
result = JNI_VERSION_1_4;
bail:
return result;
}
所以,通过动态注册还需要实现JNI_OnLoad()函数。
综上所述,使用动态注册JNI需要以下三个函数:
JNI_OnLoad()
jclass clazz = (*env)->FindClass(env, className);*
*(* env)->RegisterNatives(env, clazz, methods, numMethods);