由 FlexBox 算法強(qiáng)力驅(qū)動的 Weex 布局引擎

前言

在上篇文章里面談了Weex在iOS客戶端工作的基本流程龙亲。這篇文章將會詳細(xì)的分析Weex是如何高性能的布局原生界面的泥技,之后還會與現(xiàn)有的布局方法進(jìn)行對比蜕便,看看Weex的布局性能究竟如何。

目錄

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

一. Weex布局算法

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

Layout.h和Layout.c最開始是來自于React-Native里面的代碼思灰。也就是說Weex和React-Native的布局引擎都是同一套代碼领跛。

當(dāng)前React-Native的代碼里面已經(jīng)沒有這兩個文件了,而是換成了Yoga肉津。

Yoga本是Facebook在React Native里引入的一種跨平臺的基于CSS的布局引擎悍引,它實現(xiàn)了Flexbox規(guī)范势腮,完全遵守W3C的規(guī)范狸眼。隨著該系統(tǒng)不斷完善,F(xiàn)acebook對其進(jìn)行重新發(fā)布眶诈,于是就成了現(xiàn)在的Yoga(Yoga官網(wǎng))浴骂。

那么Flexbox是什么呢?

熟悉前端的同學(xué)一定很熟悉這個概念宪潮。2009年,W3C提出了一種新的方案——Flex布局弯屈,可以簡便遇汞、完整、響應(yīng)式地實現(xiàn)各種頁面布局怠益。目前秉版,它已經(jīng)得到了幾乎所有瀏覽器的支持督函,目前的前端主要是使用Html / CSS / JS實現(xiàn)逼庞,其中CSS用于前端的布局奠滑。任何一個Html的容器可以通過css指定為Flex布局,一旦一個容器被指定為Flex布局妒穴,其子元素就可以按照FlexBox的語法進(jìn)行布局宋税。

關(guān)于FlexBox的基本定義,更加詳細(xì)的文檔說明讼油,感興趣的同學(xué)可以去閱讀一下W3C的官方文檔弃甥,那里會有很詳細(xì)的說明。官方文檔鏈接

Weex中的Layout文件是Yoga的前身汁讼,是Yoga正式發(fā)布之前的版本淆攻。底層代碼使用C語言代碼,所以性能也不是問題嘿架。接下來就仔細(xì)分析Layout文件是如何實現(xiàn)FlexBox的瓶珊。

故以下源碼分析都基于v0.10.0這個版本。

(一)FlexBox中的基本數(shù)據(jù)結(jié)構(gòu)

Flexbox布局(Flexible Box)設(shè)計之初的目的是為了能更加高效的分配子視圖的布局情況耸彪,包括動態(tài)的改變寬度伞芹,高度,以及排列順序蝉娜。Flexbox可以更加方便的兼容各個大小不同的屏幕唱较,比如拉伸和壓縮子視圖。

在FlexBox的世界里召川,存在著主軸和側(cè)軸的概念南缓。

大多數(shù)情況,子視圖都是沿著主軸(main axis)荧呐,從主軸起點(diǎn)(main-start)到主軸終點(diǎn)(main-end)排列汉形。但是這里需要注意的一點(diǎn)是,主軸和側(cè)軸雖然永遠(yuǎn)是垂直的關(guān)系倍阐,但是誰是水平概疆,誰是豎直,并沒有確定峰搪,有可能會有如下的情況:

在上圖這種水平是側(cè)軸的情況下岔冀,子視圖是沿著側(cè)軸(cross axis),從側(cè)軸起點(diǎn)(cross-start)到側(cè)軸終點(diǎn)(cross-end)排列的概耻。

主軸(main axis):父視圖的主軸使套,子視圖主要沿著這條軸進(jìn)行排列布局罐呼。

主軸起點(diǎn)(main-start)和主軸終點(diǎn)(main-end):子視圖在父視圖里面布局的方向是從主軸起點(diǎn)(main-start)向主軸終點(diǎn)(main-start)的方向。

主軸尺寸(main size):子視圖在主軸方向的寬度或高度就是主軸的尺寸童漩。子視圖主要的大小屬性要么是寬度,要么是高度屬性春锋,由哪一個對著主軸方向決定矫膨。

側(cè)軸(cross axis):垂直于主軸稱為側(cè)軸。它的方向主要取決于主軸方向期奔。

側(cè)軸起點(diǎn)(cross-start)和側(cè)軸終點(diǎn)(cross-end):子視圖行的配置從容器的側(cè)軸起點(diǎn)邊開始侧馅,往側(cè)軸終點(diǎn)邊結(jié)束。

側(cè)軸尺寸(cross size):子視圖的在側(cè)軸方向的寬度或高度就是項目的側(cè)軸長度呐萌,伸縮項目的側(cè)軸長度屬性是「width」或「height」屬性馁痴,由哪一個對著側(cè)軸方向決定。

接下來看看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;

這是定義的子視圖在側(cè)軸上的對齊方式掠归。

在Weex這里定義了三種屬于css_align_t類型的方式缅叠,align_content,align_items虏冻,align_self肤粱。這三種類型的對齊方式略有不同。

ALIGN_AUTO只是針對align_self的一個默認(rèn)值厨相,但是對于align_content领曼,align_items子視圖的對齊方式是無效的值。

1.align_items

align_items定義的是子視圖在一行里面?zhèn)容S上排列的方式蛮穿。

上圖是ALIGN_FLEX_START

上圖是ALIGN_CENTER

上圖是ALIGN_FLEX_END

上圖是ALIGN_STRETCH

align_items在W3C的定義里面其實還有一個種baseline的對齊方式庶骄,這里在定義里面并沒有。

注意践磅,上面這種baseline的對齊方式在Weex的定義里面并沒有单刁!

2. align_content

align_content定義的是子視圖行與行之間在側(cè)軸上排列的方式。

上圖是ALIGN_FLEX_START

上圖是ALIGN_CENTER

上圖是ALIGN_FLEX_END

上圖是ALIGN_STRETCH

在FlexBox的W3C的定義里面其實還有兩種方式在Weex沒有定義府适。

上圖的這種對齊方式是對應(yīng)的justify里面的JUSTIFY_SPACE_AROUND羔飞,align-content里面的space-around這種對齊方式在Weex是沒有的。

上圖的這種對齊方式是對應(yīng)的justify里面的JUSTIFY_SPACE_BETWEEN檐春,align-content里面的space-between這種對齊方式在Weex是沒有的褥傍。

3.align_self

最后這一種對齊方式是可以在align_items的基礎(chǔ)上再分別自定義每個子視圖的對齊方式。如果是auto喇聊,是與align_items方式相同恍风。


typedef enum {
  CSS_POSITION_RELATIVE = 0,
  CSS_POSITION_ABSOLUTE
} css_position_type_t;


這個是定義坐標(biāo)地址的類型,有相對坐標(biāo)和絕對坐標(biāo)兩種誓篱。


typedef enum {
  CSS_NOWRAP = 0,
  CSS_WRAP
} css_wrap_type_t;


在Weex里面wrap只有兩種類型朋贬。

上圖是NOWRAP。所有的子視圖都會排列在一行之中窜骄。

上圖是WRAP锦募。所有的子視圖會從左到右,從上到下排列邻遏。

在W3C的標(biāo)準(zhǔn)里面還有一種wrap_reverse的排列方式糠亩。

這種排列方式,是從左到右准验,從下到上進(jìn)行排列赎线,目前在Weex里面沒有定義。


typedef enum {
  CSS_LEFT = 0,
  CSS_TOP,
  CSS_RIGHT,
  CSS_BOTTOM,
  CSS_START,
  CSS_END,
  CSS_POSITION_COUNT
} css_position_t;

這里定義的是坐標(biāo)的描述糊饱。Left和Top因為會出現(xiàn)在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過程都要重復(fù)計算
  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結(jié)構(gòu)體过椎。結(jié)構(gòu)體里面position和dimensions數(shù)組里面分別存儲的是四周的位置和寬高的尺寸疚宇。direction里面存儲的就是LTR還是RTL的方向柜去。

至于下面那些變量信息都是緩存嗓奢,用來防止沒有改變的Lauout還會重復(fù)計算的問題股耽。


typedef struct {
  float dimensions[2];
} css_dim_t;

css_dim_t結(jié)構(gòu)體里面裝的就是子視圖的尺寸信息,寬和高。


typedef struct {
  // 整個頁面CSS的方向,LTR、RTL
  css_direction_t direction;
  // Flex 的方向
  css_flex_direction_t flex_direction;
  // 子視圖在主軸上的排列對齊方式
  css_justify_t justify_content;
  // 子視圖在側(cè)軸上行與行之間的對齊方式
  css_align_t align_content;
  // 子視圖在側(cè)軸上的對齊方式
  css_align_t align_items;
  // 子視圖自己本身的對齊方式
  css_align_t align_self;
  // 子視圖的坐標(biāo)系類型(相對坐標(biāo)系措伐,絕對坐標(biāo)系)
  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的一個節(jié)點(diǎn)的數(shù)據(jù)結(jié)構(gòu)嘁字。它包含了之前的css_style_t和css_layout_t此叠。由于結(jié)構(gòu)體里面無法定義成員函數(shù),所以下面包含4個函數(shù)指針。


css_node_t *new_css_node(void);
void init_css_node(css_node_t *node);
void free_css_node(css_node_t *node);

上面3個函數(shù)是關(guān)于css_node的生命周期相關(guān)的函數(shù)涤浇。


// 新建節(jié)點(diǎn)
css_node_t *new_css_node() {
  css_node_t *node = (css_node_t *)calloc(1, sizeof(*node));
  init_css_node(node);
  return node;
}

// 釋放節(jié)點(diǎn)
void free_css_node(css_node_t *node) {
  free(node);
}


新建節(jié)點(diǎn)的時候就是調(diào)用的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;

  // 注意下面這些數(shù)組里面的值初始化為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;

  // 以下這些用來對比是否發(fā)生變化的緩存變量,初始值都為 -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是按照列排列的掂摔。

接著下面數(shù)組里面存的都是UNDEFINED释移,而不是0熏纯,因為0會和結(jié)構(gòu)體里面的0沖突。

最后緩存的變量初始化都為-1。

接下來定義了4個全局的數(shù)組想际,這4個數(shù)組非常有用,它會決定接下來layout的方向和屬性。4個數(shù)組和軸的方向是相互關(guān)聯(lián)的僻爽。


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 元素都可視作一個盒子。我們一般在討論設(shè)計或布局時滚朵,會提到「盒模型」這個概念冤灾。

盒模型描述了一個元素所占用的空間。每一個盒子有四條邊界:外邊距邊界 margin edge, 邊框邊界 border edge, 內(nèi)邊距邊界 padding edge 與內(nèi)容邊界 content edge辕近。這四層邊界韵吨,形成一層層的盒子包裹起來,這就是盒模型大體上的含義移宅。

盒子模型如上归粉,這個圖是基于LTR,并且主軸在水平方向的漏峰。

所以主軸在不同方向可能就會有不同的情況糠悼。

注意:
Weex 盒模型的 box-sizing 默認(rèn)為 border-box,即盒子的寬高包含內(nèi)容content浅乔、內(nèi)邊距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三個值的數(shù)組存儲有6個值渔期,如果是水平方向,那么CSS_START存儲的都是Leading,CSS_END存儲的都是Trailing疯趟。下面沒有特殊說明拘哨,都按照這個規(guī)則來。


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的思路一樣信峻,水平方向就是取出數(shù)組里面的padding[CSS_START]倦青,如果是豎直方向,就對應(yīng)得取出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一模一樣踢步,這里就不再贅述了癣亚。

四周邊距的計算方法都實現(xiàn)了,接下來就是如何layout了获印。


// 計算布局的方法
void layoutNode(css_node_t *node, float maxWidth, float maxHeight, css_direction_t parentDirection);

// 在調(diào)用layoutNode之前述雾,可以重置node節(jié)點(diǎn)的layout
void resetNodeLayout(css_node_t *node);

重置node節(jié)點(diǎn)的方法就是把節(jié)點(diǎn)的坐標(biāo)重置為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;

  // 對比當(dāng)前環(huán)境是否“干凈”,以及比較待布局的node節(jié)點(diǎn)和上次節(jié)點(diǎn)是否完全一致地粪。
  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) {
    // 把緩存的值直接賦值給當(dāng)前的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節(jié)點(diǎn)
    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));
    }

    // 布局視圖的核心實現(xiàn)
    layoutNodeImpl(node, parentMaxWidth, parentMaxHeight, parentDirection);

    // 布局完成取募,把此次的布局緩存起來,防止下次重復(fù)的布局重復(fù)計算
    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];
  }
}

每步都注釋了蟆技,見上述代碼注釋玩敏,在調(diào)用布局的核心實現(xiàn)layoutNodeImpl之前,會循環(huán)調(diào)用resetNodeLayout质礼,初始化所有子視圖旺聚。

所有的核心實現(xiàn)就在layoutNodeImpl這個方法里面了。Weex里面的這個方法實現(xiàn)有700多行眶蕉,在Yoga的實現(xiàn)中砰粹,布局算法有1000多行。


static void layoutNodeImpl(css_node_t *node, float parentMaxWidth, float parentMaxHeight, css_direction_t parentDirection) {

}


這里分析一下這個算法的主要流程造挽。在Weex的這個實現(xiàn)中碱璃,有7個循環(huán),假設(shè)依次分別標(biāo)上A饭入,B嵌器,C,D谐丢,E爽航,F(xiàn)蚓让,G。

先來看循環(huán)A



    float mainContentDim = 0;
    // 存在3類子視圖讥珍,支持flex的子視圖历极,不支持flex的子視圖,絕對布局的子視圖串述,我們需要知道哪些子視圖是在等待分配空間执解。
    int flexibleChildrenCount = 0;
    float totalFlexible = 0;
    int nonFlexibleChildrenCount = 0;

    // 利用一層循環(huán)在主軸上簡單的堆疊子視圖寞肖,在循環(huán)C中纲酗,會忽略這些已經(jīng)在循環(huán)A中已經(jīng)排列好的子視圖
    bool isSimpleStackMain =
        (isMainDimDefined && justifyContent == CSS_JUSTIFY_FLEX_START) ||
        (!isMainDimDefined && justifyContent != CSS_JUSTIFY_CENTER);
    int firstComplexMain = (isSimpleStackMain ? childCount : startLine);

    // 利用一層循環(huán)在側(cè)軸上簡單的堆疊子視圖,在循環(huán)D中新蟆,會忽略這些已經(jīng)在循環(huán)A中已經(jīng)排列好的子視圖
    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;

    // 循環(huán)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之前觅赊,先預(yù)填充側(cè)軸上可以被拉伸的子視圖
      if (alignItem == CSS_ALIGN_STRETCH &&
          child->style.position_type == CSS_POSITION_RELATIVE &&
          isCrossDimDefined &&
          !isStyleDimDefined(child, crossAxis)) {
          
        // 這里要進(jìn)行一個比較,比較子視圖在側(cè)軸上的尺寸 和 側(cè)軸上減去兩邊的Margin琼稻、padding吮螺、Border剩下的可拉伸的空間 進(jìn)行比較,因為拉伸是不會壓縮原始的大小的帕翻。
        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;

        // 預(yù)填充子視圖嘀掸,這里需要用到視圖在軸上面的絕對坐標(biāo)紫岩,如果是水平軸,需要用到左右的偏移量睬塌,如果是豎直軸泉蝌,需要用到上下的偏移量。
        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)
            );
          }
        }
      }



循環(huán)A的具體實現(xiàn)如上勋陪,注釋見代碼。
循環(huán)A主要是實現(xiàn)的是layout布局中不可以flex的子視圖的布局硫兰,mainContentDim變量是用來記錄所有的尺寸以及所有不能flex的子視圖的margin的總和诅愚。它被用來設(shè)置node節(jié)點(diǎn)的尺寸,和計算剩余空間以便供可flex子視圖進(jìn)行拉伸適配劫映。

每個node節(jié)點(diǎn)的next_absolute_child維護(hù)了一個鏈表违孝,這里存儲的依次是絕對布局視圖的鏈表。

接著需要再統(tǒng)計可以被拉伸的子視圖苏研。


      float nextContentDim = 0;

      // 統(tǒng)計可以拉伸flex的子視圖
      if (isMainDimDefined && isFlex(child)) {
        flexibleChildrenCount++;
        totalFlexible += child->style.flex;

        // 存儲一個鏈表維護(hù)可以flex的子視圖
        if (firstFlexChild == NULL) {
          firstFlexChild = child;
        }
        if (currentFlexChild != NULL) {
          currentFlexChild->next_flex_child = child;
        }
        currentFlexChild = child;

        // 這時我們雖然不知道確切的尺寸信息等浊,但是已經(jīng)知道了padding , border , margin,我們可以利用這些信息來給子視圖確定一個最小的size摹蘑,計算剩余可用的空間筹燕。
        // 下一個content的距離等于當(dāng)前子視圖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;
          }
        }

        // 遞歸調(diào)用layout函數(shù),進(jìn)行不能拉伸的子視圖的布局撒踪。
        if (alreadyComputedNextLayout == 0) {
          layoutNode(child, maxWidth, maxHeight, direction);
        }

        // 由于絕對布局的子視圖的位置和layout無關(guān)过咬,所以我們不能用它們來計算mainContentDim
        if (child->style.position_type == CSS_POSITION_RELATIVE) {
          nonFlexibleChildrenCount++;
          nextContentDim = getDimWithMargin(child, mainAxis);
        }
      }


上述代碼就確定出了不可拉伸的子視圖的布局。

每個node節(jié)點(diǎn)的next_flex_child維護(hù)了一個鏈表制妄,這里存儲的依次是可以flex拉伸視圖的鏈表掸绞。


      // 將要加入的元素可能會被擠到下一行
      if (isNodeFlexWrap &&
          isMainDimDefined &&
          mainContentDim + nextContentDim > definedMainDim &&
          // 如果這里只有一個元素,它可能就需要單獨(dú)占一行
          i != startLine) {
        nonFlexibleChildrenCount--;
        alreadyComputedNextLayout = 1;
        break;
      }

      // 停止在主軸上堆疊子視圖耕捞,剩余的子視圖都在循環(huán)C里面布局
      if (isSimpleStackMain &&
          (child->style.position_type != CSS_POSITION_RELATIVE || isFlex(child))) {
        isSimpleStackMain = false;
        firstComplexMain = i;
      }

      // 停止在側(cè)軸上堆疊子視圖衔掸,剩余的子視圖都在循環(huán)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) {
        // 設(shè)置子視圖主軸上的TrailingPosition
          setTrailingPosition(node, child, mainAxis);
        }
        // 可以算出了主軸上的尺寸了
        mainDim += getDimWithMargin(child, mainAxis);
        // 可以算出側(cè)軸上的尺寸了
        crossDim = fmaxf(crossDim, boundAxis(child, crossAxis, getDimWithMargin(child, crossAxis)));
      }

      if (isSimpleStackCross) {
        child->layout.position[pos[crossAxis]] += linesCrossDim + leadingPaddingAndBorderCross;
        if (isCrossDimDefined) {
        // 設(shè)置子視圖側(cè)軸上的TrailingPosition
          setTrailingPosition(node, child, crossAxis);
        }
      }

      alreadyComputedNextLayout = 0;
      mainContentDim += nextContentDim;
      endLine = i + 1;
    }
// 循環(huán)A 至此結(jié)束

循環(huán)A結(jié)束以后,會計算出endLine俺抽,計算出主軸上的尺寸敞映,側(cè)軸上的尺寸。不可拉伸的子視圖的布局也會被確定磷斧。

接下來進(jìn)入循環(huán)B的階段振愿。

循環(huán)B主要分為2個部分,第一個部分是用來布局可拉伸的子視圖弛饭。


    // 為了在主軸上布局冕末,需要控制兩個space,一個是第一個子視圖和最左邊的距離侣颂,另一個是兩個子視圖之間的距離
    float leadingMainDim = 0;
    float betweenMainDim = 0;

    // 記錄剩余的可用空間
    float remainingMainDim = 0;
    if (isMainDimDefined) {
      remainingMainDim = definedMainDim - mainContentDim;
    } else {
      remainingMainDim = fmaxf(mainContentDim, 0) - mainContentDim;
    }

    // 如果當(dāng)前還有可拉伸的子視圖档桃,它們就要填充剩余的可用空間
    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;

      // 不可以拉伸的子視圖可以在父視圖內(nèi)部overflow胳蛮,在這種情況下,假設(shè)沒有可用的拉伸space
      if (flexibleMainDim < 0) {
        flexibleMainDim = 0;
      }

      currentFlexChild = firstFlexChild;
      while (currentFlexChild != NULL) {
        // 在這層循環(huán)里面我們已經(jīng)可以確認(rèn)子視圖的最終大小了
        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結(jié)束以后丛晌,所有可以被拉伸的子視圖就都布局完成了仅炊。



 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) {
        // 這里是實現(xiàn)SPACE_AROUND的代碼
        betweenMainDim = remainingMainDim /
          (flexibleChildrenCount + nonFlexibleChildrenCount);
        leadingMainDim = betweenMainDim / 2;
      }
    }


可flex拉伸的視圖布局完成以后,這里是收尾工作澎蛛,根據(jù)justifyContent抚垄,更改betweenMainDim和leadingMainDim的大小。

接著再是循環(huán)C谋逻。


    // 在這個循環(huán)中呆馁,所有子視圖的寬和高都將被確定下來。在確定各個子視圖的坐標(biāo)的時候毁兆,同時也將確定父視圖的寬和高浙滤。
    mainDim += leadingMainDim;

    // 按照Line,一層層的循環(huán)
    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])) {
        // 到這里气堕,絕對坐標(biāo)的子視圖的坐標(biāo)已經(jīng)確定下來了纺腊,左邊距和上邊距已經(jīng)被定下來了畔咧。這時子視圖的絕對坐標(biāo)可以確定了。
        child->layout.position[pos[mainAxis]] = getPosition(child, leading[mainAxis]) +
          getLeadingBorder(node, mainAxis) +
          getLeadingMargin(child, mainAxis);
      } else {
        // 如果子視圖不是絕對坐標(biāo)揖膜,坐標(biāo)是相對的誓沸,或者還沒有確定下來左邊距和上邊距,那么就根據(jù)當(dāng)前位置確定坐標(biāo)
        child->layout.position[pos[mainAxis]] += mainDim;

        // 確定trailing的坐標(biāo)位置
        if (isMainDimDefined) {
          setTrailingPosition(node, child, mainAxis);
        }

        // 接下來開始處理相對坐標(biāo)的子視圖壹粟,具有絕對坐標(biāo)的子視圖不會參與下述的布局計算中
        if (child->style.position_type == CSS_POSITION_RELATIVE) {
          // 主軸上的寬度是由所有的子視圖的寬度累加而成
          mainDim += betweenMainDim + getDimWithMargin(child, mainAxis);
          // 側(cè)軸的高度是由最高的子視圖決定的
          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
      );
    }



在循環(huán)C中趁仙,會在主軸上計算出所有子視圖的坐標(biāo)洪添,包括各個子視圖的寬和高。

接下來就到循環(huán)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])) {
        // 到這里薇组,絕對坐標(biāo)的子視圖的坐標(biāo)已經(jīng)確定下來了,上下左右至少有一邊的坐標(biāo)已經(jīng)被定下來了坐儿。這時子視圖的絕對坐標(biāo)可以確定了。
        child->layout.position[pos[crossAxis]] = getPosition(child, leading[crossAxis]) +
          getLeadingBorder(node, crossAxis) +
          getLeadingMargin(child, crossAxis);

      } else {
        float leadingCrossDim = leadingPaddingAndBorderCross;

        // 在側(cè)軸上宋光,針對相對坐標(biāo)的子視圖貌矿,我們利用父視圖的alignItems或者子視圖的alignSelf來確定具體的坐標(biāo)位置
        if (child->style.position_type == CSS_POSITION_RELATIVE) {
          // 獲取子視圖的AlignItem屬性值
          css_align_t alignItem = getAlignItem(node, child);
          if (alignItem == CSS_ALIGN_STRETCH) {
            // 如果在側(cè)軸上子視圖還沒有確定尺寸,那么才會相應(yīng)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) {
            // 在側(cè)軸上剩余的空間等于父視圖在側(cè)軸上的高度減去子視圖的在側(cè)軸上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;
            }
          }
        }

        // 確定子視圖在側(cè)軸上的坐標(biāo)位置
        child->layout.position[pos[crossAxis]] += linesCrossDim + leadingCrossDim;

        // 確定trailing的坐標(biāo)
        if (isCrossDimDefined) {
          setTrailingPosition(node, child, crossAxis);
        }
      }
    }

    linesCrossDim += crossDim;
    linesMainDim = fmaxf(linesMainDim, mainDim);
    linesCount += 1;
    startLine = endLine;
  }


上述的循環(huán)D中主要是在側(cè)軸上計算子視圖的坐標(biāo)酌毡。如果視圖發(fā)生了大小變化,還需要遞歸子視圖蕾管,重新布局一次枷踏。

再接著是循環(huán)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;

      // 計算每一行的行高,行高根據(jù)lineHeight和子視圖在側(cè)軸上的高度加上下的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;
    }
  }



執(zhí)行循環(huán)E有一個前提旭蠕,就是,行數(shù)至少要超過一行旷坦,并且側(cè)軸上有高度定義掏熬。滿足了這個前提條件以后才會開始下面的align規(guī)則。

在循環(huán)E中會處理側(cè)軸上的align拉伸規(guī)則秒梅。這里會布局alignContent和AlignItem旗芬。

這塊代碼實現(xiàn)的算法原理請參見http://www.w3.org/TR/2012/CR-css3-flexbox-20120918/#layout-algorithm section 9.4部分。

至此可能還存在一些沒有指定寬和高的視圖捆蜀,接下來將會做最后一次的處理疮丛。



  // 如果某個視圖沒有被指定寬或者高峡迷,并且也沒有被父視圖設(shè)置寬和高沾鳄,那么在這里通過子視圖來設(shè)置寬和高
  if (!isMainDimDefined) {
    // 視圖的寬度等于內(nèi)部子視圖的寬度加上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(
      // 視圖的高度等于內(nèi)部子視圖的高度加上上下的Padding攘轩、Border的寬度和側(cè)軸上Padding、Border咐刨,兩者取最大值昙衅。
      boundAxis(node, crossAxis, linesCrossDim + paddingAndBorderAxisCross),
      paddingAndBorderAxisCross
    );

    if (crossAxis == CSS_FLEX_DIRECTION_ROW_REVERSE ||
        crossAxis == CSS_FLEX_DIRECTION_COLUMN_REVERSE) {
      needsCrossTrailingPos = true;
    }
  }



這些沒有確定寬和高的子視圖的寬和高會根據(jù)父視圖來決定。方法見上述代碼定鸟。

再就是循環(huán)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);
      }
    }
  }


這一步是設(shè)置當(dāng)前node節(jié)點(diǎn)的Trailing坐標(biāo),如果有必要的話联予。如果不需要啼县,這一步會直接跳過。

最后一步就是循環(huán)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])) {
        // 絕對坐標(biāo)的子視圖在主軸上的寬度季眷,在側(cè)軸上的高度都不能比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])) {
        // 當(dāng)前子視圖的坐標(biāo)等于當(dāng)前視圖的寬度減去子視圖的寬度再減去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;
  }



最后這一步循環(huán)G是用來給絕對坐標(biāo)的子視圖計算寬度和高度子刮。

執(zhí)行完上述7個循環(huán)以后,所有的子視圖就都layout完成了窑睁。

總結(jié)一下上述的流程挺峡,如下圖:

二. Weex布局算法性能分析

1.算法實現(xiàn)分析

上一章節(jié)看了Weex的layout算法實現(xiàn)。這里就分析一下在這個實現(xiàn)下担钮,布局能力究竟有多強(qiáng)橱赠。

Weex的實現(xiàn)是FaceBook的開源庫Yoga的前身,所以這里可以把兩個看成是一種實現(xiàn)裳朋。

Weex的這種FlexBox的實現(xiàn)其實只是W3C標(biāo)準(zhǔn)的一個實現(xiàn)的子集病线,因為FlexBox的官方標(biāo)準(zhǔn)里面還有一些并沒有實現(xiàn)出來。W3C上定義的FlexBox的標(biāo)準(zhǔn)鲤嫡,文檔在這里送挑。

FlexBox標(biāo)準(zhǔn)定義:

針對父視圖 (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

相比官方的定義,上述的實現(xiàn)有一些限制:

  1. 所有顯示屬性的node節(jié)點(diǎn)都默認(rèn)假定是Flex的視圖暖眼,當(dāng)然這里要除去文本節(jié)點(diǎn)惕耕,因為它會被假定為inline-flex。
  2. 不支持zIndex的屬性诫肠,包括任何z上的排序司澎。所有的node節(jié)點(diǎn)都是按照代碼書寫的先后順序進(jìn)行排列的欺缘。Weex 目前也不支持 z-index 設(shè)置元素層級關(guān)系,但靠后的元素層級更高挤安,因此谚殊,對于層級高的元素,可將其排列在后面蛤铜。
  3. FlexBox里面定義的order屬性嫩絮,也不支持。flex item默認(rèn)按照代碼書寫順序围肥。
  4. visibility屬性默認(rèn)都是可見的剿干,暫時不支持邊緣塌陷合并(collapse)和隱藏(hidden)屬性。
  5. 不支持forced breaks穆刻。
  6. 不支持垂直方向的inline(比如從上到下的text置尔,或者從下到上的text)

關(guān)于Flexbox 在iOS這邊的具體實現(xiàn)上一章節(jié)已經(jīng)分析過了。

接下來仔細(xì)分析一下Autolayout的具體實現(xiàn)

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

現(xiàn)在換成了Autolayout,每個View需要知道4個尺寸腐芍。left差导,top,width猪勇,height。

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

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

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

  1. 當(dāng)方程組的解的個數(shù)有無窮多個椅您,最終會得到欠約束的有歧義的布局。
  2. 當(dāng)方程無解時寡键,則表示約束有沖突掀泳。
  3. 只有當(dāng)方程組有唯一解的時候,才能得到一個穩(wěn)定的布局西轩。

Autolayout 本質(zhì)是一個線性方程解析器员舵,該解析器試圖找到一種可滿足其規(guī)則的幾何表達(dá)式。

Autolayout的底層數(shù)學(xué)模型是線性算術(shù)約束問題藕畔。

關(guān)于這個問題马僻,早在1940年,由Dantzig提出了一個the simplex algorithm算法注服,但是由于這個算法實在很難用在UI應(yīng)用上面韭邓,所以沒有得到很廣泛的應(yīng)用措近,直到1997年,澳大利亞的莫納什大學(xué)(Monash University)的兩名學(xué)生女淑,Alan Borning 和 Kim Marriott實現(xiàn)了Cassowary線性約束算法瞭郑,才得以在UI應(yīng)用上被大量的應(yīng)用起來。

Cassowary線性約束算法是基于雙simplex算法的鸭你,在增加約束或者一個對象被移除的時候屈张,通過局部誤差增益 和 加權(quán)求和比較 ,能夠完美的增量處理不同層次的約束苇本。Cassowary線性約束算法適合GUI布局系統(tǒng)袜茧,被用來計算view之間的位置的。開發(fā)者可以指定不同View之間的位置關(guān)系和約束關(guān)系瓣窄,Cassowary線性約束算法會去求處符合條件的最優(yōu)值笛厦。

下面是兩位學(xué)生寫的相關(guān)的論文,有興趣的可以讀一下俺夕,了解一下算法的具體實現(xiàn):

  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線性約束算法的偽代碼如下:

關(guān)于這個算法已經(jīng)被人們實現(xiàn)成了各個版本裳凸。1年以后,又出了一個新的QOCA算法劝贸。以下這段話摘抄自1997年ACM權(quán)威論文上的一篇文章:

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(項目主頁)也是優(yōu)先被Smalltalk實現(xiàn)了姨谷,也是用在Autolayout技術(shù)上。另外還有更加復(fù)雜的QOCA算法映九,這里就不再細(xì)談了梦湘,有興趣的同學(xué)可以看看上面三篇論文,里面有詳細(xì)的描述件甥。

2.算法性能測試準(zhǔn)備工作

開始筆者是打算連帶Weex的布局性能一起測試的捌议,但是由于Weex的布局都在子線程,刷新渲染回到主線程引有,需要測試都在主線程的情況需要改動一些代碼瓣颅,而且Weex原生的布局是從JS調(diào)用方法,如果用這種方法又會多損耗一些性能譬正,對測試結(jié)果有影響宫补。于是換成Weex相同布局方式的Yoga算法進(jìn)行測試。由于Facebook對它進(jìn)行了很好的封裝曾我,使用起來也很方便粉怕。雖然Layout算法和Weex有些差異,但是不影響定性的比較您单。

確定下來測試對象:Frame斋荞,F(xiàn)lexBox(Yoga實現(xiàn)),Autolayout虐秦。

測試前平酿,還需要準(zhǔn)備測試模型凤优,這里選出了3種測試模型。

第一種測試模型是隨機(jī)生成完全不相關(guān)聯(lián)的View蜈彼。如下圖:

第二種測試模型是生成相互嵌套的View筑辨。嵌套規(guī)則設(shè)置一個簡單的:子視圖依次比父視圖高度少一個像素。類似下圖幸逆,這是500個View相互嵌套的結(jié)果:

第三種測試模型是針對Autolayout專門加的棍辕。由于Autolayout約束的特殊性,這里針對鏈?zhǔn)郊s束額外增加的測試模型还绘。規(guī)則是前后兩個相連的View之間依次加上約束楚昭。類似下圖,這是500個View鏈?zhǔn)降募s束結(jié)果:

根據(jù)測試模型拍顷,我們可以得到如下的7組需要測試的測試用例:

1.Frame
2.嵌套的Frame
3.Yoga
4.嵌套的Yoga
5.Autolayout
6.嵌套的Autolayout
7.鏈?zhǔn)降腁utolayout

測試樣本:由于需要考慮到測試的通用性抚太,測試樣本要盡量隨機(jī)。于是針對隨機(jī)生成的坐標(biāo)全部都隨機(jī)生成昔案,View的顏色也全部都隨機(jī)生成尿贫,這樣保證了通用公正公平性質(zhì)。

測試次數(shù):為了保證測試數(shù)據(jù)能盡量真實踏揣,筆者在這里花了大量的時間庆亡。每組測試用例都針對從100,200捞稿,300又谋,400,500娱局,600搂根,700,800铃辖,900,1000個視圖進(jìn)行測試猪叙,為了保證測試的普遍性娇斩,這里每次測試都測試10000次,然后對10000次的結(jié)果進(jìn)行加和平均穴翩。加和平均取小數(shù)點(diǎn)后5位犬第。(10000次的統(tǒng)計是用計算機(jī)來算的,但是真的非常非常非常的耗時芒帕,有興趣的可以自己用電腦試試)

最后展示一下測試機(jī)器的配置和系統(tǒng)版本:

(由于iPhone真機(jī)對每個App的內(nèi)存有限制歉嗓,產(chǎn)生1000個嵌套的視圖,并且進(jìn)行10000次試驗背蟆,iPhone真機(jī)完全受不了這種計算量鉴分,App直接閃退哮幢,所以用真機(jī)測試到一半,改用模擬器測試志珍,借助Mac的性能橙垢,咬著牙從零開始,重新統(tǒng)計了所有測試用例的數(shù)據(jù))

如果有性能更強(qiáng)的Mac電腦(垃圾桶)伦糯,測試全過程花的時間可能會更少柜某。

筆者用的電腦的配置如下:

測試用的模擬器是iPad Pro(12.9 inch)iOS 10.3(14E269)

我所用的測試代碼也公布出來,有興趣的可以自己測試測試敛纲。測試代碼在這里

3.算法性能測試結(jié)果

公布測試結(jié)果:

上圖數(shù)據(jù)是10喂击,20,30淤翔,40翰绊,50,60办铡,70辞做,80,90寡具,100個View分別用7組用例測試出來的結(jié)果秤茅。將上面的結(jié)果統(tǒng)計成折線圖,如下:

結(jié)果依舊是Autolayout的3種方式都高于其他4種布局方式童叠。

上圖是3個布局算法在普通場景下的性能比較圖框喳,可以看到,F(xiàn)lexBox的性能接近于原生的Frame厦坛。

上圖是3個布局算法在嵌套情況下的性能比較圖五垮,可以看到,F(xiàn)lexBox的性能也依舊接近于原生的Frame杜秸。而嵌套情況下的Autolayout的性能急劇下降放仗。

最后這張圖也是專門針對Autolayout額外加的一組測試。目的是為了比較3種場景下不同的Autolayout的性能撬碟,可以看到诞挨,嵌套的Autolayout的性能依舊是最差的!

上圖數(shù)據(jù)是100呢蛤,200惶傻,300,400其障,500银室,600,700,800蜈敢,900辜荠,1000個View分別用7組用例測試出來的結(jié)果。將上面的結(jié)果統(tǒng)計成折線圖扶认,如下:

當(dāng)視圖多到900侨拦,1000的時候,嵌套的Autolayout直接就導(dǎo)致模擬器崩潰了辐宾。

上圖是3個布局算法在普通場景下的性能比較圖狱从,可以看到,F(xiàn)lexBox的性能接近于原生的Frame叠纹。

上圖是3個布局算法在嵌套情況下的性能比較圖季研,可以看到,F(xiàn)lexBox的性能也依舊接近于原生的Frame誉察。而嵌套情況下的Autolayout的性能急劇下降与涡。

最后這張圖是專門針對Autolayout額外加的一組測試。目的是為了比較3種場景下不同的Autolayout的性能持偏,可以看到驼卖,平時我們使用嵌套的Autolayout的性能是最差的!

三. Weex是如何布局原生界面的

上一章節(jié)看了FlexBox算法的強(qiáng)大布局能力鸿秆,這一章節(jié)就來看看Weex究竟是如何利用這個能力的對原生View進(jìn)行Layout酌畜。

在解答上面這個問題之前,先讓我們回顧一下上篇文章《Weex 是如何在 iOS 客戶端上跑起來的》里面提到的卿叽,在JSFramework轉(zhuǎn)換從網(wǎng)絡(luò)上下載下來的JS文件之前桥胞,本地先注冊了4個重要的回調(diào)函數(shù)。


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進(jìn)行相互調(diào)用的四大函數(shù)贩虾。

先來回顧一下這四大函數(shù)注冊的時候分別封裝了哪些閉包。


@interface WXBridgeContext ()
@property (nonatomic, strong) id<WXBridgeProtocol>  jsBridge;

在WXBridgeContext類里面有一個jsBridge沥阱。jsBridge初始化的時候會注冊這4個全局函數(shù)缎罢。

第一個閉包函數(shù):


    [_jsBridge registerCallNative:^NSInteger(NSString *instance, NSArray *tasks, NSString *callback) {
        return [weakSelf invokeNative:instance tasks:tasks callback:callback];
    }];



這里的閉包函數(shù)會被傳入到下面這個函數(shù)中:


- (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;
}


這里就封裝了一個函數(shù),暴露給JS用考杉。方法名叫callNative屁使,函數(shù)參數(shù)為3個,分別是instanceId奔则,tasksArray任務(wù)數(shù)組,callbackId回調(diào)ID蔽午。

所有的OC的閉包都需要封裝一層易茬,因為暴露給JS的方法不能有冒號,所有的參數(shù)都是直接跟在小括號的參數(shù)列表里面的,因為JS的函數(shù)是這樣定義的抽莱。

當(dāng)JS調(diào)用callNative方法之后范抓,就會最終執(zhí)行WXBridgeContext類里面的[weakSelf invokeNative:instance tasks:tasks callback:callback]方法。

第二個閉包函數(shù):


    [_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;
    }];

這個閉包會被傳到下面的函數(shù)中:



- (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匕垫,函數(shù)參數(shù)為4個,分別是instanceIdString虐呻,componentData組件的數(shù)據(jù)象泵,parentRef引用編號,insertIndex插入視圖的index斟叼。

當(dāng)JS調(diào)用callAddElement方法偶惠,就會最終執(zhí)行WXBridgeContext類里面的WXPerformBlockOnComponentThread閉包。

第三個閉包函數(shù):



    [_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];
    }];


這個閉包會被傳到下面的函數(shù)中:



- (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朗涩,函數(shù)參數(shù)為5個忽孽,分別是instanceIdString,moduleNameString模塊名谢床,methodNameString方法名兄一,argsArray參數(shù)數(shù)組,optionsDic字典识腿。

當(dāng)JS調(diào)用callNativeModule方法出革,就會最終執(zhí)行WXBridgeContext類里面的WXModuleMethod方法。

第四個閉包函數(shù):



    [_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];
    }];

這個閉包會被傳到下面的函數(shù)中:


- (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覆履,函數(shù)參數(shù)為5個袁余,分別是instanceIdString,componentNameString組件名坷檩,methodNameString方法名抄腔,argsArray參數(shù)數(shù)組,optionsDic字典伟众。

當(dāng)JS調(diào)用callNativeComponent方法析藕,就會最終執(zhí)行WXBridgeContext類里面的WXComponentMethod方法。

總結(jié)一下上述暴露給JS的4個方法:

  1. callNative
    這個方法是JS用來調(diào)用任意一個Native方法的凳厢。

  2. callAddElement
    這個方法是JS用來給當(dāng)前頁面添加視圖元素的账胧。

  3. callNativeModule
    這個方法是JS用來調(diào)用模塊里面暴露出來的方法。

  4. callNativeComponent
    這個方法是JS用來調(diào)用組件里面暴露出來的方法先紫。

Weex在布局的時候就只會用到前2個方法治泥。

(一)createRoot:

當(dāng)JSFramework把JS文件轉(zhuǎn)換類似JSON的文件之后,就開始調(diào)用Native的callNative方法遮精。

callNative方法會最終執(zhí)行WXBridgeContext類里面的[weakSelf invokeNative:instance tasks:tasks callback:callback]方法居夹。

當(dāng)前操作處于子線程“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;
    }
    

    // 根據(jù)JS發(fā)送過來的方法,進(jìn)行轉(zhuǎn)換成Native方法調(diào)用
    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];
        }
    }
    
    // 如果有回調(diào)准脂,回調(diào)給JS
    [self performSelector:@selector(_sendQueueLoop) withObject:nil];
    
    return 1;
}


這里會把JS從發(fā)送過來的callNative方法轉(zhuǎn)換成Native的組件component的方法調(diào)用或者模塊module的方法調(diào)用劫扒。

舉個例子:

JS從callNative方法傳過來3個參數(shù)


instance:0,

tasks:(
        {
        args =         (
                        {
                attr =                 {
                };
                ref = "_root";
                style =                 {
                    alignItems = center;
                };
                type = div;
            }
        );
        method = createBody;
        module = dom;
    }
), 

callback:-1

tasks數(shù)組里面會解析出各個方法和調(diào)用者。

這個例子里面就會解析出Dom模塊的createBody方法狸膏。

接著就會調(diào)用Dom模塊的createBody方法沟饥。



    if (isSync) {
        [invocation invoke];
        return invocation;
    } else {
        [self _dispatchInvocation:invocation moduleInstance:moduleInstance];
        return nil;
    }

調(diào)用方法之前,有一個線程切換的步驟湾戳。如果是同步方法贤旷,那么就直接調(diào)用,如果是異步方法院塞,那么嗨需要進(jìn)行線程轉(zhuǎn)換遮晚。

Dom模塊的createBody方法是異步的方法,于是就需要調(diào)用_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)]){
        // 判斷當(dāng)前是否有Queue县遣,如果沒有,就返回main_queue汹族,如果有萧求,就切換到targetQueue
        targetQueue = [moduleInstance targetExecuteQueue] ?: dispatch_get_main_queue();
    } else if([moduleInstance respondsToSelector:@selector(targetExecuteThread)]){
        // 判斷當(dāng)前是否有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耗帕,就只能切換到對應(yīng)的targetThread上。


void WXPerformBlockOnThread(void (^ _Nonnull block)(), NSThread *thread)
{
    [WXUtility performBlock:block onThread:thread];
}

+ (void)performBlock:(void (^)())block onThread:(NSThread *)thread
{
    if (!thread || !block) return;
    
    // 如果當(dāng)前線程不是目標(biāo)線程上袱贮,就要切換線程
    if ([NSThread currentThread] == thread) {
        block();
    } else {
        [self performSelector:@selector(_performBlock:)
                     onThread:thread
                   withObject:[block copy]
                waitUntilDone:NO];
    }
}

這里就是切換線程的操作仿便,如果當(dāng)前線程不是目標(biāo)線程,就要切換線程攒巍。在目標(biāo)線程上調(diào)用_performBlock:方法嗽仪,入?yún)⑦€是最初傳進(jìn)來的block閉包。

切換前線程處于子線程“com.taobao.weex.bridge”中柒莉。

在WXDomModule中調(diào)用targetExecuteThread方法



- (NSThread *)targetExecuteThread
{
    return [WXComponentManager componentThread];
}


切換線程之后闻坚,當(dāng)前線程變成了“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;
        }

        // 開啟組件任務(wù)
        [manager startComponentTasks];
        block(manager);
    });
}



當(dāng)調(diào)用了Dom模塊的createBody方法以后兢孝,會先調(diào)用WXComponentManager的startComponentTasks方法鲤氢,再調(diào)用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的初始化重點(diǎn)是會開啟DisplayLink卷玉,它會開啟一個runloop。


- (void)_startDisplayLink
{
    WXAssertComponentThread();
    
    if(!_displayLink){
        _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_handleDisplayLink)];
        [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    }
}

displayLink一旦開啟喷市,被加入到當(dāng)前runloop之中相种,每次runloop循環(huán)一次都會執(zhí)行刷新布局的方法_handleDisplayLink。


- (void)startComponentTasks
{
    [self _awakeDisplayLink];
}

- (void)_awakeDisplayLink
{
    WXAssertComponentThread();
    if(_displayLink && _displayLink.paused) {
        _displayLink.paused = NO;
    }
}

WXComponentManager的startComponentTasks方法僅僅是更改了CADisplayLink的paused的狀態(tài)品姓。CADisplayLink就是用來刷新layout的寝并。


@implementation WXComponentManager
{
    // 對WXSDKInstance的弱引用
    __weak WXSDKInstance *_weexInstance;
    // 當(dāng)前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任務(wù)的腹备。

再來看看createRoot:方法:



- (void)createRoot:(NSDictionary *)data
{
    WXAssertComponentThread();
    WXAssertParam(data);
    
    // 1.創(chuàng)建WXComponent衬潦,作為rootComponent
    _rootComponent = [self _buildComponentForData:data];

    // 2.初始化css_node_t,作為rootCSSNode
    [self _initRootCSSNode];
    
    __weak typeof(self) weakSelf = self;
    // 3.添加UI任務(wù)到uiTaskQueue數(shù)組中
    [self _addUITask:^{
        __strong typeof(self) strongSelf = weakSelf;
        strongSelf.weexInstance.rootView.wx_component = strongSelf->_rootComponent;
        [strongSelf.weexInstance.rootView addSubview:strongSelf->_rootComponent.view];
    }];
}

這里干了3件事情:

1.創(chuàng)建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;
}

這里的入?yún)ata是之前的tasks數(shù)組植酥。


- (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;
            }
        }
        
        // 設(shè)置NavBar的Style
        [self _setupNavBarWithStyles:_styles attributes:_attributes];
        // 根據(jù)style初始化cssNode數(shù)據(jù)結(jié)構(gòu)
        [self _initCSSNodeWithStyles:_styles];
        // 根據(jù)style初始化View的各個屬性
        [self _initViewPropertyWithStyles:_styles];
        // 處理Border的圓角镀岛,邊線寬度,背景顏色等屬性
        [self _handleBorders:styles isUpdating:NO];
    }
    
    return self;
}


上述函數(shù)就是初始化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需要布局的子視圖個數(shù)
    [self _recomputeCSSNodeChildren];
    // 將style各個屬性都填充到cssNode數(shù)據(jù)結(jié)構(gòu)中
    [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

在這里卸留,準(zhǔn)備開始Layout之前走越,我們需要先初始化rootCSSNode


- (void)_initRootCSSNode
{
    _rootCSSNode = new_css_node();
    
    // 根據(jù)頁面weexInstance設(shè)置rootCSSNode的坐標(biāo)和寬高尺寸
    [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的坐標(biāo)和寬高尺寸耻瑟。

3.添加UI任務(wù)到uiTaskQueue數(shù)組中


    [self _addUITask:^{
        __strong typeof(self) strongSelf = weakSelf;
        strongSelf.weexInstance.rootView.wx_component = strongSelf->_rootComponent;
        [strongSelf.weexInstance.rootView addSubview:strongSelf->_rootComponent.view];
    }];


WXComponentManager會把當(dāng)前的組件以及它對應(yīng)的View添加到頁面Instance的rootView上面的這個任務(wù)旨指,添加到uiTaskQueue數(shù)組中。

_rootComponent.view會創(chuàng)建組件對應(yīng)的WXView喳整,這個是繼承自UIView的谆构。所以Weex通過JS代碼創(chuàng)建出來的控件都是原生的,都是WXView類型的算柳,實質(zhì)就是UIView低淡。創(chuàng)建UIView這一步又是回到主線程中執(zhí)行的。

最后顯示到頁面上的工作瞬项,是由displayLink的刷新方法在主線程刷新UI顯示的蔗蹋。



- (void)_handleDisplayLink
{ 
    [self _layoutAndSyncUI];
}

- (void)_layoutAndSyncUI
{
    // Flexbox布局
    [self _layout];
    if(_uiTaskQueue.count > 0){
        // 同步執(zhí)行UI任務(wù)
        [self _syncUITasks];
        _noTaskTickCount = 0;
    } else {
        // 如果當(dāng)前一秒內(nèi)沒有任務(wù),那么智能的掛起displaylink囱淋,以節(jié)約CPU時間
        _noTaskTickCount ++;
        if (_noTaskTickCount > 60) {
            [self _suspendDisplayLink];
        }
    }
}

_layoutAndSyncUI是布局和刷新UI的核心流程猪杭。每次刷新一次,都會先調(diào)用Flexbox算法的Layout進(jìn)行布局妥衣,這個布局是在子線程“com.taobao.weex.component”執(zhí)行的皂吮。接著再去查看當(dāng)前是否有UI任務(wù)需要執(zhí)行戒傻,如果有,就切換到主線程進(jìn)行UI刷新操作蜂筹。

這里還會有一個智能的掛起操作需纳。就是判斷一秒內(nèi)如果都沒有任務(wù),那么就掛起displaylink艺挪,以節(jié)約CPU時間不翩。



- (void)_layout
{
    BOOL needsLayout = NO;
    NSEnumerator *enumerator = [_indexDict objectEnumerator];
    WXComponent *component;
    // 判斷當(dāng)前是否需要布局,即是判斷當(dāng)前組件的_isLayoutDirty這個BOLL屬性值
    while ((component = [enumerator nextObject])) {
        if ([component needsLayout]) {
            needsLayout = YES;
            break;
        }
    }

    if (!needsLayout) {
        return;
    }
    
    // Flexbox的算法核心函數(shù)
    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];
    // 計算當(dāng)前weexInstance的rootView.frame麻裳,并且重置rootCSSNode的Layout
    [self _calculateRootFrame];
  
    // 在每個需要布局的組件之間
    for (WXComponent *dirtyComponent in dirtyComponents) {
        [self _addUITask:^{
            [dirtyComponent _layoutDidFinish];
        }];
    }
}

_indexDict里面維護(hù)了一張整個頁面的布局結(jié)構(gòu)的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對應(yīng)的組件妙蔗。

_layout會先判斷當(dāng)前是否有需要布局的組件,如果有疆瑰,就從rootCSSNode開始進(jìn)行Flexbox算法的Layout眉反。執(zhí)行完成以后還需要調(diào)整一次rootView的frame,最后添加一個UI任務(wù)到taskQueue中乃摹,這個任務(wù)標(biāo)記的是組件布局完成禁漓。

注意上述所有布局操作都是在子線程“com.taobao.weex.component”中執(zhí)行的。


- (void)_syncUITasks
{
    // 用blocks接收原來uiTaskQueue里面的所有任務(wù)
    NSArray<dispatch_block_t> *blocks = _uiTaskQueue;
    // 清空uiTaskQueue
    _uiTaskQueue = [NSMutableArray array];
    // 在主線程中依次執(zhí)行uiTaskQueue里面的所有閉包
    dispatch_async(dispatch_get_main_queue(), ^{
        for(dispatch_block_t block in blocks) {
            block();
        }
    });
}


布局完成以后就調(diào)用同步的UI刷新方法孵睬。注意這里要對UI進(jìn)行操作播歼,一定要切換回主線程。

(二)callAddElement

在子線程“com.taobao.weex.bridge”中掰读,會一直相應(yīng)來自JSFramework調(diào)用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;
    }];


當(dāng)JSFramework調(diào)用callAddElement方法,就會執(zhí)行上述代碼的閉包函數(shù)蹈集。這里會接收來自JS的4個入?yún)ⅰ?/p>

舉個例子烁试,JSFramework可能會通過callAddElement方法傳過來這樣4個參數(shù):


0,
_root, 
{
    attr =     {
        value = "Hello World";
    };
    ref = 4;
    style =     {
        color = "#000000";
        fontSize = 40;
    };
    type = text;
}, 
-1


這里的insertIndex為0,parentRef是_root拢肆,componentData是當(dāng)前要創(chuàng)建的組件的信息减响,instanceIdString是-1。

之后WXComponentManager就會調(diào)用startComponentTasks開始displaylink繼續(xù)準(zhǔn)備刷新布局郭怪,最后調(diào)用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
{

   // 根據(jù)componentData構(gòu)建組件
    WXComponent *component = [self _buildComponentForData:componentData];
    
    index = (index == -1 ? supercomponent->_subcomponents.count : index);
    
    [supercomponent _insertSubcomponent:component atIndex:index];
    // 用_lazyCreateView標(biāo)識懶加載
    if(supercomponent && component && supercomponent->_lazyCreateView) {
        component->_lazyCreateView = YES;
    }
    
    // 插入一個UI任務(wù)
    [self _addUITask:^{
        [supercomponent insertSubview:component atIndex:index];
    }];

    NSArray *subcomponentsData = [componentData valueForKey:@"children"];
    
    BOOL appendTree = !appendingInTree && [component.attributes[@"append"] isEqualToString:@"tree"];
    // 再次遞歸的規(guī)則:如果父視圖是一個樹狀結(jié)構(gòu)嘴纺,子視圖即使也是一個樹狀結(jié)構(gòu)败晴,也不能再次Layout
    for(NSDictionary *subcomponentData in subcomponentsData){
        [self _recursivelyAddComponent:subcomponentData toSupercomponent:component atIndex:-1 appendingInTree:appendTree || appendingInTree];
    }
    if (appendTree) {
        // 如果當(dāng)前組件是樹狀結(jié)構(gòu),強(qiáng)制刷新layout栽渴,以防在syncQueue中堆積太多的同步任務(wù)尖坤。
        [self _layoutAndSyncUI];
    }
}


在遞歸的添加子組件的時候,如果是樹狀結(jié)構(gòu)闲擦,還需要再次強(qiáng)制進(jìn)行一次layout糖驴,同步一次UI。這里調(diào)用[self _layoutAndSyncUI]方法和createRoot:時候?qū)崿F(xiàn)是完全一樣的佛致,下面就不再贅述了。

這里會循環(huán)添加多個子視圖辙谜,相應(yīng)的也會調(diào)用多次Layout方法俺榆。

(三)createFinish

當(dāng)所有的視圖都添加完成以后,JSFramework就是再次調(diào)用callNative方法装哆。

還是會傳過來3個參數(shù)罐脊。



instance:0, 
tasks:(
        {
        args =         (
        );
        method = createFinish;
        module = dom;
    }
), 
callback:-1

callNative通過這個參數(shù)會調(diào)用到WXDomModule的createFinish方法。這里的具體實現(xiàn)見第一步的callNative蜕琴,這里不再贅述萍桌。


- (void)createFinish
{
    [self performBlockOnComponentManager:^(WXComponentManager *manager) {
        [manager createFinish];
    }];
}


這里最終也是會調(diào)用到WXComponentManager的createFinish。當(dāng)然這里是會進(jìn)行線程切換凌简,切換到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任務(wù),回調(diào)到主線程的renderFinish方法里面雏搂。

至此藕施,Weex的布局流程就完成了。

最后

雖然Autolayout是蘋果原生就支持的自動布局方案凸郑,但是在稍微復(fù)雜的界面就會出現(xiàn)性能問題裳食。大半年前,Draveness的這篇《從 Auto Layout 的布局算法談性能》文章里面也稍微“批判”了Autolayout的性能問題芙沥,但是文章里面最后提到的是用ASDK的方法來解決問題诲祸。本篇文章則獻(xiàn)上另外一種可用的布局方法——FlexBox,并且?guī)狭私?jīng)過大量測試的測試數(shù)據(jù)而昨,向大左的這篇經(jīng)典文章致敬救氯!

如今,iOS平臺上幾大可用的布局方法有:Frame原生布局配紫,Autolayout原生自動布局径密,F(xiàn)lexBox的Yoga實現(xiàn),ASDK躺孝。

當(dāng)然享扔,基于這4種基本方案以外底桂,還有一些組合方法,比如Weex的這種惧眠,用JS的CSS解析成類似JSON的DOM籽懦,再調(diào)用Native的FlexBox算法進(jìn)行布局。前段時間還有來自美團(tuán)的《布局編碼的未來》里面提到的畢加索(picasso)布局方法氛魁。原理也是會用到JSCore暮顺,將JS寫的JSON或者自定義的DSL,經(jīng)過本地的picassoEngine布局引擎轉(zhuǎn)換成Native布局秀存,最終利用錨點(diǎn)的概念做到高效的布局捶码。

最后,推薦2個iOS平臺上比較優(yōu)秀的利用了FlexBox的原理的開源庫:

來自Facebook的yoga
來自餓了么的FlexBoxLayout


Weex 源碼解析系列文章:

Weex 是如何在 iOS 客戶端上跑起來的
由 FlexBox 算法強(qiáng)力驅(qū)動的 Weex 布局引擎
Weex 事件傳遞的那些事兒
Weex 中別具匠心的 JS Framework
iOS 開發(fā)者的 Weex 偽最佳實踐指北


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末或链,一起剝皮案震驚了整個濱河市惫恼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌澳盐,老刑警劉巖祈纯,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異叼耙,居然都是意外死亡腕窥,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門筛婉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來簇爆,“玉大人,你說我怎么就攤上這事倾贰∶岬” “怎么了安寺?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵首尼,是天一觀的道長。 經(jīng)常有香客問我迎捺,道長,這世上最難降的妖魔是什么查排? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上率挣,老公的妹妹穿的比我還像新娘露戒。我一直安慰自己动漾,他們只是感情好谦炬,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著甫贯,像睡著了一般看蚜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上渴逻,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機(jī)與錄音梨撞,去河邊找鬼卧波。 笑死港粱,一個胖子當(dāng)著我的面吹牛寸宏,可吹牛的內(nèi)容都是我干的击吱。 我是一名探鬼主播覆醇,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼常摧!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起溃斋,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤梗劫,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后走哺,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體割坠,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了拴签。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蚓哩。...
    茶點(diǎn)故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡喜颁,死狀恐怖半开,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情纠永,我是刑警寧澤渺蒿,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布怠蹂,位于F島的核電站易遣,受9級特大地震影響豆茫,放射性物質(zhì)發(fā)生泄漏揩魂。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一倦挂、第九天 我趴在偏房一處隱蔽的房頂上張望没炒。 院中可真熱鬧送火,春花似錦、人聲如沸骨稿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至侠草,卻和暖如春边涕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背式撼。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工叠洗, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留灭抑,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像劈榨,于是被迫代替她去往敵國和親同辣。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 問答題47 /72 常見瀏覽器兼容性問題與解決方案? 參考答案 (1)瀏覽器兼容問題一:不同瀏覽器的標(biāo)簽?zāi)J(rèn)的外補(bǔ)...
    _Yfling閱讀 13,748評論 1 92
  • H5移動端知識點(diǎn)總結(jié) 閱讀目錄 移動開發(fā)基本知識點(diǎn) calc基本用法 box-sizing的理解及使用 理解dis...
    Mx勇閱讀 4,486評論 0 26
  • 選擇qi:是表達(dá)式 標(biāo)簽選擇器 類選擇器 屬性選擇器 繼承屬性: color,font,text-align,li...
    wzhiq896閱讀 1,747評論 0 2
  • 移動開發(fā)基本知識點(diǎn) 一.使用rem作為單位 html { font-size: 100px; } @media(m...
    橫沖直撞666閱讀 3,466評論 0 6
  • Flexbox布局官方稱之為CSS Flexible Box布局模塊,他是CSS3中的一種新的布局模式。Flexb...
    流動碼文閱讀 700評論 0 3