モバイルUIの描画パイプライン
カクつき(ジャンク)の原因は勘ではなく描画予算の超過として説明できる。View階層からGPUまでの流れを押さえれば、原因の層を一発で切り分けられる。
- 1.描画はレイアウト(測定・配置)→ 描画コマンド生成 → コンポジション → ラスタライズ → GPU合成という段階的パイプラインで進み、各段が次段の入力を作る。
- 2.60Hzなら約16.6ms、120HzのProMotionや可変リフレッシュレートでは約8.3ms以下という描画予算があり、超過した瞬間にフレームドロップ(ジャンク)が発生する。
- 3.iOSのCALayerツリーとAndroidのRenderNode/HWUIはどちらもRetained-modeのシーングラフをGPUに渡す設計で、CPU側の再計算範囲を狭めることが性能の鍵になる。
「滑らかさ」は締切問題である
モバイルUIの滑らかさは主観の話に見えて、実体は組込みのリアルタイム処理に近い締切問題です。ディスプレイは一定間隔でリフレッシュ信号(VSync)を出し、アプリはその間隔内に次のフレームを完成させなければなりません。間に合わなければ前のフレームが再表示され、体感として「カクつき」=ジャンクになります。ここではUIKit/Androidの View 階層からGPU合成までの経路を追い、どこで予算超過が起きるとどんな症状になるかを整理します。
パイプラインの5段階
モバイルUIの1フレームは、おおむね次の順序で処理されます。
| 段階 | 何をするか | 主な処理主体 |
|---|---|---|
| 1. レイアウト(測定・配置) | Viewツリーを走査し、各要素のサイズと座標を確定する | CPU(メインスレッド/UIスレッド) |
| 2. 描画コマンド生成 | 各Viewが自分の見た目を描画命令(矩形・パス・テキスト)として記録する | CPU |
| 3. コンポジション | 描画結果をレイヤー単位にまとめ、変換・透明度・重なり順を決める | CPU(専用スレッドでのレンダーツリー構築) |
| 4. ラスタライズ | ベクタ的な描画命令をピクセルのビットマップへ変換する | GPU(タイルベースGPUが主流) |
| 5. GPU合成・スキャンアウト | 複数レイヤーを1枚のフレームバッファに合成し、ディスプレイへ送出する | GPU/ディスプレイコントローラ |
段階1と2は多くの実装でメインスレッド(Androidの場合はUIスレッド)が担い、ここが重いとフレーム全体が遅延します。段階3以降は専用スレッドやGPUに委譲され、CPU側の仕事を軽くしつつ並列に進めるのが現代の設計です。
iOS:UIView から CALayer、そして Render Server へ
UIKitの UIView はタッチ処理やレイアウトの責務を持つ実体ですが、ピクセルのバッキングストアや変換・透明度といった描画状態の大半は裏側の CALayer が保持します。ツリー構造は次のように分岐します。
UIView階層(アプリのメインスレッド)が、タッチ処理やレイアウト制約の解決を担当する。- 各
UIViewは対応するCALayerを持ち、Core Animation はこのCALayerツリーを Retained-mode のシーングラフとして保持する。 - レイアウトと描画コマンド生成が終わると、アプリプロセスは
CALayerツリー(実体は差分コミット)を、別プロセスの Render Server へ送る。 - Render Server がレイヤーツリーからコンポジションを構築し、GPUにラスタライズと合成を指示する。
ここで重要なのは、アプリのメインスレッドが直接ピクセルを塗っているわけではないという点です。アプリ側の仕事は「次のフレームで見た目がどう変わるか」という宣言的な差分を作ることで終わり、実際のラスタライズと合成はプロセス境界を越えてGPU寄りの経路に渡ります。この分離により、レイアウトが少々重くても、既にコミット済みのレイヤーツリーに対する単純な変換(位置・不透明度・スケール)はアプリ側の再計算なしにGPUだけで再生できます。transform や opacity のアニメーションが frame の変更より軽いとされる理由はここにあります。
Android:View 階層から RenderNode/HWUI へ
Androidも構造は同型です。
View階層がUIスレッドでmeasure→layout→drawの3パスを実行する。drawパスは実際のピクセルではなく、RenderNodeに対する描画コマンド(DisplayList)を記録する。RenderNodeはツリー状に構成され、変換や透明度をノード単位で保持する(iOSのCALayerに相当するRetained構造)。- HWUI(ハードウェアアクセラレーションレンダラ)が
RenderNodeツリーをレンダースレッドに渡し、OpenGL ES/Vulkan経由でGPUがラスタライズする。
Androidの measure/layout/draw の3パスは、親から子への再帰と子から親への戻りを繰り返すため、ツリーが深い、あるいは requestLayout が広範囲に伝播すると、この段階だけで予算の大半を食いつぶします。invalidate (再描画のみ要求)と requestLayout (測定からやり直し)を区別し、変更の影響範囲を最小化することが、実務上の最重要ポイントです。
UIKitの CALayer ツリーも AndroidのRenderNodeツリーも、Immediate-mode(毎回全部描き直す)ではなくRetained-mode(差分だけをシーングラフに反映し、変わらない部分は保持する)を採っています。CPU側の役目は「シーングラフの更新差分を作ること」に限定され、実際のピクセル生成と合成はGPU側に閉じ込められます。これによりCPUとGPUがパイプライン的に並行動作でき、単一スレッドの逐次実行よりスループットを稼げます。
描画予算とリフレッシュレート
ディスプレイは固定間隔でVSync信号を発行し、アプリはその間隔内に段階1から5までを完了させる必要があります。間隔(予算)はリフレッシュレートで決まります。
| リフレッシュレート | 1フレームの予算 | 特徴 |
|---|---|---|
| 60Hz | 約16.6ms | 長らくの標準。予算に余裕があり多少の重い処理も吸収しやすい |
| 90Hz | 約11.1ms | ミドルレンジ機で採用増。60Hzの2/3の予算しかない |
| 120Hz(ProMotion等) | 約8.3ms | 60Hzの半分の予算。同じ処理でも予算超過しやすくなる |
| 可変リフレッシュレート(LTPO) | コンテンツに応じ1〜120Hz程度で可変 | 静止画面は低Hzで消費電力を抑え、スクロール等の動きに応じて動的に引き上げる |
高リフレッシュレート化は体感の滑らかさを上げる一方、同じ処理内容でも予算に対する余裕が減ることを意味します。60Hzで問題なかったレイアウトコードが120Hzでは恒常的にジャンクを出す、という現象は珍しくありません。可変リフレッシュレート機構は、動きのない場面でリフレッシュレートを落として電力を節約し、スクロールやアニメーションを検出すると引き上げる制御をOS側が行います。アプリ側から見ると「予算は固定ではなく状況に応じて変わる」ため、常に最も厳しい予算(最高リフレッシュレート時)を前提に設計するのが安全です。
ジャンクの原因を層で切り分ける
予算超過はパイプラインのどの段階でも起こり得ますが、原因ごとに症状と対策が変わります。
- レイアウト段の肥大化: Viewツリーが深い、制約解決が複雑、
requestLayoutが広範囲に伝播する。対策はツリーの平坦化と再測定範囲の限定。 - 描画コマンド生成の重さ: 複雑なパス描画やテキストレイアウトを毎フレーム再計算している。対策はキャッシュ化と差分更新。
- コンポジション段の過多: 重なるレイヤー数が多い、不透明度や角丸など合成コストの高い効果(オフスクリーンレンダリング)を多用している。対策はレイヤー統合とオフスクリーン合成の削減。
- ラスタライズ段の肥大化: 巨大なビットマップや複雑なシェーダーをGPUが処理しきれない。対策は解像度・オーバードローの削減。
- メインスレッドのブロック: I/Oや重い計算をメインスレッド(UIスレッド)で行い、レイアウト・描画の順番待ちが発生する。対策は非同期化。
オーバードローとは、同じピクセルを1フレーム中に何度も塗り直してしまう状態です。背景色、カード、影、その上のテキストが重なるだけで同一座標が3〜4回描かれることがあり、ラスタライズ段の負荷を線形以上に押し上げます。不要な不透明背景の重ね塗りを削るだけで、GPU側の予算を大きく取り戻せる場合があります。
「レイアウトが重い」と「合成・ラスタライズが重い」は症状が似ていても原因の層が違います。前者はメインスレッド(CPU)のプロファイルで測定時間が伸び、後者はGPU側のフレームタイムやオフスクリーンパス回数に現れます。60Hzの約16.6msと120Hzの約8.3msという予算の違いを数値で言えるようにしておくと、面接や設計レビューでの説明力が上がります。
まとめ
モバイルUIの描画は、View階層でのレイアウト確定、描画コマンド生成、Retained-modeのレイヤーツリーによるコンポジション、GPUでのラスタライズ、最終合成という段階的パイプラインで進みます。iOSの CALayer とAndroidの RenderNode はいずれもCPU側の再計算範囲をシーングラフの差分に限定し、実際のピクセル生成をGPUに委ねる設計です。この分業があるからこそ、単純な変換アニメーションは軽く、レイアウト変更やオーバードローは重いという非対称性が生まれます。60Hzで16.6ms、120Hzで8.3msという描画予算を常に意識し、ジャンクが出た際にレイアウト・描画・コンポジション・ラスタライズのどの層で予算を超えているかを切り分けることが、パフォーマンス改善の出発点になります。GPU側の合成やラスタライズの基礎は /graphics/、割り込みやスケジューリングの土台となるOS側の仕組みは /os/ と合わせて理解すると立体的になります。
モバイル開発 Article
モバイルUIの描画パイプラインを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
モバイル
比較で見る軸
難易度: advanced / カテゴリ: モバイル開発 / タグ数: 6
導入後に効く点
60Hzなら約16.6ms、120HzのProMotionや可変リフレッシュレートでは約8.3ms以下という描画予算があり、超過した瞬間にフレームドロップ(ジャンク)が発生する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- モバイル開発
- タグ数
- 6
判断チェックリスト
- 自社の用途が「モバイル / UIKit」に近いか確認する。
- 強みである「描画はレイアウト(測定・配置)→ 描画コマンド生成 → コンポジション → ラスタライズ → GPU合成という段階的パイプラインで進み、各段が次段の入力を作る。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。