遅い表示

UI レンダリングとは、アプリからフレームを生成して画面に表示することです。ユーザーのアプリ操作をスムーズにするためには、16 ms 以内にフレームをレンダリングし、60 フレーム/秒(fps)を達成する必要があります。60 fps が推奨される理由については、Android パフォーマンス パターン: Why 60fps? の動画をご覧ください。90 fps を達成しようとしている場合、この制限時間は 11 ミリ秒になり、120 fps の場合は 8 ミリ秒になります。

この制限時間を 1 ミリ秒超過すると、フレームが 1 ミリ秒遅れて表示されるわけではなく、Choreographer によってフレームが完全にドロップされます。アプリの UI レンダリングが遅い場合、システムはフレームをスキップせざるを得ず、ユーザーはアプリの動作がスムーズでないと感じます。これをジャンクと呼びます。このページでは、ジャンクを診断して修正する方法について説明します。

View システムを使用しないゲームを開発する場合は、Choreographer をバイパスします。この場合、Frame Pacing ライブラリを使用すると、OpenGL ゲームと Vulkan ゲームでスムーズにレンダリングし、Android 上のフレーム ペーシングを修正できます。

アプリの品質向上に役立つように、Android ではジャンクを自動的にモニターし、情報を Android Vitals ダッシュボードに表示します。データの収集方法については、Android Vitals によるアプリの技術的品質のモニタリングをご覧ください。

ジャンクの特定

ジャンクの原因となっているコードをアプリ内で特定することは困難な場合があります。ここでは、ジャンクを特定する 3 つの方法を説明します。

目視検査では、アプリのすべてのユースケースを数分で実行できますが、Systrace ほど詳細な情報は得られません。Systrace では詳細な情報が得られますが、アプリのすべてのユースケースに対して Systrace を実行すると、分析が困難になるほど大量のデータであふれる可能性があります。目視検査と Systrace はいずれも、ローカル デバイスでジャンク検出を行います。ローカル デバイスでジャンクを再現できない場合は、カスタム パフォーマンス モニタリングを作成すれば、フィールドで実行されているデバイス上のアプリにおける特定の部分を測定できます。

目視検査

目視検査は、ジャンクを発生させているユースケースを特定するのに役立ちます。目視検査を行うには、アプリを開き、アプリのさまざまな部分を手動で確認して、UI でジャンクを探します。

目視検査を行う際のヒントを次に示します。

  • アプリのリリース版(または少なくともデバッグ用ではないもの)を実行します。ART ランタイムは、デバッグ機能をサポートするためにいくつかの重要な最適化を無効にします。そのため、ユーザーに表示されるものと似た表示になっていることを確認します。
  • GPU レンダリングのプロファイル作成を有効にします。[GPU レンダリングのプロファイル作成] は、UI ウィンドウのフレームのレンダリングにかかる時間(1 フレームあたり 16 ms のベンチマークに対する相対的な時間)を視覚的に表現するバーを画面に表示します。各バーには、レンダリング パイプラインのステージにマッピングされる色付きのコンポーネントがあるため、どの部分が最も時間がかかっているかを確認できます。たとえば、フレームが入力処理に多くの時間を費やしている場合は、ユーザー入力を処理するアプリコードを確認します。
  • RecyclerView など、一般的なジャンク発生源であるコンポーネントを実行します。
  • アプリをコールド スタートから起動します。
  • より遅いデバイスでアプリを実行して、問題を顕著にします。

ジャンクが発生するユースケースが見つかったら、アプリでジャンクが発生する原因を把握できる可能性があります。より詳細な情報が必要な場合は、Systrace を使用すればさらに詳しく原因を調べられます。

Systrace

Systrace はデバイス全体の動作を表示するツールですが、アプリのジャンクを特定するのにも役立ちます。Systrace はシステムのオーバーヘッドが最小限に抑えられているため、計測中に現実的なジャンクが発生する場合があります。

ジャンクが発生するユースケースをデバイスで実行しながら、Systrace でトレースを記録します。Systrace の使用方法については、コマンドラインでシステム トレースをキャプチャするをご覧ください。Systrace はプロセスとスレッドによって分割されます。Systrace でアプリのプロセスを探すと、図 1 のようになります。

Systrace の例
図 1. Systrace の例

図 1 の Systrace の例には、ジャンクを特定する次の情報が含まれています。

  1. Systrace は、各フレームがいつ描画されるかを示し、各フレームを色分けして遅いレンダリング時間をハイライト表示します。これにより、目視検査よりも正確に、ジャンクが発生した個々のフレームを見つけられます。詳細については、UI フレームとアラートを検査するをご覧ください。
  2. Systrace は、アプリの問題を検出し、個々のフレームとアラートパネルの両方にアラートを表示します。アラートの指示に従うことをおすすめします。
  3. RecyclerView などの Android フレームワークとライブラリの一部にはトレース マーカーがあります。そのため、Systrace タイムラインには、そのようなメソッドが UI スレッドで実行されるタイミングと、実行にかかる時間が表示されます。

Systrace の出力を確認した結果、アプリにジャンクの原因と思われるメソッドがあるかもしれません。たとえば、RecyclerView に時間がかかることが原因で遅いフレームが発生していることがタイムラインに示されている場合は、関連するコードにカスタム トレース イベントを追加して Systrace を再実行すれば、詳細を確認できます。新しい Systrace では、アプリのメソッドが呼び出されたタイミングと、実行にかかった時間がタイムラインに表示されます。

UI スレッドの動作に長時間かかる理由の詳細が Systrace に表示されない場合は、Android CPU Profiler を使用して、サンプリングされたメソッド トレース、または計測されたメソッド トレースを記録します。一般にメソッド トレースでは、オーバーヘッドが大きいためにジャンクが誤検出され、スレッドが実行中かブロックされているかを確認できないため、ジャンクの特定には適していません。しかし、メソッド トレースはアプリで最も時間がかかっているメソッドを特定するのに役立ちます。そのようなメソッドを特定したら、トレース マーカーを追加して Systrace を再実行し、そのメソッドがジャンクの原因になっているかどうかを確認します。

詳細については、Systrace についてをご覧ください。

カスタム パフォーマンス モニタリング

ローカル デバイスでジャンクを再現できない場合は、カスタム パフォーマンス モニタリングをアプリに組み込むと、フィールドのデバイスでジャンクの発生源を特定できます。

そのためには、FrameMetricsAggregator でアプリの特定の部分からフレーム レンダリング時間を収集し、Firebase Performance Monitoring を使用してデータの記録と分析を行います。

詳細については、Android 向け Performance Monitoring を使ってみるをご覧ください。

フリーズしたフレーム

フリーズしたフレームは、レンダリングに 700 ミリ秒より長くかかる UI フレームです。フレームがレンダリングされている間、アプリが停止しているように見え、ユーザー入力にほぼ 1 秒間反応しないため、これは問題です。UI をスムーズにするため、16 ミリ秒以内にフレームをレンダリングするようアプリを最適化することをおすすめします。ただし、アプリの起動時または別の画面への移行時には、最初のフレームの描画に 16 ミリ秒以上かかることが普通です。これは、アプリがビューを拡大し、画面をレイアウトし、最初から描画する必要があるためです。そのため、Android では遅いレンダリングとは別に、フリーズしたフレームをトラックします。アプリのフレームのレンダリングに 700 ミリ秒以上かからないようにしてください。

アプリの品質向上に役立つように、Android ではアプリのフリーズしたフレームを自動的にモニターし、情報を Android Vitals ダッシュボードに表示します。データの収集方法については、Android Vitals によるアプリの技術的品質のモニタリングをご覧ください。

フリーズしたフレームは遅いレンダリングの極端な形であるため、問題の診断と解決の手順は同じです。

ジャンクのトラッキング

FrameTimeline Perfetto のインサイトを使用すると、 役立ちます。

遅いフレーム、フリーズしたフレーム、ANR の関係

遅いフレーム、フリーズしたフレーム、ANR はすべて、アプリで発生する可能性のあるさまざまな形式のジャンクです。違いについては、以下の表をご覧ください。

遅いフレーム フリーズしたフレーム ANR
表示に要する時間 16 ミリ秒~700 ミリ秒 700 ミリ秒~5 秒 5 秒より大きい
ユーザーが知覚できる影響の範囲
  • RecyclerView のスクロールが突然動作する
  • 複雑なアニメーションが画面上で適切に動作しない
  • アプリの起動時
  • 画面の遷移など、画面間の移動時
  • アクティビティがフォアグラウンドにある場合に、アプリが 5 秒以内に入力イベントまたは BroadcastReceiver(キーの押下や画面タッチイベントなど)に応答しなかった。
  • フォアグラウンドのアクティビティがない場合に、相応の時間が経過しても BroadcastReceiver の実行が終了しなかった。

遅いフレームとフリーズしたフレームの個別のトラッキング

アプリの起動時または別の画面への移行時には、最初のフレームの描画に 16 ミリ秒以上かかることが普通です。これは、アプリがビューを拡大し、画面をレイアウトし、最初から描画する必要があるためです。

ジャンクの優先順位付けと解決のためのベスト プラクティス

アプリのジャンクを解決する場合は、次のベスト プラクティスを念頭に置いてください。

  • 最も簡単に再現できるジャンクのインスタンスを特定して解決します。
  • ANR を優先します。遅いフレームやフリーズしたフレームの場合、アプリが遅く動作しているように見えることがある一方で、ANR が発生するとアプリが応答しなくなります。
  • 遅いレンダリングを再現するのは難しいですが、700 ミリ秒のフリーズしたフレームを強制終了することから始めることができます。これは、アプリが起動する際や画面を変更するときによく発生します。

ジャンクの修正

ジャンクを修正するには、16 ミリ秒で完了していないフレームを調べて、問題の原因を探ります。一部のフレームで Record View#draw または Layout に異常に長く時間がかかっていないかどうかを確認します。このような問題については、一般的なジャンク発生源をご覧ください。

ジャンクを避けるには、長時間実行タスクを UI スレッドの外部で非同期に実行します。コードが実行されているスレッドを常に把握し、重要なタスクをメインスレッドにポストしているときは注意してください。

アプリのメインの UI が複雑かつ重要な場合(中央のスクロール リストなど)は、遅いレンダリング時間を自動的に検出する計測テストを作成し、問題が再度発生しないように、テストを頻繁に実行してください。

一般的なジャンク発生源

下記のセクションでは、View システムを使用するアプリの一般的なジャンク発生源と、それに対処するためのおすすめの方法について説明します。Jetpack Compose のパフォーマンスの問題を解決する方法については、Jetpack Compose のパフォーマンスをご覧ください。

スクロール可能なリスト

ListView と、特に RecyclerView は、ジャンクの影響を非常に受けやすい複雑なスクロール リストによく使用されます。どちらにも Systrace マーカーが含まれているため、Systrace を使用して、アプリのジャンクを招いているかどうかを確認できます。コマンドライン引数 -a <your-package-name> を渡すと、RecyclerView のトレース セクションと追加したトレース マーカーが表示されます。可能であれば、Systrace 出力で生成されたアラートのガイダンスに従います。Systrace 内で RecyclerView のトレースされたセクションをクリックすると、RecyclerView による処理の説明が表示されます。

RecyclerView: notifyDataSetChanged()

RecyclerView のすべてのアイテムが 1 フレームで再バインドされている(そのため再レイアウトされ、再描画されている)場合は、小さな更新のために notifyDataSetChanged()setAdapter(Adapter)、または swapAdapter(Adapter, boolean) を呼び出していないか確認します。これらのメソッドは、リストの内容全体が変更されたことを通知し、RV FullInvalidate として Systrace に表示されます。代わりに、SortedList または DiffUtil を使用して、コンテンツが変更または追加されたときに生成される更新を最小限にします。

たとえば、サーバーから新しいバージョンのニュース コンテンツのリストを受け取るアプリについて考えてみます。この情報を Adapter にポストする際、次の例に示すように notifyDataSetChanged() を呼び出すことができます。

Kotlin

fun onNewDataArrived(news: List<News>) {
    myAdapter.news = news
    myAdapter.notifyDataSetChanged()
}

Java

void onNewDataArrived(List<News> news) {
    myAdapter.setNews(news);
    myAdapter.notifyDataSetChanged();
}

ただしこれには欠点があり、上部にアイテムを 1 つ追加するだけなどの細かい変更の場合、RecyclerView は認識しません。そのため、キャッシュされたアイテムの状態をすべて削除するように指示され、すべてをバインドし直す必要があります。

最小限の更新を計算してディスパッチする DiffUtil を使用することをおすすめします。

Kotlin

fun onNewDataArrived(news: List<News>) {
    val oldNews = myAdapter.items
    val result = DiffUtil.calculateDiff(MyCallback(oldNews, news))
    myAdapter.news = news
    result.dispatchUpdatesTo(myAdapter)
}

Java

void onNewDataArrived(List<News> news) {
    List<News> oldNews = myAdapter.getItems();
    DiffResult result = DiffUtil.calculateDiff(new MyCallback(oldNews, news));
    myAdapter.setNews(news);
    result.dispatchUpdatesTo(myAdapter);
}

DiffUtil にリストの検査方法を伝えるには、MyCallbackCallback 実装として定義します。

RecyclerView: ネストされた RecyclerView

複数の RecyclerView インスタンスをネストするのは(特に、横方向にスクロールするリストの縦方向リストでは)、一般的です。一例として、Google Play ストアのメインページに表示されるアプリのグリッドがあります。これは非常に効果的ですが、多くのビューが動き回ります。

最初にページを下にスクロールしたときに多くの内側のアイテムがインフレートしている場合は、内側(横方向)の RecyclerView インスタンス間で RecyclerView.RecycledViewPool を共有していることを確認します。デフォルトでは、各 RecyclerView に独自のアイテムプールがあります。ただし、同時に多数の itemViews が画面に表示される場合、すべての行が同様のタイプのビューを表示していると、itemViews を別の横方向リストで共有できないという問題があります。

Kotlin

class OuterAdapter : RecyclerView.Adapter<OuterAdapter.ViewHolder>() {

    ...

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        // Inflate inner item, find innerRecyclerView by ID.
        val innerLLM = LinearLayoutManager(parent.context, LinearLayoutManager.HORIZONTAL, false)
        innerRv.apply {
            layoutManager = innerLLM
            recycledViewPool = sharedPool
        }
        return OuterAdapter.ViewHolder(innerRv)
    }
    ...

Java

class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> {
    RecyclerView.RecycledViewPool sharedPool = new RecyclerView.RecycledViewPool();

    ...

    @Override
    public void onCreateViewHolder(ViewGroup parent, int viewType) {
        // Inflate inner item, find innerRecyclerView by ID.
        LinearLayoutManager innerLLM = new LinearLayoutManager(parent.getContext(),
                LinearLayoutManager.HORIZONTAL);
        innerRv.setLayoutManager(innerLLM);
        innerRv.setRecycledViewPool(sharedPool);
        return new OuterAdapter.ViewHolder(innerRv);

    }
    ...

さらに最適化する場合は、内側の RecyclerViewLinearLayoutManagersetInitialPrefetchItemCount(int) を呼び出すこともできます。たとえば、常に 3.5 個のアイテムが行内に表示されるようにする場合は、innerLLM.setInitialItemPrefetchCount(4) を呼び出します。これは RecyclerView に対して、横方向の行が画面上に表示されそうなときに、UI スレッドに空きがあれば、内側のアイテムのプリフェッチを試みるように通知します。

RecyclerView: インフレーションが多すぎる / 作成に時間がかかりすぎる

ほとんどの場合、RecyclerView のプリフェッチ機能により、UI スレッドがアイドル状態のときに事前に作業を行うことで、インフレーションのコストを回避できます。RV Prefetch というラベルのセクション内ではなく、フレーム中にインフレーションが確認されている場合は、必ずサポート対象のデバイスでテストし、最新バージョンのサポート ライブラリを使用してください。 プリフェッチは Android 5.0 API レベル 21 以降でのみサポートされます。

新しいアイテムが画面に表示されたときにジャンクにつながるインフレーションが頻繁に発生する場合は、必要以上のビュータイプがないことを確認します。RecyclerView のコンテンツのビュータイプが少ないほど、新しいアイテムタイプが画面に表示されたときに必要となるインフレーションが少なくなります。可能であれば、妥当な場合にビュータイプを統合します。タイプ間でアイコン、色、またはテキストのみを変える場合は、バインド時に変えることで、インフレーションを回避しながらアプリのメモリ使用量を削減できます。

ビュータイプが適切であれば、インフレーションのコストを削減することを考えます。 不要なコンテナビューと構造ビューを削減すると効果的です。ConstraintLayoutitemViews を作成することを検討してください。これにより、構造ビューを削減しやすくなります。

アイテムの階層がシンプルで複雑なテーマ設定やスタイル機能を必要とせず、パフォーマンスをさらに最適化したいのであれば、コンストラクタを自分で呼び出すことを検討してください。ただし多くの場合、XML のシンプルさと機能を失うに値するほどの効果はありません。

RecyclerView: バインドに時間がかかりすぎる

バインド(つまり onBindViewHolder(VH, int))はシンプルでなければならず、最も複雑なアイテムを除いては、すべて 1 ミリ秒未満で完了する必要があります。アダプター内部のアイテムデータから古い単純な Java オブジェクト(POJO)を取得し、ViewHolder のビューでセッターを呼び出すだけにします。RV OnBindView に長時間かかる場合は、バインドコードで最小限の作業しか行っていないことを確認します。

基本的な POJO オブジェクトを使用してアダプターにデータを保持している場合、データ バインディング ライブラリを使用することで、onBindViewHolder にバインディング コードを記述する必要がまったくなくなります。

RecyclerView または ListView: レイアウト / 描画に時間がかかりすぎる

描画とレイアウトの問題については、レイアウト パフォーマンスレンダリング パフォーマンスのセクションをご覧ください。

ListView: インフレーション

注意しないと、誤って ListView でリサイクルを無効にしてしまいます。アイテムが画面に表示されるたびにインフレーションが発生する場合は、Adapter.getView() の実装で convertView パラメータを使用していることや、再バインドしていること、返していることを確認します。getView() の実装が常にインフレートする場合、アプリは ListView でリサイクルのメリットを得られません。getView() の構造は、ほとんどの場合、次の実装のようになります。

Kotlin

fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    return (convertView ?: layoutInflater.inflate(R.layout.my_layout, parent, false)).apply {
        // Bind content from position to convertView.
    }
}

Java

View getView(int position, View convertView, ViewGroup parent) {

    if (convertView == null) {
        // Only inflate if no convertView passed.
        convertView = layoutInflater.inflate(R.layout.my_layout, parent, false)
    }
    // Bind content from position to convertView.
    return convertView;
}

レイアウト パフォーマンス

Choreographer#doFrameLayout セグメントが過度に動作しているか、頻繁に動作していることを Systrace が示している場合は、レイアウトのパフォーマンスに関する問題が発生していることを意味します。アプリのレイアウト パフォーマンスは、ビュー階層のどの部分でレイアウト パラメータや入力が変更されるかによって異なります。

レイアウト パフォーマンス: コスト

セグメントが数ミリ秒より長い場合、RelativeLayouts または weighted-LinearLayouts でネスト パフォーマンスが最も悪くなっている可能性があります。これらのレイアウトはそれぞれ、子の複数の測定パスとレイアウトパスをトリガーできるため、ネストすると、ネストの深さに応じて O(n^2) 倍パフォーマンスが悪くなる可能性があります。

階層の最下位のリーフノードを除くすべてのノードで、RelativeLayout または LinearLayout の重み付け機能を使用しないようにします。その方法は次のとおりです。

  • 構造ビューを再編成する。
  • カスタム レイアウト ロジックを定義する。具体例については、レイアウト階層を最適化するをご覧ください。パフォーマンスを低下させずに同様の機能を提供する ConstraintLayout への変換を試すのもよいでしょう。

レイアウト パフォーマンス: 頻度

レイアウトは、たとえば RecyclerView で新しいアイテムがスクロールして表示される場合など、新しいコンテンツが画面に表示されるときに発生することが予想されます。各フレームで重要なレイアウトが発生している場合、レイアウトをアニメーション化している可能性があり、これによってフレーム落ちが発生することがあります。

一般にアニメーションは、次のような View の描画プロパティで実行する必要があります。

これらはすべて、パディングやマージンなどのレイアウト プロパティよりも低コストで変更できます。通常は、invalidate() をトリガーして次のフレームで draw(Canvas) をトリガーするセッターを呼び出し、ビューの描画プロパティを変更する方がはるかに低コストです。これにより、無効化されたビューの描画オペレーションが再記録され、通常はレイアウトよりもかなり低コストになります。

レンダリング パフォーマンス

Android UI は次の 2 つのフェーズで動作します。

  • UI スレッドの Record View#draw。これは、無効化されたビューごとに draw(Canvas) を実行し、カスタムビューまたはコードの呼び出しを行えます。
  • RenderThreadDrawFrame。これは、ネイティブ RenderThread で実行されますが、Record View#draw フェーズで生成された作業に基づいて動作します。

レンダリング パフォーマンス: UI スレッド

Record View#draw に長時間かかる場合、ビットマップが UI スレッドでペイントされていることがよくあります。ビットマップへのペイントには CPU レンダリングが使用されるため、通常は可能であれば避けてください。Android CPU Profiler でメソッド トレースを使用すると、これが問題かどうかを確認できます。

ビットマップへのペイントは、多くの場合、アプリがビットマップを表示する前にビットマップを装飾しようとするときに行われます。角を丸くするなどの装飾が追加されることもあります。

Kotlin

val paint = Paint().apply {
    isAntiAlias = true
}
Canvas(roundedOutputBitmap).apply {
    // Draw a round rect to define the shape:
    drawRoundRect(
            0f,
            0f,
            roundedOutputBitmap.width.toFloat(),
            roundedOutputBitmap.height.toFloat(),
            20f,
            20f,
            paint
    )
    paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)
    // Multiply content on top to make it rounded.
    drawBitmap(sourceBitmap, 0f, 0f, paint)
    setBitmap(null)
    // Now roundedOutputBitmap has sourceBitmap inside, but as a circle.
}

Java

Canvas bitmapCanvas = new Canvas(roundedOutputBitmap);
Paint paint = new Paint();
paint.setAntiAlias(true);
// Draw a round rect to define the shape:
bitmapCanvas.drawRoundRect(0, 0,
        roundedOutputBitmap.getWidth(), roundedOutputBitmap.getHeight(), 20, 20, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
// Multiply content on top to make it rounded.
bitmapCanvas.drawBitmap(sourceBitmap, 0, 0, paint);
bitmapCanvas.setBitmap(null);
// Now roundedOutputBitmap has sourceBitmap inside, but as a circle.

こうした処理を UI スレッドで行っている場合、代わりにバックグラウンドのデコード スレッドで行うことができます。上記の例のように、描画時に処理することもできます。たとえば、Drawable または View のコードが次のようなものだとします。

Kotlin

fun setBitmap(bitmap: Bitmap) {
    mBitmap = bitmap
    invalidate()
}

override fun onDraw(canvas: Canvas) {
    canvas.drawBitmap(mBitmap, null, paint)
}

Java

void setBitmap(Bitmap bitmap) {
    mBitmap = bitmap;
    invalidate();
}

void onDraw(Canvas canvas) {
    canvas.drawBitmap(mBitmap, null, paint);
}

これは次のように置き換えることができます。

Kotlin

fun setBitmap(bitmap: Bitmap) {
    shaderPaint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
    invalidate()
}

override fun onDraw(canvas: Canvas) {
    canvas.drawRoundRect(0f, 0f, width, height, 20f, 20f, shaderPaint)
}

Java

void setBitmap(Bitmap bitmap) {
    shaderPaint.setShader(
            new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
    invalidate();
}

void onDraw(Canvas canvas) {
    canvas.drawRoundRect(0, 0, width, height, 20, 20, shaderPaint);
}

これは、ビットマップの変更を行う他の 2 つの一般的なオペレーションである、背景保護(ビットマップの上にグラデーションを描画)と画像フィルタリング(ColorMatrixColorFilter を使用)に対しても行えます。

別の理由でビットマップに描画する場合は(キャッシュとして使用する可能性がある場合など)、View または Drawable に直接渡される、ハードウェア アクセラレーション Canvas に描画してみます。必要に応じて、LAYER_TYPE_HARDWARE を指定して setLayerType() を呼び出し、複雑なレンダリング出力をキャッシュして、GPU レンダリングを引き続き活用することも検討してください。

レンダリング パフォーマンス: RenderThread

一部の Canvas オペレーションの記録は低コストですが、RenderThread での高コストの計算処理をトリガーします。通常、Systrace はこれらをアラートで呼び出します。

大きなパスのアニメーション化

View に渡されたハードウェア アクセラレーション CanvasCanvas.drawPath() が呼び出されると、Android はこれらのパスを最初に CPU で描画し、GPU にアップロードします。パスが大きい場合、フレーム間での編集を避け、効率的にキャッシュして描画できるようにします。drawPoints()drawLines()drawRect/Circle/Oval/RoundRect() は、より効率的です。多くの描画呼び出しを使用することになるとしても、これらを使用することをおすすめします。

Canvas.clipPath

clipPath(Path) は高コストのクリッピング動作をトリガーするため、通常は避ける必要があります。可能であれば、非矩形にクリップするのではなく、図形を描画します。パフォーマンスが向上し、アンチエイリアスがサポートされます。たとえば、次のような clipPath 呼び出しがあるとします。

Kotlin

canvas.apply {
    save()
    clipPath(circlePath)
    drawBitmap(bitmap, 0f, 0f, paint)
    restore()
}

Java

canvas.save();
canvas.clipPath(circlePath);
canvas.drawBitmap(bitmap, 0f, 0f, paint);
canvas.restore();

これは代わりに次のように表せます。

Kotlin

paint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
// At draw time:
canvas.drawPath(circlePath, mPaint)

Java

// One time init:
paint.setShader(new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
// At draw time:
canvas.drawPath(circlePath, mPaint);
ビットマップ アップロード

Android ではビットマップが OpenGL テクスチャとして表示されます。ビットマップはフレームに初めて表示されるときに、GPU にアップロードされます。これは、Systrace では Texture upload(id) width x height として表示されます。数ミリ秒で済みますが(図 2 参照)、GPU を使用して画像を表示する必要があります。

長時間かかる場合は、まずトレースの幅と高さの数値を確認します。表示されているビットマップが、その表示されている画面上の領域よりも大きくならないようにします。そうしないと、アップロード時間とメモリが無駄になります。通常、ビットマップ読み込みライブラリを使用すると、適切なサイズのビットマップをリクエストできます。

Android 7.0 では、ビットマップ読み込みコード(通常はライブラリが行います)が prepareToDraw() を呼び出して、必要になる前にアップロードを早期にトリガーできます。このようにして、RenderThread がアイドル状態のときにアップロードが早期に行われます。これは、そのビットマップがわかっている限り、デコードした後またはビットマップをビューにバインドするときに実施できます。ビットマップ読み込みライブラリで行うのが理想ですが、自身で管理している場合、または新しいデバイスでアップロードが発生しないようにする場合は、独自のコードで prepareToDraw() を呼び出せます。

大きなビットマップをアップロードするフレームに多くの時間を費やすアプリ
図 2.大きなビットマップをアップロードするフレームにアプリで多くの時間を費やしている。サイズを小さくするか、prepareToDraw() でデコードするときに早期にトリガーする。

スレッド スケジューリングの遅延

スレッド スケジューラは、Android オペレーティング システムの一部であり、システムのどのスレッドを、いつ、どのくらいの時間実行する必要があるかを決定します。

アプリの UI スレッドがブロックされているか、実行されていないために、ジャンクが発生することがあります。 図 3 に示すように、Systrace はさまざまな色を使用して、スレッドがスリープ中(グレー)、実行可能(青: 実行できるがスケジューラがまだ実行を選択していない)、アクティブに実行中(緑)、または割り込み不可スリープ(赤またはオレンジ)のいずれかの状態であることを示します。スレッド スケジューリングの遅延によるジャンクの問題をデバッグする場合に非常に便利です。

UI スレッドがスリープしている期間のハイライト表示
図 3. UI スレッドがスリープしている期間のハイライト表示

バインダー呼び出し(Android のプロセス間通信(IPC)メカニズム)により、アプリの実行が長時間一時停止することがよくあります。Android の最近のバージョンでは、UI スレッドの実行が停止する最も一般的な原因の一つになっています。バインダー呼び出しを行う関数の呼び出しを避けるのが一般的な対処法です。避けられない場合は、値をキャッシュするか、作業をバックグラウンド スレッドに移します。コードベースが大きくなるにしたがい、注意していないと、低レベルのメソッドを呼び出すことで誤ってバインダー呼び出しを追加してしまう可能性があります。ただし、トレースで見つけて修正できます。

バインダー トランザクションがある場合は、次の adb コマンドを使用して呼び出しスタックをキャプチャできます。

$ adb shell am trace-ipc start
… use the app - scroll/animate ...
$ adb shell am trace-ipc stop --dump-file /data/local/tmp/ipc-trace.txt
$ adb pull /data/local/tmp/ipc-trace.txt

getRefreshRate() などの一見無害な呼び出しでも、バインダー トランザクションをトリガーし、頻繁に呼び出されると大きな問題を引き起こす可能性があります。定期的にトレースすると、このような問題が発生したときにすばやく見つけて修正できます。

RV フリングのバインダー トランザクションが原因でスリープしている UI スレッドを示している。バインド ロジックに注意しながら、trace-ipc を使用してバインダー呼び出しをトレースし、削除する。
図 4. RV フリングのバインダー トランザクションが原因で UI スレッドがスリープしている。バインド ロジックをシンプルに保ち、trace-ipc を使用してバインダー呼び出しをトレースし、削除する。

バインダー アクティビティが表示されず、UI スレッドが実行されていない場合は、別のスレッドからのロックやその他のオペレーションを待機していないことを確認します。通常、UI スレッドは他のスレッドからの結果を待つ必要はありません。他のスレッドは情報をポストする必要があります。

オブジェクトの割り当てとガベージ コレクション

ART が Android 5.0 のデフォルトのランタイムとして導入されて以来、オブジェクトの割り当てとガベージ コレクション(GC)の問題は大幅に減りましたが、それでもこの追加作業でスレッドに負荷がかかる可能性があります。1 秒に何度も発生しないような稀なイベント(ユーザーがボタンをタップするなど)に対応して割り当てるのは構いませんが、それぞれの割り当てにはコストがかかることに注意が必要です。頻繁に呼び出されるタイトなループの場合は、GC の負荷を軽減するために割り当てを避けることを検討してください。

Systrace は GC が頻繁に実行されているかどうかを示し、Android Memory Profiler はどこから割り当てが行われているのかを示します。特にタイトなループでは、できる限り割り当てを避けると問題が生じる可能性が低くなります。

HeapTaskDaemon で 94 ms の GC が発生していることを示している
図 5. HeapTaskDaemon スレッドで発生している 94 ms の GC

Android の最近のバージョンでは、GC は通常、HeapTaskDaemon というバックグラウンド スレッドで実行されます。図 5 に示すように、割り当て量が多いと、GC で消費される CPU リソースが多くなる可能性があります。