目錄:
- 簡介
- 架構圖
- 方法時序圖
- 代碼詳解
跨端框架越來越火爆,每個公司都爲了提高效率而努力,完全是原生開發的App越來越少,就連google也出了自己的跨端方案Flutter。但是有技術實力的公司都會有自己的跨端框架,facebook的RN,滴滴的Hummer,阿里的Weex等等,這些跨端框架是如何實現的呢,本篇文章教你擼一個最簡易的跨端框架。
特別感謝Hummer團隊http://hummer.didi.cn/home#/
簡易跨端框架的架構圖如下:
大概分5層:
第一層:DSL層,也就是我們使用JavaScript寫控件的那一層,因爲JavaScript是沒有類型檢查的極難維護;所以出現了TypeScript,他相對於js來說有了強類型檢查,在編輯期間IDE會做出類型檢查,以及在編譯生成js代碼編譯器會對類型進行檢查,有了強制類型檢查程序變得更好維護了,更適用於大型項目了。
第二層:js引擎層,也就是解析js代碼的;js引擎有很多v8、Quickjs、Hermes、JavaScriptCore等,本demo使用了Quickjs作爲js引擎,因爲他代碼量小,編譯生成的so文件小,而且他是源碼相對簡單方便查找問題。
第三層:bridge層,這層非常重要,它是Android、Quickjs、之間的通信橋樑;Android代碼啓動通過JNI接口將javascript代碼傳入Quickjs引擎解析,引擎解析完成通過Quickjs回調到C語言方法上,C語言方法在通過JNI接口回調到Android代碼,映射到對應功能上,完成了整個流程。
第四層:組件層,這層是將android中的組件層對應到js中的組件層;在js層創建一個View{new View()},通過第三層映射到Android層的組件上。
第五層:android系統層,android組件是基於android系統運行的。
跨端框架調用時序圖:
其中的類名、方法名以及變量名字都是當前demo代碼中的
結合上圖分析一下簡易跨端框架如何實現:
- 下載QuickJS代碼,https://github.com/quickjs-zh/QuickJS ,下載比較乾淨的C++版本https://github.com/quickjs-zh/quickjspp ,這裏自帶CMakeLists.txt。
- AndroidStudio創建jni工程,這步可以跟着官方文檔一步一步創建寫的非常詳細,https://developer.android.com/studio/projects/add-native-code?hl=zh-cn
- 將下載的QuickJS代碼複製到jni工程的cpp目錄下,修改./cpp/CMakeLists.txt文件以及./cpp/quickjs/CMakeLists.txt文件,詳情請看如下代碼,裏面有註釋
#./cpp/CMakeLists.txt
#ndk版本和app/build.gradle中externalNativeBuild配置的要一直
cmake_minimum_required(VERSION 3.10.2)
#生成so文件的名字libquickjs-android.so
project("quickjs-android")
#編譯過程中依賴的文件夾
add_subdirectory(./quickjs)
LINK_DIRECTORIES(./quickjs)
#設置生成的so動態庫最後輸出的路徑
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
#將源碼quickjs-jni.cpp生成SHARED庫也就是so庫,名字是libquickjs-android.so
add_library(
${PROJECT_NAME}
SHARED
quickjs-jni.cpp)
#從NDK庫中找到log(日誌庫)並且將路徑保存在log-lib中
find_library(
log-lib
log)
#將log-lib這個日誌庫鏈接進libquickjs-android.so庫中
target_link_libraries(
${PROJECT_NAME}
${log-lib}
quickjs
)
#./cpp/quickjs/CMakeLists.txt
#生成so文件的名字libquickjs.so
project(quickjs LANGUAGES C)
#將編譯的源碼設置到quickjs_src中
set(quickjs_src quickjs.c libunicode.c libregexp.c cutils.c quickjs-libc.c)
#將預編譯宏設置到quickjs_def中
set(quickjs_def CONFIG_VERSION="${version}" _GNU_SOURCE)
#條件編譯值,這個條件編譯宏表示是否編譯提供大數功能,後面的NO表示不提供if條件爲false
option(QUICKJS_BIGNUM "Compile BigNum support" ON)
#設置生成的so動態庫最後輸出的路徑
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/../../jniLibs/${ANDROID_ABI})
#上面編譯條件,NO,YES
if(QUICKJS_BIGNUM)
list(APPEND quickjs_src libbf.c)
list(APPEND quickjs_def CONFIG_BIGNUM)
endif()
#生成libquick.so靜態庫
add_library(quickjs SHARED ${quickjs_src})
#將上面quickjs_def定義的宏應用到libquickjs.so這個庫中
target_compile_definitions(quickjs PRIVATE ${quickjs_def} )
- 編譯運行肯定會報錯的因爲還沒有寫quickjs-jni.cpp jni接口代碼,jni接口代碼的主要作用就是橋接java層->C層->java層;下面分析主要代碼邏輯
方法一:
/**
* 對QuickJs引擎設置屬性,也就是設置回調方法,將invoke這個C方法通過JS_SetPropertyStr設置到Quickjs引擎中
* 這樣在js代碼中就可以調用invoke這個全局方法了
*/
extern "C"
JNIEXPORT void JNICALL
Java_com_example_myapplication_QuickJS_QuickJSBridge(JNIEnv *env, jclass clazz) {
//找到QuickJS這個java類的jclass句柄
jniHandle = env->FindClass("com/example/myapplication/QuickJS");
if (NULL == jniHandle) {
LOGE("can't find JniHandle");
return;
}
//在QuickJS這個java類中找到invoke這個靜態方法 long invoke(String className,long objectID,String methodName,long ...param);
QUICKJS_BRIDGE_INVOKE_ID = env->GetStaticMethodID(jniHandle, "invoke",
"(Ljava/lang/String;JLjava/lang/String;[J)J");
//創建全局的JSContext,切記一定要用同一個JSContext
getJSContext();
//將C方法invoke創建成JS引擎中的方法
auto funcName = "invoke";
auto invokeFunc = JS_NewCFunction(context, invoke, funcName, strlen(funcName));
//將這個創建之後的js引擎中的invokeFunc方法注入到js引擎中,成爲全局方法,這樣在寫js代碼時就可以直接使用invoke這個方法
JS_SetPropertyStr(context,
JS_GetGlobalObject(context),
"invoke",
invokeFunc);
}
方法二
/**
* js引擎通過JS_SetPropertyStr將這個方法注入到js引擎中,寫js代碼時可以直接調用這個方法,
* 這個方法的聲明是參照JSCFunction方法聲明的可以查看,在這個方法中通過jni調用java中的代碼,
* 實現js引擎和java代碼的通信
* @param ctx
* @param thisObject
* @param argumentCount 參數數量
* @param arguments 這是個數組根據參數的數量取出具體的參數
* @return
*/
static JSValue invoke(JSContext* ctx, JSValueConst thisObject, int argumentCount, JSValueConst* arguments) {
JNIEnv* env = JNI_GetEnv();
//取出 long invoke(String className,long objectID,String methodName,long ...param);的最後一個參數long數組
jlongArray params = nullptr;
if (argumentCount > 3) {
int methodParamsCount = argumentCount - 3;
params = env->NewLongArray(methodParamsCount);
jlong paramsC[methodParamsCount];
for (int i = 3; i < argumentCount; i++) {
paramsC[i - 3] = QJS_VALUE_PTR(arguments[i]);
}
env->SetLongArrayRegion(params, 0, methodParamsCount, paramsC);
}
//取出objectID
int64_t objId;
JS_ToInt64(ctx, &objId, arguments[INDEX_OBJECT_ID]);
//取出className
jstring className = JSString2JavaString(ctx, arguments[INDEX_CLASS_NAME]);
//取出methodName
jstring methodName = JSString2JavaString(ctx, arguments[INDEX_METHOD_NAME]);
//jni調用com.example.myapplication.QuickJS類中的invoke靜態方法,實現C層回調到java層
//private static long invoke(String className, long objectID, String methodName, long... params)
jlong ret = env->CallStaticLongMethod(jniHandle, QUICKJS_BRIDGE_INVOKE_ID,className,objId,methodName,params);
env->DeleteLocalRef(className);
env->DeleteLocalRef(methodName);
env->DeleteLocalRef(params);
JNI_DetachEnv();
return JS_NewInt64(ctx,ret);
}
方法三
/**
* quickjs引擎執行java傳過來的js代碼
* 這個方法可以提前注入js_component_base.js組件的基礎模板
* 也可以執行js代碼
*/
extern "C" JNIEXPORT jint JNICALL
Java_com_example_myapplication_QuickJS_ExecuteIntegerScript(JNIEnv *env, jclass clazz, jstring jCode,
jstring jFileName) {
getJSContext();
const char *code = env->GetStringUTFChars(jCode, NULL);
const int code_length = env->GetStringUTFLength(jCode);
const char *file_name = env->GetStringUTFChars(jFileName, NULL);
int flags = 0;
//QuickJs引擎執行js代碼
JSValue val = JS_Eval(context, code, (size_t) code_length, file_name, JS_EVAL_TYPE_GLOBAL);
int result = JS_VALUE_GET_INT(val);
return result;
}
- 上面Quickjs引擎和quickjs-jni.cpp對java暴露的jni接口都定義好之後我們來看java層的接口如何實現;
方法一
/**
* 用來調用quickjs-jni.cpp中定義的native方法
* QuickJSBridge,實現方法的映射,具體參照上面方法解析
* ExecuteIntegerScript,將js組件的模板代碼提前注入到js引擎中,這樣後續組件可以使用
* @param onInvoke
*/
public static void QuickjsBridgeInvoke(OnInvoke onInvoke) {
QuickJS.onInvoke = onInvoke;
//設置QuickJSBridge
QuickJSBridge();
//設置JS組件的Base類代碼
QuickJS.ExecuteIntegerScript(JS_COMPONENT,"js_component_base.js");
}
方法二
/**
* 這個方法就是quickjs-jni.cpp文件中invoke通過jni方法CallStaticLongMethod調用的java方法
* JNI回調Java的代碼
* @param className
* @param objectID
* @param methodName
* @param params
* @return
*/
private static long invoke(String className, long objectID, String methodName, long... params) {
if(null != onInvoke){
onInvoke.invoke(className,objectID,methodName,params);
}
return 0;
}
方法三
//如下js代碼比較核心,只有下面定義了的組件才能在js中使用,
//我們定義了Base類,其中看構造方法constructor->其實是調用了提前已經注入到js引擎中的invoke方法
//這個invoke方法會通過jni調用java中的方法實現類的創建
//我們定義了一個類以及其中的方法(Curise.render())這個方法實際上也是調用了提前注入好的invoke方法
//invoke方法回調到java方法中進行相應的操作
private static final String JS_COMPONENT ="var count_id = 1;\n" +
"const idGenerator = () => count_id++;\n" +
"class Base {\n" +
" constructor(className, ...args) {\n" +
" this.className = className;\n" +
" this.objID = idGenerator();\n" +
" invoke(this.className, this.objID, \"constructor\", this, ...args);\n" +
" }\n" +
"\n" +
" addEventListener(...args) {\n" +
" invoke(this.className, this.objID, \"addEventListener\", ...args);\n" +
" }\n" +
"\n" +
" removeEventListener(...args) {\n" +
" invoke(this.className, this.objID, \"removeEventListener\", ...args);\n" +
" }\n" +
"}\n" +
"\n" +
"class Button extends Base {\n" +
" constructor(...args) {\n" +
" super('Button', ...args);\n" +
" }\n" +
"};\n" +
"\n" +
"const Cruiser = {\n" +
" render : ()=>{\n" +
" invoke(\"Cruiser\",0,\"render\",0);\n" +
" }\n" +
"};";
- 下面我們看如何使用
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
。。。。。。
//註冊bridge
QuickJS.QuickjsBridgeInvoke(onInvoke);
}
//如下代碼爲模擬代碼
//js代碼爲var button = new Button();,在構造方法constructor中會調用預先注入的invoke方法,
//invoke方法會通過jni回調到java方法,java方法通過判斷字符串"constructor"創建Button
//js代碼爲Cruiser.render();,實際上會調用invoke方法,通過jni回調到java方法,在java方法中判斷字符串"render"
//添加到ViewGroup上展示
private QuickJS.OnInvoke onInvoke = new QuickJS.OnInvoke() {
@Override
public long invoke(String className, long objectID, String methodName, long... params) {
Toast.makeText(sApplication, className + " : " + methodName, Toast.LENGTH_LONG).show();
switch (methodName) {
case "constructor": {
mButton = new Button(MainActivity.this);
mButton.setText("jiahongfei");
break;
}
case "render":{
if(null != mContainer){
mContainer.addView(mButton);
}
break;
}
}
return 0;
}
};
//點擊事件模擬js代碼輸入
public void onClick(View view) {
QuickJS.ExecuteIntegerScript("var button = new Button();\nCruiser.render();\n", "js_component.js");
}
總結:
總結一下跨端框架的思路,
- js引擎中注入一個js方法名字叫invoke,通過js引擎方法JS_SetPropertyStr將js方法invoke和本地的C方法關聯起來,本地C方法叫invokeC,
- 本地的C方法invokeC通過jni的方式回調到java層的方法叫invokeJava
- 這樣就實現了js代碼和java代碼的對應
參考文檔:
Hummer官網
http://hummer.didi.cn/home#/
QuickJS引擎
https://zhuanlan.zhihu.com/p/161722203
QuickJs to Android
http://events.jianshu.io/p/6ffe30df4e30
100行代碼js與c通信
https://juejin.cn/post/6844904142477983752
NDK開發java調用C方法
https://www.jianshu.com/p/0e62d00a9e59
一個跨端渲染思路
https://www.jianshu.com/p/935d2c2defc7
TypeScript優勢
https://blog.csdn.net/xyphf/article/details/81944554
V8、Quickjs、JavaScriptCore、Hemens跨端開發怎麼選
https://cloud.tencent.com/developer/article/1801742