Android Runtime JNI函数查找与绑定过程原理剖析(69)

132 阅读16分钟

码字不易,请大佬们点点关注跟关注下公众号,谢谢~

有需要咨询或者交流的请关注下面公众号

Android小码峰

一、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.hruntime.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.ccRegisterNatives函数:

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函数查找与绑定的原理分析。若需进一步探讨某部分细节,或补充特定场景的分析,欢迎随时告知。