一個(gè)跨端渲染示例和思路

[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ì)如下

image

筆者在這里嘗試使用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-index postion float等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é)果.

基準(zhǔn)測(cè)試

前期編譯工作(水一下字?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);

效果

image.png

總結(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è)跨端渲染的思路。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容