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

前言

在上篇文章裏面談了Weex在iOS客戶端工做的基本流程。這篇文章將會詳細的分析Weex是如何高性能的佈局原生界面的,以後還會與現有的佈局方法進行對比,看看Weex的佈局性能究竟如何。css

目錄

  • 1.Weex佈局算法
  • 2.Weex佈局算法性能分析
  • 3.Weex是如何佈局原生界面的

一. Weex佈局算法

打開Weex的源碼的Layout文件夾,就會看到兩個c的文件,這兩個文件就是今天要談的Weex的佈局引擎。html

Layout.h和Layout.c最開始是來自於React-Native裏面的代碼。也就是說Weex和React-Native的佈局引擎都是同一套代碼。前端

當前React-Native的代碼裏面已經沒有這兩個文件了,而是換成了Yoga。node

Yoga本是Facebook在React Native裏引入的一種跨平臺的基於CSS的佈局引擎,它實現了Flexbox規範,徹底遵照W3C的規範。隨着該系統不斷完善,Facebook對其進行從新發布,因而就成了如今的Yoga(Yoga官網)。css3

那麼Flexbox是什麼呢?git

熟悉前端的同窗必定很熟悉這個概念。2009年,W3C提出了一種新的方案——Flex佈局,能夠簡便、完整、響應式地實現各類頁面佈局。目前,它已經獲得了幾乎全部瀏覽器的支持,目前的前端主要是使用Html / CSS / JS實現,其中CSS用於前端的佈局。任何一個Html的容器能夠經過css指定爲Flex佈局,一旦一個容器被指定爲Flex佈局,其子元素就能夠按照FlexBox的語法進行佈局。github

關於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):

  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算法進行測試。因爲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個方法:

  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是蘋果原生就支持的自動佈局方案,可是在稍微複雜的界面就會出現性能問題。大半年前,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

相關文章
相關標籤/搜索