前言
在上篇文章裡面談了Weex在iOS客戶端工作的基本流程。這篇文章將會詳細的分析Weex是如何高效能的佈局原生介面的,之後還會與現有的佈局方法進行對比,看看Weex的佈局效能究竟如何。
目錄
- 1.Weex佈局演算法
- 2.Weex佈局演算法效能分析
- 3.Weex是如何佈局原生介面的
一. Weex佈局演算法
開啟Weex的原始碼的Layout資料夾,就會看到兩個c的檔案,這兩個檔案就是今天要談的Weex的佈局引擎。
Layout.h和Layout.c最開始是來自於React-Native裡面的程式碼。也就是說Weex和React-Native的佈局引擎都是同一套程式碼。
當前React-Native的程式碼裡面已經沒有這兩個檔案了,而是換成了Yoga。
Yoga本是Facebook在React Native裡引入的一種跨平臺的基於CSS的佈局引擎,它實現了Flexbox規範,完全遵守W3C的規範。隨著該系統不斷完善,Facebook對其進行重新發布,於是就成了現在的Yoga(Yoga官網)。
那麼Flexbox是什麼呢?
熟悉前端的同學一定很熟悉這個概念。2009年,W3C提出了一種新的方案——Flex佈局,可以簡便、完整、響應式地實現各種頁面佈局。目前,它已經得到了幾乎所有瀏覽器的支援,目前的前端主要是使用Html / CSS / JS實現,其中CSS用於前端的佈局。任何一個Html的容器可以通過css指定為Flex佈局,一旦一個容器被指定為Flex佈局,其子元素就可以按照FlexBox的語法進行佈局。
關於FlexBox的基本定義,更加詳細的文件說明,感興趣的同學可以去閱讀一下W3C的官方文件,那裡會有很詳細的說明。官方文件連結
Weex中的Layout檔案是Yoga的前身,是Yoga正式釋出之前的版本。底層程式碼使用C語言程式碼,所以效能也不是問題。接下來就仔細分析Layout檔案是如何實現FlexBox的。
故以下原始碼分析都基於v0.10.0這個版本。
(一)FlexBox中的基本資料結構
Flexbox佈局(Flexible Box)設計之初的目的是為了能更加高效的分配子檢視的佈局情況,包括動態的改變寬度,高度,以及排列順序。Flexbox可以更加方便的相容各個大小不同的螢幕,比如拉伸和壓縮子檢視。
在FlexBox的世界裡,存在著主軸和側軸的概念。
大多數情況,子檢視都是沿著主軸(main axis),從主軸起點(main-start)到主軸終點(main-end)排列。但是這裡需要注意的一點是,主軸和側軸雖然永遠是垂直的關係,但是誰是水平,誰是豎直,並沒有確定,有可能會有如下的情況:
在上圖這種水平是側軸的情況下,子檢視是沿著側軸(cross axis),從側軸起點(cross-start)到側軸終點(cross-end)排列的。
主軸(main axis):父檢視的主軸,子檢視主要沿著這條軸進行排列布局。
主軸起點(main-start)和主軸終點(main-end):子檢視在父檢視裡面佈局的方向是從主軸起點(main-start)向主軸終點(main-start)的方向。
主軸尺寸(main size):子檢視在主軸方向的寬度或高度就是主軸的尺寸。子檢視主要的大小屬性要麼是寬度,要麼是高度屬性,由哪一個對著主軸方向決定。
側軸(cross axis):垂直於主軸稱為側軸。它的方向主要取決於主軸方向。
側軸起點(cross-start)和側軸終點(cross-end):子檢視行的配置從容器的側軸起點邊開始,往側軸終點邊結束。
側軸尺寸(cross size):子檢視的在側軸方向的寬度或高度就是專案的側軸長度,伸縮專案的側軸長度屬性是「width」或「height」屬性,由哪一個對著側軸方向決定。
接下來看看Layout是怎麼定義FlexBox裡面的元素的。
typedef enum {
CSS_DIRECTION_INHERIT = 0,
CSS_DIRECTION_LTR,
CSS_DIRECTION_RTL
} css_direction_t;複製程式碼
這個方向是定義的上下文的整體佈局的方向,INHERIT是繼承,LTR是Left To Right,從左到右佈局。RTL是Right To Left,從右到左佈局。下面分析如果不做特殊說明,都是LTR從左向右佈局。如果是RTL就是LTR反向。
typedef enum {
CSS_FLEX_DIRECTION_COLUMN = 0,
CSS_FLEX_DIRECTION_COLUMN_REVERSE,
CSS_FLEX_DIRECTION_ROW,
CSS_FLEX_DIRECTION_ROW_REVERSE
} css_flex_direction_t;複製程式碼
這裡定義的是Flex的方向。
上圖是COLUMN。佈局的走向是從上往下。
上圖是COLUMN_REVERSE。佈局的走向是從下往上。
上圖是ROW。佈局的走向是從左往右。
上圖是ROW_REVERSE。佈局的走向是從右往左。
這裡可以看出來,在LTR的上下文中,ROW_REVERSE即等於RTL的上下文中的ROW。
typedef enum {
CSS_JUSTIFY_FLEX_START = 0,
CSS_JUSTIFY_CENTER,
CSS_JUSTIFY_FLEX_END,
CSS_JUSTIFY_SPACE_BETWEEN,
CSS_JUSTIFY_SPACE_AROUND
} css_justify_t;複製程式碼
這是定義的子檢視在主軸上的排列方式。
上圖是JUSTIFY_FLEX_START
上圖是JUSTIFY_CENTER
上圖是JUSTIFY_FLEX_END
上圖是JUSTIFY_SPACE_BETWEEN
上圖是JUSTIFY_SPACE_AROUND。這種方式是每個檢視的左右都保持著一定的寬度。
typedef enum {
CSS_ALIGN_AUTO = 0,
CSS_ALIGN_FLEX_START,
CSS_ALIGN_CENTER,
CSS_ALIGN_FLEX_END,
CSS_ALIGN_STRETCH
} css_align_t;複製程式碼
這是定義的子檢視在側軸上的對齊方式。
在Weex這裡定義了三種屬於css_align_t型別的方式,align_content,align_items,align_self。這三種型別的對齊方式略有不同。
ALIGN_AUTO只是針對align_self的一個預設值,但是對於align_content,align_items子檢視的對齊方式是無效的值。
1.align_items
align_items定義的是子檢視在一行裡面側軸上排列的方式。
上圖是ALIGN_FLEX_START
上圖是ALIGN_CENTER
上圖是ALIGN_FLEX_END
上圖是ALIGN_STRETCH
align_items在W3C的定義裡面其實還有一個種baseline的對齊方式,這裡在定義裡面並沒有。
注意,上面這種baseline的對齊方式在Weex的定義裡面並沒有!
2. align_content
align_content定義的是子檢視行與行之間在側軸上排列的方式。
上圖是ALIGN_FLEX_START
上圖是ALIGN_CENTER
上圖是ALIGN_FLEX_END
上圖是ALIGN_STRETCH
在FlexBox的W3C的定義裡面其實還有兩種方式在Weex沒有定義。
上圖的這種對齊方式是對應的justify裡面的JUSTIFY_SPACE_AROUND,align-content裡面的space-around這種對齊方式在Weex是沒有的。
上圖的這種對齊方式是對應的justify裡面的JUSTIFY_SPACE_BETWEEN,align-content裡面的space-between這種對齊方式在Weex是沒有的。
3.align_self
最後這一種對齊方式是可以在align_items的基礎上再分別自定義每個子檢視的對齊方式。如果是auto,是與align_items方式相同。
typedef enum {
CSS_POSITION_RELATIVE = 0,
CSS_POSITION_ABSOLUTE
} css_position_type_t;複製程式碼
這個是定義座標地址的型別,有相對座標和絕對座標兩種。
typedef enum {
CSS_NOWRAP = 0,
CSS_WRAP
} css_wrap_type_t;複製程式碼
在Weex裡面wrap只有兩種型別。
上圖是NOWRAP。所有的子檢視都會排列在一行之中。
上圖是WRAP。所有的子檢視會從左到右,從上到下排列。
在W3C的標準裡面還有一種wrap_reverse的排列方式。
這種排列方式,是從左到右,從下到上進行排列,目前在Weex裡面沒有定義。
typedef enum {
CSS_LEFT = 0,
CSS_TOP,
CSS_RIGHT,
CSS_BOTTOM,
CSS_START,
CSS_END,
CSS_POSITION_COUNT
} css_position_t;複製程式碼
這裡定義的是座標的描述。Left和Top因為會出現在position[2] 和 position[4]中,所以它們兩個排列在Right和Bottom前面。
typedef enum {
CSS_MEASURE_MODE_UNDEFINED = 0,
CSS_MEASURE_MODE_EXACTLY,
CSS_MEASURE_MODE_AT_MOST
} css_measure_mode_t;複製程式碼
這裡定義的是計算的方式,一種是精確計算,另外一種是估算近視值。
typedef enum {
CSS_WIDTH = 0,
CSS_HEIGHT
} css_dimension_t;複製程式碼
這裡定義的是子檢視的尺寸,寬和高。
typedef struct {
float position[4];
float dimensions[2];
css_direction_t direction;
// 快取一些資訊防止每次Layout過程都要重複計算
bool should_update;
float last_requested_dimensions[2];
float last_parent_max_width;
float last_parent_max_height;
float last_dimensions[2];
float last_position[2];
css_direction_t last_direction;
} css_layout_t;複製程式碼
這裡定義了一個css_layout_t結構體。結構體裡面position和dimensions陣列裡面分別儲存的是四周的位置和寬高的尺寸。direction裡面儲存的就是LTR還是RTL的方向。
至於下面那些變數資訊都是快取,用來防止沒有改變的Lauout還會重複計算的問題。
typedef struct {
float dimensions[2];
} css_dim_t;複製程式碼
css_dim_t結構體裡面裝的就是子檢視的尺寸資訊,寬和高。
typedef struct {
// 整個頁面CSS的方向,LTR、RTL
css_direction_t direction;
// Flex 的方向
css_flex_direction_t flex_direction;
// 子檢視在主軸上的排列對齊方式
css_justify_t justify_content;
// 子檢視在側軸上行與行之間的對齊方式
css_align_t align_content;
// 子檢視在側軸上的對齊方式
css_align_t align_items;
// 子檢視自己本身的對齊方式
css_align_t align_self;
// 子檢視的座標系型別(相對座標系,絕對座標系)
css_position_type_t position_type;
// wrap型別
css_wrap_type_t flex_wrap;
float flex;
// 上,下,左,右,start,end
float margin[6];
// 上,下,左,右
float position[4];
// 上,下,左,右,start,end
float padding[6];
// 上,下,左,右,start,end
float border[6];
// 寬,高
float dimensions[2];
// 最小的寬和高
float minDimensions[2];
// 最大的寬和高
float maxDimensions[2];
} css_style_t;複製程式碼
css_style_t記錄了整個style的所有資訊。每個變數的意義見上面註釋。
typedef struct css_node css_node_t;
struct css_node {
css_style_t style;
css_layout_t layout;
int children_count;
int line_index;
css_node_t *next_absolute_child;
css_node_t *next_flex_child;
css_dim_t (*measure)(void *context, float width, css_measure_mode_t widthMode, float height, css_measure_mode_t heightMode);
void (*print)(void *context);
struct css_node* (*get_child)(void *context, int i);
bool (*is_dirty)(void *context);
void *context;
};複製程式碼
css_node定義的是FlexBox的一個節點的資料結構。它包含了之前的css_style_t和css_layout_t。由於結構體裡面無法定義成員函式,所以下面包含4個函式指標。
css_node_t *new_css_node(void);
void init_css_node(css_node_t *node);
void free_css_node(css_node_t *node);複製程式碼
上面3個函式是關於css_node的生命週期相關的函式。
// 新建節點
css_node_t *new_css_node() {
css_node_t *node = (css_node_t *)calloc(1, sizeof(*node));
init_css_node(node);
return node;
}
// 釋放節點
void free_css_node(css_node_t *node) {
free(node);
}複製程式碼
新建節點的時候就是呼叫的init_css_node方法。
void init_css_node(css_node_t *node) {
node->style.align_items = CSS_ALIGN_STRETCH;
node->style.align_content = CSS_ALIGN_FLEX_START;
node->style.direction = CSS_DIRECTION_INHERIT;
node->style.flex_direction = CSS_FLEX_DIRECTION_COLUMN;
// 注意下面這些陣列裡面的值初始化為undefined,而不是0
node->style.dimensions[CSS_WIDTH] = CSS_UNDEFINED;
node->style.dimensions[CSS_HEIGHT] = CSS_UNDEFINED;
node->style.minDimensions[CSS_WIDTH] = CSS_UNDEFINED;
node->style.minDimensions[CSS_HEIGHT] = CSS_UNDEFINED;
node->style.maxDimensions[CSS_WIDTH] = CSS_UNDEFINED;
node->style.maxDimensions[CSS_HEIGHT] = CSS_UNDEFINED;
node->style.position[CSS_LEFT] = CSS_UNDEFINED;
node->style.position[CSS_TOP] = CSS_UNDEFINED;
node->style.position[CSS_RIGHT] = CSS_UNDEFINED;
node->style.position[CSS_BOTTOM] = CSS_UNDEFINED;
node->style.margin[CSS_START] = CSS_UNDEFINED;
node->style.margin[CSS_END] = CSS_UNDEFINED;
node->style.padding[CSS_START] = CSS_UNDEFINED;
node->style.padding[CSS_END] = CSS_UNDEFINED;
node->style.border[CSS_START] = CSS_UNDEFINED;
node->style.border[CSS_END] = CSS_UNDEFINED;
node->layout.dimensions[CSS_WIDTH] = CSS_UNDEFINED;
node->layout.dimensions[CSS_HEIGHT] = CSS_UNDEFINED;
// 以下這些用來對比是否發生變化的快取變數,初始值都為 -1。
node->layout.last_requested_dimensions[CSS_WIDTH] = -1;
node->layout.last_requested_dimensions[CSS_HEIGHT] = -1;
node->layout.last_parent_max_width = -1;
node->layout.last_parent_max_height = -1;
node->layout.last_direction = (css_direction_t)-1;
node->layout.should_update = true;
}複製程式碼
css_node的初始化的align_items是ALIGN_STRETCH,align_content是ALIGN_FLEX_START,direction是繼承自父類,flex_direction是按照列排列的。
接著下面陣列裡面存的都是UNDEFINED,而不是0,因為0會和結構體裡面的0衝突。
最後快取的變數初始化都為-1。
接下來定義了4個全域性的陣列,這4個陣列非常有用,它會決定接下來layout的方向和屬性。4個陣列和軸的方向是相互關聯的。
static css_position_t leading[4] = {
/* CSS_FLEX_DIRECTION_COLUMN = */ CSS_TOP,
/* CSS_FLEX_DIRECTION_COLUMN_REVERSE = */ CSS_BOTTOM,
/* CSS_FLEX_DIRECTION_ROW = */ CSS_LEFT,
/* CSS_FLEX_DIRECTION_ROW_REVERSE = */ CSS_RIGHT
};複製程式碼
如果主軸在COLUMN垂直方向,那麼子檢視的leading就是CSS_TOP,方向如果是COLUMN_REVERSE,那麼子檢視的leading就是CSS_BOTTOM;如果主軸在ROW水平方向,那麼子檢視的leading就是CSS_LEFT,方向如果是ROW_REVERSE,那麼子檢視的leading就是CSS_RIGHT。
static css_position_t trailing[4] = {
/* CSS_FLEX_DIRECTION_COLUMN = */ CSS_BOTTOM,
/* CSS_FLEX_DIRECTION_COLUMN_REVERSE = */ CSS_TOP,
/* CSS_FLEX_DIRECTION_ROW = */ CSS_RIGHT,
/* CSS_FLEX_DIRECTION_ROW_REVERSE = */ CSS_LEFT
};複製程式碼
如果主軸在COLUMN垂直方向,那麼子檢視的trailing就是CSS_BOTTOM,方向如果是COLUMN_REVERSE,那麼子檢視的trailing就是CSS_TOP;如果主軸在ROW水平方向,那麼子檢視的trailing就是CSS_RIGHT,方向如果是ROW_REVERSE,那麼子檢視的trailing就是CSS_LEFT。
static css_position_t pos[4] = {
/* CSS_FLEX_DIRECTION_COLUMN = */ CSS_TOP,
/* CSS_FLEX_DIRECTION_COLUMN_REVERSE = */ CSS_BOTTOM,
/* CSS_FLEX_DIRECTION_ROW = */ CSS_LEFT,
/* CSS_FLEX_DIRECTION_ROW_REVERSE = */ CSS_RIGHT
};複製程式碼
如果主軸在COLUMN垂直方向,那麼子檢視的position就是以CSS_TOP開始的,方向如果是COLUMN_REVERSE,那麼子檢視的position就是以CSS_BOTTOM開始的;如果主軸在ROW水平方向,那麼子檢視的position就是以CSS_LEFT開始的,方向如果是ROW_REVERSE,那麼子檢視的position就是以CSS_RIGHT開始的。
static css_dimension_t dim[4] = {
/* CSS_FLEX_DIRECTION_COLUMN = */ CSS_HEIGHT,
/* CSS_FLEX_DIRECTION_COLUMN_REVERSE = */ CSS_HEIGHT,
/* CSS_FLEX_DIRECTION_ROW = */ CSS_WIDTH,
/* CSS_FLEX_DIRECTION_ROW_REVERSE = */ CSS_WIDTH
};複製程式碼
如果主軸在COLUMN垂直方向,那麼子檢視在這個方向上的尺寸就是CSS_HEIGHT,方向如果是COLUMN_REVERSE,那麼子檢視在這個方向上的尺寸也是CSS_HEIGHT;如果主軸在ROW水平方向,那麼子檢視在這個方向上的尺寸就是CSS_WIDTH,方向如果是ROW_REVERSE,那麼子檢視在這個方向上的尺寸是CSS_WIDTH。
(二)FlexBox中的佈局演算法
Weex 盒模型基於 CSS 盒模型,每個 Weex 元素都可視作一個盒子。我們一般在討論設計或佈局時,會提到「盒模型」這個概念。
盒模型描述了一個元素所佔用的空間。每一個盒子有四條邊界:外邊距邊界 margin edge, 邊框邊界 border edge, 內邊距邊界 padding edge 與內容邊界 content edge。這四層邊界,形成一層層的盒子包裹起來,這就是盒模型大體上的含義。
盒子模型如上,這個圖是基於LTR,並且主軸在水平方向的。
所以主軸在不同方向可能就會有不同的情況。
注意:
Weex 盒模型的 box-sizing 預設為 border-box,即盒子的寬高包含內容content、內邊距padding和邊框的寬度border,不包含外邊距的寬度margin。
// 判斷軸是否是水平方向
static bool isRowDirection(css_flex_direction_t flex_direction) {
return flex_direction == CSS_FLEX_DIRECTION_ROW ||
flex_direction == CSS_FLEX_DIRECTION_ROW_REVERSE;
}
// 判斷軸是否是垂直方向
static bool isColumnDirection(css_flex_direction_t flex_direction) {
return flex_direction == CSS_FLEX_DIRECTION_COLUMN ||
flex_direction == CSS_FLEX_DIRECTION_COLUMN_REVERSE;
}複製程式碼
判斷軸的方向的方向就是上面這兩個。
然後接著還要計算4個方向上的padding、border、margin。這裡就舉一個方向的例子。
首先如何計算Margin的呢?
static float getLeadingMargin(css_node_t *node, css_flex_direction_t axis) {
if (isRowDirection(axis) && !isUndefined(node->style.margin[CSS_START])) {
return node->style.margin[CSS_START];
}
return node->style.margin[leading[axis]];
}複製程式碼
判斷軸的方向是不是水平方向,如果是水平方向就直接取node的margin裡面的CSS_START即是LeadingMargin,如果是豎直方向,就取出在豎直軸上面的leading方向的margin的值。
如果取TrailingMargin那麼就取margin[CSS_END]。
static float getTrailingMargin(css_node_t *node, css_flex_direction_t axis) {
if (isRowDirection(axis) && !isUndefined(node->style.margin[CSS_END])) {
return node->style.margin[CSS_END];
}
return node->style.margin[trailing[axis]];
}複製程式碼
以下padding、border、margin三個值的陣列儲存有6個值,如果是水平方向,那麼CSS_START儲存的都是Leading,CSS_END儲存的都是Trailing。下面沒有特殊說明,都按照這個規則來。
static float getLeadingPadding(css_node_t *node, css_flex_direction_t axis) {
if (isRowDirection(axis) &&
!isUndefined(node->style.padding[CSS_START]) &&
node->style.padding[CSS_START] >= 0) {
return node->style.padding[CSS_START];
}
if (node->style.padding[leading[axis]] >= 0) {
return node->style.padding[leading[axis]];
}
return 0;
}複製程式碼
取Padding的思路也和取Margin的思路一樣,水平方向就是取出陣列裡面的padding[CSS_START],如果是豎直方向,就對應得取出padding[leading[axis]]的值即可。
static float getLeadingBorder(css_node_t *node, css_flex_direction_t axis) {
if (isRowDirection(axis) &&
!isUndefined(node->style.border[CSS_START]) &&
node->style.border[CSS_START] >= 0) {
return node->style.border[CSS_START];
}
if (node->style.border[leading[axis]] >= 0) {
return node->style.border[leading[axis]];
}
return 0;
}複製程式碼
最後這是Border的計算方法,和上述Padding,Margin一模一樣,這裡就不再贅述了。
四周邊距的計算方法都實現了,接下來就是如何layout了。
// 計算佈局的方法
void layoutNode(css_node_t *node, float maxWidth, float maxHeight, css_direction_t parentDirection);
// 在呼叫layoutNode之前,可以重置node節點的layout
void resetNodeLayout(css_node_t *node);複製程式碼
重置node節點的方法就是把節點的座標重置為0,然後把寬和高都重置為UNDEFINED。
void resetNodeLayout(css_node_t *node) {
node->layout.dimensions[CSS_WIDTH] = CSS_UNDEFINED;
node->layout.dimensions[CSS_HEIGHT] = CSS_UNDEFINED;
node->layout.position[CSS_LEFT] = 0;
node->layout.position[CSS_TOP] = 0;
}複製程式碼
最後,佈局方法就是如下:
void layoutNode(css_node_t *node, float parentMaxWidth, float parentMaxHeight, css_direction_t parentDirection) {
css_layout_t *layout = &node->layout;
css_direction_t direction = node->style.direction;
layout->should_update = true;
// 對比當前環境是否“乾淨”,以及比較待佈局的node節點和上次節點是否完全一致。
bool skipLayout =
!node->is_dirty(node->context) &&
eq(layout->last_requested_dimensions[CSS_WIDTH], layout->dimensions[CSS_WIDTH]) &&
eq(layout->last_requested_dimensions[CSS_HEIGHT], layout->dimensions[CSS_HEIGHT]) &&
eq(layout->last_parent_max_width, parentMaxWidth) &&
eq(layout->last_parent_max_height, parentMaxHeight) &&
eq(layout->last_direction, direction);
if (skipLayout) {
// 把快取的值直接賦值給當前的layout
layout->dimensions[CSS_WIDTH] = layout->last_dimensions[CSS_WIDTH];
layout->dimensions[CSS_HEIGHT] = layout->last_dimensions[CSS_HEIGHT];
layout->position[CSS_TOP] = layout->last_position[CSS_TOP];
layout->position[CSS_LEFT] = layout->last_position[CSS_LEFT];
} else {
// 快取node節點
layout->last_requested_dimensions[CSS_WIDTH] = layout->dimensions[CSS_WIDTH];
layout->last_requested_dimensions[CSS_HEIGHT] = layout->dimensions[CSS_HEIGHT];
layout->last_parent_max_width = parentMaxWidth;
layout->last_parent_max_height = parentMaxHeight;
layout->last_direction = direction;
// 初始化所有子檢視node的尺寸和位置
for (int i = 0, childCount = node->children_count; i < childCount; i++) {
resetNodeLayout(node->get_child(node->context, i));
}
// 佈局檢視的核心實現
layoutNodeImpl(node, parentMaxWidth, parentMaxHeight, parentDirection);
// 佈局完成,把此次的佈局快取起來,防止下次重複的佈局重複計算
layout->last_dimensions[CSS_WIDTH] = layout->dimensions[CSS_WIDTH];
layout->last_dimensions[CSS_HEIGHT] = layout->dimensions[CSS_HEIGHT];
layout->last_position[CSS_TOP] = layout->position[CSS_TOP];
layout->last_position[CSS_LEFT] = layout->position[CSS_LEFT];
}
}複製程式碼
每步都註釋了,見上述程式碼註釋,在呼叫佈局的核心實現layoutNodeImpl之前,會迴圈呼叫resetNodeLayout,初始化所有子檢視。
所有的核心實現就在layoutNodeImpl這個方法裡面了。Weex裡面的這個方法實現有700多行,在Yoga的實現中,佈局演算法有1000多行。
static void layoutNodeImpl(css_node_t *node, float parentMaxWidth, float parentMaxHeight, css_direction_t parentDirection) {
}複製程式碼
這裡分析一下這個演算法的主要流程。在Weex的這個實現中,有7個迴圈,假設依次分別標上A,B,C,D,E,F,G。
先來看迴圈A
float mainContentDim = 0;
// 存在3類子檢視,支援flex的子檢視,不支援flex的子檢視,絕對佈局的子檢視,我們需要知道哪些子檢視是在等待分配空間。
int flexibleChildrenCount = 0;
float totalFlexible = 0;
int nonFlexibleChildrenCount = 0;
// 利用一層迴圈在主軸上簡單的堆疊子檢視,在迴圈C中,會忽略這些已經在迴圈A中已經排列好的子檢視
bool isSimpleStackMain =
(isMainDimDefined && justifyContent == CSS_JUSTIFY_FLEX_START) ||
(!isMainDimDefined && justifyContent != CSS_JUSTIFY_CENTER);
int firstComplexMain = (isSimpleStackMain ? childCount : startLine);
// 利用一層迴圈在側軸上簡單的堆疊子檢視,在迴圈D中,會忽略這些已經在迴圈A中已經排列好的子檢視
bool isSimpleStackCross = true;
int firstComplexCross = childCount;
css_node_t* firstFlexChild = NULL;
css_node_t* currentFlexChild = NULL;
float mainDim = leadingPaddingAndBorderMain;
float crossDim = 0;
float maxWidth = CSS_UNDEFINED;
float maxHeight = CSS_UNDEFINED;
// 迴圈A從這裡開始
for (i = startLine; i < childCount; ++i) {
child = node->get_child(node->context, i);
child->line_index = linesCount;
child->next_absolute_child = NULL;
child->next_flex_child = NULL;
css_align_t alignItem = getAlignItem(node, child);
// 在遞迴layout之前,先預填充側軸上可以被拉伸的子檢視
if (alignItem == CSS_ALIGN_STRETCH &&
child->style.position_type == CSS_POSITION_RELATIVE &&
isCrossDimDefined &&
!isStyleDimDefined(child, crossAxis)) {
// 這裡要進行一個比較,比較子檢視在側軸上的尺寸 和 側軸上減去兩邊的Margin、padding、Border剩下的可拉伸的空間 進行比較,因為拉伸是不會壓縮原始的大小的。
child->layout.dimensions[dim[crossAxis]] = fmaxf(
boundAxis(child, crossAxis, node->layout.dimensions[dim[crossAxis]] -
paddingAndBorderAxisCross - getMarginAxis(child, crossAxis)),
getPaddingAndBorderAxis(child, crossAxis)
);
} else if (child->style.position_type == CSS_POSITION_ABSOLUTE) {
// 這裡會儲存一個絕對佈局子檢視的連結串列。這樣我們在後面佈局的時候可以快速的跳過它們。
if (firstAbsoluteChild == NULL) {
firstAbsoluteChild = child;
}
if (currentAbsoluteChild != NULL) {
currentAbsoluteChild->next_absolute_child = child;
}
currentAbsoluteChild = child;
// 預填充子檢視,這裡需要用到檢視在軸上面的絕對座標,如果是水平軸,需要用到左右的偏移量,如果是豎直軸,需要用到上下的偏移量。
for (ii = 0; ii < 2; ii++) {
axis = (ii != 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN;
if (isLayoutDimDefined(node, axis) &&
!isStyleDimDefined(child, axis) &&
isPosDefined(child, leading[axis]) &&
isPosDefined(child, trailing[axis])) {
child->layout.dimensions[dim[axis]] = fmaxf(
// 這裡是絕對佈局,還需要減去leading和trailing
boundAxis(child, axis, node->layout.dimensions[dim[axis]] -
getPaddingAndBorderAxis(node, axis) -
getMarginAxis(child, axis) -
getPosition(child, leading[axis]) -
getPosition(child, trailing[axis])),
getPaddingAndBorderAxis(child, axis)
);
}
}
}複製程式碼
迴圈A的具體實現如上,註釋見程式碼。
迴圈A主要是實現的是layout佈局中不可以flex的子檢視的佈局,mainContentDim變數是用來記錄所有的尺寸以及所有不能flex的子檢視的margin的總和。它被用來設定node節點的尺寸,和計算剩餘空間以便供可flex子檢視進行拉伸適配。
每個node節點的next_absolute_child維護了一個連結串列,這裡儲存的依次是絕對佈局檢視的連結串列。
接著需要再統計可以被拉伸的子檢視。
float nextContentDim = 0;
// 統計可以拉伸flex的子檢視
if (isMainDimDefined && isFlex(child)) {
flexibleChildrenCount++;
totalFlexible += child->style.flex;
// 儲存一個連結串列維護可以flex的子檢視
if (firstFlexChild == NULL) {
firstFlexChild = child;
}
if (currentFlexChild != NULL) {
currentFlexChild->next_flex_child = child;
}
currentFlexChild = child;
// 這時我們雖然不知道確切的尺寸資訊,但是已經知道了padding , border , margin,我們可以利用這些資訊來給子檢視確定一個最小的size,計算剩餘可用的空間。
// 下一個content的距離等於當前子檢視Leading和Trailing的padding , border , margin6個尺寸之和。
nextContentDim = getPaddingAndBorderAxis(child, mainAxis) +
getMarginAxis(child, mainAxis);
} else {
maxWidth = CSS_UNDEFINED;
maxHeight = CSS_UNDEFINED;
// 計算出最大寬度和最大高度
if (!isMainRowDirection) {
if (isLayoutDimDefined(node, resolvedRowAxis)) {
maxWidth = node->layout.dimensions[dim[resolvedRowAxis]] -
paddingAndBorderAxisResolvedRow;
} else {
maxWidth = parentMaxWidth -
getMarginAxis(node, resolvedRowAxis) -
paddingAndBorderAxisResolvedRow;
}
} else {
if (isLayoutDimDefined(node, CSS_FLEX_DIRECTION_COLUMN)) {
maxHeight = node->layout.dimensions[dim[CSS_FLEX_DIRECTION_COLUMN]] -
paddingAndBorderAxisColumn;
} else {
maxHeight = parentMaxHeight -
getMarginAxis(node, CSS_FLEX_DIRECTION_COLUMN) -
paddingAndBorderAxisColumn;
}
}
// 遞迴呼叫layout函式,進行不能拉伸的子檢視的佈局。
if (alreadyComputedNextLayout == 0) {
layoutNode(child, maxWidth, maxHeight, direction);
}
// 由於絕對佈局的子檢視的位置和layout無關,所以我們不能用它們來計算mainContentDim
if (child->style.position_type == CSS_POSITION_RELATIVE) {
nonFlexibleChildrenCount++;
nextContentDim = getDimWithMargin(child, mainAxis);
}
}複製程式碼
上述程式碼就確定出了不可拉伸的子檢視的佈局。
每個node節點的next_flex_child維護了一個連結串列,這裡儲存的依次是可以flex拉伸檢視的連結串列。
// 將要加入的元素可能會被擠到下一行
if (isNodeFlexWrap &&
isMainDimDefined &&
mainContentDim + nextContentDim > definedMainDim &&
// 如果這裡只有一個元素,它可能就需要單獨佔一行
i != startLine) {
nonFlexibleChildrenCount--;
alreadyComputedNextLayout = 1;
break;
}
// 停止在主軸上堆疊子檢視,剩餘的子檢視都在迴圈C裡面佈局
if (isSimpleStackMain &&
(child->style.position_type != CSS_POSITION_RELATIVE || isFlex(child))) {
isSimpleStackMain = false;
firstComplexMain = i;
}
// 停止在側軸上堆疊子檢視,剩餘的子檢視都在迴圈D裡面佈局
if (isSimpleStackCross &&
(child->style.position_type != CSS_POSITION_RELATIVE ||
(alignItem != CSS_ALIGN_STRETCH && alignItem != CSS_ALIGN_FLEX_START) ||
(alignItem == CSS_ALIGN_STRETCH && !isCrossDimDefined))) {
isSimpleStackCross = false;
firstComplexCross = i;
}
if (isSimpleStackMain) {
child->layout.position[pos[mainAxis]] += mainDim;
if (isMainDimDefined) {
// 設定子檢視主軸上的TrailingPosition
setTrailingPosition(node, child, mainAxis);
}
// 可以算出了主軸上的尺寸了
mainDim += getDimWithMargin(child, mainAxis);
// 可以算出側軸上的尺寸了
crossDim = fmaxf(crossDim, boundAxis(child, crossAxis, getDimWithMargin(child, crossAxis)));
}
if (isSimpleStackCross) {
child->layout.position[pos[crossAxis]] += linesCrossDim + leadingPaddingAndBorderCross;
if (isCrossDimDefined) {
// 設定子檢視側軸上的TrailingPosition
setTrailingPosition(node, child, crossAxis);
}
}
alreadyComputedNextLayout = 0;
mainContentDim += nextContentDim;
endLine = i + 1;
}
// 迴圈A 至此結束複製程式碼
迴圈A結束以後,會計算出endLine,計算出主軸上的尺寸,側軸上的尺寸。不可拉伸的子檢視的佈局也會被確定。
接下來進入迴圈B的階段。
迴圈B主要分為2個部分,第一個部分是用來佈局可拉伸的子檢視。
// 為了在主軸上佈局,需要控制兩個space,一個是第一個子檢視和最左邊的距離,另一個是兩個子檢視之間的距離
float leadingMainDim = 0;
float betweenMainDim = 0;
// 記錄剩餘的可用空間
float remainingMainDim = 0;
if (isMainDimDefined) {
remainingMainDim = definedMainDim - mainContentDim;
} else {
remainingMainDim = fmaxf(mainContentDim, 0) - mainContentDim;
}
// 如果當前還有可拉伸的子檢視,它們就要填充剩餘的可用空間
if (flexibleChildrenCount != 0) {
float flexibleMainDim = remainingMainDim / totalFlexible;
float baseMainDim;
float boundMainDim;
// 如果剩餘的空間不能提供給可拉伸的子檢視,不能滿足它們的最大或者最小的bounds,那麼這些子檢視也要排除到計算拉伸的過程之外
currentFlexChild = firstFlexChild;
while (currentFlexChild != NULL) {
baseMainDim = flexibleMainDim * currentFlexChild->style.flex +
getPaddingAndBorderAxis(currentFlexChild, mainAxis);
boundMainDim = boundAxis(currentFlexChild, mainAxis, baseMainDim);
if (baseMainDim != boundMainDim) {
remainingMainDim -= boundMainDim;
totalFlexible -= currentFlexChild->style.flex;
}
currentFlexChild = currentFlexChild->next_flex_child;
}
flexibleMainDim = remainingMainDim / totalFlexible;
// 不可以拉伸的子檢視可以在父檢視內部overflow,在這種情況下,假設沒有可用的拉伸space
if (flexibleMainDim < 0) {
flexibleMainDim = 0;
}
currentFlexChild = firstFlexChild;
while (currentFlexChild != NULL) {
// 在這層迴圈裡面我們已經可以確認子檢視的最終大小了
currentFlexChild->layout.dimensions[dim[mainAxis]] = boundAxis(currentFlexChild, mainAxis,
flexibleMainDim * currentFlexChild->style.flex +
getPaddingAndBorderAxis(currentFlexChild, mainAxis)
);
// 計算水平方向軸上子檢視的最大寬度
maxWidth = CSS_UNDEFINED;
if (isLayoutDimDefined(node, resolvedRowAxis)) {
maxWidth = node->layout.dimensions[dim[resolvedRowAxis]] -
paddingAndBorderAxisResolvedRow;
} else if (!isMainRowDirection) {
maxWidth = parentMaxWidth -
getMarginAxis(node, resolvedRowAxis) -
paddingAndBorderAxisResolvedRow;
}
// 計算垂直方向軸上子檢視的最大高度
maxHeight = CSS_UNDEFINED;
if (isLayoutDimDefined(node, CSS_FLEX_DIRECTION_COLUMN)) {
maxHeight = node->layout.dimensions[dim[CSS_FLEX_DIRECTION_COLUMN]] -
paddingAndBorderAxisColumn;
} else if (isMainRowDirection) {
maxHeight = parentMaxHeight -
getMarginAxis(node, CSS_FLEX_DIRECTION_COLUMN) -
paddingAndBorderAxisColumn;
}
// 再次遞迴完成可拉伸的子檢視的佈局
layoutNode(currentFlexChild, maxWidth, maxHeight, direction);
child = currentFlexChild;
currentFlexChild = currentFlexChild->next_flex_child;
child->next_flex_child = NULL;
}
}複製程式碼
在上述2個while結束以後,所有可以被拉伸的子檢視就都佈局完成了。
else if (justifyContent != CSS_JUSTIFY_FLEX_START) {
if (justifyContent == CSS_JUSTIFY_CENTER) {
leadingMainDim = remainingMainDim / 2;
} else if (justifyContent == CSS_JUSTIFY_FLEX_END) {
leadingMainDim = remainingMainDim;
} else if (justifyContent == CSS_JUSTIFY_SPACE_BETWEEN) {
remainingMainDim = fmaxf(remainingMainDim, 0);
if (flexibleChildrenCount + nonFlexibleChildrenCount - 1 != 0) {
betweenMainDim = remainingMainDim /
(flexibleChildrenCount + nonFlexibleChildrenCount - 1);
} else {
betweenMainDim = 0;
}
} else if (justifyContent == CSS_JUSTIFY_SPACE_AROUND) {
// 這裡是實現SPACE_AROUND的程式碼
betweenMainDim = remainingMainDim /
(flexibleChildrenCount + nonFlexibleChildrenCount);
leadingMainDim = betweenMainDim / 2;
}
}複製程式碼
可flex拉伸的檢視佈局完成以後,這裡是收尾工作,根據justifyContent,更改betweenMainDim和leadingMainDim的大小。
接著再是迴圈C。
// 在這個迴圈中,所有子檢視的寬和高都將被確定下來。在確定各個子檢視的座標的時候,同時也將確定父檢視的寬和高。
mainDim += leadingMainDim;
// 按照Line,一層層的迴圈
for (i = firstComplexMain; i < endLine; ++i) {
child = node->get_child(node->context, i);
if (child->style.position_type == CSS_POSITION_ABSOLUTE &&
isPosDefined(child, leading[mainAxis])) {
// 到這裡,絕對座標的子檢視的座標已經確定下來了,左邊距和上邊距已經被定下來了。這時子檢視的絕對座標可以確定了。
child->layout.position[pos[mainAxis]] = getPosition(child, leading[mainAxis]) +
getLeadingBorder(node, mainAxis) +
getLeadingMargin(child, mainAxis);
} else {
// 如果子檢視不是絕對座標,座標是相對的,或者還沒有確定下來左邊距和上邊距,那麼就根據當前位置確定座標
child->layout.position[pos[mainAxis]] += mainDim;
// 確定trailing的座標位置
if (isMainDimDefined) {
setTrailingPosition(node, child, mainAxis);
}
// 接下來開始處理相對座標的子檢視,具有絕對座標的子檢視不會參與下述的佈局計算中
if (child->style.position_type == CSS_POSITION_RELATIVE) {
// 主軸上的寬度是由所有的子檢視的寬度累加而成
mainDim += betweenMainDim + getDimWithMargin(child, mainAxis);
// 側軸的高度是由最高的子檢視決定的
crossDim = fmaxf(crossDim, boundAxis(child, crossAxis, getDimWithMargin(child, crossAxis)));
}
}
}
float containerCrossAxis = node->layout.dimensions[dim[crossAxis]];
if (!isCrossDimDefined) {
containerCrossAxis = fmaxf(
// 計算父檢視的時候需要加上,上下的padding和Border。
boundAxis(node, crossAxis, crossDim + paddingAndBorderAxisCross),
paddingAndBorderAxisCross
);
}複製程式碼
在迴圈C中,會在主軸上計算出所有子檢視的座標,包括各個子檢視的寬和高。
接下來就到迴圈D的流程了。
for (i = firstComplexCross; i < endLine; ++i) {
child = node->get_child(node->context, i);
if (child->style.position_type == CSS_POSITION_ABSOLUTE &&
isPosDefined(child, leading[crossAxis])) {
// 到這裡,絕對座標的子檢視的座標已經確定下來了,上下左右至少有一邊的座標已經被定下來了。這時子檢視的絕對座標可以確定了。
child->layout.position[pos[crossAxis]] = getPosition(child, leading[crossAxis]) +
getLeadingBorder(node, crossAxis) +
getLeadingMargin(child, crossAxis);
} else {
float leadingCrossDim = leadingPaddingAndBorderCross;
// 在側軸上,針對相對座標的子檢視,我們利用父檢視的alignItems或者子檢視的alignSelf來確定具體的座標位置
if (child->style.position_type == CSS_POSITION_RELATIVE) {
// 獲取子檢視的AlignItem屬性值
css_align_t alignItem = getAlignItem(node, child);
if (alignItem == CSS_ALIGN_STRETCH) {
// 如果在側軸上子檢視還沒有確定尺寸,那麼才會相應STRETCH拉伸。
if (!isStyleDimDefined(child, crossAxis)) {
float dimCrossAxis = child->layout.dimensions[dim[crossAxis]];
child->layout.dimensions[dim[crossAxis]] = fmaxf(
boundAxis(child, crossAxis, containerCrossAxis -
paddingAndBorderAxisCross - getMarginAxis(child, crossAxis)),
getPaddingAndBorderAxis(child, crossAxis)
);
// 如果檢視的大小變化了,連帶該檢視的子檢視還需要再次layout
if (dimCrossAxis != child->layout.dimensions[dim[crossAxis]] && child->children_count > 0) {
// Reset child margins before re-layout as they are added back in layoutNode and would be doubled
child->layout.position[leading[mainAxis]] -= getLeadingMargin(child, mainAxis) +
getRelativePosition(child, mainAxis);
child->layout.position[trailing[mainAxis]] -= getTrailingMargin(child, mainAxis) +
getRelativePosition(child, mainAxis);
child->layout.position[leading[crossAxis]] -= getLeadingMargin(child, crossAxis) +
getRelativePosition(child, crossAxis);
child->layout.position[trailing[crossAxis]] -= getTrailingMargin(child, crossAxis) +
getRelativePosition(child, crossAxis);
// 遞迴子檢視的佈局
layoutNode(child, maxWidth, maxHeight, direction);
}
}
} else if (alignItem != CSS_ALIGN_FLEX_START) {
// 在側軸上剩餘的空間等於父檢視在側軸上的高度減去子檢視的在側軸上padding、Border、Margin以及高度
float remainingCrossDim = containerCrossAxis -
paddingAndBorderAxisCross - getDimWithMargin(child, crossAxis);
if (alignItem == CSS_ALIGN_CENTER) {
leadingCrossDim += remainingCrossDim / 2;
} else { // CSS_ALIGN_FLEX_END
leadingCrossDim += remainingCrossDim;
}
}
}
// 確定子檢視在側軸上的座標位置
child->layout.position[pos[crossAxis]] += linesCrossDim + leadingCrossDim;
// 確定trailing的座標
if (isCrossDimDefined) {
setTrailingPosition(node, child, crossAxis);
}
}
}
linesCrossDim += crossDim;
linesMainDim = fmaxf(linesMainDim, mainDim);
linesCount += 1;
startLine = endLine;
}複製程式碼
上述的迴圈D中主要是在側軸上計運算元檢視的座標。如果檢視發生了大小變化,還需要遞迴子檢視,重新佈局一次。
再接著是迴圈E
if (linesCount > 1 && isCrossDimDefined) {
float nodeCrossAxisInnerSize = node->layout.dimensions[dim[crossAxis]] -
paddingAndBorderAxisCross;
float remainingAlignContentDim = nodeCrossAxisInnerSize - linesCrossDim;
float crossDimLead = 0;
float currentLead = leadingPaddingAndBorderCross;
// 佈局alignContent
css_align_t alignContent = node->style.align_content;
if (alignContent == CSS_ALIGN_FLEX_END) {
currentLead += remainingAlignContentDim;
} else if (alignContent == CSS_ALIGN_CENTER) {
currentLead += remainingAlignContentDim / 2;
} else if (alignContent == CSS_ALIGN_STRETCH) {
if (nodeCrossAxisInnerSize > linesCrossDim) {
crossDimLead = (remainingAlignContentDim / linesCount);
}
}
int endIndex = 0;
for (i = 0; i < linesCount; ++i) {
int startIndex = endIndex;
// 計算每一行的行高,行高根據lineHeight和子檢視在側軸上的高度加上下的Margin之和比較,取最大值
float lineHeight = 0;
for (ii = startIndex; ii < childCount; ++ii) {
child = node->get_child(node->context, ii);
if (child->style.position_type != CSS_POSITION_RELATIVE) {
continue;
}
if (child->line_index != i) {
break;
}
if (isLayoutDimDefined(child, crossAxis)) {
lineHeight = fmaxf(
lineHeight,
child->layout.dimensions[dim[crossAxis]] + getMarginAxis(child, crossAxis)
);
}
}
endIndex = ii;
lineHeight += crossDimLead;
for (ii = startIndex; ii < endIndex; ++ii) {
child = node->get_child(node->context, ii);
if (child->style.position_type != CSS_POSITION_RELATIVE) {
continue;
}
// 佈局AlignItem
css_align_t alignContentAlignItem = getAlignItem(node, child);
if (alignContentAlignItem == CSS_ALIGN_FLEX_START) {
child->layout.position[pos[crossAxis]] = currentLead + getLeadingMargin(child, crossAxis);
} else if (alignContentAlignItem == CSS_ALIGN_FLEX_END) {
child->layout.position[pos[crossAxis]] = currentLead + lineHeight - getTrailingMargin(child, crossAxis) - child->layout.dimensions[dim[crossAxis]];
} else if (alignContentAlignItem == CSS_ALIGN_CENTER) {
float childHeight = child->layout.dimensions[dim[crossAxis]];
child->layout.position[pos[crossAxis]] = currentLead + (lineHeight - childHeight) / 2;
} else if (alignContentAlignItem == CSS_ALIGN_STRETCH) {
child->layout.position[pos[crossAxis]] = currentLead + getLeadingMargin(child, crossAxis);
// TODO(prenaux): Correctly set the height of items with undefined
// (auto) crossAxis dimension.
}
}
currentLead += lineHeight;
}
}複製程式碼
執行迴圈E有一個前提,就是,行數至少要超過一行,並且側軸上有高度定義。滿足了這個前提條件以後才會開始下面的align規則。
在迴圈E中會處理側軸上的align拉伸規則。這裡會佈局alignContent和AlignItem。
這塊程式碼實現的演算法原理請參見www.w3.org/TR/2012/CR-… section 9.4部分。
至此可能還存在一些沒有指定寬和高的檢視,接下來將會做最後一次的處理。
// 如果某個檢視沒有被指定寬或者高,並且也沒有被父檢視設定寬和高,那麼在這裡通過子檢視來設定寬和高
if (!isMainDimDefined) {
// 檢視的寬度等於內部子檢視的寬度加上Trailing的Padding、Border的寬度和主軸上Leading的Padding、Border+ Trailing的Padding、Border,兩者取最大值。
node->layout.dimensions[dim[mainAxis]] = fmaxf(
boundAxis(node, mainAxis, linesMainDim + getTrailingPaddingAndBorder(node, mainAxis)),
paddingAndBorderAxisMain
);
if (mainAxis == CSS_FLEX_DIRECTION_ROW_REVERSE ||
mainAxis == CSS_FLEX_DIRECTION_COLUMN_REVERSE) {
needsMainTrailingPos = true;
}
}
if (!isCrossDimDefined) {
node->layout.dimensions[dim[crossAxis]] = fmaxf(
// 檢視的高度等於內部子檢視的高度加上上下的Padding、Border的寬度和側軸上Padding、Border,兩者取最大值。
boundAxis(node, crossAxis, linesCrossDim + paddingAndBorderAxisCross),
paddingAndBorderAxisCross
);
if (crossAxis == CSS_FLEX_DIRECTION_ROW_REVERSE ||
crossAxis == CSS_FLEX_DIRECTION_COLUMN_REVERSE) {
needsCrossTrailingPos = true;
}
}複製程式碼
這些沒有確定寬和高的子檢視的寬和高會根據父檢視來決定。方法見上述程式碼。
再就是迴圈F了。
if (needsMainTrailingPos || needsCrossTrailingPos) {
for (i = 0; i < childCount; ++i) {
child = node->get_child(node->context, i);
if (needsMainTrailingPos) {
setTrailingPosition(node, child, mainAxis);
}
if (needsCrossTrailingPos) {
setTrailingPosition(node, child, crossAxis);
}
}
}複製程式碼
這一步是設定當前node節點的Trailing座標,如果有必要的話。如果不需要,這一步會直接跳過。
最後一步就是迴圈G了。
currentAbsoluteChild = firstAbsoluteChild;
while (currentAbsoluteChild != NULL) {
for (ii = 0; ii < 2; ii++) {
axis = (ii != 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN;
if (isLayoutDimDefined(node, axis) &&
!isStyleDimDefined(currentAbsoluteChild, axis) &&
isPosDefined(currentAbsoluteChild, leading[axis]) &&
isPosDefined(currentAbsoluteChild, trailing[axis])) {
// 絕對座標的子檢視在主軸上的寬度,在側軸上的高度都不能比Padding、Border的總和小。
currentAbsoluteChild->layout.dimensions[dim[axis]] = fmaxf(
boundAxis(currentAbsoluteChild, axis, node->layout.dimensions[dim[axis]] -
getBorderAxis(node, axis) -
getMarginAxis(currentAbsoluteChild, axis) -
getPosition(currentAbsoluteChild, leading[axis]) -
getPosition(currentAbsoluteChild, trailing[axis])
),
getPaddingAndBorderAxis(currentAbsoluteChild, axis)
);
}
if (isPosDefined(currentAbsoluteChild, trailing[axis]) &&
!isPosDefined(currentAbsoluteChild, leading[axis])) {
// 當前子檢視的座標等於當前檢視的寬度減去子檢視的寬度再減去trailing
currentAbsoluteChild->layout.position[leading[axis]] =
node->layout.dimensions[dim[axis]] -
currentAbsoluteChild->layout.dimensions[dim[axis]] -
getPosition(currentAbsoluteChild, trailing[axis]);
}
}
child = currentAbsoluteChild;
currentAbsoluteChild = currentAbsoluteChild->next_absolute_child;
child->next_absolute_child = NULL;
}複製程式碼
最後這一步迴圈G是用來給絕對座標的子檢視計算寬度和高度。
執行完上述7個迴圈以後,所有的子檢視就都layout完成了。
總結一下上述的流程,如下圖:
二. Weex佈局演算法效能分析
1.演算法實現分析
上一章節看了Weex的layout演算法實現。這裡就分析一下在這個實現下,佈局能力究竟有多強。
Weex的實現是FaceBook的開源庫Yoga的前身,所以這裡可以把兩個看成是一種實現。
Weex的這種FlexBox的實現其實只是W3C標準的一個實現的子集,因為FlexBox的官方標準裡面還有一些並沒有實現出來。W3C上定義的FlexBox的標準,文件在這裡。
FlexBox標準定義:
針對父檢視 (flex container):
- display
- flex-direction
- flex-wrap
- flex-flow
- justify-content
- align-items
- align-content
針對子檢視 (flex items):
- order
- flex-grow
- flex-shrink
- flex-basis
- flex
- align-self
相比官方的定義,上述的實現有一些限制:
- 所有顯示屬性的node節點都預設假定是Flex的檢視,當然這裡要除去文字節點,因為它會被假定為inline-flex。
- 不支援zIndex的屬性,包括任何z上的排序。所有的node節點都是按照程式碼書寫的先後順序進行排列的。Weex 目前也不支援 z-index 設定元素層級關係,但靠後的元素層級更高,因此,對於層級高的元素,可將其排列在後面。
- FlexBox裡面定義的order屬性,也不支援。flex item預設按照程式碼書寫順序。
- visibility屬性預設都是可見的,暫時不支援邊緣塌陷合併(collapse)和隱藏(hidden)屬性。
- 不支援forced breaks。
- 不支援垂直方向的inline(比如從上到下的text,或者從下到上的text)
關於Flexbox 在iOS這邊的具體實現上一章節已經分析過了。
接下來仔細分析一下Autolayout的具體實現
原來我們用Frame進行佈局的時候,需要知道一個點(origin或者center)和寬高就可以確定一個View。
現在換成了Autolayout,每個View需要知道4個尺寸。left,top,width,height。
但是一個View的約束是相對於另一個View的,比如說相對於父檢視,或者是相對於兩兩View之間的。
那麼兩兩個View之間的約束就會變成一個八元一次的方程組。
解這個方程組可能有以下3種情況:
- 當方程組的解的個數有無窮多個,最終會得到欠約束的有歧義的佈局。
- 當方程無解時,則表示約束有衝突。
- 只有當方程組有唯一解的時候,才能得到一個穩定的佈局。
Autolayout 本質是一個線性方程解析器,該解析器試圖找到一種可滿足其規則的幾何表示式。
Autolayout的底層數學模型是線性算術約束問題。
關於這個問題,早在1940年,由Dantzig提出了一個the simplex algorithm演算法,但是由於這個演算法實在很難用在UI應用上面,所以沒有得到很廣泛的應用,直到1997年,澳大利亞的莫納什大學(Monash University)的兩名學生,Alan Borning 和 Kim Marriott實現了Cassowary線性約束演算法,才得以在UI應用上被大量的應用起來。
Cassowary線性約束演算法是基於雙simplex演算法的,在增加約束或者一個物件被移除的時候,通過區域性誤差增益 和 加權求和比較 ,能夠完美的增量處理不同層次的約束。Cassowary線性約束演算法適合GUI佈局系統,被用來計算view之間的位置的。開發者可以指定不同View之間的位置關係和約束關係,Cassowary線性約束演算法會去求處符合條件的最優值。
下面是兩位學生寫的相關的論文,有興趣的可以讀一下,瞭解一下演算法的具體實現:
- Alan Borning, Kim Marriott, Peter Stuckey, and Yi Xiao, Solving Linear Arithmetic Constraints for User Interface Applications, Proceedings of the 1997 ACM Symposium on User Interface Software and Technology, October 1997, pages 87-96.
- Greg J. Badros and Alan Borning, "The Cassowary Linear Arithmetic Constraint Solving Algorithm: Interface and Implementation", Technical Report UW-CSE-98-06-04, June 1998 (pdf)
- Greg J. Badros, Alan Borning, and Peter J. Stuckey, "The Cassowary Linear Arithmetic Constraint Solving Algorithm," ACM Transactions on Computer Human Interaction, Vol. 8 No. 4, December 2001, pages 267-306. (pdf)
Cassowary線性約束演算法的虛擬碼如下:
關於這個演算法已經被人們實現成了各個版本。1年以後,又出了一個新的QOCA演算法。以下這段話摘抄自1997年ACM權威論文上的一篇文章:
Both of our algorithms have been implemented, Cassowary
in Smalltalk and QOCA in C++. They perform surprisingly
well. The QOCA implementation is considerably more sophisticated
and has much better performance than the current version of
Cassowary. However, QOCA is inherently a more complex
algorithm, and re-implementing it with a comparable level
of performance would be a daunting task. In contrast, Cassowary
is straightforward, and a reimplementation based on
this paper is more reasonable, given a knowledge of the simplex
algorithm.
Cassowary(專案主頁)也是優先被Smalltalk實現了,也是用在Autolayout技術上。另外還有更加複雜的QOCA演算法,這裡就不再細談了,有興趣的同學可以看看上面三篇論文,裡面有詳細的描述。
2.演算法效能測試準備工作
開始筆者是打算連帶Weex的佈局效能一起測試的,但是由於Weex的佈局都在子執行緒,重新整理渲染回到主執行緒,需要測試都在主執行緒的情況需要改動一些程式碼,而且Weex原生的佈局是從JS呼叫方法,如果用這種方法又會多損耗一些效能,對測試結果有影響。於是換成Weex相同佈局方式的Yoga演算法進行測試。由於Facebook對它進行了很好的封裝,使用起來也很方便。雖然Layout演算法和Weex有些差異,但是不影響定性的比較。
確定下來測試物件:Frame,FlexBox(Yoga實現),Autolayout。
測試前,還需要準備測試模型,這裡選出了3種測試模型。
第一種測試模型是隨機生成完全不相關聯的View。如下圖:
第二種測試模型是生成相互巢狀的View。巢狀規則設定一個簡單的:子檢視依次比父檢視高度少一個畫素。類似下圖,這是500個View相互巢狀的結果:
第三種測試模型是針對Autolayout專門加的。由於Autolayout約束的特殊性,這裡針對鏈式約束額外增加的測試模型。規則是前後兩個相連的View之間依次加上約束。類似下圖,這是500個View鏈式的約束結果:
根據測試模型,我們可以得到如下的7組需要測試的測試用例:
1.Frame
2.巢狀的Frame
3.Yoga
4.巢狀的Yoga
5.Autolayout
6.巢狀的Autolayout
7.鏈式的Autolayout
測試樣本:由於需要考慮到測試的通用性,測試樣本要儘量隨機。於是針對隨機生成的座標全部都隨機生成,View的顏色也全部都隨機生成,這樣保證了通用公正公平性質。
測試次數:為了保證測試資料能儘量真實,筆者在這裡花了大量的時間。每組測試用例都針對從100,200,300,400,500,600,700,800,900,1000個檢視進行測試,為了保證測試的普遍性,這裡每次測試都測試10000次,然後對10000次的結果進行加和平均。加和平均取小數點後5位。(10000次的統計是用計算機來算的,但是真的非常非常非常的耗時,有興趣的可以自己用電腦試試)
最後展示一下測試機器的配置和系統版本:
(由於iPhone真機對每個App的記憶體有限制,產生1000個巢狀的檢視,並且進行10000次試驗,iPhone真機完全受不了這種計算量,App直接閃退,所以用真機測試到一半,改用模擬器測試,藉助Mac的效能,咬著牙從零開始,重新統計了所有測試用例的資料)
如果有效能更強的Mac電腦(垃圾桶),測試全過程花的時間可能會更少。
筆者用的電腦的配置如下:
測試用的模擬器是iPad Pro(12.9 inch)iOS 10.3(14E269)
我所用的測試程式碼也公佈出來,有興趣的可以自己測試測試。測試程式碼在這裡
3.演算法效能測試結果
公佈測試結果:
上圖資料是10,20,30,40,50,60,70,80,90,100個View分別用7組用例測試出來的結果。將上面的結果統計成折線圖,如下:
結果依舊是Autolayout的3種方式都高於其他4種佈局方式。
上圖是3個佈局演算法在普通場景下的效能比較圖,可以看到,FlexBox的效能接近於原生的Frame。
上圖是3個佈局演算法在巢狀情況下的效能比較圖,可以看到,FlexBox的效能也依舊接近於原生的Frame。而巢狀情況下的Autolayout的效能急劇下降。
最後這張圖也是專門針對Autolayout額外加的一組測試。目的是為了比較3種場景下不同的Autolayout的效能,可以看到,巢狀的Autolayout的效能依舊是最差的!
上圖資料是100,200,300,400,500,600,700,800,900,1000個View分別用7組用例測試出來的結果。將上面的結果統計成折線圖,如下:
當檢視多到900,1000的時候,巢狀的Autolayout直接就導致模擬器崩潰了。
上圖是3個佈局演算法在普通場景下的效能比較圖,可以看到,FlexBox的效能接近於原生的Frame。
上圖是3個佈局演算法在巢狀情況下的效能比較圖,可以看到,FlexBox的效能也依舊接近於原生的Frame。而巢狀情況下的Autolayout的效能急劇下降。
最後這張圖是專門針對Autolayout額外加的一組測試。目的是為了比較3種場景下不同的Autolayout的效能,可以看到,平時我們使用巢狀的Autolayout的效能是最差的!
三. Weex是如何佈局原生介面的
上一章節看了FlexBox演算法的強大布局能力,這一章節就來看看Weex究竟是如何利用這個能力的對原生View進行Layout。
在解答上面這個問題之前,先讓我們回顧一下上篇文章《Weex 是如何在 iOS 客戶端上跑起來的》裡面提到的,在JSFramework轉換從網路上下載下來的JS檔案之前,本地先註冊了4個重要的回撥函式。
typedef NSInteger(^WXJSCallNative)(NSString *instance, NSArray *tasks, NSString *callback);
typedef NSInteger(^WXJSCallAddElement)(NSString *instanceId, NSString *parentRef, NSDictionary *elementData, NSInteger index);
typedef NSInvocation *(^WXJSCallNativeModule)(NSString *instanceId, NSString *moduleName, NSString *methodName, NSArray *args, NSDictionary *options);
typedef void (^WXJSCallNativeComponent)(NSString *instanceId, NSString *componentRef, NSString *methodName, NSArray *args, NSDictionary *options);複製程式碼
這4個block非常重要,是JS和OC進行相互呼叫的四大函式。
先來回顧一下這四大函式註冊的時候分別封裝了哪些閉包。
@interface WXBridgeContext ()
@property (nonatomic, strong) id<WXBridgeProtocol> jsBridge;複製程式碼
在WXBridgeContext類裡面有一個jsBridge。jsBridge初始化的時候會註冊這4個全域性函式。
第一個閉包函式:
[_jsBridge registerCallNative:^NSInteger(NSString *instance, NSArray *tasks, NSString *callback) {
return [weakSelf invokeNative:instance tasks:tasks callback:callback];
}];複製程式碼
這裡的閉包函式會被傳入到下面這個函式中:
- (void)registerCallNative:(WXJSCallNative)callNative
{
JSValue* (^callNativeBlock)(JSValue *, JSValue *, JSValue *) = ^JSValue*(JSValue *instance, JSValue *tasks, JSValue *callback){
NSString *instanceId = [instance toString];
NSArray *tasksArray = [tasks toArray];
NSString *callbackId = [callback toString];
WXLogDebug(@"Calling native... instance:%@, tasks:%@, callback:%@", instanceId, tasksArray, callbackId);
return [JSValue valueWithInt32:(int32_t)callNative(instanceId, tasksArray, callbackId) inContext:[JSContext currentContext]];
};
_jsContext[@"callNative"] = callNativeBlock;
}複製程式碼
這裡就封裝了一個函式,暴露給JS用。方法名叫callNative,函式引數為3個,分別是instanceId,tasksArray任務陣列,callbackId回撥ID。
所有的OC的閉包都需要封裝一層,因為暴露給JS的方法不能有冒號,所有的引數都是直接跟在小括號的引數列表裡面的,因為JS的函式是這樣定義的。
當JS呼叫callNative方法之後,就會最終執行WXBridgeContext類裡面的[weakSelf invokeNative:instance tasks:tasks callback:callback]方法。
第二個閉包函式:
[_jsBridge registerCallAddElement:^NSInteger(NSString *instanceId, NSString *parentRef, NSDictionary *elementData, NSInteger index) {
// Temporary here , in order to improve performance, will be refactored next version.
WXSDKInstance *instance = [WXSDKManager instanceForID:instanceId];
if (!instance) {
WXLogInfo(@"instance not found, maybe already destroyed");
return -1;
}
WXPerformBlockOnComponentThread(^{
WXComponentManager *manager = instance.componentManager;
if (!manager.isValid) {
return;
}
[manager startComponentTasks];
[manager addComponent:elementData toSupercomponent:parentRef atIndex:index appendingInTree:NO];
});
return 0;
}];複製程式碼
這個閉包會被傳到下面的函式中:
- (void)registerCallAddElement:(WXJSCallAddElement)callAddElement
{
id callAddElementBlock = ^(JSValue *instanceId, JSValue *ref, JSValue *element, JSValue *index, JSValue *ifCallback) {
NSString *instanceIdString = [instanceId toString];
NSDictionary *componentData = [element toDictionary];
NSString *parentRef = [ref toString];
NSInteger insertIndex = [[index toNumber] integerValue];
WXLogDebug(@"callAddElement...%@, %@, %@, %ld", instanceIdString, parentRef, componentData, (long)insertIndex);
return [JSValue valueWithInt32:(int32_t)callAddElement(instanceIdString, parentRef, componentData, insertIndex) inContext:[JSContext currentContext]];
};
_jsContext[@"callAddElement"] = callAddElementBlock;
}複製程式碼
這裡的包裝方法和第一個方法是相同的。這裡暴露給JS的方法名叫callAddElement,函式引數為4個,分別是instanceIdString,componentData元件的資料,parentRef引用編號,insertIndex插入檢視的index。
當JS呼叫callAddElement方法,就會最終執行WXBridgeContext類裡面的WXPerformBlockOnComponentThread閉包。
第三個閉包函式:
[_jsBridge registerCallNativeModule:^NSInvocation*(NSString *instanceId, NSString *moduleName, NSString *methodName, NSArray *arguments, NSDictionary *options) {
WXSDKInstance *instance = [WXSDKManager instanceForID:instanceId];
if (!instance) {
WXLogInfo(@"instance not found for callNativeModule:%@.%@, maybe already destroyed", moduleName, methodName);
return nil;
}
WXModuleMethod *method = [[WXModuleMethod alloc] initWithModuleName:moduleName methodName:methodName arguments:arguments instance:instance];
return [method invoke];
}];複製程式碼
這個閉包會被傳到下面的函式中:
- (void)registerCallNativeModule:(WXJSCallNativeModule)callNativeModuleBlock
{
_jsContext[@"callNativeModule"] = ^JSValue *(JSValue *instanceId, JSValue *moduleName, JSValue *methodName, JSValue *args, JSValue *options) {
NSString *instanceIdString = [instanceId toString];
NSString *moduleNameString = [moduleName toString];
NSString *methodNameString = [methodName toString];
NSArray *argsArray = [args toArray];
NSDictionary *optionsDic = [options toDictionary];
WXLogDebug(@"callNativeModule...%@,%@,%@,%@", instanceIdString, moduleNameString, methodNameString, argsArray);
NSInvocation *invocation = callNativeModuleBlock(instanceIdString, moduleNameString, methodNameString, argsArray, optionsDic);
JSValue *returnValue = [JSValue wx_valueWithReturnValueFromInvocation:invocation inContext:[JSContext currentContext]];
return returnValue;
};
}複製程式碼
這裡暴露給JS的方法名叫callNativeModule,函式引數為5個,分別是instanceIdString,moduleNameString模組名,methodNameString方法名,argsArray引數陣列,optionsDic字典。
當JS呼叫callNativeModule方法,就會最終執行WXBridgeContext類裡面的WXModuleMethod方法。
第四個閉包函式:
[_jsBridge registerCallNativeComponent:^void(NSString *instanceId, NSString *componentRef, NSString *methodName, NSArray *args, NSDictionary *options) {
WXSDKInstance *instance = [WXSDKManager instanceForID:instanceId];
WXComponentMethod *method = [[WXComponentMethod alloc] initWithComponentRef:componentRef methodName:methodName arguments:args instance:instance];
[method invoke];
}];複製程式碼
這個閉包會被傳到下面的函式中:
- (void)registerCallNativeComponent:(WXJSCallNativeComponent)callNativeComponentBlock
{
_jsContext[@"callNativeComponent"] = ^void(JSValue *instanceId, JSValue *componentName, JSValue *methodName, JSValue *args, JSValue *options) {
NSString *instanceIdString = [instanceId toString];
NSString *componentNameString = [componentName toString];
NSString *methodNameString = [methodName toString];
NSArray *argsArray = [args toArray];
NSDictionary *optionsDic = [options toDictionary];
WXLogDebug(@"callNativeComponent...%@,%@,%@,%@", instanceIdString, componentNameString, methodNameString, argsArray);
callNativeComponentBlock(instanceIdString, componentNameString, methodNameString, argsArray, optionsDic);
};
}複製程式碼
這裡暴露給JS的方法名叫callNativeComponent,函式引數為5個,分別是instanceIdString,componentNameString元件名,methodNameString方法名,argsArray引數陣列,optionsDic字典。
當JS呼叫callNativeComponent方法,就會最終執行WXBridgeContext類裡面的WXComponentMethod方法。
總結一下上述暴露給JS的4個方法:
callNative
這個方法是JS用來呼叫任意一個Native方法的。callAddElement
這個方法是JS用來給當前頁面新增檢視元素的。callNativeModule
這個方法是JS用來呼叫模組裡面暴露出來的方法。callNativeComponent
這個方法是JS用來呼叫元件裡面暴露出來的方法。
Weex在佈局的時候就只會用到前2個方法。
####(一)createRoot:
當JSFramework把JS檔案轉換類似JSON的檔案之後,就開始呼叫Native的callNative方法。
callNative方法會最終執行WXBridgeContext類裡面的[weakSelf invokeNative:instance tasks:tasks callback:callback]方法。
當前操作處於子執行緒“com.taobao.weex.bridge”中。
- (NSInteger)invokeNative:(NSString *)instanceId tasks:(NSArray *)tasks callback:(NSString __unused*)callback
{
WXAssertBridgeThread();
if (!instanceId || !tasks) {
WX_MONITOR_FAIL(WXMTNativeRender, WX_ERR_JSFUNC_PARAM, @"JS call Native params error!");
return 0;
}
WXSDKInstance *instance = [WXSDKManager instanceForID:instanceId];
if (!instance) {
WXLogInfo(@"instance already destroyed, task ignored");
return -1;
}
// 根據JS傳送過來的方法,進行轉換成Native方法呼叫
for (NSDictionary *task in tasks) {
NSString *methodName = task[@"method"];
NSArray *arguments = task[@"args"];
if (task[@"component"]) {
NSString *ref = task[@"ref"];
WXComponentMethod *method = [[WXComponentMethod alloc] initWithComponentRef:ref methodName:methodName arguments:arguments instance:instance];
[method invoke];
} else {
NSString *moduleName = task[@"module"];
WXModuleMethod *method = [[WXModuleMethod alloc] initWithModuleName:moduleName methodName:methodName arguments:arguments instance:instance];
[method invoke];
}
}
// 如果有回撥,回撥給JS
[self performSelector:@selector(_sendQueueLoop) withObject:nil];
return 1;
}複製程式碼
這裡會把JS從傳送過來的callNative方法轉換成Native的元件component的方法呼叫或者模組module的方法呼叫。
舉個例子:
JS從callNative方法傳過來3個引數
instance:0,
tasks:(
{
args = (
{
attr = {
};
ref = "_root";
style = {
alignItems = center;
};
type = div;
}
);
method = createBody;
module = dom;
}
),
callback:-1複製程式碼
tasks陣列裡面會解析出各個方法和呼叫者。
這個例子裡面就會解析出Dom模組的createBody方法。
接著就會呼叫Dom模組的createBody方法。
if (isSync) {
[invocation invoke];
return invocation;
} else {
[self _dispatchInvocation:invocation moduleInstance:moduleInstance];
return nil;
}複製程式碼
呼叫方法之前,有一個執行緒切換的步驟。如果是同步方法,那麼就直接呼叫,如果是非同步方法,那麼嗨需要進行執行緒轉換。
Dom模組的createBody方法是非同步的方法,於是就需要呼叫_dispatchInvocation: moduleInstance:方法。
- (void)_dispatchInvocation:(NSInvocation *)invocation moduleInstance:(id<WXModuleProtocol>)moduleInstance
{
// dispatch to user specified queue or thread, default is main thread
dispatch_block_t dispatchBlock = ^ (){
[invocation invoke];
};
NSThread *targetThread = nil;
dispatch_queue_t targetQueue = nil;
if([moduleInstance respondsToSelector:@selector(targetExecuteQueue)]){
// 判斷當前是否有Queue,如果沒有,就返回main_queue,如果有,就切換到targetQueue
targetQueue = [moduleInstance targetExecuteQueue] ?: dispatch_get_main_queue();
} else if([moduleInstance respondsToSelector:@selector(targetExecuteThread)]){
// 判斷當前是否有Thread,如果沒有,就返回主執行緒,如果有,就切換到targetThread
targetThread = [moduleInstance targetExecuteThread] ?: [NSThread mainThread];
} else {
targetThread = [NSThread mainThread];
}
WXAssert(targetQueue || targetThread, @"No queue or thread found for module:%@", moduleInstance);
if (targetQueue) {
dispatch_async(targetQueue, dispatchBlock);
} else {
WXPerformBlockOnThread(^{
dispatchBlock();
}, targetThread);
}
}複製程式碼
在整個Weex模組中,目前只有2個模組是有targetQueue的,一個是WXClipboardModule,另一個是WXStorageModule。所以這裡沒有targetQueue,就只能切換到對應的targetThread上。
void WXPerformBlockOnThread(void (^ _Nonnull block)(), NSThread *thread)
{
[WXUtility performBlock:block onThread:thread];
}
+ (void)performBlock:(void (^)())block onThread:(NSThread *)thread
{
if (!thread || !block) return;
// 如果當前執行緒不是目標執行緒上,就要切換執行緒
if ([NSThread currentThread] == thread) {
block();
} else {
[self performSelector:@selector(_performBlock:)
onThread:thread
withObject:[block copy]
waitUntilDone:NO];
}
}複製程式碼
這裡就是切換執行緒的操作,如果當前執行緒不是目標執行緒,就要切換執行緒。在目標執行緒上呼叫_performBlock:方法,入參還是最初傳進來的block閉包。
切換前執行緒處於子執行緒“com.taobao.weex.bridge”中。
在WXDomModule中呼叫targetExecuteThread方法
- (NSThread *)targetExecuteThread
{
return [WXComponentManager componentThread];
}複製程式碼
切換執行緒之後,當前執行緒變成了“com.taobao.weex.component”。
- (void)createBody:(NSDictionary *)body
{
[self performBlockOnComponentManager:^(WXComponentManager *manager) {
[manager createRoot:body];
}];
}
- (void)performBlockOnComponentManager:(void(^)(WXComponentManager *))block
{
if (!block) {
return;
}
__weak typeof(self) weakSelf = self;
WXPerformBlockOnComponentThread(^{
WXComponentManager *manager = weakSelf.weexInstance.componentManager;
if (!manager.isValid) {
return;
}
// 開啟元件任務
[manager startComponentTasks];
block(manager);
});
}複製程式碼
當呼叫了Dom模組的createBody方法以後,會先呼叫WXComponentManager的startComponentTasks方法,再呼叫createRoot:方法。
這裡會初始化一個WXComponentManager。
- (WXComponentManager *)componentManager
{
if (!_componentManager) {
_componentManager = [[WXComponentManager alloc] initWithWeexInstance:self];
}
return _componentManager;
}
- (instancetype)initWithWeexInstance:(id)weexInstance
{
if (self = [self init]) {
_weexInstance = weexInstance;
_indexDict = [NSMapTable strongToWeakObjectsMapTable];
_fixedComponents = [NSMutableArray wx_mutableArrayUsingWeakReferences];
_uiTaskQueue = [NSMutableArray array];
_isValid = YES;
[self _startDisplayLink];
}
return self;
}複製程式碼
WXComponentManager的初始化重點是會開啟DisplayLink,它會開啟一個runloop。
- (void)_startDisplayLink
{
WXAssertComponentThread();
if(!_displayLink){
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_handleDisplayLink)];
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
}複製程式碼
displayLink一旦開啟,被加入到當前runloop之中,每次runloop迴圈一次都會執行重新整理佈局的方法_handleDisplayLink。
- (void)startComponentTasks
{
[self _awakeDisplayLink];
}
- (void)_awakeDisplayLink
{
WXAssertComponentThread();
if(_displayLink && _displayLink.paused) {
_displayLink.paused = NO;
}
}複製程式碼
WXComponentManager的startComponentTasks方法僅僅是更改了CADisplayLink的paused的狀態。CADisplayLink就是用來重新整理layout的。
@implementation WXComponentManager
{
// 對WXSDKInstance的弱引用
__weak WXSDKInstance *_weexInstance;
// 當前WXComponentManager是否可用
BOOL _isValid;
// 是否停止重新整理佈局
BOOL _stopRunning;
NSUInteger _noTaskTickCount;
// access only on component thread
NSMapTable<NSString *, WXComponent *> *_indexDict;
NSMutableArray<dispatch_block_t> *_uiTaskQueue;
WXComponent *_rootComponent;
NSMutableArray *_fixedComponents;
css_node_t *_rootCSSNode;
CADisplayLink *_displayLink;
}複製程式碼
以上就是WXComponentManager的所有屬性,可以看出WXComponentManager就是用來處理UI任務的。
再來看看createRoot:方法:
- (void)createRoot:(NSDictionary *)data
{
WXAssertComponentThread();
WXAssertParam(data);
// 1.建立WXComponent,作為rootComponent
_rootComponent = [self _buildComponentForData:data];
// 2.初始化css_node_t,作為rootCSSNode
[self _initRootCSSNode];
__weak typeof(self) weakSelf = self;
// 3.新增UI任務到uiTaskQueue陣列中
[self _addUITask:^{
__strong typeof(self) strongSelf = weakSelf;
strongSelf.weexInstance.rootView.wx_component = strongSelf->_rootComponent;
[strongSelf.weexInstance.rootView addSubview:strongSelf->_rootComponent.view];
}];
}複製程式碼
這裡幹了3件事情:
1.建立WXComponent
- (WXComponent *)_buildComponentForData:(NSDictionary *)data
{
NSString *ref = data[@"ref"];
NSString *type = data[@"type"];
NSDictionary *styles = data[@"style"];
NSDictionary *attributes = data[@"attr"];
NSArray *events = data[@"event"];
Class clazz = [WXComponentFactory classWithComponentName:type];
WXComponent *component = [[clazz alloc] initWithRef:ref type:type styles:styles attributes:attributes events:events weexInstance:self.weexInstance];
WXAssert(component, @"Component build failed for data:%@", data);
[_indexDict setObject:component forKey:component.ref];
return component;
}複製程式碼
這裡的入參data是之前的tasks陣列。
- (instancetype)initWithRef:(NSString *)ref
type:(NSString *)type
styles:(NSDictionary *)styles
attributes:(NSDictionary *)attributes
events:(NSArray *)events
weexInstance:(WXSDKInstance *)weexInstance
{
if (self = [super init]) {
pthread_mutexattr_init(&_propertMutexAttr);
pthread_mutexattr_settype(&_propertMutexAttr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&_propertyMutex, &_propertMutexAttr);
_ref = ref;
_type = type;
_weexInstance = weexInstance;
_styles = [self parseStyles:styles];
_attributes = attributes ? [NSMutableDictionary dictionaryWithDictionary:attributes] : [NSMutableDictionary dictionary];
_events = events ? [NSMutableArray arrayWithArray:events] : [NSMutableArray array];
_subcomponents = [NSMutableArray array];
_absolutePosition = CGPointMake(NAN, NAN);
_isNeedJoinLayoutSystem = YES;
_isLayoutDirty = YES;
_isViewFrameSyncWithCalculated = YES;
_async = NO;
//TODO set indicator style
if ([type isEqualToString:@"indicator"]) {
_styles[@"position"] = @"absolute";
if (!_styles[@"left"] && !_styles[@"right"]) {
_styles[@"left"] = @0.0f;
}
if (!_styles[@"top"] && !_styles[@"bottom"]) {
_styles[@"top"] = @0.0f;
}
}
// 設定NavBar的Style
[self _setupNavBarWithStyles:_styles attributes:_attributes];
// 根據style初始化cssNode資料結構
[self _initCSSNodeWithStyles:_styles];
// 根據style初始化View的各個屬性
[self _initViewPropertyWithStyles:_styles];
// 處理Border的圓角,邊線寬度,背景顏色等屬性
[self _handleBorders:styles isUpdating:NO];
}
return self;
}複製程式碼
上述函式就是初始化WXComponent的佈局的各個屬性。這裡會用到FlexBox裡面的一些計算屬性的方法就在_initCSSNodeWithStyles:方法裡面。
- (void)_initCSSNodeWithStyles:(NSDictionary *)styles
{
_cssNode = new_css_node();
_cssNode->print = cssNodePrint;
_cssNode->get_child = cssNodeGetChild;
_cssNode->is_dirty = cssNodeIsDirty;
if ([self measureBlock]) {
_cssNode->measure = cssNodeMeasure;
}
_cssNode->context = (__bridge void *)self;
// 重新計算_cssNode需要佈局的子檢視個數
[self _recomputeCSSNodeChildren];
// 將style各個屬性都填充到cssNode資料結構中
[self _fillCSSNode:styles];
// To be in conformity with Android/Web, hopefully remove this in the future.
if ([self.ref isEqualToString:WX_SDK_ROOT_REF]) {
if (isUndefined(_cssNode->style.dimensions[CSS_HEIGHT]) && self.weexInstance.frame.size.height) {
_cssNode->style.dimensions[CSS_HEIGHT] = self.weexInstance.frame.size.height;
}
if (isUndefined(_cssNode->style.dimensions[CSS_WIDTH]) && self.weexInstance.frame.size.width) {
_cssNode->style.dimensions[CSS_WIDTH] = self.weexInstance.frame.size.width;
}
}
}複製程式碼
在_fillCSSNode:方法裡面會對FlexBox演算法裡面定義的各個屬性值就行賦值。
2.初始化css_node_t
在這裡,準備開始Layout之前,我們需要先初始化rootCSSNode
- (void)_initRootCSSNode
{
_rootCSSNode = new_css_node();
// 根據頁面weexInstance設定rootCSSNode的座標和寬高尺寸
[self _applyRootFrame:self.weexInstance.frame toRootCSSNode:_rootCSSNode];
_rootCSSNode->style.flex_wrap = CSS_NOWRAP;
_rootCSSNode->is_dirty = rootNodeIsDirty;
_rootCSSNode->get_child = rootNodeGetChild;
_rootCSSNode->context = (__bridge void *)(self);
_rootCSSNode->children_count = 1;
}複製程式碼
在上述方法中,會初始化rootCSSNode的座標和寬高尺寸。
3.新增UI任務到uiTaskQueue陣列中
[self _addUITask:^{
__strong typeof(self) strongSelf = weakSelf;
strongSelf.weexInstance.rootView.wx_component = strongSelf->_rootComponent;
[strongSelf.weexInstance.rootView addSubview:strongSelf->_rootComponent.view];
}];複製程式碼
WXComponentManager會把當前的元件以及它對應的View新增到頁面Instance的rootView上面的這個任務,新增到uiTaskQueue陣列中。
_rootComponent.view會建立元件對應的WXView,這個是繼承自UIView的。所以Weex通過JS程式碼建立出來的控制元件都是原生的,都是WXView型別的,實質就是UIView。建立UIView這一步又是回到主執行緒中執行的。
最後顯示到頁面上的工作,是由displayLink的重新整理方法在主執行緒重新整理UI顯示的。
- (void)_handleDisplayLink
{
[self _layoutAndSyncUI];
}
- (void)_layoutAndSyncUI
{
// Flexbox佈局
[self _layout];
if(_uiTaskQueue.count > 0){
// 同步執行UI任務
[self _syncUITasks];
_noTaskTickCount = 0;
} else {
// 如果當前一秒內沒有任務,那麼智慧的掛起displaylink,以節約CPU時間
_noTaskTickCount ++;
if (_noTaskTickCount > 60) {
[self _suspendDisplayLink];
}
}
}複製程式碼
_layoutAndSyncUI是佈局和重新整理UI的核心流程。每次重新整理一次,都會先呼叫Flexbox演算法的Layout進行佈局,這個佈局是在子執行緒“com.taobao.weex.component”執行的。接著再去檢視當前是否有UI任務需要執行,如果有,就切換到主執行緒進行UI重新整理操作。
這裡還會有一個智慧的掛起操作。就是判斷一秒內如果都沒有任務,那麼就掛起displaylink,以節約CPU時間。
- (void)_layout
{
BOOL needsLayout = NO;
NSEnumerator *enumerator = [_indexDict objectEnumerator];
WXComponent *component;
// 判斷當前是否需要佈局,即是判斷當前元件的_isLayoutDirty這個BOLL屬性值
while ((component = [enumerator nextObject])) {
if ([component needsLayout]) {
needsLayout = YES;
break;
}
}
if (!needsLayout) {
return;
}
// Flexbox的演算法核心函式
layoutNode(_rootCSSNode, _rootCSSNode->style.dimensions[CSS_WIDTH], _rootCSSNode->style.dimensions[CSS_HEIGHT], CSS_DIRECTION_INHERIT);
NSMutableSet<WXComponent *> *dirtyComponents = [NSMutableSet set];
[_rootComponent _calculateFrameWithSuperAbsolutePosition:CGPointZero gatherDirtyComponents:dirtyComponents];
// 計算當前weexInstance的rootView.frame,並且重置rootCSSNode的Layout
[self _calculateRootFrame];
// 在每個需要佈局的元件之間
for (WXComponent *dirtyComponent in dirtyComponents) {
[self _addUITask:^{
[dirtyComponent _layoutDidFinish];
}];
}
}複製程式碼
_indexDict裡面維護了一張整個頁面的佈局結構的Map,舉個例子:
NSMapTable {
[7] _root -> <div ref=_root> <WXView: 0x7fc59a416140; frame = (0 0; 331.333 331.333); layer = <WXLayer: 0x608000223180>>
[12] 5 -> <image ref=5> <WXImageView: 0x7fc59a724430; baseClass = UIImageView; frame = (110.333 192.333; 110.333 110.333); clipsToBounds = YES; layer = <WXLayer: 0x60000002f780>>
[13] 3 -> <image ref=3> <WXImageView: 0x7fc59a617a00; baseClass = UIImageView; frame = (110.333 55.3333; 110.333 110.333); clipsToBounds = YES; opaque = NO; gestureRecognizers = <NSArray: 0x60000024b760>; layer = <WXLayer: 0x60000003e8c0>>
[15] 4 -> <text ref=4> <WXText: 0x7fc59a509840; text: hello Weex; frame:0.000000,441.666667,331.333333,26.666667 frame = (0 441.667; 331.333 26.6667); opaque = NO; layer = <WXLayer: 0x608000223480>>
}複製程式碼
所有的元件都是由ref引用值作為Key儲存的,只要知道這個頁面上全域性唯一的ref,就可以拿到這個ref對應的元件。
_layout會先判斷當前是否有需要佈局的元件,如果有,就從rootCSSNode開始進行Flexbox演算法的Layout。執行完成以後還需要調整一次rootView的frame,最後新增一個UI任務到taskQueue中,這個任務標記的是元件佈局完成。
注意上述所有佈局操作都是在子執行緒“com.taobao.weex.component”中執行的。
- (void)_syncUITasks
{
// 用blocks接收原來uiTaskQueue裡面的所有任務
NSArray<dispatch_block_t> *blocks = _uiTaskQueue;
// 清空uiTaskQueue
_uiTaskQueue = [NSMutableArray array];
// 在主執行緒中依次執行uiTaskQueue裡面的所有閉包
dispatch_async(dispatch_get_main_queue(), ^{
for(dispatch_block_t block in blocks) {
block();
}
});
}複製程式碼
佈局完成以後就呼叫同步的UI重新整理方法。注意這裡要對UI進行操作,一定要切換回主執行緒。
(二)callAddElement
在子執行緒“com.taobao.weex.bridge”中,會一直相應來自JSFramework呼叫Native的方法。
[_jsBridge registerCallAddElement:^NSInteger(NSString *instanceId, NSString *parentRef, NSDictionary *elementData, NSInteger index) {
// Temporary here , in order to improve performance, will be refactored next version.
WXSDKInstance *instance = [WXSDKManager instanceForID:instanceId];
if (!instance) {
WXLogInfo(@"instance not found, maybe already destroyed");
return -1;
}
WXPerformBlockOnComponentThread(^{
WXComponentManager *manager = instance.componentManager;
if (!manager.isValid) {
return;
}
[manager startComponentTasks];
[manager addComponent:elementData toSupercomponent:parentRef atIndex:index appendingInTree:NO];
});
return 0;
}];複製程式碼
當JSFramework呼叫callAddElement方法,就會執行上述程式碼的閉包函式。這裡會接收來自JS的4個入參。
舉個例子,JSFramework可能會通過callAddElement方法傳過來這樣4個引數:
0,
_root,
{
attr = {
value = "Hello World";
};
ref = 4;
style = {
color = "#000000";
fontSize = 40;
};
type = text;
},
-1複製程式碼
這裡的insertIndex為0,parentRef是_root,componentData是當前要建立的元件的資訊,instanceIdString是-1。
之後WXComponentManager就會呼叫startComponentTasks開始displaylink繼續準備重新整理佈局,最後呼叫addComponent: toSupercomponent: atIndex: appendingInTree:方法新增新的元件。
注意,WXComponentManager的這兩步操作,又要切換執行緒,切換到“com.taobao.weex.component”子執行緒中。
- (void)addComponent:(NSDictionary *)componentData toSupercomponent:(NSString *)superRef atIndex:(NSInteger)index appendingInTree:(BOOL)appendingInTree
{
WXComponent *supercomponent = [_indexDict objectForKey:superRef];
WXAssertComponentExist(supercomponent);
[self _recursivelyAddComponent:componentData toSupercomponent:supercomponent atIndex:index appendingInTree:appendingInTree];
}複製程式碼
WXComponentManager會在“com.taobao.weex.component”子執行緒中遞迴的新增子元件。
- (void)_recursivelyAddComponent:(NSDictionary *)componentData toSupercomponent:(WXComponent *)supercomponent atIndex:(NSInteger)index appendingInTree:(BOOL)appendingInTree
{
// 根據componentData構建元件
WXComponent *component = [self _buildComponentForData:componentData];
index = (index == -1 ? supercomponent->_subcomponents.count : index);
[supercomponent _insertSubcomponent:component atIndex:index];
// 用_lazyCreateView標識懶載入
if(supercomponent && component && supercomponent->_lazyCreateView) {
component->_lazyCreateView = YES;
}
// 插入一個UI任務
[self _addUITask:^{
[supercomponent insertSubview:component atIndex:index];
}];
NSArray *subcomponentsData = [componentData valueForKey:@"children"];
BOOL appendTree = !appendingInTree && [component.attributes[@"append"] isEqualToString:@"tree"];
// 再次遞迴的規則:如果父檢視是一個樹狀結構,子檢視即使也是一個樹狀結構,也不能再次Layout
for(NSDictionary *subcomponentData in subcomponentsData){
[self _recursivelyAddComponent:subcomponentData toSupercomponent:component atIndex:-1 appendingInTree:appendTree || appendingInTree];
}
if (appendTree) {
// 如果當前元件是樹狀結構,強制重新整理layout,以防在syncQueue中堆積太多的同步任務。
[self _layoutAndSyncUI];
}
}複製程式碼
在遞迴的新增子元件的時候,如果是樹狀結構,還需要再次強制進行一次layout,同步一次UI。這裡呼叫[self _layoutAndSyncUI]方法和createRoot:時候實現是完全一樣的,下面就不再贅述了。
這裡會迴圈新增多個子檢視,相應的也會呼叫多次Layout方法。
(三)createFinish
當所有的檢視都新增完成以後,JSFramework就是再次呼叫callNative方法。
還是會傳過來3個引數。
instance:0,
tasks:(
{
args = (
);
method = createFinish;
module = dom;
}
),
callback:-1複製程式碼
callNative通過這個引數會呼叫到WXDomModule的createFinish方法。這裡的具體實現見第一步的callNative,這裡不再贅述。
- (void)createFinish
{
[self performBlockOnComponentManager:^(WXComponentManager *manager) {
[manager createFinish];
}];
}複製程式碼
這裡最終也是會呼叫到WXComponentManager的createFinish。當然這裡是會進行執行緒切換,切換到WXComponentManager的執行緒“com.taobao.weex.component”子執行緒上。
- (void)createFinish
{
WXAssertComponentThread();
WXSDKInstance *instance = self.weexInstance;
[self _addUITask:^{
UIView *rootView = instance.rootView;
WX_MONITOR_INSTANCE_PERF_END(WXPTFirstScreenRender, instance);
WX_MONITOR_INSTANCE_PERF_END(WXPTAllRender, instance);
WX_MONITOR_SUCCESS(WXMTJSBridge);
WX_MONITOR_SUCCESS(WXMTNativeRender);
if(instance.renderFinish){
instance.renderFinish(rootView);
}
}];
}複製程式碼
WXComponentManager的createFinish方法最後就是新增一個UI任務,回撥到主執行緒的renderFinish方法裡面。
至此,Weex的佈局流程就完成了。
最後
雖然Autolayout是蘋果原生就支援的自動佈局方案,但是在稍微複雜的介面就會出現效能問題。大半年前,Draveness的這篇《從 Auto Layout 的佈局演算法談效能》文章裡面也稍微“批判”了Autolayout的效能問題,但是文章裡面最後提到的是用ASDK的方法來解決問題。本篇文章則獻上另外一種可用的佈局方法——FlexBox,並且帶上了經過大量測試的測試資料,向大左的這篇經典文章致敬!
如今,iOS平臺上幾大可用的佈局方法有:Frame原生布局,Autolayout原生自動佈局,FlexBox的Yoga實現,ASDK。
當然,基於這4種基本方案以外,還有一些組合方法,比如Weex的這種,用JS的CSS解析成類似JSON的DOM,再呼叫Native的FlexBox演算法進行佈局。前段時間還有來自美團的《佈局編碼的未來》裡面提到的畢加索(picasso)佈局方法。原理也是會用到JSCore,將JS寫的JSON或者自定義的DSL,經過本地的picassoEngine佈局引擎轉換成Native佈局,最終利用錨點的概念做到高效的佈局。
最後,推薦2個iOS平臺上比較優秀的利用了FlexBox的原理的開源庫:
來自Facebook的yoga
來自餓了麼的FlexBoxLayout