Android本地搜索業(yè)務(wù)優(yōu)化方案
引言
在本文中,我們將通過(guò) Android 本地搜索業(yè)務(wù)介紹如何使用 JavaScriptCore(以下簡(jiǎn)稱 JSC)和Java Native Interface(以下簡(jiǎn)稱 JNI)相關(guān)技術(shù)來(lái)實(shí)現(xiàn)搜索效率提升。
背景
本地搜索業(yè)務(wù)內(nèi)部使用動(dòng)態(tài)下發(fā) JS 代碼實(shí)現(xiàn)一些業(yè)務(wù)邏輯,用戶觸發(fā)搜索到最終展示數(shù)據(jù)耗時(shí)久,體驗(yàn)很差 ( 8000 首歌曲的處理量大概在 7 秒左右),分析:
- 本地的 DB 和數(shù)據(jù)處理耗時(shí)占 50%
- JS 引擎的數(shù)據(jù)傳輸上占 50%
DB 和數(shù)據(jù)處理不做討論,這里主要解決 JS 引擎的數(shù)據(jù)傳輸問(wèn)題
基于現(xiàn)有方案的分析:
可以發(fā)現(xiàn) Native 在和 JVM 傳輸次數(shù)過(guò)多,且跨語(yǔ)言的數(shù)據(jù)傳輸序列化耗時(shí)
方案
結(jié)合現(xiàn)有業(yè)務(wù)特點(diǎn):
- 算法是變化的、動(dòng)態(tài)下發(fā)的,所以代碼由 JS 實(shí)現(xiàn),故需要在 JS 引擎中執(zhí)行
- Java 使用 JSC 需要借助 JNI,并加入一些邏輯處理
- JNI 需要向 JS 引擎輸入數(shù)據(jù),同時(shí)需要獲取執(zhí)行得結(jié)果
得出如下流程圖
如何實(shí)現(xiàn)?
- 準(zhǔn)備好 JavaScriptCore 庫(kù),這里復(fù)用 ReactNative 中的 so 庫(kù)
- C++調(diào)用 JavaScriptCore 庫(kù),實(shí)現(xiàn)部分邏輯,輸出業(yè)務(wù)層 a.so 庫(kù)
- 上層使用 a.so 對(duì)庫(kù)進(jìn)行調(diào)用
前置知識(shí)
方案實(shí)現(xiàn)需要了解 JavaScriptCore 和 JNI 的相關(guān)知識(shí),下面分別介紹
JavaScriptCore 簡(jiǎn)介
JavaScriptCore 是一個(gè)開源的 JavaScript 引擎,可以用來(lái)解析和執(zhí)行 JavaScript 代碼,類似的還有 V8、Hermes 等。
JSAPI 是 JavaScriptCore 的 C++接口,它提供了一組 C++類和函數(shù),可以用于將 JavaScript 嵌入到 C++程序中。JSAPI 提供了以下功能:
- 創(chuàng)建和管理 JavaScript 對(duì)象和值
- 執(zhí)行 JavaScript 代碼
- 訪問(wèn) JavaScript 對(duì)象的屬性和方法
- 注冊(cè) JavaScript 函數(shù)
- 處理 JavaScript 異常
- 進(jìn)行垃圾回收
JavaScriptCore 類型
- JSC::JSObject:表示一個(gè) JavaScript 對(duì)象。
- JSC::JSValue:表示一個(gè) JavaScript 值。
- JSC::JSGlobalObject:表示 JavaScript 對(duì)象的全局對(duì)象。
- JSC::JSGlobalObjectFunctions:包含一組函數(shù),用于實(shí)現(xiàn) JSAPI 的功能,如執(zhí)行 JavaScript 代碼、訪問(wèn) JavaScript 對(duì)象的屬性和方法等。
在 JSAPI 中,JavaScript 對(duì)象和值通過(guò) JSC::JSObject 和 JSC::JSValue 類進(jìn)行表示。
JSC::JSObject 表示一個(gè) JavaScript 對(duì)象,它可以包含一組屬性和方法;
JSC::JSValue 表示一個(gè) JavaScript 值,它可以是一個(gè)對(duì)象、一個(gè)數(shù)值、一個(gè)字符串或一個(gè)布爾值等。
JSAPI 提供了 JSC::JSGlobalObject 類作為 JavaScript 對(duì)象的全局對(duì)象,所有的 JavaScript 對(duì)象都是從該全局對(duì)象繼承而來(lái)。
API 介紹
JSContextGroupCreate
JSContextGroupRef 是一個(gè)包含多個(gè) JSContext 的分組,它們可以共享內(nèi)存池和垃圾回收器,從而提高 JavaScript 執(zhí)行效率和減少內(nèi)存占用。
JSGlobalContextCreateInGroup
JSGlobalContextCreateInGroup 函數(shù)會(huì)創(chuàng)建一個(gè) JSGlobalContextRef 類型的對(duì)象,表示一個(gè) JavaScript 上下文對(duì)象,該對(duì)象包含一個(gè)虛擬機(jī)對(duì)象、內(nèi)存池、全局對(duì)象等成員變量。該函數(shù)返回值為創(chuàng)建的 JSGlobalContextRef 類型的對(duì)象,表示 JavaScript 上下文對(duì)象。
由于不同的 JSGlobalContextRef 對(duì)象擁有不同的全局對(duì)象,因此它們之間不會(huì)相互影響。在不同的 JSGlobalContextRef 對(duì)象中創(chuàng)建的 JavaScript 對(duì)象、函數(shù)、變量等,都是相互獨(dú)立的,它們之間不會(huì)共享數(shù)據(jù)或狀態(tài)。
JSEvaluateScript
用于執(zhí)行一段 JavaScript 代碼。其內(nèi)部工作機(jī)制主要包括以下幾個(gè)步驟:
- 將 JavaScript 代碼轉(zhuǎn)換為抽象語(yǔ)法樹(AST)
在執(zhí)行 JavaScript 代碼之前,JavaScriptCore 需要將其轉(zhuǎn)換為抽象語(yǔ)法樹(AST),這樣才能對(duì)其進(jìn)行解析和執(zhí)行。JavaScriptCore 的 AST 解析器可以將 JavaScript 代碼轉(zhuǎn)換為一棵 AST 樹,其中每個(gè)節(jié)點(diǎn)代表了一條 JavaScript 語(yǔ)句或表達(dá)式。 - 解析和執(zhí)行 AST 樹
一旦生成了 AST 樹,JavaScriptCore 就可以對(duì)其進(jìn)行解析和執(zhí)行了。在解析過(guò)程中,JavaScriptCore 會(huì)對(duì) AST 樹進(jìn)行遍歷,同時(shí)將其中的變量、函數(shù)等標(biāo)識(shí)符與對(duì)應(yīng)的值進(jìn)行綁定。在執(zhí)行過(guò)程中,JavaScriptCore 會(huì)按照 AST 樹的結(jié)構(gòu)逐步執(zhí)行其中的語(yǔ)句和表達(dá)式,同時(shí)根據(jù)需要調(diào)用相應(yīng)的函數(shù)和方法。 - 將執(zhí)行結(jié)果返回給調(diào)用方
一旦 JavaScript 代碼執(zhí)行完畢,JavaScriptCore 就會(huì)將其執(zhí)行結(jié)果返回給調(diào)用方。這個(gè)結(jié)果可以是任何 JavaScript 值,包括數(shù)字、字符串、對(duì)象、函數(shù)等。調(diào)用方可以根據(jù)需要對(duì)這個(gè)結(jié)果進(jìn)行處理和使用。
JSEvaluateScript 是一個(gè)同步函數(shù),即在執(zhí)行完 JavaScript 代碼之前,它會(huì)一直等待,直到 JavaScript 代碼執(zhí)行完畢并返回結(jié)果。這意味著,在執(zhí)行長(zhǎng)時(shí)間運(yùn)行的 JavaScript 代碼時(shí),JSEvaluateScript 函數(shù)可能會(huì)阻塞程序的運(yùn)行。
我們可以通過(guò)線程來(lái)對(duì) 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) { // 異步執(zhí)行 std::thread([ctx, script, thisObject, exception, completionHandler]() { // 執(zhí)行腳本 JSStringRef scriptStr = JSStringCreateWithUTF8CString(script); JSValueRef result = JSEvaluateScript(ctx, scriptStr, thisObject, nullptr, 0, exception); JSStringRelease(scriptStr); // 回調(diào) completionHandler completionHandler(result, exception); }).detach(); }
此外還應(yīng)關(guān)注注冊(cè)到 JS 環(huán)境中的 C 接口回調(diào),這里因盡快返回,如果有耗時(shí)任務(wù),則需要將結(jié)果通過(guò)異步去通知 JS 層,否則會(huì)阻塞 JS 線程(也就是調(diào)用該函數(shù)的線程)。
關(guān)鍵代碼示例
下面實(shí)現(xiàn)了一個(gè)向 global 中添加 getData 的 Native 函數(shù)
// 回調(diào)函數(shù) 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)); ... // 省略參數(shù)、類型等判斷 executor->xxx(); // C++業(yè)務(wù)側(cè) return xxx; // 返回到JS內(nèi) } bool JSCExecutor::initJSC() { // 初始化 JSC 引擎 context_group_ = JSContextGroupCreate(); JSClassDefinition global_class_definition = kJSClassDefinitionEmpty; global_class_ = JSClassCreate(&global_class_definition); // 在js執(zhí)行上下文環(huán)境(Group)中創(chuàng)建一個(gè)全局的js執(zhí)行上下文 context_ = JSGlobalContextCreateInGroup(context_group_, global_class_); if (!context_) { LOGE(TAG, "create js context error!"); return false; } // 獲取js執(zhí)行上下文的全局對(duì)象 global_ = JSContextGetGlobalObject(context_); if (!global_) { LOGE(TAG, "get js context error!"); return false; } // 綁定c++對(duì)象地址 JSObjectSetPrivate(global_, this); // 注冊(cè)函數(shù) 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)代碼交互的技術(shù)。JNI 提供了一組 API,可以使 Java 程序訪問(wèn)和調(diào)用本地方法和資源,也可以使本地代碼訪問(wèn)和調(diào)用 Java 對(duì)象和方法。
此方案需要使用 JNI 進(jìn)行雙向調(diào)用。
C 調(diào)用 Java
步驟:
- 獲取 JNIEnv 指針:JNIEnv 是一個(gè)結(jié)構(gòu)體指針,代表了 Java 虛擬機(jī)調(diào)用本地方法時(shí)的環(huán)境信息。JNIEnv 指針可以通過(guò) Java 虛擬機(jī)實(shí)例、調(diào)用線程等參數(shù)獲取。
- 獲取 Java 類、方法、字段等的 ID:通過(guò) JNIEnv 指針,可以使用函數(shù) FindClass()、GetMethodID()、GetStaticMethodID()、GetFieldID()等函數(shù)獲取 Java 類、方法、字段等的 ID。比如在 C 中去創(chuàng)建 Java 對(duì)象,并操作相關(guān) Java 對(duì)象
- 調(diào)用 Java 方法或訪問(wèn) Java 字段:通過(guò) JNIEnv 指針和 Java 對(duì)象的 ID,可以使用 CallObjectMethod()、CallStaticObjectMethod()、GetDoubleField()、SetObjectField()等函數(shù)調(diào)用 Java 方法或訪問(wèn) Java 字段。
JavaC
步驟:
- 設(shè)計(jì)規(guī)劃功能、接口
- Java 聲明 Native 方法
- 按照 JNI 標(biāo)準(zhǔn)實(shí)現(xiàn)方法,并通過(guò) System.loadLibrary()加載
public class TestJNI { static { System.loadLibrary("xxx.so"); // 加載動(dòng)態(tài)鏈接庫(kù) } // 聲明本地方法 private native void PrintHelloWorld(); // 靜態(tài)方法 public static native String GetVersion(); } // C實(shí)現(xiàn)函數(shù) JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { ... } // so初始化回調(diào)函數(shù) JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *jvm, void *reserved) { ... } // so卸載回調(diào)函數(shù) // 實(shí)現(xiàn) 包名_PrintHelloWorld(JNIEnv *env, jobject thiz) { ... } 包名_GetVersion(JNIEnv *env, jclass clazz) { ... }
關(guān)注點(diǎn)
JNI 的編寫會(huì)遇到有很多坑,比如 Java 封裝對(duì)象和 C++對(duì)象的生命周期關(guān)系、異步調(diào)用邏輯、編譯器報(bào)錯(cuò)不完善、類型不匹配、JVM 環(huán)境不一致、運(yùn)行線程不一致等等,下面是一些常用的規(guī)則
內(nèi)存
- 在 C/C++代碼中,使用對(duì)象或智能指針去管理內(nèi)存,若使用 malloc、calloc 等函數(shù)分配內(nèi)存,然后使用 free 函數(shù)釋放內(nèi)存。
- 在 JNI 中,通過(guò) jobject 等 JNI 對(duì)象的創(chuàng)建和銷毀方法,手動(dòng)管理 Java 內(nèi)存。例如,在 JNI 中創(chuàng)建 Java 對(duì)象時(shí),需要調(diào)用 NewObject 等 JNI 方法創(chuàng)建 Java 對(duì)象,然后在使用完后,需要調(diào)用 DeleteLocalRef 等 JNI 方法釋放 Java 對(duì)象。
性能
- 避免頻繁創(chuàng)建和銷毀 JNI 引用:創(chuàng)建和銷毀 JNI 引用(如 jobject、jclass、jstring 等)的開銷比較大,應(yīng)該盡量避免頻繁創(chuàng)建和銷毀 JNI 引用。
- 使用本地?cái)?shù)據(jù)類型:JNI 支持本地?cái)?shù)據(jù)類型(如 jint、jfloat、jboolean 等),這些數(shù)據(jù)類型與 Java 數(shù)據(jù)類型相對(duì)應(yīng),可以直接傳遞給 Java 代碼,避免了數(shù)據(jù)類型轉(zhuǎn)換的開銷。
- 使用緩存:如果有一些數(shù)據(jù)在 JNI 函數(shù)中需要重復(fù)使用,可以考慮使用緩存,避免重復(fù)計(jì)算,比如 GetObjectClass、GetMethodID,這些可以保存起來(lái)重復(fù)使用。
- 避免頻繁切換線程:JNI 函數(shù)會(huì)涉及到 Java 線程和本地線程之間的切換,這個(gè)過(guò)程比較耗時(shí)。因此,應(yīng)該盡量避免頻繁切換線程。
避免 Native 側(cè)代碼對(duì)整體性能造成得侵入,如 NDK 下 std::vector 分配大數(shù)據(jù)造成得性能低下,如 RN0.63 版本以前存在這個(gè)問(wèn)題:Make JSStringToSTLString 23x faster (733532e5e9 by @radex)這需要對(duì)不同得編譯環(huán)境差異性有所了解。
使用 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
線程安全
- 當(dāng)一個(gè)線程調(diào)用 Java 方法時(shí),JNI 系統(tǒng)將自動(dòng)為該線程創(chuàng)建一個(gè) JNIEnv。因此,在訪問(wèn) Java 對(duì)象之前,需要手動(dòng)將當(dāng)前線程與 JVM 綁定,以便獲取 JNIEnv 指針,這個(gè)過(guò)程就叫做 "Attach"。可以使用 AttachCurrentThread 方法將當(dāng)前線程附加到 JVM 上,然后就可以使用 JNIEnv 指針來(lái)訪問(wèn) Java 對(duì)象了。
在 JNI 中,一般建議每個(gè)線程在使用完 JNIEnv 之后,立即 Detach,以釋放資源,避免內(nèi)存泄漏 - Native 層線程安全需要針對(duì)自己得業(yè)務(wù)去區(qū)分是否需要加鎖
數(shù)據(jù)優(yōu)化結(jié)果
根據(jù)數(shù)據(jù)分析,性比之前減少了 50%的耗時(shí)
總結(jié)
上面概括性介紹了 JSC 和 JNI 的相關(guān)知識(shí)及經(jīng)驗(yàn)總結(jié),由于篇幅有限一些問(wèn)題沒有說(shuō)明白或理解有誤,歡迎一起交流~~
參考
https://developer.apple.com/documentation/javascriptcore
以上就是Android本地搜索業(yè)務(wù)優(yōu)化方案的詳細(xì)內(nèi)容,更多關(guān)于Android本地搜索優(yōu)化的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android Studio如何為Activity添加自定義注解信息
好久沒用寫文章了,今天給大家分享Android Studio如何為Activity添加自定義注解信息,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2021-06-06Android 中的危險(xiǎn)權(quán)限詳細(xì)整理
這篇文章主要介紹了Android 中的危險(xiǎn)權(quán)限詳細(xì)整理的相關(guān)資料,Android 中有上百種權(quán)限,現(xiàn)在將所有的權(quán)限歸為兩類,一類是普通權(quán)限,一類的危險(xiǎn)權(quán)限,危險(xiǎn)權(quán)限則表示那些可能會(huì)觸及到用戶安全隱私或者對(duì)設(shè)備安全造成影響的權(quán)限,需要的朋友可以參考下2017-07-07Android開發(fā)一行代碼解決安卓重復(fù)點(diǎn)擊
這篇文章主要為大家介紹了Android開發(fā)一行代碼解決安卓重復(fù)點(diǎn)擊,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06Android語(yǔ)音識(shí)別技術(shù)詳解及實(shí)例代碼
這篇文章主要介紹了Android語(yǔ)音識(shí)別技術(shù)的相關(guān)資料,并附實(shí)例代碼及實(shí)例實(shí)現(xiàn)效果圖,需要的朋友可以參考下2016-09-09Android中Xposed框架篇---修改系統(tǒng)位置信息實(shí)現(xiàn)自身隱藏功能實(shí)例
本篇文章介紹了Android中Xposed框架的使用,詳細(xì)的介紹了修改系統(tǒng)位置信息實(shí)現(xiàn)自身隱藏功能實(shí)例,有需要的朋友可以了解一下。2016-11-11Android中fragment與activity之間的交互(兩種實(shí)現(xiàn)方式)
本篇文章主要介紹了Android中fragment與activity之間的交互(兩種實(shí)現(xiàn)方式),相信對(duì)大家學(xué)習(xí)會(huì)有很好的幫助,需要的朋友一起來(lái)看下吧2016-12-12