Fair下發產物-佈局DSL生成原理

發表於2023-09-22

一、概述

大家都知道,Flutter在release環境是以AOT模式執行的,這就決定了我們要做動態化的話無法簡單的透過動態下發dart程式碼執行的。根據Fair團隊的前期調研,我們對佈局動態化和邏輯動態化的實現採用了兩套不同的實現方案,對於佈區域性分,我們在解析dart原始檔之後生成DSL產物下發,然後在端上解析DSL構建佈局的方式,邏輯動態化的部分,我們採用的是dart原始碼轉js下發的方式。

整個動態化流程大致如下:

image.png

二、整體流程概述

詳述具體流程之前,我們先來看看整體的流程,然後再去講解各個流程的原理細節。

image.png

整個流程大致分為兩部分:

  • 透過fair_ast_gen將原始碼解析並生成AstMap;
  • 透過fair_dsl_gen將AstMap轉換成我們需要的Fair DSL。

這裡涉及到兩個概念,大家需要先了解一下

  • AST 全稱是Abstract Syntax Tree,中文名為抽象語法樹。
  • DSL 全稱是Domain Specific Language,中文名為領域特定語言。

三、AST解析

3.1 原始碼解析

要把dart原始碼轉換成我們需要的DSL,首先要對dart原始碼進行抽象語法分析,這裡是整個轉換過程的第一個關鍵點,甚至可以說是整個DSL生成的基礎。好在dart 官方提供瞭解析工具包analyzer,這為我們整個的dsl生成工作大大減輕了工作量。

analyzer包的utilities類提供了parseFile函式,這個函式返回的CompilationUnit,實際上是一個編譯單元,繼承自AstNode,正是我們後面AST解析的入口。

3.2 AST解析

前面我們提到,AST是一種與程式語言無關的抽象語法樹,對於這塊的概念不是太熟同學,我們還是先看一個小例子。比如下面這段程式碼:

image.png

那如果轉換成AST的話,差不多是下面這樣的:

image.png

實際轉換產物很長,我們只擷取一部分,不過可以大致看出AST的整個結構,可以看出,整個AST實際上是對原始碼的一個樹狀結構描述,整個結構裡包含很多節點物件,每個節點下面又包含各種樹形和子節點。

我們將100+的語法節點分類抽象為識別符號、字面量、表示式、語法塊,其它五大類,30+種的常用節點,同時剝離了與Fair產物解析無關的資訊,只保留原始node中的關鍵資訊,使得節點解析更加清晰。

image.png

前面我們說到analyzer的praseFile方法解析完dart原始檔後返回了AstNode例項,我們對AstNode的分析主要由該類的accept方法提供,這裡用到了訪問者設計模式。

accept方法接收的引數型別是AstVisitor,這是一個介面,我們正是透過這個介面的一系列方法實現對上述例項中各個節點的遍歷的。

正如上面的例子看到的,原始AstNode資料量很大,哪怕是一個簡單的Demo,解析出來的AST實際上是包含很多節點資訊的,所以我們並不透過實現AstVisitor介面來實現所有節點型別的訪問。analayzer包提供了SimpleAstVisitor,我們可以繼承這個類來自定義Visitor,按需要選擇我們支援的節點去實現方法就可以了。相關程式碼如下:

image.png

最後返回的是一個Map型別的Ast節點樹,感興趣的同學可以直接透過原始碼瞭解細節

四、DSL生成

4.1 從AST到DSL生成流程

以下是AST到DSL的整個生成的過程:

image.png

有了第一步生成的AST語法樹Map產物,再根據AST Map來生成DSL就比較好理解了。在DSL的生成流程當中,主要是對節點的遍歷,然後針對方法,表示式和變數的處理。

因為DSL主要處理的是佈局動態化的部分,實際上對於一個Wiget的解析處理,我們主要是針對build方法中return的內容部分進行了提取並生成DSL(此處的methodMap,我們先放到後面再講解,並不影響對主流程的理解)。相應程式碼如下所示:

image.png

對於DSL的格式,在debug環境,為了方便除錯與直觀的發現問題,我們的產物採用json格式,線上上環境,處於產物大小的控制及解析速度的考慮,我們下發產物格式改為flatbuffer(google推出的一種高效能,小體積的序列化方案)。

4.2 佈局動態化原理

實際上有了analyzer作為基礎,DSL的生成在技術上的難點並不大,可是我們的DSL的結構應該是什麼樣的,這取決於fair在執行時怎麼對DSL進行還原,畢竟我們的DSL生成最後是為了動態還原成Wiget樹並渲染的。針對這部分內容,我們做個大致的瞭解,這樣能更好的理解下面的內容。

我們知道,Flutter因為某些原因,對dart:mirror包進行了移除,這就決定了我們沒法透過反射對DSL進行佈局構建還原,不過Flutter還有一個萬能的方法Function.apply。

image.png

這個方法,是我們動態化方案中的第二個重要方法,是Widget樹還原的基礎。

端上接收到下發的DSL後,只能解析到對應的字串String型別,我們只需將對應的String對映到對應的方法(此處主要支援構造方法和類靜態方法),便可以將對應的DSL還原並構建Widget樹。在fair當中,大致是這樣的。此處我們寫了一個工具庫,以方便對flutter widget對映關係的自動生成。

image.png

4.3 DSL結構

瞭解了fair對DSL解析執行的大致原理之後,我們再來理解DSL的大致結構就比較容易了。

image.png

上面的className對應的是上面對映關係中的key,na和pa對應的是可選命名引數和位引數,此處我們需要解釋的是上面提到的methodMap。所謂的methodMap,從字面意思上理解,其實就是方法快取,在這裡我們同樣以上面的HelloWord類為例。

image.png

可以看到,在我們的示例中build方法巢狀了一個佈局構建方法_buildText()。前面我們講到,在佈局動態化DSL生成過程中,我們主要是對build方法進行了提取,對於這種方法巢狀的佈局構建程式碼,我們該怎麼處理呢。

大致的應對方法有幾種:

  1. 在框架層面做限制,不支援這種寫法;
  2. 解析時提取巢狀方法返回的Widget內容,在開發時支援方法巢狀,實際生成DSL時變成純Widget巢狀方式;
  3. 快取巢狀的Widget構建方法,在執行時解析Json內容後對函式進行實時的替換。

以上方式中,顯然1是讓人不可接受的,如果要接入動態化框架有這樣的限制,恐怕會讓使用的開發者望而卻步。至於方法2和方法3,其實大同小異,一個是在解析時替換,一個是在執行時替換,考慮到我們生成DSL儘量不要改變原有的程式碼結構,我們選擇了方案3。這就是為什麼我們的DSL Json中需要有methodMap的原因。

image.png

以上面HelloWord類DSL解析結果為例,可以看到methodMap當中實際上快取的是除build方法外的Widget構建的相關方法。

4.4 變數和表示式的處理

上面我們主要講的都是方法的處理,但是對於變數和表示式,比如下面這種:

Text('$_counter')

以及這種:

onPressed: _incrementCounter

這兩種型別的變數引用,在AST中實際上對應的不同的型別,但是如果在DSL中我們還是簡單的處理成了字串,實際在DSL解析時就無法與普通字串進行區分了,這裡我們採取了一種比較簡單的方式,在透過新增不同的特殊符號字首進行了區分。

例如上面兩個示例,處理後如下:

Text('$_counter') => #($_counter)
onPressed: _incrementCounter => @(incrementCounter)

然後,在Fair解析支援層面透過正則匹配到不同的表示式型別,來做變數的資料繫結已經方法的邏輯呼叫等。

五、總結

整個DSL生成流程細節很多,但是總結下來就是透過analyzert提供的AST解析工具提取並精煉對我們有用的資訊,並且根據我們的Fair框架需要,組合成抽象化的佈局DSL結構資訊。最後,我們借用Flutter動態化框架Fair的設計與思考中的關於DSL生成流程的一副圖來總結一下詳細的流程。

image.png