かくつかないiOSアプリを作るために


自己紹介

Link-Uの原野です。iOS 50% / Web フロント 50%くらいで仕事をしてます。

iOSにおけるカクツキとは

iOS では画面をスクロールした際に、滑らかにスクロールせず一瞬アニメーションが飛んでしまうフレーム落ちをするタイミングがあります。
これはユーザー体験によくない影響があり、iOS アプリでは改善の対象となります。

Appleも最近この辺に力を入れているっぽく、WWDCでも一大テーマとして多くのセッションがありました。

iOS の描画の流れ

まずはカクツキの機序を知るために、iOSが描画のために何をやっているかをざっくり追っていきましょう。

リフレッシュレート

iOS では画面の描画の処理は一定のリフレッシュレートで行われています。
その速度は iPhone/iPad では 60Hz(16.67ms/回)、最新の iPadPro では 120Hz(8.33ms/回)となります。
カクツキは描画のための処理がこのリフレッシュレート内で処理しきれない場合に発生します。

RenderLoop

iOS では描画のための処理を RenderLoop と呼ばれ、大きく 3 つのフェーズに分かれています。

  1. CommitPhase
  2. RenderPahse
  3. DisplayPhase

CommitPhase

ユーザーからの入力に応じて、変更が必要なビューを検知し、レイアウトや背景色などをまとめ描画可能な状態のデータ構造を作る

RnderPhase

CommitPhase 作成されたデータ構造を元に実際に表示可能な画像に変換する。主にGPUが頑張る領域。

DisplayPhase

最終的な画像を画面に表示する

RenderLoopで見るカクツキの原因

iOS では各フェイズは並列なパイプラインと処理されます。
その各フェーズがリフレッシュレートの範囲内に完了しなかった場合、
最終的な画面が更新されず 、ユーザーにはカクツキという形で見えてしまいます。

この中で、カクツキの原因は大きく2つの要因になります。

  1. CommitPhase のレイアウト計算に時間がかった
  2. RenderPhase でビューをレンダリングするのに時間がかかった

実際にカクツキを減らすには

アプリのカクツキを減らすには上記のCommitPhase/RenderPhaseの処理を高速化する必要があります。
主に対処法としては以下があります。

CommitPhaseの高速化

CommitPhaseの高速化にはレイアウト処理の高速化をする必要があります。
主に以下の方法があります。

  • 不要なViewの削除
  • AutoLayoutをやめてマニュアルレイアウトを用いる(残念ながらAutoLayoutのパフォーマンスはそこまでよくありません)
  • レイアウトの自動計算をやめて、極力手動計算や固定値を用いる
  • 一度高さを計算した値はキャッシュしておく

RenderPhaseの高速化

RenderPhaseの高速化は主に以下の方法があります

  • 角丸などCALayer上での処理を避ける
  • 半透明なビューなど、重ね合わせの計算が大変なものを無くす
  • 不要なビューを削除する

この辺は画面の見栄えに影響することも多く、デザイナーや企画の方などステークホルダーとの調整が必要なことが多くあります。

アプリのカクツキ度合いを調べる

問題の発生や解決手段が分かったところで、実際に自分のアプリでその問題が発生しているかを判断するにはいくつかの方法があります。

CodeOrganizer

XCode11からXCodeOrganizer上でHitchRate(フレーム落ちの割合、英語ではフレーム落ちをHitchと呼ぶそうです)を調べられるようになりました。
HitchRateは1秒間にHitchが発生した時間の割合が指定され、Appleでは5ms以下がユーザーがフレーム落ちを経験しない良いアプリであるとしています。
逆に10を超えると、ユーザーの体験はかなり悪いと判断して良いそうです。

まだ実際の案件で実用までいっておらず、今後取り入れていけたらなと思っています。

XCTest

iOS14以上の環境ではXCTestでHitchRatioを計測できるようになりました。

func testPerformanceExample() throws {
    // This is an example of a performance test case.

    app.launch()
    let view = app.descendants(matching: .table).firstMatch
    let measureOptions = XCTMeasureOptions()
    measureOptions.invocationOptions = [.manuallyStop]
    measure(metrics: [XCTOSSignpostMetric.scrollDecelerationMetric], options: measureOptions) {
        view.swipeUp(velocity: .fast)
        stopMeasuring()
        view.swipeDown(velocity: .fast)
        // Put the code you want to measure the time of here.
    }
}

ただ・・・手元の環境で試したところ、なぜか計測できませんでした。
まだ新しい機能なこともあり情報が少なそうです。

Appleによるとこの項目にHitchRatioが並んでいるらしい・・・。

性能の低い実機で目視

何やかんや現状今のところ一番利用されている手法です。
Link-U内部ではデバッグ時にiPodTouchで検証がされており、ひどいカクツキが発生する場合はエンジニアにフィードバックがあります。
エンジニアは何だかんだ最新機種で触っていることも多く、巷で使われている中の最低レベルの端末でのテストの大事さがよくわかります。

まとめ

カクツキの機序・改善方法・計測の方法を見ていきました。
とりあえず、XCTestでHitchRatioの取り方が分かったら知りたい。

参考資料