您当前的位置:首页 > 电脑百科 > 程序开发 > 移动端 > Android

Android NDK-深入理解JNI

时间:2019-09-16 13:43:38  来源:  作者:

JAVA调用C/C++在Java语言里面本来就有的,并非Android独有的,即JNI。JNI就是Java调用C++的规范。

JNI 概述

JNI,全称为Java Native Interface,即Java本地接口,JNI是Java调用Native语言的一种特性,通过JNI可以使JAVA和 C/C++进行交互。

Java语言是跨平台的语言,而这跨平台的背后都是依靠Java虚拟机,虚拟机采用C/C++编写,适配各个系统,通过JNI为上层Java提供各种服务,保证跨平台性。

在Java语言出现前,就有很多程序和库都是由Native语言写的,如果想重复利用这些库,就可以所使用JNI来实现。在Android平台上,JNI就是一座将Java世界和Native世界联通的一座桥梁。

Android NDK-深入理解JNI

 

jni.png

通过JNI,Java世界和Native世界的代码就可以相互访问了。JNI实例:Camera

最新有在看系统的Camera相关,所以从系统Camera角度来分析下JNI的应用,下面讲的实例基于Camera2

Android5.0(21)之后android.hardware.Camera就被废弃了,取而代之的是全新的android.hardware.Camera2

相关代码:

frameworks/base/core/jni/AndroidRuntime.cpp
frameworks/base/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
frameworks/base/core/jni/android_hardware_camera2_CameraMetadata.cpp

Camera2 Java层对应的是CameraMetadataNative.java,Native层对应的是android_hardware_camera2_CameraMetadata.cpp

Java层CameraMetadataNative

相关代码在CameraMetadataNative.java

Camera2使用CameraManager(摄像头管理器)进行控制,CameraManager具体的操作会通过CameraMetadataNative来执行。

CameraMetadataNative的初始化

public class CameraMetadataNative implements Parcelable
 static {
 /*
 * We use a class initializer to allow the native code to cache some field offsets
 */
 nativeClassInit();
 registerAllMarshalers();
 }
 private static native void nativeClassInit();
}

静态方法初始化调用了Native层的方法nativeClassInit,这个方法对应的Native层具体实现,是在android_hardware_camera2_CameraMetadata.cpp

Native层CameraMetadata

Native层相关代码在android_hardware_camera2_CameraMetadata.cpp

Native方法初始化

static const JNINativeMethod gCameraMetadataMethods[] = {
// static methods
 { "nativeClassInit",
 "()V",
 (void *)CameraMetadata_classInit }, //和Java层nativeClassInit()对应
 { "nativeGetAllVendorKeys",
 "(Ljava/lang/Class;)Ljava/util/ArrayList;",
 (void *)CameraMetadata_getAllVendorKeys},
 { "nativeGetTagFromKey",
 "(Ljava/lang/String;)I",
 (void *)CameraMetadata_getTagFromKey },
 { "nativeGetTypeFromTag",
 "(I)I",
 (void *)CameraMetadata_getTypeFromTag },
 { "nativeSetupGlobalVendorTagDescriptor",
 "()I",
 (void*)CameraMetadata_setupGlobalVendorTagDescriptor },
// instance methods
 { "nativeAllocate",
 "()J",
 (void*)CameraMetadata_allocate },
 { "nativeAllocateCopy",
 "(L" CAMERA_METADATA_CLASS_NAME ";)J",
 (void *)CameraMetadata_allocateCopy },
 { "nativeIsEmpty",
 "()Z",
 (void*)CameraMetadata_isEmpty },
 { "nativeGetEntryCount",
 "()I",
 (void*)CameraMetadata_getEntryCount },
 { "nativeClose",
 "()V",
 (void*)CameraMetadata_close },
 { "nativeSwap",
 "(L" CAMERA_METADATA_CLASS_NAME ";)V",
 (void *)CameraMetadata_swap },
 { "nativeReadValues",
 "(I)[B",
 (void *)CameraMetadata_readValues },
 { "nativeWriteValues",
 "(I[B)V",
 (void *)CameraMetadata_writeValues },
 { "nativeDump",
 "()V",
 (void *)CameraMetadata_dump },
// Parcelable interface
 { "nativeReadFromParcel",
 "(Landroid/os/Parcel;)V",
 (void *)CameraMetadata_readFromParcel },
 { "nativeWriteToParcel",
 "(Landroid/os/Parcel;)V",
 (void *)CameraMetadata_writeToParcel },
};

gCameraMetadataMethods什么时候会被加载?

int register_android_hardware_camera2_CameraMetadata(JNIEnv *env)
{
 ......
 // Register native functions
 return RegisterMethodsOrDie(env,
 CAMERA_METADATA_CLASS_NAME,
 gCameraMetadataMethods,
 NELEM(gCameraMetadataMethods));
}
......
static inline int RegisterMethodsOrDie(JNIEnv* env, const char* className,
 const JNINativeMethod* gMethods, int numMethods) {
 int res = AndroidRuntime::registerNativeMethods(env, className, gMethods, numMethods);
 LOG_ALWAYS_FATAL_IF(res < 0, "Unable to register native methods.");
 return res;
}

register_android_hardware_camera2_CameraMetadata何时会被调用到,这个就需要了解下JNI的查找方式。

JNI查找方式

Android系统在启动启动过程中,先启动Kernel创建init进程,紧接着由init进程fork第一个横穿Java和C/C++的进程,即Zygote进程。Zygote启动过程中会AndroidRuntime.cpp中的startVm创建虚拟机,VM创建完成后,紧接着调用startReg完成虚拟机中的JNI方法注册。

刚才CameraMetadata中register_android_hardware_camera2_CameraMetadata方法,在AndroidRuntime.cpp的声明:

extern int register_android_hardware_camera2_CameraMetadata(JNIEnv *env);

然后在gRegJNI中的静态声明

static const RegJNIRec gRegJNI[] = {
 ......
 REG_JNI(register_android_hardware_camera2_CameraMetadata),
 ......
}

gRegJNI方法在startReg中被调用

/*static*/ int AndroidRuntime::startReg(JNIEnv* env)
{
 ATRACE_NAME("RegisterAndroidNatives");
 androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);
 ALOGV("--- registering native functions ---n");
 env->PushLocalFrame(200);
 if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
 env->PopLocalFrame(NULL);
 return -1;
 }
 env->PopLocalFrame(NULL);
 //createJavaThread("fubar", quickTest, (void*) "hello");
 return 0;
}

register_jni_procs(gRegJNI, NELEM(gRegJNI), env)会循环调用gRegJNI数组成员所对应的方法

static int register_jni_procs(const RegJNIRec array[], size_t count, JNIEnv* env)
{
 for (size_t i = 0; i < count; i++) {
 if (array[i].mProc(env) < 0) {
#ifndef NDEBUG
 ALOGD("----------!!! %s failed to loadn", array[i].mName);
#endif
 return -1;
 }
 }
 return 0;
}

这样android_hardware_camera2_CameraMetadata.cpp中的int register_android_hardware_camera2_CameraMetadata(JNIEnv *env)就会被调用到。

除了这种Android系统启动时,就注册JNI所对应的方法。还有一种就是程序自定义的JNI方法,以 MediePlay 为例:

相关代码路径

frameworks/base/media/java/android/media/MediaPlayer.java
frameworks/base/media/jni/android_media_MediaPlayer.cpp

MediaPlayer声明:

public class MediaPlayer extends PlayerBase
 implements SubtitleController.Listener
{
 ......
 private static native final void native_init();
 ......
 static {
 System.loadLibrary("media_jni");
 native_init();
 }
}

静态代码块中使用System.loadLibrary加载动态库,media_jni在Android平台对应的是libmedia_jni.so库。

在jni目录/frameworks/base/media/jni/Android.mk中有相应的声明:

LOCAL_SRC_FILES:= 
android_media_MediaPlayer.cpp 
...
LOCAL_MODULE:= libmedia_jni

在android_media_MediaPlayer.cpp找到对应的Native(natvie_init)方法:

static void
android_media_MediaPlayer_native_init(JNIEnv *env)
{
 jclass clazz;
 clazz = env->FindClass("android/media/MediaPlayer");
 if (clazz == NULL) {
 return;
 }
 ......
}

JNI注册的方法就是上面描述的两种方法:

  • 在Android系统启动时注册,在AndroidRuntime.cpp中的gRegJNI方法中声明
  • 使用System.loadLibrary()方式注册

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调用本地代码。

数据类型

基本数据类型

Android NDK-深入理解JNI

 

引用数据类型

Android NDK-深入理解JNI

 

方法签名

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结构

Android NDK-深入理解JNI

 

javavm.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查找方式中介绍系统的流程,并没有详细说明静态注册的查找。这里简单说明下这个过程(以上面的声明为例子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注册的两种时间,第一种已经介绍过了,我们自定义的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:清晰讲解JNI 与 NDK(含实例教学)
  • Android JNI学习
  • Android JNI原理分析
  • Android深入理解JNI(一)JNI原理与静态、动态注册
  • JNI Tips


Tags:Android NDK   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
Java调用C/C++在Java语言里面本来就有的,并非Android独有的,即JNI。JNI就是Java调用C++的规范。JNI 概述JNI,全称为Java Native Interface,即Java本地接口,JNI是Java调用Native语...【详细内容】
2019-09-16  Tags: Android NDK  点击:(236)  评论:(0)  加入收藏
▌简易百科推荐
今天面试遇到同学说做过内存优化,于是我一般都会问那 Bitmap 的像素内存存在哪?大多数同学都回答在 java heap 里面,就比较尴尬,理论上你做内存优化,如果连图片这个内存大户内存...【详细内容】
2021-12-23  像程序那样思考    Tags:Android开发   点击:(8)  评论:(0)  加入收藏
Android logcat日志封装logcat痛点在Android开发中使用logcat非常频繁,logcat能帮我们定位问题,但是在日常使用中发现每次使用都需要传递tag,并且会遇到输出频率很高的log,在多...【详细内容】
2021-12-22  YuCoding    Tags:Android   点击:(8)  评论:(0)  加入收藏
对项目的基本介绍 1.整个框架主要是给MVVM框架使用的,自己写完interface接口后,通过自定义的注解就能自动生成接口方法 2.用Kotlin的Flow去代替Rxjava,因为我发现RxJava功能很...【详细内容】
2021-12-08  网易Leo    Tags:Android开发   点击:(17)  评论:(0)  加入收藏
前言在Android开发过程中,有些时候会根据需要引用别的项目到当前项目里面,而且以Module形式引用。所以本篇博文就来分享一下怎么以Module形式引用别的项目到当前项目中,方便开...【详细内容】
2021-12-07  网易Leo    Tags:Android开发   点击:(22)  评论:(0)  加入收藏
作者:fundroid这篇文章偏阅读一些,大家可以了解下 Android 的一些最新动向。每年9/10月份 Google 都会举行约为期2天的 Android Dev Summit,在活动上 Google 的技术专家们会分...【详细内容】
2021-11-30  像程序那样思考    Tags:Android开发   点击:(15)  评论:(0)  加入收藏
一、 准备工作1、安装JDK,下载地址(可能需要一个oracle账号,大家百度一下或者自行注册一个就行。尽可能选择8或者11,这两个是长期版本)Java SE | Oracle Technology Network | Or...【详细内容】
2021-11-23  永沧    Tags:Android   点击:(28)  评论:(0)  加入收藏
使用Maven Publish Plugin插件。(官方支持)一、在Library的build.gradle中配置plugins { id &#39;com.android.library&#39; id &#39;kotlin-android&#39; id &#39;k...【详细内容】
2021-11-05  羊城小阳    Tags:Android   点击:(37)  评论:(0)  加入收藏
谷歌离推出Play Store应用程序的新数据隐私部分又近了一步。应用程序开发人员现在可以通过谷歌在Play控制台的新 "数据安全表 "填写相关细节。该公司表示,所需信息将从2022年...【详细内容】
2021-10-20    中关村在线  Tags:安卓   点击:(58)  评论:(0)  加入收藏
架构究竟是什么?如何更好的理解架构?我们知道一个APP通常是由class组成,而这些class之间如何组合,相互之间又如何产生作用,就是影响这个APP的关键点。细分的话我们可以将其分为类...【详细内容】
2021-09-17  像程序那样思考    Tags:Android架构   点击:(52)  评论:(0)  加入收藏
概述当Android应用程序需要访问设备上的敏感资源时,应用程序开发人员会使用权限模型。虽然该模型使用起来非常简单,但开发人员在使用权限时容易出错,从而导致安全漏洞。本文中,...【详细内容】
2021-09-07  SecTr安全团队    Tags:Android开发   点击:(66)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条