[TOC]
前言&背景
近期由于Fuchsia正式發(fā)布的信息也再一次讓Flutter變得更加火熱起來,Flutter是一個(gè)可以運(yùn)行在Linux,Window,Android,Ios等多平臺(tái)上的開源移動(dòng)應(yīng)用軟件開發(fā)工具包.但是Flutter默認(rèn)集成的語言是Dart,這就給我們帶來了較強(qiáng)的上手學(xué)習(xí)成本,且需要長(zhǎng)時(shí)間的Dart踩坑.如果能夠使用Typescript和JSX就可以在各個(gè)平臺(tái)進(jìn)行圖形化編程,我想應(yīng)該會(huì)給React開發(fā)者很大方便.
同樣針對(duì)IOT 時(shí)代的到來,越來越多的圖形化設(shè)備的出現(xiàn),如何高效可復(fù)用動(dòng)態(tài)化的在低配硬件設(shè)備進(jìn)行圖形編程是一個(gè)可以進(jìn)行探索的課題.
項(xiàng)目其核心設(shè)計(jì)關(guān)鍵就是利用JSON語言承載界面描述的DSL,再利用排版引擎解析DSL后利用skia繪制出來.demo的核心就在于如何工程化的結(jié)合這些東西。
對(duì)看到這里,相信大家可能已經(jīng)看出了筆者想要做什么,就是一個(gè)及其簡(jiǎn)版的瀏覽器內(nèi)核.
現(xiàn)在chrome 設(shè)計(jì)如下

筆者在這里嘗試使用Quickjs和Skia平臺(tái)嘗試搭建一個(gè)Iot設(shè)備的渲染Demo,其實(shí)就要將V8和Blink替換掉,讓IOT圖形開發(fā)者可以利用類xml這樣強(qiáng)大的dsl語言和現(xiàn)代高級(jí)語言javascript就能高效的在IOT設(shè)備上盡顯才華(反正吹牛不要錢...)
排版引擎的選擇
IOT 圖形設(shè)備的特性
- 主要展示2d為主,
- 交互多為點(diǎn)擊為主
- 顯示面積小,不支持滾動(dòng)(多如,監(jiān)控設(shè)備上的圖形展示)
- 布局簡(jiǎn)單,頁面大的更新少
- 設(shè)備配置低
筆者簡(jiǎn)單總結(jié)了一下iot圖形設(shè)備的特性.
筆者選擇描述界面ui的dsl為vitrual dom,本想著可以借鑒Blink如下的渲染方式
rendertree-> renderobj->renderlayer->paint->composite
(刪了一大段筆者的實(shí)踐過程),本文的絕大數(shù)時(shí)間都花費(fèi)在了這里(斷斷續(xù)續(xù)大半個(gè)月,因?yàn)槭×司筒毁N上了,.....)
但是當(dāng)筆者看了一下相關(guān)文檔并去翻閱了一下Blink的render和paint的模塊的代碼后就順利放棄了如上的打算,被其代碼復(fù)雜性征服了...
在這里 筆者同時(shí)也借鑒了 html2canvas的相關(guān)思路和代碼html2canvas 有兩種模式,一種通過svg的方式.,一種通過純canvas的方式,顯然我們需要關(guān)注的后者的生成方式.
純Canvas
- 遞歸取出目標(biāo)模版的所有DOM節(jié)點(diǎn),填充到一個(gè)
rederList,并附加是否為頂層元素/包含內(nèi)容的容器 等信息- 通過
z-indexpostionfloat等css屬性和元素的層級(jí)信息將rederList排序,計(jì)算出一個(gè)canvas的renderQueue- 遍歷renderQueue,將css樣式轉(zhuǎn)為
setFillStyle可識(shí)別的參數(shù),依據(jù)nodeType調(diào)用相對(duì)應(yīng)canvas方法,如文本則調(diào)用fillText,圖片drawImage,設(shè)置背景色的div調(diào)用fillRect等- 將畫好的canvas填充進(jìn)頁面
看到這里,我們也可以看出來可能html2canvas的作者是利用js實(shí)現(xiàn)blink的相關(guān)算法,例如根據(jù)z-index等css屬性生成renderList列表,和RenderLayer的生成有很多想通之處.
筆者又去調(diào)研了一下Flutter的渲染管線和相關(guān)設(shè)計(jì)概念,非常適合借鑒。flutter的和blink的技術(shù)路線非常相似,且flutter的代碼具有較強(qiáng)的可閱讀性。在技術(shù)調(diào)研的過程中筆者發(fā)現(xiàn)了由FaceBook 推出的YogaLayout 彈性布局庫,非常適合適配我們的需求,雖然只支持彈性布局,但是其生態(tài)完善和健壯性也能夠滿足在iot設(shè)備上布局渲染。
為什么選擇QuickJs
Quickjs最近也是大火,頻頻出現(xiàn)在筆者的閱讀視線中.選擇QuickJs,是因?yàn)镼uickJs本身就是的定位就是一款嵌入式引擎,采用 c語言編寫,沒有太多的外部依賴,這非常適合一些基于微內(nèi)核的IOT操作系統(tǒng).且Quickjs 的性能也不差,Quickjs向比較與V8來說雖然還有很多不足之處,但是在IOT場(chǎng)景下Quickjs的小和快筆者更愿意選擇前者.
Quickjs到底多么好,筆者也就不說了,直接上bellard大佬博客上的基準(zhǔn)測(cè)試結(jié)果.

前期編譯工作(水一下字?jǐn)?shù))
由于Skia是C++ 寫的,QuickJs是C寫的,所以我們需要將它們編譯好,在我們的主項(xiàng)目進(jìn)行鏈接,筆者還是選擇了C++ 作為我們的項(xiàng)目語言.
QuickJs 編譯安裝和生靜態(tài)鏈接庫
Quickjs的編譯安裝非常簡(jiǎn)單,具體可以參考Quickjs的項(xiàng)目主頁.,筆者就不在這里多敘述了.
https://bellard.org/quickjs/quickjs.html#Installation
我們需要編譯QuickJs項(xiàng)目為一個(gè)靜態(tài)鏈接庫,由于筆者的菜雞MakeFile的水平,所以這里我們采用Cmake作為我們的預(yù)構(gòu)建工具. 以下是CmakeList.txt
cmake_minimum_required(VERSION 3.15)
project(quickjs C)
file(STRINGS VERSION version)
set(quickjs_src quickjs.c libbf.c libunicode.c libregexp.c cutils.c quickjs-libc.c)
set(quickjs_def CONFIG_VERSION="${version}" _GNU_SOURCE CONFIG_BIGNUM)
add_compile_definitions(${quickjs_def})
set(CMAKE_C_STANDARD 99)
add_library(quickjs ${quickjs_src})
target_link_libraries(quickjs ${CMAKE_DL_LIBS} m )
編譯后,我們可以得到一個(gè)靜態(tài)鏈接庫文件libquickjs.a
Skia的編譯
Skia使用和V8相同的項(xiàng)目管理工具和編譯系統(tǒng)Ninja.
首先我們需要翻墻拉一下代碼,然后走一下編譯流程
git clone 'https://chromium.googlesource.com/chromium/tools/depot_tools.git'
export PATH="${PWD}/depot_tools:${PATH}"
git clone https://skia.googlesource.com/skia.git
cd skia
python2 tools/git-sync-deps
## 安裝需要的依賴,如果不是知名發(fā)行版,需要簡(jiǎn)單改一下腳本,mac中需要自己安裝相關(guān)依賴。
tools/install_dependencies.sh
bin/gn gen out/Shared --args='is_official_build=true is_component_build=true'
ninja -C out/Shared
PS: 需要注意的是編譯Skia至少需要c++17 ,也就是至少g++7的版本,如果版本過低會(huì)提示語法錯(cuò)誤,安裝好新版本的g++7版本后,由于不知道如何更改編譯器選項(xiàng),可以link 一下c++ 到g++7上去.
結(jié)合Skia 和QuickJS
示例項(xiàng)目是一個(gè)新建的cmake項(xiàng)目,引用Skia和QuickJs的頭文件和靜態(tài)庫以及相關(guān)在macos上的服務(wù)框架等。
cmake_minimum_required(VERSION 3.15)
project(demo)
set(CMAKE_CXX_STANDARD 17)
set(QUICKJSDIR /Users/xxx/sources/quickjs)
set(SKIADIR /Users/xxx/sources/skia)
include_directories(${QUICKJSDIR})
include_directories(${SKIADIR})
include_directories(/usr/local/include)
link_directories(${SKIADIR}/out/Release)
link_directories(${QUICKJSDIR})
link_directories(/usr/local/lib)
find_library(CoreServices CoreServices)
find_library(CoreGraphics CoreGraphics)
find_library(CoreText CoreText)
find_library(CoreFoundation CoreFoundation)
find_library(OpenGL_LIBRARY OpenGL)
link_libraries(quickjs)
link_libraries(skia jpeg icu png webp)
link_libraries(SDL2 SDL2main pthread fontconfig freetype)
set(CMAKE_BUILD_TYPE "Release")
add_executable(demo src/cpp/main.cpp)
target_link_libraries(demo ${OpenGL_LIBRARY} ${CMAKE_DL_LIBS} ${CoreServices} ${CoreGraphics} ${CoreText} ${CoreFoundation} ${COCOA_LIBRARY} m )
工程介紹
自定義JSX 解析
我們的JSX可能長(zhǎng)這樣
<flex height={200}
width={200}
flexDirection={Yoga.FLEX_DIRECTION_ROW}
justifyContent={Yoga.JUSTIFY_SPACE_AROUND}
alignItem={Yoga.ALIGN_FLEX_START}
borderRadiusPercent={10}
>
<flex backGroundColor="000000" borderRadiusPercent={50} height={50} width={50}/>
<flex backGroundColor="000000" borderRadiusPercent={50} height={50} width={50}/>
</flex>
這里主要利用typescript的jsxFactory,自定義轉(zhuǎn)化器來解析并構(gòu)造我們的"Virtual Dom Tree“。
首先我們需要在tsconfig.json中指定
"jsx": "react",
"jsxFactory": "Dtf.createElement",
這樣子typescript在編譯我們的tsx文件時(shí)就會(huì)使用使用jsxFactory 使用類React.createElement 函數(shù)傳參一樣包裹住住我們的VirtualNode。
我們只需要定義好Dtf.createElement 函數(shù)即可。
類似這樣
export default {
createElement<T extends keyof JSX.IntrinsicElements>(type:T,props:JSX.IntrinsicElements[T],...childrens:FlexNode[]):FlexNode{
const node = Node.create();
const flexNode=new FlexNode(node);
flexNode.setWidth(props.width || 0);
flexNode.setHeight(props.height || 0);
flexNode.setJustifyContent(props.justifyContent || Yoga.JUSTIFY_FLEX_START);
flexNode.setFlexDirection(props.flexDirection || Yoga.FLEX_DIRECTION_ROW);
//.......
childrens.forEach((element,index) => {
flexNode.insertChild(element,index);
});
return flexNode
},
FlexNode:FlexNode
}
這里我們并不需要考慮遞歸等問題,因?yàn)閠ypescript會(huì)把jsx node 都包裹進(jìn)來通過此函數(shù)傳遞進(jìn)來,
在這里我們定義FlexNode 同時(shí)把相關(guān)的props和children關(guān)聯(lián)上去
同時(shí)我還需要暴露出全局namespace jsx,方便用戶使用我們的自定義的jsx。
declare namespace JSX{
interface IntrinsicElements {
flex:{
flexDirection?:import("yoga-layout").YogaFlexDirection;
height?: number;
width?: number;
justifyContent?:import("yoga-layout").YogaJustifyContent,
//.....
}
}
}
同時(shí)我們需要像React一樣
import React from "react"
在我們的tsx中文件 引入我們的Dtf
import Dtf from "./Dtf";
最終tsc 預(yù)編譯出來的就會(huì)類似這樣
var node = (Dtf_1.default.createElement("flex", { height: 200, width: 200, flexDirection: yoga_layout_1.default.FLEX_DIRECTION_ROW, justifyContent: yoga_layout_1.default.JUSTIFY_SPACE_AROUND, alignItem: yoga_layout_1.default.ALIGN_FLEX_START, borderRadiusPercent: 10 },
Dtf_1.default.createElement("flex", { backGroundColor: "000000", borderRadiusPercent: 50, height: 50, width: 50 }),
Dtf_1.default.createElement("flex", { backGroundColor: "000000", borderRadiusPercent: 50, height: 50, width: 50 })));
Yoga Layout 引擎
YogaLayout 是一個(gè)彈性布局引擎,擁有非常優(yōu)秀的排版速度和性能,且對(duì)開發(fā)者提供的相關(guān)接口非常簡(jiǎn)單, 我們只需要關(guān)聯(lián)好設(shè)置好渲染節(jié)點(diǎn)的屬性和關(guān)聯(lián)屬性,就可以全局或局部進(jìn)行布局計(jì)算。
FlexNode是我們?cè)赮ogaNode上多封裝一層的node,可以增強(qiáng)對(duì)于渲染節(jié)點(diǎn)的表達(dá)豐富性。
這里為了完成示例DEMO,可以直接獲取根節(jié)點(diǎn)進(jìn)行全局計(jì)算。
node.getNode().calculateLayout(200,200);
這樣子我們就可以獲取到靜態(tài)的布局的內(nèi)容,我們只需要把根節(jié)點(diǎn)的引用傳遞進(jìn)入繪制函數(shù)即可。
編譯JS成字節(jié)碼
我們使用qjsc 將我們的webpack打包文件編譯成字節(jié)碼。
qjsc -c -o dist/out.c dist/out.js
準(zhǔn)備運(yùn)行環(huán)境
我們?cè)?quickjs 中全局變量注入render函數(shù)。
void AddDtfRender(JSContext *ctx) {
JSValue dtf= JS_NewObject(ctx);
JSValue dtfRender =JS_NewCFunction(ctx,js_dtf_render,"render",1);
JS_SetPropertyStr(ctx,dtf,"render",dtfRender);
JS_SetPropertyStr(ctx, JS_GetGlobalObject(ctx), "EngineDtf", dtf);
};
這樣子我們就可以在js中使用EngineDtf.render() 調(diào)用到我們的內(nèi)置的函數(shù)。
構(gòu)建渲染樹
在c++中我們需要把布局的信息構(gòu)建成一個(gè)有序,完整的結(jié)構(gòu)。
為此我們簡(jiǎn)單設(shè)計(jì)了如下的Layout對(duì)象
class Layout {
private:
JSContext *ctx;
JSValue *layout = NULL;
JSValue *realNode = NULL;
Layout *fistChild = NULL;
Layout *next = NULL;
CssStyle *cssStyle = NULL;
BoxContainer *container =NULL;
}
在從render中獲取到根節(jié)點(diǎn)引用后我們就可以使用 getComputeLayout 獲取到一些位置信息。
在經(jīng)歷過遞歸遍歷后我們即可獲取到一個(gè)完整的Layout樹,攜帶了 位置,大小,嵌套關(guān)系和樣式屬性的Layout 樹。
繪制渲染樹
我們采用廣度優(yōu)先的方式進(jìn)行繪制。
if(rootLayout->getNext() != NULL){
paintLayout(rootLayout->getNext(),NULL);
}
if(rootLayout->getFirstChild()!= NULL){
paintLayout(rootLayout->getFirstChild(),NULL);
}
同時(shí)我們這里針對(duì)僅支持的兩個(gè)樣式屬性進(jìn)行一些畫筆上的一些設(shè)置。
CssStyle *cssStyle = rootLayout->getCssStyle();
int borderRadiusPercent =0;
if(cssStyle != NULL){
borderRadiusPercent=cssStyle->getBorderRadiusPercent();
string backgroundColor =cssStyle->getBackGroundColor();
if(checkStr(backgroundColor)){
paint->setStyle(SkPaint::kFill_Style);
paint->setColor( convertHexToColor(backgroundColor));
}
}
_canvas->drawRoundRect(rect, rootLayout->getContainer()->getWidth() *intborderRadiusPercent /100, rootLayout->getContainer()->getHeight() * intborderRadiusPercent / 100, *paint);
效果

總結(jié)和展望
在本文中我們實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的跨端渲染DEMO,淺層次的驗(yàn)證了一些技術(shù)調(diào)研的可行性。
但是更多的是了解到一些渲染問題的復(fù)雜性和性能提升上的問題。本文針對(duì)布局和繪制兩大重要步驟僅僅進(jìn)行了遍歷操作,并未做任何優(yōu)化操作。這些地方都是可以向優(yōu)秀的開源庫進(jìn)行學(xué)習(xí)的。
希望本文可以給各位讀者提供一個(gè)跨端渲染的思路。