使用Flutter + V8開發小程式引擎(二)

codeC發表於2019-09-01

flutter dart 、java/kotlin、V8引擎之間互動

在前一篇《使用Flutter + V8開發小程式引擎(一)》介紹了我們是用html+css來做介面描述,互動是用js,那麼具體是怎麼處理這兩者的關係,以達到可動態下發及渲染頁面的呢?

  • 先看下整體的通訊通,下面我們再詳細介紹
    使用Flutter + V8開發小程式引擎(二)

java/kotlin 與 Dart互動,這部分應該不用說,做過flutter的同學應該都知道

java/kotlin 與 V8互動,這部分應該很少人接觸過,有什麼用呢?那麼請繼續往下看

一、java/kotlin與V8互動

1、java/kotlin注入V8物件及執行V8裡面的js函式

首先我們先用微信小程式舉例,做過微信小程式或者瞭解過原理的同學應該知道,小程式開發在介面描述上是使用資料繫結,則在需要動態資料的文字或者屬性時,通過雙大花括號寫一個表示式與對應資料進行繫結。請看以下一個簡單例子:

<text>{{message1 + message2}}</text>

Page({
  data: {
    message1: "hello ",
    message2: " world"
  },
  onLoad(e) {},
  onUnload() {}
})
複製程式碼

以上例子,text 元件的文字與Page中的data的message1、message2進行繫結,那麼如何計算表示式message1 + message2的值呢?則需要用到V8引擎來計算

  • 首先要在V8引擎構造Page物件,為什麼呢?因為這樣我們才能在計算表示式的時候拿到對應data的資料

通過eval將Page載入到V8引擎(ps:這個方法是通過framework.js檔案在初始化V8的時候提前注入到V8裡面,具體請檢視原始碼實現)

        this.__native__evalInPage = function (jsContent) {
            if (!jsContent) {
                console.log("js content is empty!");
            }
            eval(jsContent);
        }
複製程式碼

我們看下載入後在V8裡面的Page物件

使用Flutter + V8開發小程式引擎(二)

還有data裡面的資料

使用Flutter + V8開發小程式引擎(二)

  • 那麼資料已經載入到V8引擎裡面了,怎麼計算表達數呢?那麼需要在V8裡面拼接表示式了,具體如下:

同上__native__evalInPage方法,__native__getExpValue也是通過framework.js檔案提前注入到V8的一個方法

this.__native__getExpValue = function (script) {
            const expFunc = exp => {
                return new Function('', 'with(this){' + exp + '}').bind(
                    this.data
                )();
            };
            var value = expFunc(script);
            if (value instanceof Object) {
                return JSON.stringify(value);
            }
            if (value instanceof Array) {
                return JSON.stringify(value);
            }
            return value;
        }
複製程式碼

在java/kotlin呼叫以下方法,先獲取到V8裡面對應的頁面物件,在通過executeFunction執行V8裡面的__native__getExpValue方法

    fun handleExpression(pageId: String, expression: String): String? {
        val page = getV8Page(pageId)
        return page?.executeFunction("__native__getExpValue", V8Array(V8Manager.v8).push(expression)).toString()
    }
複製程式碼
    val pageId = "0x0001"
    val exp = "return message1 + message2"
    val result = handleExpression(pageId, exp)
複製程式碼

2、V8裡面的JS函式回撥java/kotlin

這裡注入一個console的物件給到V8,即在js中執行console.log()的時候會回撥到JSConsole的log方法(ps:這裡只是列舉一種,還有其他方式,後續有機會再介紹)

    private fun registerFunc() {
        val v8Console = V8Object(v8)
        v8.add("console", v8Console)
        val jsConsole = JSConsole()
        v8Console.registerJavaMethod(jsConsole, "log", "log", arrayOf<Class<*>>(java.lang.Object::class.java))
        v8Console.release()
    }
複製程式碼
class JSConsole {
    fun log(string: Any) {
        Log.d("js", string.toString())
    }
}
複製程式碼

到這裡我們就講完了java/kotlin與V8互動的部分了

二、java/kotlin與dart互動

  • dart呼叫java/kotlin

在dart端通過MethodChannel呼叫

  var _methodChannel = MethodChannel("com.cc.hybrid/method");
  
  void _initScript(String script) {
    _methodChannel.invokeMethod(
        "attach_page", {"pageId": _pageId, "script": decodeBase64(script)});
  }
複製程式碼

在java/kotlin端實現onMethodCall

override fun onMethodCall(methodCall: MethodCall, result: MethodChannel.Result) {
        when (methodCall.method) {
            Methods.ATTACH_PAGE -> {
                if (methodCall.hasArgument("pageId") && methodCall.hasArgument("script")) {
                    val id = methodCall.argument<String>("pageId")
                    val script = methodCall.argument<String>("script")
                    Logger.d("lry", "attach_page pageId = $id script = $script")
                    JSPageManager.attachPageScriptToJsCore(id!!, script!!)
                    result.success("success")
                }
            }
            ...
        }
    }
複製程式碼

-java/kotlin通知dart

在dart端實現BasicMessageChannel訊息監聽

var _basicChannel = BasicMessageChannel<String>('com.cc.hybrid/basic', StringCodec());
_initBasicChannel() async {
    _basicChannel.setMessageHandler((String message) {
      print('Flutter Received: $message');
      var jsonObj = jsonDecode(message);
      var pageId = jsonObj['pageId'];
      MessageHandler handler = _handlers[pageId];
      if (null != handler) {
        handler.onMessage(jsonObj);
      } else {
        // 實時除錯socket過來的資料
        var jsonObject = jsonDecode(jsonObj['message']);
        var pageCode = jsonObject['pageCode'];
        var content = jsonObject['content'];
        _pages.putIfAbsent(pageCode, () => content);
        _handlers.forEach((k, v) {
          if (k.startsWith(pageCode)) {
            v.onMessage(jsonObj);
          }
        });
      }
      return Future<String>.value("success");
    });
  }
複製程式碼

在java/kotlin端傳送訊息

    private fun sendMessage2Flutter(type: Int, pageId: String, content: String) {
        val jsonObject = JSONObject()
        jsonObject.put("type", type)
        jsonObject.put("pageId", pageId)
        jsonObject.put("message", content)
        channel.send(jsonObject.toString())
    }
複製程式碼

三、總結

dart -> java/kotlin 場景(以上面text渲染的流程)

<text>{{message1 + message2}}</text>

Page({
  data: {
    message1: "hello ",
    message2: " world"
  }
})
複製程式碼
  • 首先,頁面在初始化的時候先將Page載入到V8引擎
  • 其次,在dart端生成widget時候,遇到資料繫結的表示式,先通過MethodChannel將表示式傳到java/kotlin端
  • 然後,java/kotlin端通過executeFunction執行V8裡面對應Page下面的__native__getExpValue方法得到表示式的值
  • 最後,java/kotlin端通過MethodChannel.Result將表示式的值直接返回給dart端

java/kotlin -> dart 場景(例如:點選列印日誌)

<raisedbutton onclick="onclick">
    <text>{{message1 + message2}}</text>
</raisedbutton>

Page({
  data: {
    message1: "hello ",
    message2: " world"
  },
  onclick(e) {
    var msg = this.data.message1 + this.data.message2;
    console.log(msg);  
  }
})
複製程式碼
  • 具體參照上面介紹的 V8裡面的JS函式回撥java/kotlin

四、不足與擴充

  • 不足

從上面的介紹,我們可以看到,要實現動態下發,需要dart->java/kotlin->V8->java/kotlin->dart這樣一條長鏈路,這樣子效率相對較低,而且對Android跟iOS來說是需要分別對接與原生互動的差異部分

  • 擴充

js引擎方面,Android是用V8,iOS用的是JsCore,目前沒有實現,當然,如果實現dart直接呼叫V8及JsCore的Api,則整個鏈路可以簡化為dart->V8/JsCore->dart,提高效率的同時,兩端也沒有那麼多的差異處理

相關文章