JNI调用速度优化

作者: 嘉伟咯 | 来源:发表于2022-03-29 22:38 被阅读0次

    FastJNI

    最近在看JNI HOOK的时候看到了个叫做fastJNI的东西,它可以加速JNI方法的调用,比较有意思。

    首先我们都知道RegisterNativeMethods用于动态注册JNI方法:

    static const JNINativeMethod jniNativeMethod[] = {
            {"stringFromJNI", "()Ljava/lang/String;", (void *) (stringFromJNI)},
    };
    
    JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *javaVm, void *pVoid) {
        JNIEnv *jniEnv = nullptr;
        jint result = javaVm->GetEnv(reinterpret_cast<void **>(&jniEnv), JNI_VERSION_1_6); 
        if (result != JNI_OK) {
            return -1;
        }
        
        jclass jniClass = jniEnv->FindClass("me/linjw/demo/MainActivity");
        jniEnv->RegisterNatives(
            jniClass, 
            jniNativeMethod,
            sizeof(jniNativeMethod) / sizeof(JNINativeMethod)
        );
        return JNI_VERSION_1_6;
    }
    

    如果我们在方法签名的前面加上"!",就可以指定使用fastJNI的方式去调用这个native方法:

    static const JNINativeMethod jniNativeMethod[] = {
            {"stringFromJNI", "!()Ljava/lang/String;", (void *) (stringFromJNI)},
    };
    

    查看RegisterNativeMethods可以知道,它实际上是给Native方法设置了kAccFastNative标志位:

    // jni_internal.cc
    static jint RegisterNativeMethods(JNIEnv* env, jclass java_class, const JNINativeMethod* methods,
                                    jint method_count, bool return_errors) {
        ...
        for (jint i = 0; i < method_count; ++i) {
            const char* name = methods[i].name;
            const char* sig = methods[i].signature;
            const void* fnPtr = methods[i].fnPtr;
    
            ...
            bool is_fast = false;
            // Notes about fast JNI calls:
            //
            // On a normal JNI call, the calling thread usually transitions
            // from the kRunnable state to the kNative state. But if the
            // called native function needs to access any Java object, it
            // will have to transition back to the kRunnable state.
            //
            // There is a cost to this double transition. For a JNI call
            // that should be quick, this cost may dominate the call cost.
            //
            // On a fast JNI call, the calling thread avoids this double
            // transition by not transitioning from kRunnable to kNative and
            // stays in the kRunnable state.
            //
            // There are risks to using a fast JNI call because it can delay
            // a response to a thread suspension request which is typically
            // used for a GC root scanning, etc. If a fast JNI call takes a
            // long time, it could cause longer thread suspension latency
            // and GC pauses.
            //
            // Thus, fast JNI should be used with care. It should be used
            // for a JNI call that takes a short amount of time (eg. no
            // long-running loop) and does not block (eg. no locks, I/O,
            // etc.)
            //
            // A '!' prefix in the signature in the JNINativeMethod
            // indicates that it's a fast JNI call and the runtime omits the
            // thread state transition from kRunnable to kNative at the
            // entry.
            if (*sig == '!') {
                is_fast = true;
                ++sig;
            }
            ...
            m->RegisterNative(fnPtr, is_fast);
            ...
        }
    
        return JNI_OK;
    }
    
    // art_method.cc
    void ArtMethod::RegisterNative(const void* native_method, bool is_fast) {
        CHECK(IsNative()) << PrettyMethod(this);
        CHECK(!IsFastNative()) << PrettyMethod(this);
        CHECK(native_method != nullptr) << PrettyMethod(this);
        if (is_fast) {
            SetAccessFlags(GetAccessFlags() | kAccFastNative);
        }
        SetEntryPointFromJni(native_method);
    }
    

    源码的注释里面也描述了fastJNI的原理:

    1. java方法运行在kRunnable state,native方法运行在kNative state
    2. java进入native方法,从kRunnable state切换到kNative state会消耗时间
    3. 如果native方法需要调到java的代码,从kNative state切换回kRunnable state也会耗时
    4. 如果在方法签名前面加上"!"可以将native方法定义成fastJNI方法
    5. fastJNI方法运行在kRunnable state,避免了state的切换耗时

    以我的理解是这样的,默认情况下虚拟机栈和本地方法栈在两个不同的state下,相当于退出和进入java虚拟机环境,所以会有一系列的环境的存储与恢复:

    截屏2022-03-29 下午10.37.20.png

    虚拟机是c/c++写的,而fastJNI相当于在执行虚拟机栈的环境上直接调用了native方法,所以java和本地方法是直接相互调用的:

    截屏2022-03-29 下午10.37.25.png

    JAVA GC的stop the work实际上只是停止了java虚拟机的世界,并没没有办法停止native层的代码。

    普通jni会有java虚拟机环境的进出,单纯的执行native代码对虚拟机环境没有任何影响,所以只需要在进入虚拟机的时候判断是否已经停止。

    但fastJNI由于native代码会直接调用运行java层的代码,所以stop the work的时候反而需要判断是否在fastJNI过程中,以避免stop the work的过程中java代码被native层执行。

    因此fastJNI使用的时候需要注意:

    fastJNI会导致Java GC之类的线程挂起请求操作被推迟,所以fastJNI方法需要尽量的短小和不要在里面做一些阻塞操作

    @FastNative & @CriticalNative

    fastJNI在安卓8.0之后就被废弃了:

    // jni_internal.cc
    static jint RegisterNativeMethods(JNIEnv* env, jclass java_class, const JNINativeMethod* methods,
                                    jint method_count, bool return_errors) {
        ...
        for (jint i = 0; i < method_count; ++i) {
            const char* name = methods[i].name;
            const char* sig = methods[i].signature;
            const void* fnPtr = methods[i].fnPtr;
    
            ...
            if (*sig == '!') {
                is_fast = true;
                ++sig;
            }
            ...
            if (UNLIKELY(is_fast)) {
                // There are a few reasons to switch:
                // 1) We don't support !bang JNI anymore, it will turn to a hard error later.
                // 2) @FastNative is actually faster. At least 1.5x faster than !bang JNI.
                //    and switching is super easy, remove ! in C code, add annotation in .java code.
                // 3) Good chance of hitting DCHECK failures in ScopedFastNativeObjectAccess
                //    since that checks for presence of @FastNative and not for ! in the descriptor.
                LOG(WARNING) << "!bang JNI is deprecated. Switch to @FastNative for " << m->PrettyMethod();
                is_fast = false;
                // TODO: make this a hard register error in the future.
            }
    
            const void* final_function_ptr = m->RegisterNative(fnPtr, is_fast);
            ...
        }
    
        return JNI_OK;
    }
    

    注释上说高版本的安卓提供了@FastNative去替代fastJNI。我们从官方文档上可以找到它和另外一个叫 @CriticalNative 的东西:

    更快速的原生方法

    使用 @FastNative@CriticalNative 注解可以更快速地对 Java 原生接口 (JNI) 进行原生调用。这些内置的 ART 运行时优化可以加快 JNI 转换速度,并取代了现已弃用的 !bang JNI 标记。这些注解对非原生方法没有任何影响,并且仅适用于 bootclasspath 上的平台 Java 语言代码(无 Play 商店更新)。

    @FastNative 注解支持非静态方法。如果某种方法将 jobject 作为参数或返回值进行访问,请使用此注解。

    利用 @CriticalNative 注解,可更快速地运行原生方法,但存在以下限制:

    • 方法必须是静态方法 - 没有参数、返回值或隐式 this 的对象。
    • 仅将基元类型传递给原生方法。
    • 原生方法在其函数定义中不使用 JNIEnv 和 jclass 参数。
    • 方法必须使用 RegisterNatives 进行注册,而不是依靠动态 JNI 链接。

    @FastNative 和 @CriticalNative 注解在执行原生方法时会停用垃圾回收。不要与长时间运行的方法一起使用,包括通常很快但一般不受限制的方法。

    停顿垃圾回收可能会导致死锁。如果锁尚未得到本地释放(即尚未返回受管理代码),请勿在原生快速调用期间获取锁。此要求不适用于常规的 JNI 调用,因为 ART 将正执行的原生代码视为已暂停的状态。

    @FastNative 可以使原生方法的性能提升高达 3 倍,而 @CriticalNative 可以使原生方法的性能提升高达 5 倍。例如,在 Nexus 6P 设备上测量的 JNI 转换如下:

    Java 原生接口 (JNI) 调用 执行时间(以纳秒计)
    常规 JNI 115
    !bang JNI 60
    @FastNative 35
    @CriticalNative 25

    使用@FastNative和@CriticalNative

    这两个东西的效果这么好,Framework里面也大量用到了。那么当我们充分了解了它们的影响之后,可以在适当的情景下使用。

    但是如果你直接import它们的话会发现,在Android Studio里面报红色的Error,找不到具体的定义:

    import dalvik.annotation.optimization.CriticalNative;
    import dalvik.annotation.optimization.FastNative;
    

    原因是它们都是Hide的接口对应用层隐藏。网上有不少调用隐藏API的方式,但是可能这两个类的位置比较特别,我也没有能从隐藏接口里面找到它们。于是乎我用了一个比较取巧的方式,直接把它们的代码拷贝了下来,在自己的工程里面创建同样的package去放:

    1.png

    从结果来看,速度的确是有比较明显的优化的:

    2.png

    完整的DEMO可以到Github上下载

    相关文章

      网友评论

        本文标题:JNI调用速度优化

        本文链接:https://www.haomeiwen.com/subject/mimbjrtx.html