React NativeのレイアウトエンジンYogaの仕組み [後編]

adwaysengineerblog.hatenablog.com

こんにちは、@binaryta です。
前編ではYogaのデバッグ環境を整えて終わりました。
まだ読まれていない方は是非上記リンクから見てみてください。

前編の末尾の方で僕は次のような一文を残しました。

Yogaのレイアウトは現時点ではW3C標準規格のFlex Layout Algorithmに準拠しているのでW3CのFlex Layout Algorithmに目を通しておくといいと思います。
また、Flex Layoutでよく出てくる用語についても確認しておいたほうがいいでしょう。

先にこれらについて軽くおさらいします (僕もあやふやなので)。
そして今回は内部を深く追ってはいくものの主にメインルーチンの関数を読んでいきます。
後編は次の流れで進みます。

 

まずはW3CのFlexible Box Layoutについてゆるく解説していきます。
日本語訳ドキュメントも存在しますので安心です。


Flex Layout のBoxモデルと用語

Flex Layoutを構成する要素が2つあります。flex itemflex containerです。

Web開発において、display: flexと指定したDOM要素はflex containerとなり、その内部要素は全てflex itemと呼びます。また、flex containerの内側にflex containerを配置することも可能なのでflex itemはflex containerにもなり得ます(containerはネストできます)。

React Nativeの場合は全てがdisplay: flexなstyle要素なため暗黙的に全てflex containerまたはflex itemとなります。
ちなみにYoga内部では全ての要素をnodeという変数で木構造で保持され、flex itemはnodeのchildrenメンバで保持しています。

それ以外の用語は図の通りです。

f:id:AdwaysEngineerBlog:20180827035159p:plain:w480
参考: Figure 2 An illustration of the various directions and sizing terms as applied to a row flex container.


Flex Layout のアルゴリズム

僕自身全て把握し切っているわけではないので、flex レイアウトアルゴリズム(日本語)をご覧ください。(原文はこちら)
以下、日本語資料から拝借したアルゴリズムの手順になります。

  1. ライン長さの決定
  2. 主サイズの決定
  3. 交叉サイズの決定
  4. 主軸方向の整列
  5. 交叉軸方向の整列
  6. flex 可能な長さの解決法
  7. 確定的サイズ/不定サイズ
  8. 内在的サイズ

Yogaが規定するレイアウトの制約

YogaはW3Cの規定するFlexboxの仕様と比較すると制約がいくつかあるので、それについてまとめます。
https://github.com/facebook/yoga/blob/1.9.0/yoga/Yoga.cpp#L2454-L2487

  • 'inline-flex'とみなされるTextノードを除き、常に 'flex'とみなされる
  • 'zIndex'プロパティ(またはzオーダーの任意の形式)はサポートされていない
  • ノードはドキュメント順に積み重ねられる
  • 'order'プロパティはサポートされていない
  • flex itemの順序は、常にドキュメントの順序で定義される
  • 'visibility'プロパティは常に 'visible'とみなされ、'visible'と 'hidden'の値はサポートされていない
  • 垂直インライン方向(上から下、下から上のテキスト)はサポートされていない

 


メインルーチンの大まかな処理の流れ

前編で少しだけ触れましたが、Yogaのメインルーチンが記述された関数はYGNodelayoutImplという関数になります。
この関数が何をやっているのかについて大まかにまとめます。

まずツリー構造で保持するflex containerを再帰的に処理して配置しています。
node変数の読み取り専用のstyleメンバの情報を使用して、そのnodeのlayout.directionlayout.measuredDimensions、及びその子ノードのlayout.positionlayout.lineIndexを設定します。
この設定値によりまずflex containerの配置が決まり、子nodeで保持されるflex itemの配置も決まります。
ここでnodeという変数と、そのnodeが持つ2つのメンバ (style, layout) が出てきましたが、次の節で見ていきます。

以降は定義元を参照しにいくことが多いので、それぞれ愛用のエディタで定義ジャンプを多用していきましょう。(ちなみに僕はVimです)
GitHub上のURLも載せていくのでそこだけ見るのもアリです。


YogaのメインルーチンをSTEPごとに解読する

前述した通りメインルーチンが記述された関数はYGNodelayoutImplという関数です。
該当箇所がYoga.cppというファイルの以下の箇所になります。
https://github.com/facebook/yoga/blob/1.9.0/yoga/Yoga.cpp#L2539-L2548

Yoga.cppというファイルには詳細にコメントが記述されていますので、かなりコードリーディングはしやすいと思います。
( Yogaに関わらずFacebook製のOSSというのは基本的にコメントアウトでしっかり情報共有してくれているので非常に親切な気がしています。)
YGNodelayoutImpl関数には処理順序が11ステップ存在しています。
これもコメントアウトから得た情報なので、見てみると解ります。
今回はその11ステップをステップ順に見ていくことにしましょう。
筆者もまだ若手のプログラマ(23歳)で読み解くのに非常に苦戦したので、是非一緒に苦行の道にお付き合いください。

前処理

前処理でおよそ100行あります。
まず引数が何を指し示すのか?について見ていきます。

static void YGNodelayoutImpl(const YGNodeRef node,
    const float availableWidth,
    const float availableHeight,
    const YGDirection ownerDirection,
    const YGMeasureMode widthMeasureMode,
    const YGMeasureMode heightMeasureMode,
    const float ownerWidth,
    const float ownerHeight,
    const bool performLayout,
    const YGConfigRef config) {
  // ...
}
変数名 説明
YGNodeRef node サイズを確定し画面上に配置するためのflex item
float availableWidth サイジングするために使用可能な幅
float availableHeight サイジングするために使用可能な高さ
YGDirection ownerDirection 配置するための主方向
YGMeasureMode widthMeasureMode 幅のサイジングルール
YGMeasureMode heightMeasureMode 高さのサイジングルール
float ownerWidth flex containerの幅
float ownerHeight flex containerの高さ
bool performLayout 呼び出し元がノードの次元だけに関心があるかどうか、またはノード全体とそのサブツリーをレイアウトする必要があるかどうかを指定するフラグ
YGConfigRef config ...

この中でも特に変数nodeの型であるYGNodeRef構造体について少し見ておきます。
まずはYGNodeRef構造体の定義箇所について。
YGNodeRef構造体はtypedefによりエイリアス宣言されています。
根幹で定義されているのは以下の箇所で、YGNode構造体です。
https://github.com/facebook/yoga/blob/1.9.0/yoga/YGNode.h#L15-L291

struct YGNode {
 private:
  void* context_;
  YGPrintFunc print_;
  bool hasNewLayout_;
  YGNodeType nodeType_;
  YGMeasureFunc measure_;
  YGBaselineFunc baseline_;
  YGDirtiedFunc dirtied_;
  YGStyle style_;
  YGLayout layout_; 
  // ...
}

たくさんのメンバが宣言されていますが、頻出するのはstyle_layout_の2つのメンバです。
これらはその名の通りスタイルと配置に関する構造体メンバです。

では早速、前処理部分を解剖していきましょう。
https://github.com/facebook/yoga/blob/1.9.0/yoga/Yoga.cpp#L2539-L2647

static void YGNodelayoutImpl(const YGNodeRef node,
    const float availableWidth,
    const float availableHeight,
    const YGDirection ownerDirection,
    const YGMeasureMode widthMeasureMode,
    const YGMeasureMode heightMeasureMode,
    const float ownerWidth,
    const float ownerHeight,
    const bool performLayout,
    const YGConfigRef config) {
  YGAssertWithNode(node,
      YGFloatIsUndefined(availableWidth) ? widthMeasureMode == YGMeasureModeUndefined : true,
      "availableWidth is indefinite so widthMeasureMode must be YGMeasureModeUndefined");
  YGAssertWithNode(node,
      YGFloatIsUndefined(availableHeight) ? heightMeasureMode == YGMeasureModeUndefined : true,
      "availableHeight is indefinite so heightMeasureMode must be YGMeasureModeUndefined");

  // Set the resolved resolution in the node's layout.
  const YGDirection direction = node->resolveDirection(ownerDirection);
  node->setLayoutDirection(direction);

  const YGFlexDirection flexRowDirection = YGResolveFlexDirection(YGFlexDirectionRow, direction);
  const YGFlexDirection flexColumnDirection = YGResolveFlexDirection(YGFlexDirectionColumn, direction);

  node->setLayoutMargin(
      YGUnwrapFloatOptional(node->getLeadingMargin(flexRowDirection, ownerWidth)),
      YGEdgeStart);
  node->setLayoutMargin(
      YGUnwrapFloatOptional(node->getTrailingMargin(flexRowDirection, ownerWidth)),
      YGEdgeEnd);
  node->setLayoutMargin(
      YGUnwrapFloatOptional(node->getLeadingMargin(flexColumnDirection, ownerWidth)),
      YGEdgeTop);
  node->setLayoutMargin(
      YGUnwrapFloatOptional(node->getTrailingMargin(flexColumnDirection, ownerWidth)),
      YGEdgeBottom);

  node->setLayoutBorder(node->getLeadingBorder(flexRowDirection), YGEdgeStart);
  node->setLayoutBorder(node->getTrailingBorder(flexRowDirection), YGEdgeEnd);
  node->setLayoutBorder(node->getLeadingBorder(flexColumnDirection), YGEdgeTop);
  node->setLayoutBorder(node->getTrailingBorder(flexColumnDirection), YGEdgeBottom);

  node->setLayoutPadding(
      YGUnwrapFloatOptional(node->getLeadingPadding(flexRowDirection, ownerWidth)),
      YGEdgeStart);
  node->setLayoutPadding(
      YGUnwrapFloatOptional(node->getTrailingPadding(flexRowDirection, ownerWidth)),
      YGEdgeEnd);
  node->setLayoutPadding(
      YGUnwrapFloatOptional(node->getLeadingPadding(flexColumnDirection, ownerWidth)),
      YGEdgeTop);
  node->setLayoutPadding(
      YGUnwrapFloatOptional(node->getTrailingPadding(flexColumnDirection, ownerWidth)),
      YGEdgeBottom);

  if (node->getMeasure() != nullptr) {
    YGNodeWithMeasureFuncSetMeasuredDimensions(node,
        availableWidth,
        availableHeight,
        widthMeasureMode,
        heightMeasureMode,
        ownerWidth,
        ownerHeight);
    return;
  }

  const uint32_t childCount = YGNodeGetChildCount(node);
  if (childCount == 0) { 
    YGNodeEmptyContainerSetMeasuredDimensions(node,
        availableWidth,
        availableHeight,
        widthMeasureMode,
        heightMeasureMode,
        ownerWidth,
        ownerHeight);
    return;
  }

  // If we're not being asked to perform a full layout we can skip the algorithm if we already know
  // the size
  if (!performLayout && YGNodeFixedSizeSetMeasuredDimensions(node,
        availableWidth,
        availableHeight,
        widthMeasureMode,
        heightMeasureMode,
        ownerWidth,
        ownerHeight)) {
    return;
  }

  // At this point we know we're going to perform work. Ensure that each child has a mutable copy.
  node->cloneChildrenIfNeeded();
  // Reset layout flags, as they could have changed.
  node->setLayoutHadOverflow(false);

  // STEP 1: CALCULATE VALUES FOR REMAINDER OF ALGORITHM

  // ...
}

まず、サイジングするために使用可能な幅(availableWidth)、高さ(availableHeight)が未定義な場合は、それぞれ幅、高さのサイジングルールwidthMeasureMode, heightMeasureModeがYGMeasureModeUndefinedではないといけないので、そのための検証をします。
これがどういうことか軽く説明します。
サイジングルールは引数から与えられるYGMeasureMode型のwidthMeasureModeheightMeasureModeです。
YGMeasureModeはEnumで定義されており、3つのEnum定数が存在します。
https://github.com/facebook/yoga/blob/1.9.0/yoga/YGEnums.h#L101-L105

  • YGMeasureModeUndefined
  • YGMeasureModeExactly
  • YGMeasureModeAtMost

YGMeasureModeUndefinedの場合は最大収容サイズに基づいた配置になります。
つまり内容に応じて伸縮可能なnodeです。
最大収容サイズとは無限の空き領域が与えられたときの、指定された主軸のボックスの「理想」サイズと規定されています。

YGMeasureModeExactlyの場合は主軸の利用可能なスペース全体を埋めます。
つまりReact Nativeのstyle指定で{flex:1}を使用した場合だと思います。
これは、使用可能なスペースをすべて埋めるようにコンポーネントに指示しています。

YGMeasureModeAtMostの場合は利用可能なスペースにnodeを収めます。
つまりYGMeasureModeUndefinedと近しいけれど、使用可能な幅(availableWidth)、高さ(availableHeight)を上限にするようです。

ということでavailableWidth, availableHeightが未定義の場合は利用可能なスペースというのが未定義なため最大収容サイズに基づいた配置(YGMeasureModeUndefined)でなければいけません。
そうでない場合は異常終了するという仕組みになっています。

以降はnodeのmarginやpaddingなどのスタイル設定していく処理になります。

  1. nodeのdirection(配置方向)の解決
  2. nodeにmarginを設定
  3. nodeにborderを設定
  4. nodeにpaddingを設定
  5. nodeのサイズを確定
  6. 子nodeを持たない場合は再度サイズ計算を行う
  7. 子nodeに現在のnodeが親となるように設定し、子nodeを更新する

少し省いた処理もありますが、このような流れになります。
前処理の主な計算はスタイルと配置に関することです。
親nodeと現在のnode間でのstyle継承や、flex container内の配置計算等、少し複雑な処理もありますが、基本的な流れは上記のようになります。
ということで、解説不足感が漂いますが前処理の解剖は以上です。

STEP 01 ~ STEP 11

今回は時間的に間に合わなかったため、来週にて「続編」という形で書くことにします。

( もしかしたら来週中に書き切れない可能性もありますw )
( 来週の記事で最後にさせて頂きますので、何かあればTwitter: @binarytaまでご連絡ください )


付録1: Yogaは一体どこから呼ばれているんだ?

メインルーチンYGNodelayoutImpl関数はYoga.cpp内でYGLayoutNodeInternal関数内から呼ばれています。次の箇所です。
https://github.com/facebook/yoga/blob/1.9.0/yoga/Yoga.cpp#L3649

さらに読み進めると、YGLayoutNodeInternal関数はYGNodeCalculateLayout関数の中で呼び出されているのが確認できます。
https://github.com/facebook/yoga/blob/1.9.0/yoga/Yoga.cpp#L3862

YGNodeCalculateLayout自体はどこから呼び出されているのかがまだ判明していません。
yogaディレクトリ内でコード検索をしても特定できません。
ということは、YGNodeCalculateLayout関数はiOSではObjective C、AndroidではJavaから呼ばれているということになりそうです。
ではターミナル上でコード検索してみます。(以下検索結果です)
f:id:AdwaysEngineerBlog:20180831181243p:plain

やはり見つかりました。
ちなみにAndroid側の方では直接YGNodeCalculateLayoutが呼ばれているわけではなく、jni_YGNodeCalculateLayoutという関数が呼ばれています。
(jniという接頭辞はJava Native Interfaceの略です)

これはlithoというAndroid UIフレームワークの一部のディレクトリをモノリシックに管理していて、Yogaはlithoによりラップされているためだと思います。
ということで以上です。
興味のある方は是非さらに深追いしてみてください!

付録2: YGNodeツリーのデバッグ出力

Xcode前提で話を進めます。
Yogaが保持するnodeツリーを詳細出力するための関数は、実は既に内部で定義されています。
この処理はファイルYGNodePrint.cpp内のYGNodeToString関数が担います。
この関数を使用するための処理も既に組み込まれていて、Yoga.cpp内に定義されているデバッグフラグをtrueにしてあげるだけで済みます。
次の箇所を反転させたら⌘Rでビルド実行してあげましょう。
https://github.com/facebook/yoga/blob/1.9.0/yoga/Yoga.cpp#L3345

// bool gPrintTree = false;
bool gPrintTree = true;

この変更によりデバッグ出力が有効になり、Xcodeのデバッグ出力パネルに次のようなログがプリントされます。
この詳細情報を使った方が理解の助けになると思うので、とりあえずデバッグ出力は有効にしておくと良さそうです。

2018-09-04 01:44:02.867 [info][tid:main][RCTCxxBridge.mm:926] Invalidating <RCTCxxBridge: 0x6000003d4af0> (parent: <RCTBridge: 0x6000000d97c0>, executor: (null))
2018-09-04 01:44:02.867685+0900 YogaTest[86394:7870598] Invalidating <RCTCxxBridge: 0x6000003d4af0> (parent: <RCTBridge: 0x6000000d97c0>, executor: (null))
2018-09-04 01:44:02.868 [info][tid:main][RCTCxxBridge.mm:209] Initializing <RCTCxxBridge: 0x6040003c0780> (parent: <RCTBridge: 0x6000000d97c0>, executor: (null))
2018-09-04 01:44:02.869215+0900 YogaTest[86394:7870598] Initializing <RCTCxxBridge: 0x6040003c0780> (parent: <RCTBridge: 0x6000000d97c0>, executor: (null))
2018-09-04 01:44:03.277 [info][tid:main][RCTRootView.m:293] Running application YogaTest ({
    initialProps =     {
    };
    rootTag = 51;
})
2018-09-04 01:44:03.276873+0900 YogaTest[86394:7870598] Running application YogaTest ({
    initialProps =     {
    };
    rootTag = 51;
})
RCTRootContentView(51), <div layout="width: 375; height: 812; top: 0; left: 0;" style="" ></div>2018-09-04 01:44:03.282 [info][tid:com.facebook.react.JavaScript] Running application "YogaTest" with appParams: {"rootTag":51,"initialProps":{}}. __DEV__ === true, development-level warning are ON, performance optimizations are OFF
2018-09-04 01:44:03.282215+0900 YogaTest[86394:7876668] Running application "YogaTest" with appParams: {"rootTag":51,"initialProps":{}}. __DEV__ === true, development-level warning are ON, performance optimizations are OFF
RCTRootContentView(51), RCTView(33), RCTView(29), RCTView(27), RCTView(15), RCTText(5), RCTView(7), RCTView(9), RCTView(13), RCTView(25), RCTView(17), RCTView(19), RCTView(23), <div layout="width: 375; height: 812; top: 0; left: 0;" style="" >
  <div layout="width: 375; height: 812; top: 0; left: 0;" style="flex: 1; " >
    <div layout="width: 375; height: 812; top: 0; left: 0;" style="flex: 1; " >
      <div layout="width: 375; height: 812; top: 0; left: 0;" style="flex: 1; " >
        <div layout="width: 375; height: 569.667; top: 0; left: 0;" style="justify-content: space-around; align-items: center; flex: 8; " >
          <div layout="width: 100.333; height: 29; top: 101.333; left: 137.333;" style="margin-top: 60px; " has-custom-measure="true"></div>
          <div layout="width: 50; height: 50; top: 212.667; left: 162.667;" style="width: 50px; height: 50px; " ></div>
          <div layout="width: 50; height: 50; top: 345.667; left: 162.667;" style="width: 50px; height: 50px; " ></div>
          <div layout="width: 50; height: 50; top: 478.333; left: 162.667;" style="width: 50px; height: 50px; " ></div>
        </div>
        <div layout="width: 375; height: 142.333; top: 569.667; left: 0;" style="flex-direction: row; justify-content: space-around; flex: 2; margin-bottom: 100px; " >
          <div layout="width: 60; height: 60; top: 20; left: 32.6667;" style="width: 60px; height: 60px; " ></div>
          <div layout="width: 60; height: 60; top: 20; left: 157.667;" style="width: 60px; height: 60px; " ></div>
          <div layout="width: 60; height: 60; top: 20; left: 282.667;" style="width: 60px; height: 60px; " ></div>
        </div>
      </div>
    </div>
  </div>
</div>

f:id:AdwaysEngineerBlog:20180904014746p:plain