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

BillieReich 8年前發布 | 17K 次閱讀 iOS開發 算法 Weex 移動開發 flexbox

前言

這篇文章將會詳細的分析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本是非死book在React Native里引入的一種跨平臺的基于CSS的布局引擎,它實現了Flexbox規范,完全遵守W3C的規范。隨著該系統不斷完善,非死book對其進行重新發布,于是就成了現在的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。

這塊代碼實現的算法原理請參見 http://www.w3.org/TR/2012/CR-css3-flexbox-20120918/#layout-algorithm 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的實現是非死book的開源庫Yoga的前身,所以這里可以把兩個看成是一種實現。

Weex的這種FlexBox的實現其實只是W3C標準的一個實現的子集,因為FlexBox的官方標準里面還有一些并沒有實現出來。W3C上定義的FlexBox的標準,文檔在 這里

FlexBox標準定義:

針對父視圖 (flex container):

  1. display
  2. flex-direction
  3. flex-wrap
  4. flex-flow
  5. justify-content
  6. align-items
  7. align-content

針對子視圖 (flex items):

  1. order
  2. flex-grow
  3. flex-shrink
  4. flex-basis
  5. flex
  6. align-self

相比官方的定義,上述的實現有一些限制:

  1. 所有顯示屬性的node節點都默認假定是Flex的視圖,當然這里要除去文本節點,因為它會被假定為inline-flex。
  2. 不支持zIndex的屬性,包括任何z上的排序。所有的node節點都是按照代碼書寫的先后順序進行排列的。Weex 目前也不支持 z-index 設置元素層級關系,但靠后的元素層級更高,因此,對于層級高的元素,可將其排列在后面。
  3. FlexBox里面定義的order屬性,也不支持。flex item默認按照代碼書寫順序。
  4. visibility屬性默認都是可見的,暫時不支持邊緣塌陷合并(collapse)和隱藏(hidden)屬性。
  5. 不支持forced breaks。
  6. 不支持垂直方向的inline(比如從上到下的text,或者從下到上的text)

關于Flexbox 在iOS這邊的具體實現上一章節已經分析過了。

接下來仔細分析一下Autolayout的具體實現

原來我們用Frame進行布局的時候,需要知道一個點(origin或者center)和寬高就可以確定一個View。

現在換成了Autolayout,每個View需要知道4個尺寸。left,top,width,height。

但是一個View的約束是相對于另一個View的,比如說相對于父視圖,或者是相對于兩兩View之間的。

那么兩兩個View之間的約束就會變成一個八元一次的方程組。

解這個方程組可能有以下3種情況:

  1. 當方程組的解的個數有無窮多個,最終會得到欠約束的有歧義的布局。
  2. 當方程無解時,則表示約束有沖突。
  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線性約束算法會去求處符合條件的最優值。

下面是兩位學生寫的相關的論文,有興趣的可以讀一下,了解一下算法的具體實現:

  1. 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.
  2. 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 )
  3. 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算法進行測試。由于非死book對它進行了很好的封裝,使用起來也很方便。雖然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。

在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個方法:

  1. callNative

    這個方法是JS用來調用任意一個Native方法的。

  2. callAddElement

    這個方法是JS用來給當前頁面添加視圖元素的。

  3. callNativeModule

    這個方法是JS用來調用模塊里面暴露出來的方法。

  4. 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是蘋果原生就支持的自動布局方案,但是在稍微復雜的界面就會出現性能問題。本篇文章則獻上另外一種可用的布局方法——FlexBox,并且帶上了經過大量測試的測試數據,向大左的這篇經典文章致敬!

如今,iOS平臺上幾大可用的布局方法有:Frame原生布局,Autolayout原生自動布局,FlexBox的Yoga實現,ASDK。

當然,基于這4種基本方案以外,還有一些組合方法,比如Weex的這種,用JS的CSS解析成類似JSON的DOM,再調用Native的FlexBox算法進行布局。原理也是會用到JSCore,將JS寫的JSON或者自定義的DSL,經過本地的picassoEngine布局引擎轉換成Native布局,最終利用錨點的概念做到高效的布局。

 

來自:http://www.jianshu.com/p/d085032d4788

 

 本文由用戶 BillieReich 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!