動態介面:DSL&佈局引擎

折騰範兒_味精發表於2019-03-01

Jasonette 與 Tangram

很早的時候火了一陣子Jasonette,打出來的宣傳語是用json寫出純native的app(牛皮其實有點大,其實只是寫動態介面,完全不是寫動態App)。

前一陣子,天貓又開源了跨多個平臺的Tangram,一套通用的UI解決方案,仔細閱讀文件我們會發現,他們也是在用json來實現這套七巧板佈局。一套靈活的跨平臺的UI解決方案。

Jasonette的牛皮其實有點大,很多人看到動態用Json寫出純native的app,就很激動,彷彿客戶端也能有H5那樣的能力,但其實他只是focus在解決app中的介面的問題。Tangram的定位就很精準了,是一套為業務出發的通用跨平臺UI解決方案,把佈局渲染效能與多段一致性考慮在框架內的UI框架。這二者有個共同點都是用json來描述介面與內容,從而用native進行呈現,json這種資料是一種天然便於下發與動態更新的資料,因此這些其實都能讓客戶端做到類似H5網頁一樣的趕腳。雖然沒有使用WebView,但他們的設計思路和網頁技術的發展歷史如出一轍,因此@響馬大叔說過"這其實是最純正的網頁技術,雖然他是native的"。

順著這個話題繼續問幾個問題

  • DSL

為什麼Jasonette與Tangram都是用json?

  • 佈局排版

為什麼Jasonette寫出來的json有些屬性看著很像css?padding & align(拿Jasonette舉例)

  • 渲染

Jasonette呼叫UIKit進行渲染,H5用WebView渲染,所以Jasonette就叫native?

從DSL說起

DSL 是 Domain Specific Language 的縮寫,意思就是特定領域下的語言,與DSL對應的就是通用程式語言,比如Java/C/C++這種。換個通俗易懂的說法,DSL是為了解決某些特定場景下的任務而專門設計的語言。

舉幾個很著名的DSL的例子

  • 正規表示式

通過一些規定好的符號和組合規則,通過正規表示式引擎來實現字串的匹配

  • HTML&CSS

雖然寫的是類似XML 或者 .{} 一樣的字元規則,但是最終都會被瀏覽器核心轉變成Dom樹,從而渲染到Webview上

  • SQL

雖然是一些諸如 create select insert 這種單詞後面跟上引數,這樣的語句實現了對資料庫的增刪改查一系列程式工作

計算機領域需要用程式碼解決很多專業問題,往往需要同時具備編碼能力以及專業領域的能力,為了提高工作於生產效率,需要把一些複雜但更偏向專業領域的處理,以一種更簡單,更容易學習的語言或者規範(即DSL),抽象提供給領域專家,交給不懂編碼的領域專家編寫。

然後程式碼程式設計能力者通過讀取解析自己定製出來的這些語言規範,來領會領域專家的意圖,最終轉化成真正的通用程式語言程式碼實現,接入底層程式碼框架,從而實現讓領域專家只需要學習更簡單的DSL,就能影響程式碼程式的最終結果。

(雖然DSL的原始定義是為了非程式設計的專業領域人才使用,但到後來直接交給程式設計師使用,但能大幅度提高程式設計師編寫效率的非通用語言,也被當做是DSL的一種)

DSL在設計上的應用

設計師會設計出很多精美的介面,最後交給程式設計師去用程式碼編寫成漂亮的網頁或者App。每個介面如果純用程式碼編寫,都會面臨大量的程式碼工作量,這裡面可能更多的是一些重複的機械性的程式碼工作。諸如:某個元素設定長寬,設定居中,設定文字,設定距離上個元素XX畫素,N個元素一起縱向,橫向平均排列等等。對於程式碼實現介面開發來說,程式設計師需要編寫一系列諸如:setFrame,setTitle,setColor,addSubview這樣的程式碼,一邊寫這樣的程式碼,一邊查閱設計師給出的各種標註。

為了提高工作效率,如果能把一些設計師產出的長寬色值文字居中距上等設計後設資料(設計的標註資訊等),以一種約定的簡潔的語言規則(即DSL)輸入給程式程式碼,由程式和程式碼自動的分析和處理,從而生成真正的介面開發程式碼setFrame,setTitle,setColor,addSubview,這樣就可以大幅度的減少程式碼量與工作量,程式設計師來寫這種簡潔的語法規則會更快更高效,甚至可以把這種簡潔的語法規則教會設計師,讓設計師有能力直接寫出DSL,然後輸入給底層程式,這樣介面就自然完成。

做iOS客戶端開發的同學有興趣可以用文字編輯器開啟以下XIB檔案,你會看到我們拖來拖去拖線出來的Xib,其實就是XML語法,而Jasonette就是Json語法,他們用XML/JSON這種通用的結構化語法來儲存這些設計資料,用一些自定義的標籤,來標記這些資料的用途,XIB/JSON經過解析後就會生成簽到字典的樹狀結構,因此程式碼就可以進行遍歷執行,從而轉變成最終的UIKit的渲染程式碼。

HTML/CSS,是網頁開發普遍使用的,他們也是一種DSL,你寫出的每一個HTML的DIV以及CSS的屬性樣式,最後都不是通過.html .css檔案渲染到螢幕到瀏覽器上的,都是通過瀏覽器核心最後呼叫OpenGL,C++程式碼渲染上去的。從這個層面講,Jasonette客戶端框架用的json,native客戶端開發的XIB,與網頁瀏覽器的HTML/CSS是一回事。

XIB的XML程式碼其實也一般不會交給設計師去學習和掌握,但是在XIB的XML基礎之上,製作出一個InterFaceBuilder,可以讓設計師用圖形介面拖來拖去,這種UI編輯器式的設計其實都離不開DSL,都是規劃出了一種比通用程式碼語言更簡潔的DSL後,再輔助開發介面編輯器生成這種簡潔DSL來實現的。

Jasonette裡的json如何工作

扯淡了這麼多,親自看看Jasonette原始碼是如何執行DSL的。

{
  "$jason": {
    "head": {
      "title": "{ ˃̵̑ᴥ˂̵̑}",
      "actions": {
        "$foreground": {
          "type": "$reload"
        },
        "$pull": {
          "type": "$reload"
        }
      }
    },
    "body": {
      "header": {
        "style": {
          "background": "#ffffff"
        }
      },
      "style": {
        "background": "#ffffff",
        "border": "none"
      },
      "sections": [
        {
          "items": [
            {
              "type": "vertical",
              "style": {
                "padding": "30",
                "spacing": "20",
                "align": "center"
              },
              "components": [
                {
                  "type": "label",
                  "text": "It's ALIVE!",
                  "style": {
                    "align": "center",
                    "font": "Courier-Bold",
                    "size": "18"
                  }
                },
                {
                  ......省略
                }
              ]
            },{
                ......省略
            },
            {
              "type": "label",
              "style": {
                "align": "right",
                "padding": "10",
                "color": "#000000",
                "font": "HelveticaNeue",
                "size": "12"
              },
              "text": "Watch the tutorial video",
              "href": {
                "url": "https://www.youtube.com/watch?v=hfevBAAfCMQ",
                "view": "Web"
              }
            }
          ]
        }
      ]
    }
  }
}複製程式碼

這是demo裡的頁面json程式碼,你會看到很多很像網頁開發的東西,head,body,padding,align等等,是不是覺得和CSS很像

這個demo寫的json檔案其實就是一個helloworld介面,裡面有一些按鈕,點選可以跳轉,還有一些圖片,我先簡單介紹一下Jasonette DemoApp的啟動流程

  • Application didFinishLaunchingWithOptions

程式初始化,觸發[[Jason client] start:nil],初始化Jason,在這個start裡面,會建立JasonViewController,並且給這個VC設定rootUrl,設定這個VC作為Window的Key,從而進行App展現

  • VC viewWillAppear

這個KeyVC,當viewWillAppear的時候,觸發[[Jason client] attach:self],這個函式內會呼叫[self reload]來進行網路資料拉取,剛剛說的rootUrl其實是一個json網路檔案(也可以設定成bundle內檔案),換句話說這個vc的json檔案可以每次從網路上拉取最新的json檔案來實現動態更新的(跟網頁實際上是一樣的),這個過程就是觸發網路框架AF去拉取最新的json

  • AFNetworking download

在網路資料拉取回來後,會經過一系列的處理,包括請求非同步的其他相關json(像不像非同步請求其他css),把請求到的json字典經過JasonParser這個類的一些其他處理最後生成最終的Dom字典(Dom這個詞寫在Jason drawViewFromJason的原始碼裡,原始碼就將這個資料字典的變數起名叫dom,可見他做的和網頁工作原理是一個思路)

  • Jason drawViewFromJason 進行主執行緒渲染

找到Jason類的drawViewFromJason:函式,這才是我們DSL之所以能渲染成介面的最重要的一步,前面都是一直在下載DSL,處理DSL,結果就是json生成了最終需要的後設資料字典--Dom字典,這一步就是將DSL轉變成App介面

Dom字典生成介面的過程

簡單的看看這個流程都分別依次呼叫了哪些函式,不一一講解了,最後我們挑最有代表的進行說明。

  • [Jason drawViewFromJason:DomDic]

  • [JasonViewController reload:DomDic]

  • Set Stylesheet //CSS

  • [JasonViewController setupSections:DomDic]

  • [JasonViewController setupLayers:DomDic]

setupSections與setupLayers基本上涵蓋了頁面主元素的所有渲染方式

先以簡單的setupLayers的程式碼邏輯舉例,先按著約定的標籤從Dom字典中有目的的讀取需要的資料欄位Layers,迴圈遍歷Layers欄位陣列下的所有資料,每一次都先判斷子節點的Type屬性,如果Type寫了Image,就會建立UIImageView,如果Type寫了Label,就會建立UILabel,根據子節點其他屬性一一設定不同的UIView的屬性,最後AddSubview到介面上。(我會略過大量實際程式碼,以虛擬碼形式進行說明,實際程式碼可以看原始碼檢視)

NSArray *layer_items = body[@"layers"];
NSMutableArray *layers = [[NSMutableArray alloc] init];
//迴圈遍歷Dom樹下的layer欄位
if(layer_items && layer_items.count > 0){
    for(int i = 0 ; i < layer_items.count ; i++){
        NSDictionary *layer = layer_items[i];
        layer = [self applyStylesheet:layer];
        //設定Css

        //判斷type欄位是否為image,是否有image url
        if(layer[@"type"] && [layer[@"type"] isEqualToString:@"image"] && layer[@"url"]){

            //NEW一個UIImageView

            //設定UIImageView的style

            //設定UIImageView的 image URL

            //將UIImageView Add subview

            //非同步拉取圖片回來後,通過style,運算UIImageView的frame


        } 
        //判斷type欄位是否為label,是否有text
        else if(layer[@"type"] && [layer[@"type"] isEqualToString:@"label"] && layer[@"text"]){

            //NEW一個TTTAttributedLabel

            //設定TTTAttributedLabel的style

            //設定文字

            //addSubview
        }
    }
}複製程式碼

再說說setupSections,他其實充分利用了tableview的能力,首先將Dom字典下的sections欄位進行儲存與整理,然後並不立刻進行渲染,而是直接呼叫[UITableview reloadData],觸發heightForRowAtIndexPathcellForRowAtIndexPath。(我會略過大量實際程式碼,以虛擬碼形式進行說明,實際程式碼可以看原始碼檢視)

heightForRowAtIndexPath獲取cell高度

//取出indexPath.section對應的dom節點資料
NSArray *rows = [[self.sections objectAtIndex:indexPath.section] valueForKey:@"items"];
//取出indexPath.row對應的dom節點資料
NSDictionary *item = [rows objectAtIndex:indexPath.row];

//取出樣式屬性
item = [JasonComponentFactory applyStylesheet:item];
NSDictionary *style = item[@"style"];

//通過JasonHelper傳入style[@"height"]樣式屬性計算寬高
//一些樣式演算法算出
return [JasonHelper pixelsInDirection:@"vertical" fromExpression:style[@"height"]];複製程式碼

cellForRowAtIndexPath獲取cell

NSDictionary *s = [self.sections objectAtIndex:indexPath.section];
NSArray *rows = s[@"items"];
//獲取對應的Dom節點資料
iNSDictionary *item = [rows objectAtIndex:indexPath.row];
//渲染豎著滑的CELL
//只支援SWTableViewCell這一種客戶端預先寫好的這種通用cell
//支援Dom節點迴圈內嵌stackview,按著內嵌形式,橫豎佈局都支援
//stackview內的子元素通過JasonComponentFactory建立對應的UIKit UIView
//建立方式如同layer,判斷type等於'image'建立UIImageView,判斷等於'text'建立UILabel
//frame通過style等欄位,進行系統autolayout計算
return [self getVerticalSectionItem:item forTableView:tableView atIndexPath:indexPath];複製程式碼

上面講的其實力度很粗,並且很多程式碼沒有詳細展開,其實目的是讓大家發現,Jasonette的原始碼持續在幹一件事情:

  • 從Dom字典中,讀取約定好的固定欄位

  • 迴圈遍歷Dom字典,遍歷所有設計資料

  • 然後用字串匹配去判斷每個節點的key與值,指引OC程式碼應該怎麼呼叫

    • 匹配出label就建立UILabel

    • 匹配出iamge就建立UIImageView

    • 匹配出style就呼叫autolayout賦值屬性進行autolayout計算,或者進行自行演算法計算。

Jasonette的DSL工作特點就是這樣,先從設計師給出的後設資料入手,把所有的後設資料抽象抽離出來,約定成固定的標籤與值,然後客戶端一一遍歷整個Dom後設資料的節點,一一解讀這些標籤與值,走入對應的客戶端程式碼,從而呼叫對應的客戶端程式碼功能。

客戶端的這套框架寫完之後,以後在寫全新的介面,其實是無需再重複寫一套客戶端程式碼,而是直接寫全新的DSL也就是Jasonette的json檔案就可以了。

DSL小結

拿JSON/HTML/CSS舉例子其實,這些都是一種外部DSL,與之對應的還有某些語言支援的內部DSL,這裡也就不展開了

Never's Blog 外部DSL的實現

XML DSL
很多常見的XML配置檔案實際上就是DSL,但不是所有的配置檔案都是DSL。比如“屬性列表”和DSL是不同的,那只是一份簡單的“鍵-值對”列表,可能再加上分類。
XML不是程式語言,是一種沒有語義的語法結構。XML是DSL的承載語法,但是它又引入了太多語法噪音—太多的尖括號、引號和斜線,每個巢狀元素都必須有開始標籤和結束標籤。
自定義的外部DSL也帶來了一個煩惱:它們處理引用、字元轉義之類事情的方式總是難以統一。

所以DSL叫特殊領域語言,離開了為某一DSL專門開發的語言環境或者程式碼框架,DSL是無法執行的,沒有效果的,沒有正則表達引擎的原始碼,你寫出來的正規表示式沒人認識。沒有底層資料庫框架,sql語句就只是一行字串,沒法進行資料管理。沒有Jasonette這個框架,你寫出來json也不可能生成介面,有了Jasonette這個框架,你不按著約定的標籤寫,自己單純的在json裡憑空建立標籤,也是不可能正確生成你想要的東西。

在最後,筆者想說的是,當我們在某一個領域經常需要解決重複性問題時,可以考慮實現一個 DSL 專門用來解決這些類似的問題。

from 談談 DSL 以及 DSL 的應用

佈局與排版

既然說到動態介面,那一定得聊螢幕適配,這其實不管是不是動態介面,不管用不用到DSL,做客戶端都要考慮的一點,其實網頁在這方面發展的更完善,畢竟客戶端的螢幕尺寸就那麼幾種,就算安卓碎片化,也比不上PC電腦上,桌面瀏覽器使用者可以任意伸縮視窗的大小,因此對於在不同尺寸的限定螢幕大小(即排版區域)內,把設計出來的元素以最美觀的形式進行展現,這就是佈局與排版。

剛才在講解DSL,講解Jasonette的時候其實迴避一些問題,我們光提到了通過Dom的type資訊,來建立不同的UIView,但是每一個UIView應該擺放在螢幕的什麼位置,在哪進行展現,在上面的文章中被一帶而過,有的描述,讀取style欄位後分別賦值給對應的autolayout,有的被我說成了進行一定的演算法從而算出高度。這背後其實都是佈局與排版的演算法。

做iOS客戶端的同學很多會有感觸,早些年的時候寫絕對座標,那時候iOS的螢幕尺寸還不是太多,用程式碼手寫frame進行元素定位,試想一下如果純用frame進行app開發,那麼去開發一套對應的DSL動態介面其實更容易,我們只需要給每個字典節點,規定上{x:N,y:N,w:N,h:N}的屬性,然後在框架裡別的跟佈局相關的style都不需要寫了,只需要用xywh生成CGRect,然後呼叫setFrame就好了,想開發出這樣一種絕對佈局的動態介面框架其實還真是挺簡單的。

到後來有了iPad,有了iPhone5,有了iPhone6,6Plus,iOS的螢幕尺寸變的碎片化,如果繼續使用frame,客戶端同學開發工作量會變的異常繁瑣,於是在IOS7引入了蘋果的autolayout,引入了VFL語言Visual Format Language。其實VFL也應該算是一種DSL吧,他不是用來繪製出一個個的介面元素,而是用來在繪製前,計算清楚每一個元素在動態的螢幕區域下的最終位置。我們學會了如何寫VFL,或者說我們學會了如何用masnory這個框架實現autolayout,但我們並不需要深入去了解這裡面的排版佈局演算法。

需要記住的一點是,最終渲染一定是通過frame去頁面上進行繪製,有了明確的座標才能繪製出UI,手寫frame式的絕對佈局程式碼,直接由程式設計師指定,因此一定是效能開銷最小的,可以說沒有或者少量的佈局運算開銷直接進行渲染,但在多屏適配的需求下才引入了一整套龐大的佈局演算法體系(不一定非得是蘋果的autolayout),引入龐大布局演算法的目的是希望根據可排區域動態的計算frame,但並不代表採用自動佈局,就與frame無關,自動佈局演算法只是間接的運算出frame再渲染。

佈局排版的流程圖

  • RenderTree parse
    • 瀏覽器核心的方案是
      • 解析HTML,生成Dom
      • 解析CSS,生成style rules
      • attach Render Tree CSS與HTML掛載到一起
    • Jasonette的方案是
      • 反序列化Json,直接生成Dom字典
  • RenderTree layout
    • 從RenderTree RootNode 遍歷
    • 不同節點對應呼叫不同layout演算法
    • 運算出每個可顯示介面元素的位置資訊
  • RenderTree render
    • 遍歷Tree
    • 渲染

佈局排版資訊解析

  • 瀏覽器解析HTML/CSS 生成RenderTree

將HTML檔案以字串的形式輸入,經過解析,生成了Dom樹,Dom樹就好比是iOS開發裡面頁面View的層級樹,但是每個View/div裡面並沒有css資訊,只寫了每個div所對應的css的名字

將CSS檔案以字串形式輸入,經過解析,得到了一系列不同名字的style rules,樣式規則

Dom樹上的div並不包含樣式資訊,而是隻記錄了樣式的名字,然後從style rules裡找到對應名字的具體樣式資訊,Attech到一起,生成了Render Tree渲染樹,此時的渲染樹只是Dom與CSS的合併,他依然不包含真正可以用於渲染的位置資訊,因為他還沒經過佈局排版。

  • Jasonette直接生成RenderTree

網頁將View的層級,與View的樣式進行了分離,View就是HTML,樣式是CSS,但是Jasonette的Json沒有做這樣的分離,Json直接描述的就是view與style歸併到一起的資料,因此在Jasonette經過了parse解析後直接就拿到了樣式與檢視的合體結構資訊。我們在Json裡明顯可以看到head,footer,layers,sections這種欄位其實就是HTML裡面的類似Dom的物件,而style這個欄位其實就是CSS裡面的物件。Jasonette的原始碼裡直把這個字典起名叫 NSDictionary * dom,其實就可以感知到,雖然Jasonette使用的是json,但是他的思路跟瀏覽器核心是一樣的。

  • iOS autolayout的操作過程

當你使用程式碼執行addsubview的操作的時候,你其實就是在對一個view(一種節點),新增了一個子view(一個子節點),當所有subview新增完成的時候,你已經建立好了一個介面層級樹,你addSubview一個子view以後,會對這個view要麼設定VFL,要麼使用masnory,總之會對這個view設定樣式屬性(其實就是在用oc程式碼,attach css),之後在layoutIfNeeded的時候,autolayout開始自己悶頭計算排版

換句話說iOS autolayout與HTML/CSS在解析上的區別是,iOS的佈局是用程式碼寫死的,生成一種介面層級樹形結構,而網頁HTML/CSS是用可隨意下發的字串,進行解析,從而生成了一種介面層級樹形結構(RenderTree)

佈局排版

無論使用的是網頁,還是Jasonette,還是iOS autolayout,當我們拿到沒有經過排版的Render Tree的時候,雖然裡面的節點包含著樣式資訊,但是並沒有具體的繪製位置資訊,因此需要從Tree的根節點開始依次遍歷每個節點,每個節點都根據自己的樣式資訊以及子節點的樣式資訊進行排版演算法計算。

在排版引擎的設計模式裡(一種設計概念,不是指具體某個排版原始碼實現),一個RenderTree上每一個節點是一種RenderNode,他可能是不同的介面元素,甚至是介面容器,每個RenderNode都可以有自己的layout()方法用於計算自己和自己的子節點的演算法,一個position絕對佈局的節點,他及內部的子節點佈局演算法layout(),肯定與一個listview,tableview那種有規律的排布容器節點佈局演算法layout()不一樣,從根節點rootNode開始,迴圈遍歷遞迴下去,直到把Tree上的所有節點的位置資訊都執行了layout(),就完成了佈局排版。

我們知道不同的節點,是可以用不同的演算法進行他與內部子節點的佈局計算的。

拿iOS開發舉例子,我們完全可以同一個頁面內,有的view是用frame方式寫死的絕對佈局,有的view是用masnory進行的autolayout,甚至父view是寫死的絕對佈局,子view是autolayou,或者反過來。

拿瀏覽器CSS來說,瀏覽器核心C++程式碼裡一個RenderObject的基本子類之一就是RenderBox,該類表示遵從CSS盒子模型的物件,每一個盒子有四條邊界:外邊距邊界 margin edge, 邊框邊界 border edge, 內邊距邊界 padding edge 與內容邊界 content edge。這四層邊界,形成一層層的盒子包裹起來。這種基礎RenderBox有著自己的layout()演算法。而在新的CSS裡引入了更多不同的佈局方式,比如運用非常廣泛的Flexbox彈性盒子佈局,Grid佈局,多列布局等等。

動態介面:DSL&佈局引擎

在排版引擎的設計模式裡,如果你想引入一種新的佈局演算法,或者一種新的專屬佈局效果鎖對應的佈局計算,你只需要建立一種新的RenderNode,並且實現這種node的layout()函式,你就可以為你的排版引擎,持續擴充套件支援更多的排版能力了

Jasonette是怎麼做的?

jasonette其實根本沒自己實現佈局演算法,也沒有抽象出renderNode這種樹狀結構,他直接用原始的Dom字典直接開始遍歷遞迴。

遍歷到layers節點,就呼叫[JasonLayer setupLayers]函式,內部是自己寫的一套xywh的演算法,有那麼點像CSS盒子模型,但簡單的多。

遍歷到sections節點,就呼叫[JasonViewController setupSections]函式,走系統的tableview的reload佈局,在heightforrow的時候,用自己的一套演算法計算高度,而在cellforrow的時候,他使用系統stackview與系統autolayoutAPI進行設定,最後走系統autolayout佈局

Jasonette的佈局過程看起來很山寨,從設計上把Dom字典直接快速遍歷,識別標籤,用if else直接對接到不同的iOS程式碼裡,有的佈局程式碼是一些簡單盒子運算,有的佈局程式碼則是直接接入系統autolayout,可以看出來他從DSL的角度,多快好省的快速實現了一個介面DSL框架,但從程式碼架構設計的角度上,他距離完善龐大的排版引擎,從模組抽象以及功能擴充套件上,還欠缺不少。

佈局排版的幾種演算法

  • 絕對佈局

這就不說了,固定精確的座標,其實不需要計算了

  • iOS autolayout

從 Auto Layout 的佈局演算法談效能

Auto Layout 的原理就是對線性方程組或者不等式的求解。

這篇文章寫得非常非常清楚,我就不詳細展開了,簡單的說一下就是,iOS會把父view,子view之間的座標關係,樣式資訊,轉化成一個N元一次方程組,子view越多,方程組的元數越多,方程組求解起來越耗時,因此運算效能也會越來越底下,這一點iOS 的Auto Layout其實被廣泛吐槽,廣受詬病。

  • CSS BOX 盒子模型

傳統的CSS盒子模型佈局,這個前端開發應該是基本功級別的東西,可以自行查閱

  • FlexBox 彈性盒子

CSS3被引入的更好更快更強的強力佈局演算法FlexBox,因為其優秀的演算法效率,不僅僅在瀏覽器標準協議裡,被廣泛運用,在natie的hyrbid技術方面,甚至純native技術裡也被廣泛運用。

Facebook的ASDK也用的是Flexbox,一套純iOS的完全與系統UIKit不同的佈局方式

大前端Hybrid技術棧裡,RN與Weex中都用的是FlexBox,阿里的另外一套LuaView用Lua寫熱更新app的方案也用的是FlexBox演算法

由FlexBox演算法強力驅動的Weex佈局引擎

  • Grid 佈局

網格佈局(CSS Grid Layout)淺談

CSS佈局模組

Grid佈局被正式的納入了CSS3中的佈局模組,但似乎目前瀏覽器支援情況不佳,看起來從設計上補全了Flexbox的一些痛點。

  • 多列布局

CSS佈局模組

CSS3的新佈局方式,效果就好像看報刊雜誌那樣的分欄的效果。

渲染

經過了整個排版過程之後,renderTree上已經明確知道了每個節點/每個介面元素具體的位置資訊,剩下的就是按著這個資訊渲染到螢幕上。

  • Jasonette

Jasonette直接呼叫的addSubview來進行view的繪製,Dom字典遍歷完了,view就已經被add到目標的rootview裡面去了,渲染機制和正常客戶端開發沒區別,完全交給系統在適當的時候進行螢幕渲染。

  • ReactNative & Weex

ReactNative iOS原始碼解析(二)

Weex 是如何在 iOS 客戶端上跑起來的

這兩個Link其實介紹了,RN與Weex也是通過addSubview的方式,呼叫原生native進行渲染,在iOS上來說就是addSubview

  • WebKit

在繪製階段,瀏覽器核心並不會直接使用RenderTree進行繪製,還會進一步將renderTree處理成LayerTree,遍歷這個LayerTree,將內容顯示在螢幕上。

瀏覽器本身並不能直接改變螢幕的畫素輸出,它需要通過系統本身的 GUI Toolkit。所以,一般來說瀏覽器會將一個要顯示的網頁包裝成一個 UI 元件,通常叫做 WebView,然後通過將 WebView 放置於應用的 UI 介面上,從而將網頁顯示在螢幕上。但具體瀏覽器核心內部的渲染機制是怎麼工作的有什麼弊端,還取決於各個瀏覽器的底層實現。

How Rendering Work (in WebKit and Blink)

從這裡面可以詳細看出來,瀏覽器核心的渲染其實是可以做到下面這些多種功能的,但不同平臺,不懂瀏覽器核心的支援能力不同,不是所有的WebView或者瀏覽器App都是同樣的效能與效果

  • 直接呼叫平臺的系統GUI API
  • 設計自己的高效的Webview圖形快取
  • 設計多執行緒渲染架構
  • 融入硬體加速
  • 圖層合成加速
  • WebGL網頁渲染

仔細想想,真正到渲染這一步,你需要做的都是操作CPU和GPU去計算圖形,然後提交給顯示器進行逐幀繪製,webview與native其實殊途同歸。

native介面?動態? 我們其實一直在聊的是瀏覽器核心技術

@響馬大叔說過"這其實是最純正的網頁技術,雖然他是native的"。

本文從Jasonette出發,從這個號稱純native,又動態,又用json寫app的技術上入手,看看這native+動態的巨大吸引力到底有多神奇,挖下來看一看。

我們看到了和瀏覽器核心一脈相承的技術方案

  • 通過DSL,下發設計後設資料資訊

  • 構建Dom樹

  • 遍歷Dom樹,排版(計算演算法與接入autolayout)

  • 遍歷Dom樹,渲染(addsubview接入系統渲染)

瀏覽器核心

動態介面:DSL&佈局引擎

Webkit瀏覽器核心就是按著這樣的結構分為2部分

  • WebCore

綠色虛線部分是WebCore,HTML/CSS都是以String的形式輸入,經過了parse,attach,layout,display,最終呼叫底層渲染api進行展現

  • JSCore(本文之前一直沒提)

紅色部分是JSCore,JS以string的形式輸入,輸入JS虛擬機器,形成JS上下文,將Dom的一些事件繫結到js上,將操作Dom的api繫結到js上,將一些瀏覽器底層native API繫結到js上

動態介面,其實就是瀏覽器核心的WebCore

整個WebCore不是一個虛擬機器,他裡面都是C++程式碼,因此HTML/CSS在執行效率上,從原理上講和native是一回事,沒區別。

而我們今天提到的動態介面,無論是Jasonette還是Tangram,甚至把xib或者storyboard動態下發後動態展示,用iOS系統API就完全可以做到動態介面(滴滴的DynamicCocoa裡面提到把xib當做資源動態下發與裝載),其實都和瀏覽器內涵的WebCore部分是一個思路與設計,沒錯。

  • Jasonette的設計思路和HTML/CSS是一回事
  • iOS的xib/storyboard的設計思路和HTML/CSS是一回事

動態介面,可以介面熱更新,但不是app功能熱更新

本文從開頭到現在,重點圍繞著WebCore的設計思路,講了N多,但是看到Webkit結構圖的時候,你會發現,有個東西我始終沒提到過--JSCore,但我在開頭提到了一句話

Jasonette牛皮其實有點大,其實只是寫動態介面,完全不是寫動態App

介面動態這個詞與App功能動態有什麼區別呢?

一個APP不僅僅需要有漂亮的介面,還需要有業務處理的邏輯。

  • 一個按鈕點選後怎麼響應?
    • 是否要執行一些業務邏輯,處理一些資料,然後返回來重新整理介面?
    • 是否要儲存一些資料到本地儲存?
    • 是否要向伺服器發起請求?
  • 伺服器請求回來後怎麼做?
    • 是否重新整理資料和介面?
    • 發現伺服器介面請求錯誤,客戶端做業務處理?

Jasonette號稱是用json開發native app,但是json只是一種DSL,DSL是不具備命令和運算的能力的,DSL被譽為一種宣告式程式設計,但這些業務邏輯運算,DSL這種領域專用語言是不可能滿足的,他需要的是通用程式語言。

因此Jasonette對點選事件的處理,其實就是一種路由,json的物件裡面有個標籤約定為action,action的值是一個url字串,url指向另一個介面的json檔案,也就是說,DSL可以把這個view的點選事件寫死,一旦發生點選,固定會跳轉到url所指向全新的json頁面,換句話說,這就是網頁開發的url跳轉href欄位。

換個說法你就理解了,Jasonette在技術上相當於用iOS的native程式碼,仿寫了一個處於刀耕火種的原始時代的瀏覽器核心思路,一個還沒有誕生js技術,只是純HTML的超文字連結的上個世紀的瀏覽器技術。那個時候網頁裡每一個超連結,點進去都是一個新的網頁。

所以這不叫App功能動態,充其量只是介面動態

JSCore的引入給瀏覽器核心注入了動態執行邏輯指令碼程式碼的能力,先不說指令碼引擎執行起來效率不如native,但指令碼引擎至少是一個通用程式語言,通用程式語言就有能力執行動態的通用程式碼(JS/LUA等),通用程式碼比DSL有更強大的邏輯與運算能力,因此可以更加靈活的擴充套件,甚至還可以將指令碼語言對接native,這就是webkit架構圖裡提到的jsbinding。

將指令碼語言對接到本地localstorage,js就有了本地儲存能力,將指令碼語言對接到network,js就有了網路的能力,將指令碼語言對接上dom api,js就有了修改WebCore Dom樹,從而實現業務邏輯二次改變介面的能力。

因此ReactNative & Weex 可以算作App功能動態,他們不僅僅巨有WebCore的能力,同時還巨有JSCore的能力(這裡面其實有個區別,瀏覽器核心的WebCore是純native環境C++程式碼,不依賴js虛擬機器,但RN與Weex負責實現WebCore能力的程式碼,都是js程式碼,是執行在虛擬機器環境之下的,但他們的渲染部分是bridge到native呼叫的系統原生api,有興趣看我寫的RN原始碼詳解吧,ReactNative iOS原始碼解析(一)ReactNative iOS原始碼解析(二)

阿里的LuaView我沒細看過原始碼,但其實內部機制和RN&WEEX沒啥區別,用的是FlexBox排版,但是選用的是Lua Engine這個指令碼引擎,而非JSCore

在native動態化的道路上,不論大家走哪條路,有一個共識還是大家都找到了的,那就是看齊Web的幾個標準。因為Web的技術體系在UI的描述能力以及靈活度上確實設計得很優秀的,而且相關的開發人員也好招。所以,如果說混合開發指的是Native裡執行一個Web標準,來看齊Runtime來寫GUI,並橋接一部分Native能力給這個Runtime來呼叫的話,那麼它應該是一個永恆的潮流。

from “站在10年研發路上,眺望前端未來”:讀後感

雖然有點扯遠了,但是這句話確實又回到響馬叔的那個思路,Jasonette用json寫出native app,他的思路依然是web思路,RN用js寫出native app,也不能改變他一整套web技術的基因。一個介面最終渲染是以native系統級Api實現,並不能說明什麼,渲染只是龐大Web核心技術的末端模組,把末端渲染模組換成native的,其實說明不了什麼。

webview效能真的比native慢很多麼?

這裡就要強調一下了,瀏覽器核心在介面這塊是純C++實現,沒有使用任何虛擬機器,所謂的瀏覽器核心下的Dom環境是純C++環境,也就是純native環境,所以瀏覽器在單次渲染效能上,不見得比native慢。

CSS的佈局排版相容很多種佈局演算法,有些演算法在保證前端開發人員以高素質高質量的開發前提下,同樣的介面,其效能是完全可能碾壓autolayout的,所以單說佈局這塊,webview也不見得慢。

webview的渲染的時候還存在很多非同步資源載入,但這個問題是動態能力帶來的代價,啥都遠端實時拉最新的資源當然會這樣,如果在App下以hybrid的形式,內建本地靜態資源,通過延遲更新本地資源快取的方式,設計hybrid底層app框架,那麼這種開銷也能減少。更何況瀏覽器新技術PWA也好SW也好都從瀏覽器層面深度優化了WAP APP的資源與快取。

webview效能慢的原因很多,多方面綜合來看確實很容寫出效能不佳的頁面,但話也不能絕對了,web技術所帶來的靈活多變,是會給業務帶來巨大收益的。在需要靈活多變,快速響應,敏捷迭代的業務場景下,web技術(泛指這類用web的思路做出來的範hybrid技術)所帶來的優勢也是巨大的

動態介面沒那麼神祕,意義並不在技術實現

Jasonette寫了這麼多,雖然沒有深度剖析每一行原始碼,但把他的實現思路講解了一下,其實自己實現一個動態介面也不是不可以。

我們的工作業務需要深度處理文字,我們也有一套跨平臺的C++排版引擎核心,思路是一脈相承的,區別是文字排版會比介面區塊盒子排版更復雜的多,用的也是json當做DSL,但是我們利用我們的文字排版引擎,去實現相對簡單的各種在native系統上的什麼圖片環繞,圖文繞排,瀑布流介面UI等,其實非常的容易,甚至還是跨平臺的(比native程式碼實現要容易的多)。就連Jasonette程式碼裡也就只支援section(tableview佈局)和layer(盒子模型)2中常見形式,複雜頁面一樣實現不了。

但是!但是!但是!

DSL是領域專業語言,DSL就註定巨有著侷限性,你為自己的排版引擎設計出一套DSL規則,就算都使用的是json,那又如何,新來的一個人能很快上手寫出複雜頁面?DSL的規則越龐大,引擎支援的能力越強,越代表著DSL的學習成本直線加大。

HTML/CSS已經發展成為一個國際標準,甚至是一種被廣泛傳播和學習的DSL,因此他有著很多技術資料,技術社群,方便這門語言的發展,並且隨著應用越廣,語言層面的抽象越來越合理,擴充套件能力也越來越強。

但是你自己設計出來的DSL能走多遠?能應用多遠?

  • 學習成本大,哪怕只是在自己業務內,也很大的,需要有效的建立文件說明,維護業務迭代帶來的功能變化,還要給新來的同事培訓如何寫這種DSL。

  • 應用範圍小,想應付自己一個業務,可能初步設計出來的介面和功能就滿足需求了,但也只能自己使用,如果想推廣,必然會帶來更大的維護成本,需要更加精細化合理化的API設計,擴充套件性設計

  • 人員的遷移成本大,DSL的特點是會讓寫DSL的人員遮蔽對底層程式碼的理解,甚至一些初中是為了能給一些不會編碼的專業領域人員學習和運用,如果程式設計的人員長時間寫這種專有的DSL,遷移到別的公司以後,該公司不用這種DSL,那麼這些技能就徹底廢掉,如果開發者自身不保持一些對底層原始碼的自行探索,那麼換工作將會帶來很大的損失

前端人員在寫各種HTML/CSS的時候,想要深刻理解透其中的作用機制,也是需要深入到瀏覽器核心層面去了解內部機制的

所以我覺得天貓的Tangram,是很值得尊敬的,因為想做出一個動態介面框架,沒那麼難,想做大,做到通用性,擴充套件性,做到推廣,做到持續維護,是非常艱難的,真的很贊!

參考文獻

談談 DSL 以及 DSL 的應用(以 CocoaPods 為例)

DSL(五)-內部DSL vs 外部DSL (N篇系列文章)

由FlexBox演算法強力驅動的Weex佈局引擎

從 Auto Layout 的佈局演算法談效能

CSS佈局模組

走進Webkit

WebCore中的渲染機制(一):基礎知識

理解WebKit和Chromium: WebKit佈局 (Layout)

瀏覽器渲染原理簡介

ReactNative iOS原始碼解析(二)

Weex 是如何在 iOS 客戶端上跑起來的

How Rendering Work (in WebKit and Blink)

“站在10年研發路上,眺望前端未來”:讀後感

ReactNative iOS原始碼解析(一)

ReactNative iOS原始碼解析(二)

相關文章