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