本文作者:lsnbing

导言

在本文中,咱们将经过 Android 本地查找事务介绍怎么运用 JavaScriptCore(以下简称 JSC)和Java Native Interface(以下简称 JNI)相关技能来完成查找功率提高。

布景

本地查找事务内部运用动态下发 JS 代码完成一些事务逻辑,用户触发查找到终究展示数据耗时久,体验很差 ( 8000 首歌曲的处理量大概在 7 秒左右),剖析:

  • 本地的 DB 和数据处理耗时占 50%
  • JS 引擎的数据传输上占 50%

DB 和数据处理不做讨论,这里首要解决 JS 引擎的数据传输问题

基于现有计划的剖析:

Android本地搜索优化

能够发现 Native 在和 JVM 传输次数过多,且跨言语的数据传输序列化耗时

计划

结合现有事务特色:

  • 算法是改变的、动态下发的,所以代码由 JS 完成,故需求在 JS 引擎中履行
  • Java 运用 JSC 需求借助 JNI,并参加一些逻辑处理
  • JNI 需求向 JS 引擎输入数据,一同需求获取履行得成果

得出如下流程图

Android本地搜索优化

怎么完成?

  1. 准备好 JavaScriptCore 库,这里复用 ReactNative 中的 so 库
  2. C++调用 JavaScriptCore 库,完成部分逻辑,输出事务层 a.so 库
  3. 上层运用 a.so 对库进行调用

前置常识

计划完成需求了解 JavaScriptCore 和 JNI 的相关常识,下面别离介绍

JavaScriptCore 简介

JavaScriptCore 是一个开源的 JavaScript 引擎,能够用来解析和履行 JavaScript 代码,相似的还有 V8、Hermes 等。

JSAPI 是 JavaScriptCore 的 C++接口,它提供了一组 C++类和函数,能够用于将 JavaScript 嵌入到 C++程序中。JSAPI 提供了以下功用:

  • 创立和办理 JavaScript 目标和值
  • 履行 JavaScript 代码
  • 拜访 JavaScript 目标的特点和办法
  • 注册 JavaScript 函数
  • 处理 JavaScript 异常
  • 进行垃圾收回

JavaScriptCore 类型

  • JSC::JSObject:表明一个 JavaScript 目标。
  • JSC::JSValue:表明一个 JavaScript 值。
  • JSC::JSGlobalObject:表明 JavaScript 目标的大局目标。
  • JSC::JSGlobalObjectFunctions:包含一组函数,用于完成 JSAPI 的功用,如履行 JavaScript 代码、拜访 JavaScript 目标的特点和办法等。

在 JSAPI 中,JavaScript 目标和值经过 JSC::JSObject 和 JSC::JSValue 类进行表明。 JSC::JSObject 表明一个 JavaScript 目标,它能够包含一组特点和办法; JSC::JSValue 表明一个 JavaScript 值,它能够是一个目标、一个数值、一个字符串或一个布尔值等。

JSAPI 提供了 JSC::JSGlobalObject 类作为 JavaScript 目标的大局目标,所有的 JavaScript 目标都是从该大局目标承继而来。

API 介绍

JSContextGroupCreate

JSContextGroupRef 是一个包含多个 JSContext 的分组,它们能够同享内存池和垃圾收回器,然后提高 JavaScript 履行功率和削减内存占用。

JSGlobalContextCreateInGroup

JSGlobalContextCreateInGroup 函数会创立一个 JSGlobalContextRef 类型的目标,表明一个 JavaScript 上下文目标,该目标包含一个虚拟机目标、内存池、大局目标等成员变量。该函数回来值为创立的 JSGlobalContextRef 类型的目标,表明 JavaScript 上下文目标。 由于不同的 JSGlobalContextRef 目标具有不同的大局目标,因而它们之间不会彼此影响。在不同的 JSGlobalContextRef 目标中创立的 JavaScript 目标、函数、变量等,都是彼此独立的,它们之间不会同享数据或状态。

JSEvaluateScript

用于履行一段 JavaScript 代码。其内部工作机制首要包含以下几个步骤:

  • 将 JavaScript 代码转化为抽象语法树(AST) 在履行 JavaScript 代码之前,JavaScriptCore 需求将其转化为抽象语法树(AST),这样才能对其进行解析和履行。JavaScriptCore 的 AST 解析器能够将 JavaScript 代码转化为一棵 AST 树,其间每个节点代表了一条 JavaScript 语句或表达式。
  • 解析和履行 AST 树 一旦生成了 AST 树,JavaScriptCore 就能够对其进行解析和履行了。在解析进程中,JavaScriptCore 会对 AST 树进行遍历,一同将其间的变量、函数等标识符与对应的值进行绑定。在履行进程中,JavaScriptCore 会依照 AST 树的结构逐步履行其间的语句和表达式,一同依据需求调用相应的函数和办法。
  • 将履行成果回来给调用方 一旦 JavaScript 代码履行完毕,JavaScriptCore 就会将其履行成果回来给调用方。这个成果能够是任何 JavaScript 值,包含数字、字符串、目标、函数等。调用方能够依据需求对这个成果进行处理和运用。

JSEvaluateScript 是一个同步函数,即在履行完 JavaScript 代码之前,它会一向等候,直到 JavaScript 代码履行完毕并回来成果。这意味着,在履行长期运转的 JavaScript 代码时,JSEvaluateScript 函数可能会堵塞程序的运转。

咱们能够经过线程来对 JS 代码的异步化(以下省略一些判空逻辑)

void completionHandler(JSContextRef ctx, JSValueRef value, void *userData) {
    JSValueRef *result = (JSValueRef *)userData;
    *result = value;
}
void evaluateAsync(JSContextRef ctx, const char* script, JSObjectRef thisObject, JSValueRef* exception, JSAsyncEvaluateCallback completionHandler) {
    // 异步履行
    std::thread([ctx, script, thisObject, exception, completionHandler]() {
        // 履行脚本
        JSStringRef scriptStr = JSStringCreateWithUTF8CString(script);
        JSValueRef result = JSEvaluateScript(ctx, scriptStr, thisObject, nullptr, 0, exception);
        JSStringRelease(scriptStr);
        // 回调 completionHandler
        completionHandler(result, exception);
    }).detach();
}

此外还应重视注册到 JS 环境中的 C 接口回调,这里因赶快回来,如果有耗时任务,则需求将成果经过异步去告诉 JS 层,否则会堵塞 JS 线程(也便是调用该函数的线程)。

要害代码示例

下面完成了一个向 global 中添加 getData 的 Native 函数

// 回调函数
JSValueRef JSCExecutor::onGetDataCallback(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject,
                                   size_t argumentCount, const JSValueRef arguments[],
                                   JSValueRef *exception) {
        LOGD(TAG, "onGetDataCallback");
        NativeBridge::JSCExecutor *executor = static_cast<NativeBridge::JSCExecutor *>(JSObjectGetPrivate(
                thisObject));
        ... // 省略参数、类型等判断
        executor->xxx(); // C++事务侧
        return xxx; // 回来到JS内
}
bool JSCExecutor::initJSC() {
        // 初始化 JSC 引擎
        context_group_ = JSContextGroupCreate();
        JSClassDefinition global_class_definition = kJSClassDefinitionEmpty;
        global_class_ = JSClassCreate(&global_class_definition);
        // 在js履行上下文环境(Group)中创立一个大局的js履行上下文
        context_ = JSGlobalContextCreateInGroup(context_group_, global_class_);
        if (!context_) {
            LOGE(TAG, "create js context error!");
            return false;
        }
        // 获取js履行上下文的大局目标
        global_ = JSContextGetGlobalObject(context_);
        if (!global_) {
            LOGE(TAG, "get js context error!");
            return false;
        }
        // 绑定c++目标地址
        JSObjectSetPrivate(global_, this);
        // 注册函数
        JSStringRef dynamic_get_data_func_name = JSStringCreateWithUTF8CString("getData");
        JSObjectRef dynamic_get_data_obj = JSObjectMakeFunctionWithCallback(context_,
                                                                            dynamic_get_data_func_name,
                                                                            onGetDataCallback);
        JSObjectSetProperty(context_,
                            obj,
                            dynamic_get_data_func_name,
                            dynamic_get_data_obj,
                            kJSPropertyAttributeDontDelete,
                            NULL);
        return true;
    }

JNI(Java Native Interface)

JNI 全称为 Java Native Interface,是一种允许 Java 代码与本地(Native)代码交互的技能。JNI 提供了一组 API,能够使 Java 程序拜访和调用本地办法和资源,也能够使本地代码拜访和调用 Java 目标和办法。 此计划需求运用 JNI 进行双向调用。

C 调用 Java

步骤:

  • 获取 JNIEnv 指针:JNIEnv 是一个结构体指针,代表了 Java 虚拟机调用本地办法时的环境信息。JNIEnv 指针能够经过 Java 虚拟机实例、调用线程等参数获取。
  • 获取 Java 类、办法、字段等的 ID:经过 JNIEnv 指针,能够运用函数 FindClass()、GetMethodID()、GetStaticMethodID()、GetFieldID()等函数获取 Java 类、办法、字段等的 ID。比方在 C 中去创立 Java 目标,并操作相关 Java 目标
  • 调用 Java 办法或拜访 Java 字段:经过 JNIEnv 指针和 Java 目标的 ID,能够运用 CallObjectMethod()、CallStaticObjectMethod()、GetDoubleField()、SetObjectField()等函数调用 Java 办法或拜访 Java 字段。

JavaC

步骤:

  1. 设计规划功用、接口
  2. Java 声明 Native 办法
  3. 依照 JNI 标准完成办法,并经过 System.loadLibrary()加载
public class TestJNI {
   static {
      System.loadLibrary("xxx.so"); // 加载动态链接库
   }
   // 声明本地办法
   private native void PrintHelloWorld();
   // 静态办法
   public static native String GetVersion();
}
// C完成函数
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { ... } // so初始化回调函数
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *jvm, void *reserved) { ... } // so卸载回调函数
// 完成
包名_PrintHelloWorld(JNIEnv *env, jobject thiz) { ... }
包名_GetVersion(JNIEnv *env, jclass clazz) { ... }

重视点

JNI 的编写会遇到有很多坑,比方 Java 封装目标和 C++目标的生命周期关系、异步调用逻辑、编译器报错不完善、类型不匹配、JVM 环境不一致、运转线程不一致等等,下面是一些常用的规则

内存

  • 在 C/C++代码中,运用目标或智能指针去办理内存,若运用 malloc、calloc 等函数分配内存,然后运用 free 函数开释内存。
  • 在 JNI 中,经过 jobject 等 JNI 目标的创立和毁掉办法,手动办理 Java 内存。例如,在 JNI 中创立 Java 目标时,需求调用 NewObject 等 JNI 办法创立 Java 目标,然后在运用完后,需求调用 DeleteLocalRef 等 JNI 办法开释 Java 目标。

功用

  1. 防止频频创立和毁掉 JNI 引证:创立和毁掉 JNI 引证(如 jobject、jclass、jstring 等)的开支比较大,应该尽量防止频频创立和毁掉 JNI 引证。
  2. 运用本地数据类型:JNI 支撑本地数据类型(如 jint、jfloat、jboolean 等),这些数据类型与 Java 数据类型相对应,能够直接传递给 Java 代码,防止了数据类型转化的开支。
  3. 运用缓存:如果有一些数据在 JNI 函数中需求重复运用,能够考虑运用缓存,防止重复计算,比方 GetObjectClass、GetMethodID,这些能够保存起来重复运用。
  4. 防止频频切换线程:JNI 函数会涉及到 Java 线程和本地线程之间的切换,这个进程比较耗时。因而,应该尽量防止频频切换线程。
  5. 防止 Native 侧代码对全体功用形成得侵入,如 NDK 下 std::vector 分配大数据形成得功用低下,如 RN0.63 版本以前存在这个问题:Make JSStringToSTLString 23x faster (733532e5e9 by @radex)这需求对不同得编译环境差异性有所了解。运用 NDK 编译汇编代码/YourPath/Android/sdk/ndk/23.1.7779620/toolchains/llvm/prebuilt/darwin-x86_64/bin/clang++ --target=armv7-none-linux-androideabi21 --gcc-toolchain=/YourPath/Android/sdk/ndk/23.1.7779620/toolchains/llvm/prebuilt/darwin-x86_64 --sysroot=/YourPath/Android/sdk/ndk/23.1.7779620/toolchains/llvm/prebuilt/darwin-x86_64/sysroot -S native-lib.cpp

线程安全

  1. 当一个线程调用 Java 办法时,JNI 系统将主动为该线程创立一个 JNIEnv。因而,在拜访 Java 目标之前,需求手动将当时线程与 JVM 绑定,以便获取 JNIEnv 指针,这个进程就叫做 “Attach”。能够运用 AttachCurrentThread 办法将当时线程附加到 JVM 上,然后就能够运用 JNIEnv 指针来拜访 Java 目标了。 在 JNI 中,一般建议每个线程在运用完 JNIEnv 之后,当即 Detach,以开释资源,防止内存泄漏
  2. Native 层线程安全需求针对自己得事务去区分是否需求加锁

数据优化成果

Android本地搜索优化
依据数据剖析,性比之前削减了 50%的耗时

总结

上面概括性介绍了 JSC 和 JNI 的相关常识及经验总结,由于篇幅有限一些问题没有说明白或了解有误,欢迎一同交流~~

参考

webkit.org/blog

developer.apple.com/documentati…

本文发布自网易云音乐技能团队,文章未经授权禁止任何方式的转载。咱们常年接收各类技能岗位,如果你准备换工作,又刚好喜欢云音乐,那就参加咱们 grp.music-fe(at)corp.netease.com!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。