碼一個簡易的跨端框架

目錄:

  1. 簡介
  2. 架構(gòu)圖
  3. 方法時序圖
  4. 代碼詳解

跨端框架越來越火爆闷板,每個公司都為了提高效率而努力,完全是原生開發(fā)的App越來越少恤煞,就連google也出了自己的跨端方案Flutter元咙。但是有技術(shù)實力的公司都會有自己的跨端框架浑槽,facebook的RN,滴滴的Hummer领舰,阿里的Weex等等夫嗓,這些跨端框架是如何實現(xiàn)的呢,本篇文章教你碼一個最簡易的跨端框架冲秽。
特別感謝Hummer團隊http://hummer.didi.cn/home#/

簡易跨端框架的架構(gòu)圖如下:
碼一個簡易跨端框架.png

大概分5層:
第一層:DSL層舍咖,也就是我們使用JavaScript寫控件的那一層,因為JavaScript是沒有類型檢查的極難維護锉桑;所以出現(xiàn)了TypeScript排霉,他相對于js來說有了強類型檢查,在編輯期間IDE會做出類型檢查民轴,以及在編譯生成js代碼編譯器會對類型進行檢查攻柠,有了強制類型檢查程序變得更好維護了球订,更適用于大型項目了。

第二層:js引擎層瑰钮,也就是解析js代碼的冒滩;js引擎有很多v8、Quickjs浪谴、Hermes开睡、JavaScriptCore等,本demo使用了Quickjs作為js引擎苟耻,因為他代碼量小篇恒,編譯生成的so文件小,而且他是源碼相對簡單方便查找問題梁呈。

第三層:bridge層婚度,這層非常重要,它是Android官卡、Quickjs、之間的通信橋梁醋虏;Android代碼啟動通過JNI接口將javascript代碼傳入Quickjs引擎解析寻咒,引擎解析完成通過Quickjs回調(diào)到C語言方法上,C語言方法在通過JNI接口回調(diào)到Android代碼颈嚼,映射到對應(yīng)功能上毛秘,完成了整個流程。

第四層:組件層阻课,這層是將android中的組件層對應(yīng)到j(luò)s中的組件層叫挟;在js層創(chuàng)建一個View{new View()},通過第三層映射到Android層的組件上限煞。

第五層:android系統(tǒng)層抹恳,android組件是基于android系統(tǒng)運行的。

跨端框架調(diào)用時序圖:

其中的類名署驻、方法名以及變量名字都是當前demo代碼中的


跨端框架調(diào)用時序圖.jpg
結(jié)合上圖分析一下簡易跨端框架如何實現(xiàn):
  1. 下載QuickJS代碼奋献,https://github.com/quickjs-zh/QuickJS ,下載比較干凈的C++版本https://github.com/quickjs-zh/quickjspp 旺上,這里自帶CMakeLists.txt瓶蚂。
  2. AndroidStudio創(chuàng)建jni工程,這步可以跟著官方文檔一步一步創(chuàng)建寫的非常詳細宣吱,https://developer.android.com/studio/projects/add-native-code?hl=zh-cn
  3. 將下載的QuickJS代碼復(fù)制到j(luò)ni工程的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)
#設(shè)置生成的so動態(tài)庫最后輸出的路徑
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)

#將編譯的源碼設(shè)置到quickjs_src中
set(quickjs_src quickjs.c libunicode.c libregexp.c cutils.c quickjs-libc.c)
#將預(yù)編譯宏設(shè)置到quickjs_def中
set(quickjs_def CONFIG_VERSION="${version}" _GNU_SOURCE)
#條件編譯值洒试,這個條件編譯宏表示是否編譯提供大數(shù)功能,后面的NO表示不提供if條件為false
option(QUICKJS_BIGNUM "Compile BigNum support" ON)

#設(shè)置生成的so動態(tài)庫最后輸出的路徑
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靜態(tài)庫
add_library(quickjs SHARED ${quickjs_src})
#將上面quickjs_def定義的宏應(yīng)用到libquickjs.so這個庫中
target_compile_definitions(quickjs PRIVATE ${quickjs_def} )
  1. 編譯運行肯定會報錯的因為還沒有寫quickjs-jni.cpp jni接口代碼,jni接口代碼的主要作用就是橋接java層->C層->java層痪宰;下面分析主要代碼邏輯
方法一:
/**
 * 對QuickJs引擎設(shè)置屬性叼架,也就是設(shè)置回調(diào)方法,將invoke這個C方法通過JS_SetPropertyStr設(shè)置到Quickjs引擎中
 * 這樣在js代碼中就可以調(diào)用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這個靜態(tài)方法 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");

    //創(chuàng)建全局的JSContext衣撬,切記一定要用同一個JSContext
    getJSContext();

    //將C方法invoke創(chuàng)建成JS引擎中的方法
    auto funcName = "invoke";
    auto invokeFunc = JS_NewCFunction(context, invoke, funcName, strlen(funcName));

    //將這個創(chuàng)建之后的js引擎中的invokeFunc方法注入到j(luò)s引擎中乖订,成為全局方法,這樣在寫js代碼時就可以直接使用invoke這個方法
    JS_SetPropertyStr(context,
            JS_GetGlobalObject(context),
            "invoke",
            invokeFunc);
}
方法二
/**
 * js引擎通過JS_SetPropertyStr將這個方法注入到j(luò)s引擎中具练,寫js代碼時可以直接調(diào)用這個方法乍构,
 * 這個方法的聲明是參照JSCFunction方法聲明的可以查看,在這個方法中通過jni調(diào)用java中的代碼扛点,
 * 實現(xiàn)js引擎和java代碼的通信
 * @param ctx
 * @param thisObject
 * @param argumentCount 參數(shù)數(shù)量
 * @param arguments 這是個數(shù)組根據(jù)參數(shù)的數(shù)量取出具體的參數(shù)
 * @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);的最后一個參數(shù)long數(shù)組
    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調(diào)用com.example.myapplication.QuickJS類中的invoke靜態(tài)方法哥遮,實現(xiàn)C層回調(diào)到j(luò)ava層
    //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引擎執(zhí)行java傳過來的js代碼
 * 這個方法可以提前注入js_component_base.js組件的基礎(chǔ)模板
 * 也可以執(zhí)行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引擎執(zhí)行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;
}
  1. 上面Quickjs引擎和quickjs-jni.cpp對java暴露的jni接口都定義好之后我們來看java層的接口如何實現(xiàn);
方法一
/**
     * 用來調(diào)用quickjs-jni.cpp中定義的native方法
     * QuickJSBridge陵究,實現(xiàn)方法的映射眠饮,具體參照上面方法解析
     * ExecuteIntegerScript,將js組件的模板代碼提前注入到j(luò)s引擎中铜邮,這樣后續(xù)組件可以使用
     * @param onInvoke
     */
    public static void QuickjsBridgeInvoke(OnInvoke onInvoke) {
        QuickJS.onInvoke = onInvoke;
        //設(shè)置QuickJSBridge
        QuickJSBridge();
        //設(shè)置JS組件的Base類代碼
        QuickJS.ExecuteIntegerScript(JS_COMPONENT,"js_component_base.js");

    }
方法二
/**
     * 這個方法就是quickjs-jni.cpp文件中invoke通過jni方法CallStaticLongMethod調(diào)用的java方法
     * JNI回調(diào)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類松蒜,其中看構(gòu)造方法constructor->其實是調(diào)用了提前已經(jīng)注入到j(luò)s引擎中的invoke方法
    //這個invoke方法會通過jni調(diào)用java中的方法實現(xiàn)類的創(chuàng)建
    //我們定義了一個類以及其中的方法(Curise.render())這個方法實際上也是調(diào)用了提前注入好的invoke方法
    //invoke方法回調(diào)到j(luò)ava方法中進行相應(yīng)的操作
    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" +
            "};";

  1. 下面我們看如何使用
 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        扔茅。。秸苗。召娜。。难述。
        //注冊bridge
        QuickJS.QuickjsBridgeInvoke(onInvoke);
    }

//如下代碼為模擬代碼
    //js代碼為var button = new Button();萤晴,在構(gòu)造方法constructor中會調(diào)用預(yù)先注入的invoke方法,
    //invoke方法會通過jni回調(diào)到j(luò)ava方法胁后,java方法通過判斷字符串"constructor"創(chuàng)建Button
    //js代碼為Cruiser.render();店读,實際上會調(diào)用invoke方法,通過jni回調(diào)到j(luò)ava方法攀芯,在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");
    }

總結(jié):
總結(jié)一下跨端框架的思路屯断,

  1. js引擎中注入一個js方法名字叫invoke,通過js引擎方法JS_SetPropertyStr將js方法invoke和本地的C方法關(guān)聯(lián)起來,本地C方法叫invokeC殖演,
  2. 本地的C方法invokeC通過jni的方式回調(diào)到j(luò)ava層的方法叫invokeJava
  3. 這樣就實現(xiàn)了js代碼和java代碼的對應(yīng)

參考文檔:
Hummer官網(wǎng)
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開發(fā)java調(diào)用C方法
http://www.reibang.com/p/0e62d00a9e59

一個跨端渲染思路
http://www.reibang.com/p/935d2c2defc7

TypeScript優(yōu)勢
https://blog.csdn.net/xyphf/article/details/81944554

V8氧秘、Quickjs、JavaScriptCore趴久、Hemens跨端開發(fā)怎么選
https://cloud.tencent.com/developer/article/1801742

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末丸相,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子彼棍,更是在濱河造成了極大的恐慌灭忠,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件座硕,死亡現(xiàn)場離奇詭異弛作,居然都是意外死亡,警方通過查閱死者的電腦和手機华匾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門映琳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蜘拉,你說我怎么就攤上這事萨西。” “怎么了诸尽?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵原杂,是天一觀的道長。 經(jīng)常有香客問我您机,道長,這世上最難降的妖魔是什么年局? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任际看,我火速辦了婚禮,結(jié)果婚禮上矢否,老公的妹妹穿的比我還像新娘仲闽。我一直安慰自己,他們只是感情好僵朗,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布赖欣。 她就那樣靜靜地躺著,像睡著了一般验庙。 火紅的嫁衣襯著肌膚如雪顶吮。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天粪薛,我揣著相機與錄音悴了,去河邊找鬼。 笑死,一個胖子當著我的面吹牛湃交,可吹牛的內(nèi)容都是我干的熟空。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼搞莺,長吁一口氣:“原來是場噩夢啊……” “哼息罗!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起才沧,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤迈喉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后糜工,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體弊添,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年捌木,在試婚紗的時候發(fā)現(xiàn)自己被綠了油坝。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡刨裆,死狀恐怖澈圈,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情帆啃,我是刑警寧澤瞬女,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站努潘,受9級特大地震影響诽偷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜疯坤,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一报慕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧压怠,春花似錦眠冈、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至雨让,卻和暖如春雇盖,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背宫患。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工刊懈, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留这弧,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓虚汛,卻偏偏與公主長得像匾浪,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子卷哩,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容