码一个简易的跨端框架

目录:

  1. 简介
  2. 架构图
  3. 方法时序图
  4. 代码详解

跨端框架越来越火爆,每个公司都为了提高效率而努力,完全是原生开发的App越来越少,就连google也出了自己的跨端方案Flutter。但是有技术实力的公司都会有自己的跨端框架,facebook的RN,滴滴的Hummer,阿里的Weex等等,这些跨端框架是如何实现的呢,本篇文章教你码一个最简易的跨端框架。
特别感谢Hummer团队http://hummer.didi.cn/home#/

简易跨端框架的架构图如下:
码一个简易跨端框架.png

大概分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代码中的


跨端框架调用时序图.jpg
结合上图分析一下简易跨端框架如何实现:
  1. 下载QuickJS代码,https://github.com/quickjs-zh/QuickJS ,下载比较干净的C++版本https://github.com/quickjs-zh/quickjspp ,这里自带CMakeLists.txt。
  2. AndroidStudio创建jni工程,这步可以跟着官方文档一步一步创建写的非常详细,https://developer.android.com/studio/projects/add-native-code?hl=zh-cn
  3. 将下载的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} )
  1. 编译运行肯定会报错的因为还没有写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;
}
  1. 上面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" +
            "};";

  1. 下面我们看如何使用
 @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");
    }

总结:
总结一下跨端框架的思路,

  1. js引擎中注入一个js方法名字叫invoke,通过js引擎方法JS_SetPropertyStr将js方法invoke和本地的C方法关联起来,本地C方法叫invokeC,
  2. 本地的C方法invokeC通过jni的方式回调到java层的方法叫invokeJava
  3. 这样就实现了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方法
//www.greatytc.com/p/0e62d00a9e59

一个跨端渲染思路
//www.greatytc.com/p/935d2c2defc7

TypeScript优势
https://blog.csdn.net/xyphf/article/details/81944554

V8、Quickjs、JavaScriptCore、Hemens跨端开发怎么选
https://cloud.tencent.com/developer/article/1801742

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容