Facebook: iOSアプリのアーキテクチャ

https://www.youtube.com/watch?v=XhXC4SKOGfQ

1 comment | 1 point | by WazanovaNews 約3年前 edited


Jshiike 約3年前 edited | ▲upvoteする | link

FacebookのiOSチーム、Adam ErnstとAri Grantによる@Sacle 2014での講演。データモデルとビューレイヤの改善の取組みについて紹介してくれてます。

1) データモデル

背景

  • 2年前からHTML5からネイティブに切り替えて一旦大きく改善したが、その後機能を追加するたびにアプリのパフォーマンスが悪化。
  • ネイティブに移行後、オブジェクトのキャッシュレイヤとしてiOSのCore Dataを使ったのが失敗であった。
  • Core Dataの役割は「整合性を含むオブジェクトグラフ管理」
  • Facebook iOSアプリの場合、サーバ側を正のデータとするが、デバイス側で、すぐにコンテンツを表示できるように、またコンテンツの整合性(例えば、Timelineに表示される特定のコンテンツのLike数は、同じコンテンツがNews Feedに表示された際のLike数と一致させる。)のためにCore Dataをキャッシュに利用。
  • 具体的には、初期化とランタイムにおけるパフォーマンス、及びプログラムを書く際の非効率さという三点が問題であった。

初期化のパフォーマンス

  • Core Dataを初期化する時間が増えていく問題。オブジェクトタイプ数は、ネイティブアプリに追加する機能数に概ね比例して増加する。しかし、タイプ間のリレーションシップはそれ以上に増える構造なので、パフォーマンスの悪化ペースがどんどん加速していくという状況。
  • Core DataはSQLiteに対してどう機能するかというと、データベーステーブルをタイプごとに作成、かつリレーションシップごとにインデックスもしくはテーブルを用意する。よって、デバイスに単一の大きなCore Dataができあがる。
  • ビルド前のデータベースファイルをアプリと合わせて準備し、初期化時にコピーするというかなりクリエイティブな工夫もしたが、それでも改善はせず。
  • 単一の大きなCore Dataではなく、機能ごとにキャッシュを分割し、かつ整合性を維持できる仕組みが必要となった。

ランタイムのパフォーマンス

  • 理想的には、News Feed / Timeline / Notifications / Groupsなどの機能ごとにデータを保存でき、非同期でお互いに整合性をとり、ブロックしないようにしたかった。しかし現実的には、Core Dataは、グローバルに整合性をとるデータストア。個別の機能が新しいプロセスの処理をしようとすると、全ての機能にまたがるグローバルロックをする。つまり、一つの機能の処理をするのに、他の機能もブロックしてしまう。アプリ全体をフリーズさせることになる。
  • Core DataのFaultingは、エンジニアがメモリ管理を気にする必要のないお任せ機能。例えば、コメントのオブジェクトがメモリにあり、そのコメントを書いたユーザのオブジェクトはメモリにないとする。Faultingは、フェイクのコメントユーザのオブジェクトを渡しておき、呼び出しスレッドをブロックして、データをディスクから読み取って返してくれる。ユーザがコメント画面をどんどんスクロールするとフレームがドロップしてしまう。Faultingは事前にfetchすることでも対処してくれるが、どこかで限界はきてしまう。お任せではなく、自分たちでしっかりコントロールできるようにしたかった。

プログラムを書く際の非効率

  • 誰にでもいつでも変更されてしまう可能性のあるmutableは、
    • どこのコードの変更が、どの階層でのデータ更新を起こし障害になったのか、特定が難しい。
    • オブジェクトをそのまますぐにバックグランドのスレッドに渡せない。オブジェクトをつかまえて、identifierがスレッドに渡して、バックグランドの異なるストアに読込ませたりと回りくどい手順になる。
    • 双方向のバインディング: iOSで言うところのKey-Value Observingでは、オブジェクトモデルが各プロパティの変更を待ち受けてから、UIを更新する。小さなプロジェクトであれば、シンプルでわかりやすい仕組みだが、多くのメンバが絡む大規模で複雑なプロジェクトになると、更新が巨大なチェーンのようにつながって、デバッグができなくなる。双方向のバインディングはあちこちのコードのどこかに隠れていて、全体像を把握できない。競合状態となる原因究明は相当難しい。
  • リレーショナルデータベースに必要とされるACIDの基準で考えてみよう。個別のキャッシュなので全部のデータは必要ない。
    • 原子性: 是非ほしい。
    • 整合性: 結果整合性でよい。銀行でないので、各機能の同コンテンツが多少づれても問題ない。
    • 独立性: 是非ほしい。
    • 耐久性: いらない。ただのキャッシュなので、欠落したらまたサーバから読込めばよい。ベストエフォートで問題なし。

100% immutableなモデル構築

  • 100% immutableとは、
    • Deep-immutableモデル: 例えば、Storyオブジェクトであれば、Storyオブジェクト自体もimmutableだが、それが参照しているものもimmutableであること。
    • 直接すぐにモデルは変更できない。階層のトップレベルにあるストアに連絡し、そこから非同期にツリーを下って反映させる。
    • 整合性マネジャがコーディネートする。
  • 例えば、Storyを頂上として、author / Photoという枝があるツリーにおいて、Storyにlikeを1件足すとする。その際は、Storyだけでなくツリー全体をコピーして、Storyのlike数を1件増加させる。
  • 各機能にまたがるやり取りは、整合性マネジャがコーディネートする。
  • 整合性についてはどこか中央でまとまったものがあるわけではない。アプリの各機能が好きなものをディスクに書込みできる。ディスクに書込まれないものもある。例えば、20秒しか存在する意味がないスポーツのリアルタイム情報など。ちなみに、MessagePackCoderを利用して最適化はしている。
  • オブジェクトにユニーク性はもたせない。Core Dataなら、例えばAに対する参照は全て同じ扱いだが、immutableモデルではそれぞれ別のオブジェクトという建付けになる。
  • ではどうやって変更を知るのか?差分とかバインディングではなく、常にツリーのトップから変更が反映されてくる。変更がないところは変更しないという効率的なUIのアップデート手法を取る。
  • その他の特徴としては、
    • サーバのスキーマに基づきコードを生成する。
    • マジックには頼らない。プレーンなNSObject (faulting / uniquing / lazy loadingなし)
    • カスタマイズしたテンプレ + mogeneratorでコードを生成。

成果

  • News Feedの読込みスピードが35%改善。
  • 全体的にボトルネックがほぼなくなった。
  • エンジニアとしては、コードが理解しやすく、フローが追いやすくなったことが大きい。

2) Viewレイヤへの応用

背景

  • UIの制約が大きく、プログラムは複雑で壊れやすい状態。
  • 複雑さをシステム側で吸収したい。
  • 上記1) のModelレイヤの考え方をViewレイヤに反映させたい。

MVC

  • 例えば、News Feedのページでは、Feed View -> Story View -> Like Viewという階層でViewが構成されている。それに対して、Controllerも、Feed Controller -> Story Controller -> Like Controllerという階層にマップされている。これにModelが絡んで、各Model / Controller / Viewがそれぞれやり取りをしながらUIを更新するという複雑な仕組みであった。ビデオ: 29分17秒時点
  • データとイベントのやり取りが各所で発生し、エンジニアはどこで何が起きているか把握できない。

mutableステートとマルチスレッド

  • どうするか?
    • 全てをmain (UI) スレッドで処理 -> パフォーマンスを担保できない。
    • 全てをアトミックに -> ロジックの競合があらゆる箇所で起こる
    • 関連するプロパティを共有ロック -> 複雑。デッドロックになる危険性あり。
  • mutableなステート & マルチスレッドのコードにおいて、競合状態を解明するのはものすごく難しい
  • 障害例として、コメントが挿入 & レンダリングされても、heightのが効かず位置づれして、タップできない。どこが原因でそうなったかテストしてみる。
    • 各Storyは正しく設定されている。
    • heightの変更は伝わっている。
    • Viewステートは正しく更新されている。
    • 再利用される際にviewプロパティはセットされている。
    • ページ内に複数のviewがあり、再利用の可能性まで考慮すると設定が変わる組み合わせは、際限がなく、テストしきれない。。

iOSのview API

  • DOMに似た階層view構造
  • mutateなviewツリー。subviewが追加/削除される。
  • viewのサイズを計算するためにメソッドが上書きされる。
  • viewのsubviewをレイアウトするためにメソッドが上書きされる。
  • viewは main (UI) スレッドでのみ利用できる。

Componentの要件

  • 複雑なところはシステム側で対応する。
    • viewのレイアウトと再利用を簡単にできるようにする。
    • ステートの変更の際は一方通行のデータフローとする。
    • compositionだけでできるようにし、immutabilityを担保する。
    • システム側でまとめて処理することで、レイアウトが崩れるような最悪なバグの発生を防ぐ。
    • テキストのレイアウトとか画像のデコードとかの計算を心配しなくても、非同期に対応してくれる。

Componentとは

  • Componentを開発し、ControllerをComponentで置き換えた。実現したいことを記述すると、後はシステム側が対応してくれる仕組み。コードサンプル: ビデオ33分5秒時点
    • ReactのコンセプトをObjective-C++(C++ & Objective-C)に移植。
    • functionにデータを渡すと、viewのimmutableな構成が返ってくる。
    • システム側がfunctionを呼び出し、レンダリングする。データに変更があれば再実行してくれる。
    • どのスレッドにおいても、Componentは新規作成、サイズ変更、レイアウト可能
    • ビルトイン & カスタムメイドのレイアウトタイプ
  • エンジニアがStory Modelを修正して、Story Componentのツリー詳細が定義される。それから、システム側で正しいStory view階層を生成してくれる。従来のアーキテクチャのContorollerをComponentに変更することで、複雑に絡んだフローが、シンプルな一方向のやり取りに集約できる。概念図: ビデオ36分38秒時点
  • Viewのツリーを部分的に再利用することでコストをかけずにレンダリングしている。
  • [Immutableなviewを実現するComponentの設定例。コードサンプル: ビデオ40分時点

    • 「このようなviewの階層にしたい。」-> システム側がviewの再利用/設定をしてくれる。
    • 「このようなレイアウトにしたい。」-> システム側が計算してくれる。
    • 「このようにステートを変更した。」-> システム側がcomponentを生成するfunctionを実行し、新しいステートを渡してくれる。
  • ステートはどうなる?: Immutabilityとカプセル化

    • Componentはimmutable、ライフサイクルは短い。
    • Componentはmutableなステートはもてない。
    • 新しいステートは新しいcomponentで扱う。
  • Objective-C++(C++ & Objective-C)のメリット

    • Type and const safety
    • Nil-safe collection
    • 効率(スタックの割当、フィールドのルックアップがコストかからないなど。)
    • まとめて初期化

成果

  • 70%のコードを削減。
  • 再利用による効率化。
  • パフォーマンス向上。
  • 不要になったこと。
    • viewの設定
    • viewの再利用の実装
    • レイアウトとサイズの計算
    • ステートの変更の待ち受け
    • スレッドやmutable性の心配
  • 次のステップとしては、この仕組み一式をSwiftに移植したい。

#Facebook #ios


ワザノバTop200アクセスランキング


Back