『Akiba.swift × エウレカ』で「AutoLayout以外の選択肢」について発表しました

エウレカ iOS エンジニアのmuukiiです🤠

2018/3/28にエウレカで開催した『Akiba.swift』にて、iOSアプリ開発におけるレイアウトの話をしました。

AutoLayoutによってUIの動作が遅くなっている可能性があり、どのように高速化ができるのか。

という内容です。

スライドを作る前のメモをベースに記事にしておきます 🤩

もし「UIの動きが遅くて困ってる!」みたいなのがありましたら私でよければ相談乗ります 🙋🏻‍♂️

AutoLayout以外の選択肢

コードで書くAutoLayout

AutoLayoutはコードで設定することもできます。

NSLayoutConstraintとiOS9から使えるNSLayoutAnchorを使用する。

NSLayoutAnchorの登場によりコードでも相当書きやすくなりましたが、それでもまだ惜しい感じです。

そのため、より簡潔に記述するためのライブラリがたくさん存在しています。

これだけの数が存在していることから、IBではなくコードでレイアウトを記述したいニーズがあることが考えられます

AutoLayoutはパフォーマンスの問題を抱えている

AutoLayoutは制約の数が増えるとどんどん遅くなる

引用 : https://github.com/layoutBox/FlexLayout#performanceんです。

リフレッシュレートが60Hzのディスプレイにおける一回のイベントループでブロックして良い時間は、約16msです。
この間にUIKit自体の処理も入るので私たちが書くコードは10ms程度に抑えられると良さそうです。

余談ですが、最近の iPad Proは120Hzになってるので、もっと速くしないとディスプレイの価値は発揮されないです。

一方で、動きの少ない一枚の画面であれば大きな問題にはなりません。

例えば、ログイン画面のようなシンプルな画面です。
むしろスクロール可能でない画面におけるマルチデバイス対応のためのレスポンシブレイアウトではAutoLayoutはかなり強力です。

また、AutoLayoutの制約を工夫して高速化を行う記事がありました。

UITableView/UICollectionViewで60fpsが欲しい

Cellの中でAutoLayoutを使用するとどうしても時間がかかってしまいます。

fpsの向上にはCellのサイズ計算よりもCellの中身を設定するタイミングが重要です。

Cellのサイズ計算はreloadData直後に全て行われる

  • contentSizeを確定するため
  • しかし、estimated~を使用している場合、動作は異なります。 (ここはあんまり詳しくないです😱 そして、いい思い出がない。)

Viewに変更が走ると再計算が走る

  • 制約が存在するUIViewにaddSubview
  • 制約の組まれたUILabel.textを変更する時など
  • textが変わるとUILabelから`invalidateIntrinsictContentSize`が呼ばれ再レイアウト

AutoLayoutを使わない方法を考える

AutoLayoutではないレイアウトエンジンはいくつか存在しています。
有名どころのライブラリを紹介します。

全部は紹介しきれないのでいくつかピックアップします。

🧘🏻‍♀ ️facebook/Yoga

  • 非常に高速なクロスプラットフォームなレイアウトエンジンライブラリ
    iOS, Android, Web
  • AutoLayoutと比較して8倍程度高速らしい
    https://github.com/layoutBox/FlexLayout#performance
  • PinLayoutのほうがさらに速いらしいけど今回は割愛
    これ高速かつAutoLayoutみたいなこと出来るのでオススメかも
  • CSS Flexbox Layoutと非常に近い動作と記述方式
    Flexboxの基本はUIStackViewのverticalとhorizontalを組み合わせてレイアウトする感覚に近い
  • コアとなるコードはC++で3000行ほど
  • ReactNativeでも使われている
    これはつまり、ReactNativeのアプリの方がUIの動作が速い可能性があるってことです。

FlexLayout(Yoga)を使ったCellの高さ計算のサンプルはここにあります。

TextureGroup/Texture

もとの名はAsyncDisplayKit

  • PinterestとFacebookの共同開発だった
    昔あったFacebookPaperというアプリで使われて話題になった
    同じ時期にfacebook/popも登場
    その後FacebookはComponentKitに集中し、AsyncDisplayKitはほぼPinterestによる開発になる。
    ComponentKitはFacebookが開発するReactの考え方を用いたコンポーネント指向なUIライブラリ
  • Pinterestアプリで全面的に使用されている
  • Pinterestを触るとその強さが分かる
  • Objective-C++で実装されている

ライブラリ利用者はUIKitコンポーネントを操作するのではなくNodeというオブジェクトを通して操作する

  • Nodeはスレッドセーフで、バックグラウンドスレッドで更新可能
  • Node -> UIViewへの展開はTextureが行う (AsyncにDisplayする)
  • MainThreadがUI操作で忙しければ展開を見送る仕組みがある
  • レイアウトはNode内にコードで記述

驚くほどの最適化

可能な限りバックグラウンドスレッドで処理を行う

  • メインスレッドはUIのインタラクション用に空けておく
  • レスポンス速度が最大化

UILabelやUIImageViewは使われずにレイヤーにレンダリング

  • タップ判定の必要のないものはCALayerとして表示 (layerBacking)
  • UIViewよりメモリ消費が抑えられるため

バックグラウンドでメモリ解放を行うキューがある

deallocationも一定のコストがかかるため多くのNodeオブジェクトがメインスレッドで解放されてしまうとUIが固まる可能性があります。

これを防ぐためにバックグラウンドスレッド上で良いタイミングでdeallocを行うキュー(runloop)が動いています。

Nodeに記述したレイアウトはバックグラウンドでサイズ計算が可能

  • concurrentで実行される
  • Nodeが持つデータが他スレッドに依存していなければ、CPUのコアがフルで使える
  • iPhoneXなら5個Cell同時に計算 😲

現時点で最も最適化されたフロントエンドライブラリ
=> 直列でも高速な処理を並行で処理可能にしているから

まとめ(感想)

(前提として私はAutoLayout大好きです。これが超高速になったら世界が変わりますね。)

AutoLayoutは柔軟で強力なレイアウトエンジンだけど、まだパフォーマンス面で課題があると思います。

現状、CPUのクロック数の伸びには期待しづらいので、UIパフォーマンス向上のためにはマルチスレッドによるアプローチが必須になってくるはずです。
しかし、UIKitは現時点ではマルチスレッドには対応していないんですよね。

こういった現状からFacebook, Instagram, PinterestのようなサービスはプロダクトではAutoLayoutの使用を避けており、それぞれがUIパフォーマンスを最大化するOSSを公開しています。

- Pinterest — Texture
- Facebook — ComponentKit, ReactNative, Texture
- Instagram — IGListKit (ちょっと趣旨は違うけど)

このような状況に対して、今後Appleが取るUIKitへの考えが楽しみです。
AutoLayout頑張って欲しいですね!