JNI调用速度优化

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上下载

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容