遅いレンダリング

UI レンダリングとは、アプリからフレームを生成して画面に表示することです。ユーザーのアプリ操作をスムーズにするためには、16ms 以内にフレームをレンダリングし、60 フレーム/秒を達成する必要があります(Why 60fps? の動画をご覧ください)。アプリの UI レンダリングが遅い場合、システムはフレームをスキップせざるを得ず、ユーザーはアプリの動作がスムーズでないと感じます。これをジャンクと呼びます。

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

アプリでジャンクが発生する場合、このページのガイダンスが問題の診断と解決に役立ちます。

ジャンクの特定

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

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

目視検査

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

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

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

Systrace

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

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

図 1. Systrace

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

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

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

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

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

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

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

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

詳細については、Android Vitals で Firebase Performance Monitoring を使用するをご覧ください。

ジャンクの修正

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

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

アプリのメインの UI が複雑かつ重要な場合(おそらく中央のスクロール リスト)は、遅いレンダリング時間を自動的に検出し、テストを頻繁に実行して回帰を防ぐインストゥルメント化テストを作成することを検討してください。詳細については、自動化されたパフォーマンス テストのコードラボをご覧ください。

一般的なジャンク発生源

下記のセクションでは、アプリの一般的なジャンク発生源と、それに対処するためのおすすめの方法について説明します。

スクロール可能なリスト

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);
    }
    

単に MyCallback を DiffUtil.Callback 実装として定義して、DiffUtil にリストの検査方法を伝えます。

RecyclerView: ネストされた RecyclerViews

RecyclerView をネストするのは、特に横方向にスクロールするリストの縦方向のリスト(Play ストア メインページのアプリグリッドなど)で一般的です。これは非常に効果的ですが、多くのビューが動き回ります。最初にページを下にスクロールしたときに多くの内側のアイテムがインフレートしている場合は、内側(横方向)の RecyclerViews 間で 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);

        }
        ...
    

さらに最適化する場合は、内部の RecyclerView の LinearLayoutManagersetInitialPrefetchItemCount(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 ミリ秒よりはるかに少ない時間しかかかりません。単にアダプターの内部アイテムデータから 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 の描画プロパティ(setTranslationX/Y/Z()setRotation()setAlpha() など)で実行する必要があります。これらはすべて、レイアウト プロパティ(パディング、マージンなど)より安価に変更できます。また、ビューの描画プロパティを変更する方がはるかに安価です。通常は、invalidate() をトリガーして次のフレームで draw(Canvas) をトリガーするセッターを呼び出します。これにより、無効化されたビューの描画オペレーションが再記録され、通常はレイアウトよりもかなり安価になります。

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

Android UI は、UI スレッドの Record View#draw、RenderThread の DrawFrame という 2 つのフェーズで動作します。前者は無効化されたすべての Viewdraw(Canvas) を実行し、カスタムビューまたはコードへの呼び出しを行います。後者はネイティブ 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 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 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 を使用)に対してもよく行われます。

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

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

一部のキャンバス オペレーションの記録は安価ですが、RenderThread での高価なコンピューティングをトリガーします。通常、Systrace はこれらをアラートで呼び出します。

Canvas.saveLayer()

Canvas.saveLayer() は避けてください。フレームごとに、高価でキャッシュされないオフスクリーン レンダリングが発生します。Android 6.0 ではパフォーマンスが向上しましたが(GPU でのレンダリング ターゲットの切り替えを回避する最適化が行われた場合)、可能であれば、この高価な API は避けたほうが無難です。あるいは少なくとも、必ず Canvas.CLIP_TO_LAYER_SAVE_FLAG を渡す(またはフラグを取らない変数を呼び出す)ようにします。

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

ビューに渡されたハードウェア アクセラレーション キャンバスで Canvas.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 で Upload 幅 x 高さ Texture として表示されます。数ミリ秒で済みますが(図 2 参照)、GPU を使用して画像を表示する必要があります。

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

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

図 2. 1.8 メガピクセルのビットマップをアップロードするフレームに 10ms 以上かかるアプリ。サイズを小さくするか、prepareToDraw() でデコードしたとき早期にトリガーします。

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

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

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

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

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

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

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

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

図 5. HeapTaskDaemon スレッドに 94ms の GC があることを示しています。

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