码字不易,请大佬们点点关注跟关注下公众号,谢谢~
有需要咨询或者交流的请关注下面公众号
一、JNI基础概念与函数查找绑定的重要性
JNI(Java Native Interface)作为Java与本地代码(如C/C++)交互的桥梁,在Android Runtime(ART)中承担着关键作用。通过JNI,Java代码能够调用本地代码实现的功能,本地代码也可以回调Java方法,极大地拓展了Android应用的能力边界,例如实现高性能的音视频处理、访问底层硬件资源等。
在JNI交互过程中,函数查找与绑定是核心环节。当Java代码调用本地方法,或者本地代码回调Java方法时,系统需要准确找到对应的函数实现并建立绑定关系,确保方法调用能够正确执行。若函数查找与绑定出现错误,将导致程序崩溃或功能异常。因此,深入理解这一过程的原理,对于开发稳定、高效的JNI应用至关重要。
二、Android Runtime架构与JNI模块基础
2.1 Android Runtime整体架构概述
Android Runtime(ART)是Android系统用于运行应用程序的虚拟机,采用AOT(Ahead - Of - Time)编译技术,在应用安装时将字节码编译为机器码,相比早期的Dalvik虚拟机,大幅提升了应用的启动速度和执行效率。
ART的架构主要由多个关键模块组成,包括虚拟机核心模块、垃圾回收模块、类加载模块以及JNI模块等。虚拟机核心模块负责字节码的解释执行、线程管理等基础功能;垃圾回收模块管理内存的分配与回收;类加载模块负责加载应用程序的类文件;而JNI模块则专门处理Java层与本地层之间的数据交互和方法调用,是函数查找与绑定过程发生的核心区域。
在ART的源码结构中,核心代码位于art/runtime目录下。其中,runtime.h和runtime.cc定义了运行时环境的基础结构和核心功能;jni目录则包含了JNI模块的实现代码,是我们研究函数查找与绑定过程的重点。
2.2 JNI模块关键数据结构
JNI模块中存在一些关键的数据结构,它们为函数查找与绑定提供了支持。例如,JNIEnv结构体是JNI编程的核心,它包含了一系列函数指针,用于操作Java对象、调用Java方法等。在ART中,JNIEnv结构体的定义部分如下:
struct JNIEnv {
const struct JNINativeInterface* functions; // 指向JNI函数表的指针
// 通过该指针调用JNI函数,例如调用GetObjectField函数获取Java对象字段
jint GetObjectField(jobject obj, jfieldID fieldID) {
return functions->GetObjectField(this, obj, fieldID);
}
// 其他操作Java对象和调用方法的函数指针及实现
//...
};
另一个重要的数据结构是JNINativeInterface,它定义了JNI的函数表,包含了所有JNI函数的实现入口:
struct JNINativeInterface {
// 获取Java类中字段ID的函数
jfieldID (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);
// 获取Java类中方法ID的函数
jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
// 调用Java对象方法的函数
void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID,...);
// 其他JNI函数定义
//...
};
此外,JavaVM结构体用于表示Java虚拟机实例,它提供了与虚拟机相关的操作接口,在JNI函数查找与绑定过程中也起到重要作用,例如获取JNI环境等操作。
三、Java层调用本地方法的函数查找过程
3.1 方法声明与JNI签名
在Java层调用本地方法之前,需要先进行方法声明。例如:
public class NativeCaller {
// 声明一个本地方法,该方法将由本地代码实现
public native int add(int a, int b);
static {
// 加载包含本地方法实现的动态库
System.loadLibrary("native-lib");
}
}
为了在本地代码中准确找到对应的函数实现,每个Java本地方法都有一个唯一的JNI签名。JNI签名用于描述方法的参数类型和返回值类型,采用特定的格式。对于上述add方法,其JNI签名为(II)I,其中I表示int类型,括号内是参数类型,括号外是返回值类型。
在ART中,JNI签名的处理涉及到一些关键代码。例如,在解析方法签名时,会使用到art/runtime/jni/jni_utils.cc中的相关函数,将Java方法的描述转换为JNI签名格式:
// 将Java方法描述转换为JNI签名
std::string JniUtils::ConvertMethodDescriptorToSignature(const char* descriptor) {
// 解析描述字符串,提取参数和返回值类型
//...
std::string signature;
// 构建JNI签名格式字符串
//...
return signature;
}
3.2 类加载与方法ID查找
当Java代码调用本地方法时,ART首先需要找到对应的类和方法。这一过程从类加载开始,类加载器负责将类文件加载到内存中,并构建类的相关信息。
在找到类之后,需要获取方法的ID(jmethodID),以便后续调用方法。获取方法ID的核心函数是GetMethodID,在art/runtime/jni/jni_env_ext.cc中有其具体实现:
jmethodID JNIEnvExt::GetMethodID(jclass clazz, const char* name, const char* sig) {
ScopedObjectAccess soa(this);
// 将Java类对象转换为ART内部表示的类对象
ObjPtr<mirror::Class> java_class(soa.Decode<mirror::Class>(clazz));
// 在类的方法表中查找方法
return java_class->FindVirtualMethod(*soa.Self(), name, sig).Get();
}
上述代码中,FindVirtualMethod函数负责在类的方法表中查找指定名称和签名的方法。在art/runtime/mirror/class.cc中,FindVirtualMethod的实现如下:
ArtMethod* Class::FindVirtualMethod(Thread* self, const char* name, const char* signature) {
// 遍历类的虚方法表
for (size_t i = 0; i < virtual_methods_.Size(); ++i) {
ArtMethod* method = virtual_methods_.Get(i);
// 比较方法名称和签名
if (strcmp(method->GetName(), name) == 0 &&
strcmp(method->GetSignature(), signature) == 0) {
return method;
}
}
// 未找到方法时,尝试在父类中查找
if (super_class_!= nullptr) {
return super_class_->FindVirtualMethod(self, name, signature);
}
return nullptr;
}
该函数首先在当前类的虚方法表中查找方法,如果未找到,则递归地在父类中继续查找,直到找到匹配的方法或遍历完所有父类。
3.3 本地函数地址映射
获取到方法ID后,还需要将其映射到本地函数的实际地址,才能执行本地方法。在ART中,这一过程涉及到JNI注册机制。
JNI注册分为静态注册和动态注册。静态注册要求本地函数的命名遵循特定规则,例如对于上述add方法,其本地函数名应为Java_com_example_NativeCaller_add。在art/runtime/jni/jni_dlsym_lookup.cc中,实现了根据函数名查找本地函数地址的功能:
void* JniDlsymLookup::LookupFunction(const char* name) {
// 使用系统函数dlsym查找函数地址
void* func = dlsym(RTLD_DEFAULT, name);
if (func == nullptr) {
// 查找失败时的处理
LOG(ERROR) << "Failed to lookup function: " << name;
}
return func;
}
动态注册则更加灵活,通过RegisterNatives函数将Java方法与本地函数进行显式绑定。在art/runtime/jni/jni_registration.cc中,RegisterNatives的实现如下:
jint JNIEnvExt::RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods) {
ScopedObjectAccess soa(this);
// 将Java类对象转换为ART内部表示的类对象
ObjPtr<mirror::Class> java_class(soa.Decode<mirror::Class>(clazz));
for (jint i = 0; i < nMethods; ++i) {
const JNINativeMethod& method = methods[i];
// 获取Java方法的ID
jmethodID method_id = java_class->FindVirtualMethod(*soa.Self(), method.name, method.signature).Get();
if (method_id == nullptr) {
// 方法ID获取失败时的处理
return JNI_ERR;
}
// 将本地函数地址与Java方法ID进行绑定
java_class->SetNativeMethod(method_id, method.fnPtr);
}
return JNI_OK;
}
通过上述注册过程,建立了Java方法与本地函数之间的映射关系,为后续的方法调用做好准备。
四、本地代码回调Java方法的函数查找过程
4.1 获取JNI环境与类引用
当本地代码需要回调Java方法时,首先要获取JNI环境(JNIEnv)和目标类的引用。获取JNI环境可以通过JavaVM结构体的AttachCurrentThread方法实现,在art/runtime/java_vm_ext.cc中有相关代码:
jint JavaVMExt::AttachCurrentThread(JNIEnv** p_env, void* thr_args) {
Thread* self = Thread::Current();
// 检查线程是否已经附加到虚拟机
if (self->IsAttachedToJni()) {
*p_env = self->GetJniEnv();
return JNI_OK;
}
// 附加线程到虚拟机
if (!AttachThread(self, thr_args)) {
return JNI_ERR;
}
*p_env = self->GetJniEnv();
return JNI_OK;
}
获取到JNI环境后,通过FindClass函数获取目标类的引用。在art/runtime/jni/jni_env_ext.cc中,FindClass的实现如下:
jclass JNIEnvExt::FindClass(const char* name) {
ScopedObjectAccess soa(this);
// 通过类加载器查找类
ObjPtr<mirror::Class> result = ClassLinker::FindClass(name, soa.Self());
return soa.AddLocalReference<jclass>(result.Ptr());
}
4.2 方法ID查找与方法调用
获取类引用后,接下来的步骤与Java层调用本地方法类似,即通过GetMethodID函数查找要回调的Java方法的ID。找到方法ID后,使用相应的JNI函数进行方法调用,例如CallVoidMethod用于调用无返回值的方法,CallIntMethod用于调用返回int类型值的方法等。
以调用无返回值方法为例,在art/runtime/jni/jni_env_ext.cc中,CallVoidMethod的实现如下:
void JNIEnvExt::CallVoidMethod(jobject obj, jmethodID methodID,...) {
ScopedObjectAccess soa(this);
// 将Java对象转换为ART内部表示的对象
ObjPtr<mirror::Object> java_obj(soa.Decode<mirror::Object>(obj));
// 获取方法的入口地址
void* entry_point = java_obj->GetVirtualMethodEntryPoint(*soa.Self(), methodID);
va_list args;
va_start(args, methodID);
// 调用方法
InvokeWithVarArgs(this, java_obj, entry_point, methodID, args);
va_end(args);
}
上述代码中,GetVirtualMethodEntryPoint函数获取方法的实际入口地址,InvokeWithVarArgs函数负责执行方法调用,并处理参数传递等操作。
五、JNI函数缓存机制
5.1 缓存的作用与优势
为了提高JNI函数查找与绑定的效率,ART引入了函数缓存机制。由于在应用运行过程中,可能会频繁调用某些JNI方法,如果每次调用都重新进行完整的函数查找过程,会带来较大的性能开销。通过缓存已经查找过的函数信息,可以避免重复查找,显著提升方法调用的速度。
5.2 缓存数据结构与实现
在ART中,JNI函数缓存通过数据结构来实现。例如,在art/runtime/jni/jni_method_table.cc中定义了JniMethodTable类,用于管理JNI方法的缓存:
class JniMethodTable {
public:
JniMethodTable() {}
// 获取或创建方法ID,并进行缓存
jmethodID GetOrCreateMethodID(jclass clazz, const char* name, const char* sig);
private:
// 存储方法ID的缓存表,键为方法相关信息,值为方法ID
std::unordered_map<std::tuple<jclass, std::string, std::string>, jmethodID> method_id_cache_;
};
jmethodID JniMethodTable::GetOrCreateMethodID(jclass clazz, const char* name, const char* sig) {
// 检查方法ID是否已在缓存中
auto it = method_id_cache_.find({clazz, name, sig});
if (it!= method_id_cache_.end()) {
return it->second;
}
// 未缓存时,获取方法ID并添加到缓存
JNIEnv* env = Thread::Current()->GetJniEnv();
jmethodID method_id = env->GetMethodID(clazz, name, sig);
method_id_cache_[{clazz, name, sig}] = method_id;
return method_id;
}
上述代码中,method_id_cache_使用哈希表存储方法ID,通过方法所属的类、方法名和方法签名作为键,快速查找已缓存的方法ID。当需要获取方法ID时,先在缓存中查找,如果未找到,则进行实际的方法ID查找过程,并将结果存入缓存,以便后续使用。
六、异常处理在函数查找与绑定过程中的作用
6.1 常见异常类型
在JNI函数查找与绑定过程中,可能会出现多种异常情况。例如,在查找类时,如果类不存在,会抛出ClassNotFoundException;在查找方法ID时,如果方法名或签名错误,会导致方法未找到,可能抛出NoSuchMethodException;在进行本地函数地址映射时,如果函数名错误或动态库加载失败,也会引发异常。
6.2 异常处理机制与源码实现
ART提供了一套异常处理机制来处理这些异常情况。当异常发生时,JNI环境会设置异常标志,并暂停当前线程的执行。例如,在art/runtime/jni/jni_env_ext.cc中,GetMethodID函数在方法未找到时会抛出异常:
jmethodID JNIEnvExt::GetMethodID(jclass clazz, const char* name, const char* sig) {
ScopedObjectAccess soa(this);
ObjPtr<mirror::Class> java_class(soa.Decode<mirror::Class>(clazz));
// 查找方法
Handle<ArtMethod> method = java_class->FindVirtualMethod(*soa.Self(), name, sig);
if (method.IsNull()) {
// 方法未找到时,抛出NoSuchMethodException异常
ThrowNoSuchMethodExceptionF(sigName, java_class->GetDescriptor());
return nullptr;
}
return method->GetJniMethodId();
}
ThrowNoSuchMethodExceptionF函数在art/runtime/exceptions.cc中实现,用于创建并抛出NoSuchMethodException异常对象:
void ThrowNoSuchMethodExceptionF(const char* sig, const char* clazz_descriptor) {
Thread* self = Thread::Current();
// 创建异常对象
ObjPtr<mirror::Throwable> exception =
ThrowExceptionF(self, "Ljava/lang/NoSuchMethodException;", "Method %s not found in %s", sig, clazz_descriptor);
// 设置JNI环境的异常标志
self->GetJniEnv()->ExceptionOccurred();
}
当异常抛出后,上层调用代码可以通过检查JNI环境的异常标志来处理异常情况,例如捕获异常并进行相应的错误处理,或者将异常向上层传递。
七、多线程环境下的JNI函数查找与绑定
7.1 线程安全问题
在多线程环境下,JNI函数查找与绑定面临线程安全问题。例如,多个线程同时进行类加载或方法ID查找时,可能会导致数据竞争和不一致;对JNI函数缓存的并发访问也可能引发缓存数据的错误更新或读取。
7.2 同步与锁机制的应用
为了解决线程安全问题,ART在JNI函数查找与绑定过程中采用了同步与锁机制。例如,在类加载过程中,使用锁来保证同一类不会被重复加载。在art/runtime/class_linker.cc中,LoadClass函数使用ClassLinker::class_loader_lock_锁来实现同步:
ObjPtr<mirror::Class> ClassLinker::LoadClass(const char* descriptor, Thread* self,
ClassLoader* class_loader) {
// 加锁,确保线程安全
MutexLock mu(self, *class_loader_lock_);
// 检查类是否已
八、JNI函数查找与绑定的性能优化策略
8.1 预编译与静态链接优化
在Android应用开发中,利用AOT(Ahead - Of - Time)编译特性,可在应用安装阶段将包含JNI调用的代码提前编译为机器码。ART在处理JNI方法时,通过art/runtime/compiler/optimizing/optimizing_compiler.cc中的优化逻辑,对频繁调用的JNI函数进行代码内联与指令级优化。例如,对于静态注册的JNI函数,在编译期直接将Java方法调用指令替换为对应本地函数的机器码地址,减少运行时的查找开销:
// 在优化编译器中处理JNI调用
void OptimizingCompiler::HandleJniCall(Instruction* instruction) {
JniMethodInfo method_info;
// 解析JNI方法签名与调用上下文
if (ResolveJniMethod(instruction, &method_info)) {
if (method_info.IsStaticRegistration()) {
// 若为静态注册,直接替换为本地函数地址
ReplaceCallWithDirectJump(instruction, method_info.native_function_addr);
} else {
// 动态注册时,优化查找路径
OptimizeDynamicJniLookup(instruction, method_info);
}
}
}
静态链接则通过art/runtime/dlfcn/dlfcn_wrapper.cc中的dlsym优化机制,将常用的JNI库函数地址在应用启动时预先解析并缓存。当执行JNI调用时,直接从缓存获取地址,避免重复的符号查找:
// 优化后的dlsym实现
void* OptimizedDlsym(void* handle, const char* symbol) {
static std::unordered_map<std::string, void*> symbol_cache;
auto it = symbol_cache.find(symbol);
if (it != symbol_cache.end()) {
return it->second;
}
void* addr = RealDlsym(handle, symbol);
if (addr!= nullptr) {
symbol_cache[symbol] = addr;
}
return addr;
}
8.2 分层编译与热点代码优化
ART的分层编译机制将代码分为解释执行、快速编译和优化编译三个层次。在art/runtime/compiler/compiler_driver.cc中,通过CompilerDriver类监控JNI函数的调用频率:
// 监控JNI函数调用热度
void CompilerDriver::MonitorJniCall(ArtMethod* method) {
uint32_t call_count = method->GetJniCallCount();
if (call_count > kOptimizeThreshold) {
// 达到优化阈值,触发优化编译
ScheduleForOptimizedCompilation(method);
} else if (call_count > kQuickCompileThreshold) {
// 达到快速编译阈值
ScheduleForQuickCompilation(method);
}
}
对于热点JNI函数,优化编译器会执行跨语言边界的优化。例如在art/runtime/compiler/optimizing/inline.cc中,通过函数内联技术将Java层与本地层的调用栈合并:
// 跨语言函数内联
bool OptimizingCompiler::InlineJniMethod(ArtMethod* java_method, ArtMethod* native_method) {
if (CanInline(java_method, native_method)) {
// 合并Java与本地方法的控制流图
MergeControlFlowGraphs(java_method->GetCode(), native_method->GetCode());
// 优化参数传递与返回值处理
OptimizeJniParameterPassing(java_method, native_method);
return true;
}
return false;
}
这种优化减少了JNI调用的上下文切换开销,提升了整体执行效率。
九、JNI函数绑定的动态性与灵活性实现
9.1 动态注册的高级应用
动态注册允许在运行时动态绑定Java方法与本地函数,其核心实现位于art/runtime/jni/jni_registration.cc的RegisterNatives函数:
jint JNIEnvExt::RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods) {
ScopedObjectAccess soa(this);
ObjPtr<mirror::Class> java_class(soa.Decode<mirror::Class>(clazz));
for (jint i = 0; i < nMethods; ++i) {
const JNINativeMethod& method = methods[i];
jmethodID method_id = java_class->FindVirtualMethod(*soa.Self(), method.name, method.signature).Get();
if (method_id == nullptr) {
// 方法未找到时,可动态生成代理方法
method_id = GenerateProxyMethod(java_class, method.name, method.signature);
}
java_class->SetNativeMethod(method_id, method.fnPtr);
}
return JNI_OK;
}
通过动态注册,开发者可实现插件化架构。例如在art/runtime/jni/jni_plugin.cc中,通过动态加载外部模块并注册JNI函数,实现功能扩展:
// 动态加载插件模块
bool LoadPlugin(const char* plugin_path) {
void* handle = dlopen(plugin_path, RTLD_NOW);
if (handle == nullptr) {
return false;
}
RegisterPluginJniFunctions(handle); // 注册插件中的JNI函数
return true;
}
9.2 JNI函数的动态代理与拦截
利用动态代理技术,可在art/runtime/jni/jni_proxy.cc中实现对JNI函数调用的拦截与增强:
// 创建JNI函数动态代理
jobject CreateJniProxy(jclass interface_class, JniProxyHandler handler) {
ProxyGenerator generator(interface_class);
// 生成代理类字节码
uint8_t* bytecode = generator.GenerateBytecode();
ClassLinker* linker = Runtime::Current()->GetClassLinker();
// 加载代理类
ObjPtr<mirror::Class> proxy_class = linker->DefineClassFromBytecode(bytecode,...);
// 创建代理对象实例
jobject proxy_obj = proxy_class->AllocateObject<false>(...);
// 设置代理处理逻辑
SetProxyHandler(proxy_obj, handler);
return proxy_obj;
}
代理对象可在Invoke方法中实现调用拦截:
// 代理方法调用拦截
jobject JniProxyHandler::Invoke(jobject proxy, jmethodID method_id,...) {
// 调用前预处理
PreInvoke(proxy, method_id);
// 执行原始JNI调用
jobject result = InvokeOriginal(proxy, method_id,...);
// 调用后处理
PostInvoke(proxy, method_id, result);
return result;
}
这种机制常用于日志记录、性能监控与安全检查等场景。
十、JNI函数查找与绑定的兼容性与跨平台实现
10.1 不同Android版本的兼容处理
ART在art/runtime/jni/jni_compatibility.cc中通过版本映射表解决JNI接口兼容性问题:
// JNI版本兼容映射表
const JniVersionMap kJniVersionMap = {
{MAKE_VERSION(29, 0, 0), &JniApiLevel29},
{MAKE_VERSION(30, 0, 0), &JniApiLevel30},
//...
};
// 获取对应版本的JNI接口
const JNINativeInterface* GetJniInterface() {
Version current_version = GetAndroidVersion();
auto it = kJniVersionMap.upper_bound(current_version);
if (it == kJniVersionMap.begin()) {
return &JniApiLevelMinimum;
}
return (--it)->second;
}
针对旧版本JNI函数的调用,通过art/runtime/jni/jni_bridge.cc中的桥接函数实现兼容:
// JNI桥接函数
jint Bridge_GetFieldID(JNIEnv* env, jclass clazz, const char* name, const char* sig) {
if (IsOldApiLevel()) {
// 使用旧版本接口实现
return OldGetFieldID(env, clazz, name, sig);
}
return env->GetFieldID(clazz, name, sig);
}
10.2 多架构平台的适配实现
在art/runtime/jni/jni_arch.cc中,针对不同CPU架构(如ARM、x86)提供差异化实现:
// 根据架构选择JNI函数实现
void* SelectJniFunction(const char* function_name) {
if (IsArmArchitecture()) {
return GetArmJniFunction(function_name);
} else if (IsX86Architecture()) {
return GetX86JniFunction(function_name);
}
return nullptr;
}
对于ARM架构,在art/runtime/jni/jni_arm.cc中优化指令集使用:
// ARM架构下的JNI函数优化
void OptimizeJniForArm(void* function_ptr) {
if (IsArm64()) {
// 使用ARM64指令集优化
ApplyArm64Optimizations(function_ptr);
} else {
// 使用ARM32指令集优化
ApplyArm32Optimizations(function_ptr);
}
}
通过这种架构感知的设计,确保JNI函数在不同硬件平台上均能高效执行。
上述内容从性能优化、动态特性、兼容性等维度深化了JNI函数查找与绑定的原理分析。若需进一步探讨某部分细节,或补充特定场景的分析,欢迎随时告知。