引言
背景
方案
前置知识
JavaScriptCore 简介
JavaScriptCore 类型
API 介绍
JSContextGroupCreate
JSGlobalContextCreateInGroup
JSEvaluateScript
关键代码示例
JNI(Java Native Interface)
C 调用 Java
JavaC
关注点
内存
性能
线程安全
数据优化结果
总结
引言在本文中,我们将通过 Android 本地搜索业务介绍如何使用 JavaScriptCore(以下简称 JSC)和Java Native Interface(以下简称 JNI)相关技术来实现搜索效率提升。
背景本地搜索业务内部使用动态下发 JS 代码实现一些业务逻辑,用户触发搜索到最终展示数据耗时久,体验很差 ( 8000 首歌曲的处理量大概在 7 秒左右),分析:
本地的 DB 和数据处理耗时占 50%
JS 引擎的数据传输上占 50%
DB 和数据处理不做讨论,这里主要解决 JS 引擎的数据传输问题
基于现有方案的分析:
可以发现 Native 在和 JVM 传输次数过多,且跨语言的数据传输序列化耗时
方案结合现有业务特点:
算法是变化的、动态下发的,所以代码由 JS 实现,故需要在 JS 引擎中执行
Java 使用 JSC 需要借助 JNI,并加入一些逻辑处理
JNI 需要向 JS 引擎输入数据,同时需要获取执行得结果
得出如下流程图
如何实现?
准备好 JavaScriptCore 库,这里复用 ReactNative 中的 so 库
C++调用 JavaScriptCore 库,实现部分逻辑,输出业务层 a.so 库
上层使用 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 介绍 JSContextGroupCreateJSContextGroupRef 是一个包含多个 JSContext 的分组,它们可以共享内存池和垃圾回收器,从而提高 JavaScript 执行效率和减少内存占用。
JSGlobalContextCreateInGroupJSGlobalContextCreateInGroup 函数会创建一个 JSGlobalContextRef 类型的对象,表示一个 JavaScript 上下文对象,该对象包含一个虚拟机对象、内存池、全局对象等成员变量。该函数返回值为创建的 JSGlobalContextRef 类型的对象,表示 JavaScript 上下文对象。
由于不同的 JSGlobalContextRef 对象拥有不同的全局对象,因此它们之间不会相互影响。在不同的 JSGlobalContextRef 对象中创建的 JavaScript 对象、函数、变量等,都是相互独立的,它们之间不会共享数据或状态。
用于执行一段 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 进行双向调用。
步骤:
获取 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步骤:
设计规划功能、接口
Java 声明 Native 方法
按照 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 对象。
性能避免频繁创建和销毁 JNI 引用:创建和销毁 JNI 引用(如 jobject、jclass、jstring 等)的开销比较大,应该尽量避免频繁创建和销毁 JNI 引用。
使用本地数据类型:JNI 支持本地数据类型(如 jint、jfloat、jboolean 等),这些数据类型与 Java 数据类型相对应,可以直接传递给 Java 代码,避免了数据类型转换的开销。
使用缓存:如果有一些数据在 JNI 函数中需要重复使用,可以考虑使用缓存,避免重复计算,比如 GetObjectClass、GetMethodID,这些可以保存起来重复使用。
避免频繁切换线程:JNI 函数会涉及到 Java 线程和本地线程之间的切换,这个过程比较耗时。因此,应该尽量避免频繁切换线程。
避免 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
当一个线程调用 Java 方法时,JNI 系统将自动为该线程创建一个 JNIEnv。因此,在访问 Java 对象之前,需要手动将当前线程与 JVM 绑定,以便获取 JNIEnv 指针,这个过程就叫做 "Attach"。可以使用 AttachCurrentThread 方法将当前线程附加到 JVM 上,然后就可以使用 JNIEnv 指针来访问 Java 对象了。
在 JNI 中,一般建议每个线程在使用完 JNIEnv 之后,立即 Detach,以释放资源,避免内存泄漏
Native 层线程安全需要针对自己得业务去区分是否需要加锁
数据优化结果根据数据分析,性比之前减少了 50%的耗时
总结上面概括性介绍了 JSC 和 JNI 的相关知识及经验总结,由于篇幅有限一些问题没有说明白或理解有误,欢迎一起交流~~
参考
https://webkit.org/blog
https://developer.apple.com/documentation/javascriptcore
以上就是Android本地搜索业务优化方案的详细内容,更多关于Android本地搜索优化的资料请关注软件开发网其它相关文章!