Android パフォーマンス最適化シリーズ - 04 Matrix での TraceCanary ソース コード分析

I. 概要

UIのパフォーマンスを最適化する際には、最適化の前後を比較することが非常に重要ですが、そうでない場合、最適化が効果的かどうか、どの程度効果があるかを判断するにはどうすればよいでしょうか。比較に関しては、観察による直感的な比較と、データによる客観的な比較の2つに分類できると個人的には考えています。

直感的な比較。开发者选项过渡绘制GPU 分析 を使用して、遷移描画と UI があるかどうかを確認できます。レンダリング中のページの GPU レンダリング

客観的に比較するには、NetEase のEmmagee や Tencent の などのさまざまなテスト ツールを通じて分析および比較できます。 3>matrix お待ちください。残念ながら、システムの制限により Emmagee は Android 7.0 以降では使用できないため、matrix を使用します。

ソフトウェア開発を行う場合、より大きな進歩を遂げるためには、何が起こっているのかだけでなく、なぜそれが起こっているのかを知る必要があります。 UI パフォーマンスを最適化するときに、マトリックスで TraceCanary モジュールを使用しました。その機能は主に UI フリーズ、起動時間、ページ切り替え、および遅い機能の検出を検出することです。ここでは主にフレーム レートがどのように計算されるかを分析します。その他のコード機能はそれほど複雑ではなく、興味のある友人が自分で分析できるようにする必要があります。

2. 原則

Androidアプリ開発を始めた当初から、メインスレッドでは時間のかかる操作(IOデータの読み込みやネットワークリクエストなど)を行うことができず、UIのラグなどの問題が発生するという話をよく聞きました。フレームは 16.67 ミリ秒でレンダリングされます。メイン スレッドに時間のかかるタスクがある場合、次のフレームが 16.67 ミリ秒以内にレンダリングされず、フレームがドロップされることがあります。視覚的には、フレームのドロップは一般にラグと呼ばれます。フレーム落ちの原因はさまざまですが、メイン スレッドと 16.67 ミリ秒という 2 つの変数が固定されており、これら 2 つの固定値から開始して、メイン スレッドにラグがあるかどうかを分析して判断できます。

Android のパフォーマンスの最適化について学んだ友人なら、遅延監視を実装する業界の主流の考え方は、時間のかかるタスクのステータスをメインスレッドで監視することであることを知っているかもしれません。しきい値、ダンプ 現在のメイン スレッドのスタック情報を使用して、ラグの原因を分析できます。これら 2 つのアイデアの典型的な代表例は、ArgusAPM と < /span> など。考え方はこんな感じですが、実装方法としてはメインスレッドのLooper機構を利用する方法とChoreographerモジュールを利用する方法の2つに分けられますので、 簡単に紹介BlockCanaryExBlockCanary

2.1 ルーパー機構

メイン スレッドにMainLooper があることは誰もが知っています。メイン スレッドが開始されると、MainLooper#loop() メソッドが呼び出され、タスクが実行されます。メインスレッドでの実行は間接的となります (Handler のサブクラス) の によって実行されます3>. 分析してみましょう。 方法は次のとおりですActivityThreadHhandleMessage(Message msg)Looper.loop()

  • コード 1:Looper.loop() からメッセージを継続的に読み取るメソッド内に for(;;) の無限ループが発生します。およびプロセスMessageQueue
  • コード 3: 最後に、msg.target.dispatchMessage(msg) がこのメッセージを処理するために呼び出されます。msg.target は実際にはハンドラーであり、MainLooper に対応するすべてのハンドラーを指します。
  • コード 2 とコード 4: メッセージの処理の前後で、myLooper().mLogging を通じて取得された Printer オブジェクトは別々に出力されます< a i= 3> と>>>>> Dispatching to<<<<< Finished to
  • myLooper().mLoggingLooper.getMainLooper().setMessageLogging(Printer printer) で設定できます。

したがって、2 つのログ>>>>> Dispatching to<<<<< Finished to の間の時間がしきい値を超えているかどうかを判断するだけで、メインスレッドが正常に動作しているかどうかを判断できます。現時点で実行中 タスクが時間のかかるタスクかどうか

public final class Looper {

    ......

        public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        for (;;) {                                                           // 代码 1
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            // This must be in a local variable, in case a UI event sets the logger
            final Printer logging = me.mLogging;                             // 代码 2
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }

            final long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;

            final long traceTag = me.mTraceTag;
            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }
            final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            final long end;
            try {
                msg.target.dispatchMessage(msg);                            // 代码 3
                end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }

            if (logging != null) {                                          // 代码 4
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }

                    ......
        }
    }

    ......

}

コードは次のとおりです

Looper.getMainLooper().setMessageLogging(new Printer() {

        private static final String START = ">>>>> Dispatching";
        private static final String END = "<<<<< Finished";

                @Override
        public void println(String x) {
                if (x.startsWith(">>>>> Dispatching")) {
                                ......       // 开始记录                    
            }
            if (x.startsWith("<<<<< Finished")) {
                                ......       // 结束记录,判断处理时间间隔是否超过阈值
            }
        }
});

2.2 コレオグラファーモジュール

Android システムは 16.67 ミリ秒ごとに VSYNC 信号を送信して、UI のレンダリングをトリガーします。通常の状況では、2 つの VSYNC 信号の間隔は 16.67 ミリ秒です。16.67 ミリ秒を超える場合、レンダリングが停止していると見なすことができます。

Android SDK の クラスの がしきい値を超えます。しきい値を超えると、フリーズが発生します。現在のメインスレッドのスタック情報を別のサブスレッドにダンプして分析できますChoreographer は、VSYNC 信号が送信されるたびにコールバックされるため、隣接するかどうかを判断するだけで済みます。 2 回の間隔FrameCallback.doFrame(long l)FrameCallback.doFrame(long l)

スケマティック コードは次のとおりです。FrameCallback.doFrame(long l) コールバック メソッドでは、 毎回コールバックを再登録する必要があることに注意してください。Choreographer.getInstance().postFrameCallback(this)

Choreographer.getInstance()
            .postFrameCallback(new Choreographer.FrameCallback() {
                @Override
                public void doFrame(long l) {
                     if(frameTimeNanos - mLastFrameNanos > 100) {
                        ...
                     }
                     mLastFrameNanos = frameTimeNanos;
                     Choreographer.getInstance().postFrameCallback(this);
                }
        });

2.3 Matrix-TraceCanary の実装

UIにラグがあるかどうかの判断は、大きく分けて上記の2つの実装方法があります。

Matrix の TraceCanary モジュールでは、古いバージョンでは Choreographer
メソッドが使用されていましたが、新しいバージョンでは Looper メカニズムが使用されています。そのような違いがあるかどうかはまだ明らかではありません

ただし、Looper メカニズムの使用には欠点があります。メッセージ処理開始ログとメッセージ処理終了ログを出力するときに、文字列の結合が実行され、文字列が結合されます。スプライシングはパフォーマンスにも影響します

発行アドレス

3. ソースコード分析

ここでの Matrix と TraceCanary の分析はバージョン 0.5.2 です

その実装原理がわかったので、Looper.getMainLooper().setMessageLogging() に関連するコードを見つけるために Huanglong に直接移動し、分析がより明確になるように手がかりを追っていきます。

Matrix と TraceCanary のディレクトリ構造は以下のとおりです。

3.1 Looperにプリンターを追加する

上の図で 選択したLooperMonitor クラスは、最初に使用したクラスです。Looper.getMainLooper().setMessageLogging() メソッドを呼び出すことで Printer オブジェクトが追加されます。コードは次のとおりです。次のように

  • コード 1:LooperMonitor のコンストラクターはプライベートであり、静的な最終オブジェクトを持ちます LooperMonitor monitor

  • コード 2 とコード 3:LooperMonitor は、MessageQueue.IdleHandler インターフェースとその抽象メソッドqueueIdle() を実装します。このメソッドでは、バージョンの違いに応じて、この MessageQueue.IdleHandler インスタンス オブジェクトを MessageQueue に追加するメソッドが異なります。 queueIdle() メソッドメッセージ キューを確保するために true を返します。各メッセージが処理された後、queueIdle() メソッドがコールバックされます

  • コード 4: メソッドはコンストラクタと queueIdle() メソッドで呼び出されます。これが実際のパスです プリンターをセットアップする場所resetPrinter()Looper.getMainLooper().setMessageLogging()

    各追加の前に、現在の Looper.getMainLooper() セットmLogging オブジェクトがリフレクションを通じて取得され、それが以前に LooperMonitor によって設定されたかどうかが判断されます。 If < /span> オブジェクトは LooperMonitor によって設定され、再度設定されることはありません。それ以外の場合、独自の Printer オブジェクトが Looper.getMainLooper()mLoggingLooper.getMainLooper()

  • コード 5:Printer.println(String x) では、受信パラメータの最初の文字に基づいてString xパラメータが有効であるかどうかが判断されます< a i=3> ログ、 で始まるログはメッセージ処理が開始されるログ、 で始まるログはメッセージ処理が終了するログ、< /span> メソッドLooper.loop()>>dispatch(boolean isBegin)

public class LooperMonitor implements MessageQueue.IdleHandler {
    private static final HashSet<LooperDispatchListener> listeners = new HashSet<>();
    private static Printer printer;

    private static final LooperMonitor monitor = new LooperMonitor();                           // 代码 1

    private LooperMonitor() {                                                                                                           // 代码 2
        resetPrinter();

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            Looper.getMainLooper().getQueue().addIdleHandler(this);
        } else {
            MessageQueue queue = reflectObject(Looper.getMainLooper(), "mQueue");
            queue.addIdleHandler(this);
        }
    }

    @Override
    public boolean queueIdle() {                                                                                                    // 代码 3
        resetPrinter();
        return true;
    }

    private static void resetPrinter() {                                                                                    // 代码 4
        final Printer originPrinter = reflectObject(Looper.getMainLooper(), "mLogging");
        if (originPrinter == printer && null != printer) {
            return;
        }
        if (null != printer) {
            MatrixLog.w(TAG, "[resetPrinter] maybe looper printer was replace other!");
        }
        Looper.getMainLooper().setMessageLogging(printer = new Printer() {        // 代码 5
            boolean isHasChecked = false;
            boolean isValid = false;

            @Override
            public void println(String x) {
                if (null != originPrinter) {
                    originPrinter.println(x);
                }

                if (!isHasChecked) {
                    isValid = x.charAt(0) == '>' || x.charAt(0) == '<';
                    isHasChecked = true;
                    if (!isValid) {
                        MatrixLog.e(TAG, "[println] Printer is inValid! x:%s", x);
                    }
                }

                if (isValid) {
                    dispatch(x.charAt(0) == '>');
                }

            }
        });
    }

}

Looper.loop() メッセージ処理の開始と終了が監視されました。次に、メッセージ処理の開始と終了がどのように処理されるかを見てみましょう。主に dispatch(boolean isBegin) メソッド a>

  • dispatch(boolean isBegin) では、LooperMonitor に追加された LooperDispatchListener リスニング コールバックが順番に処理されます
  • コード 1: dispatch(boolean isBegin) メソッドは、パラメータ boolean isBeginlistener.isValid()、< に基づきます。 a i=4 > 条件付き実行 メソッドと メソッド、メッセージ処理の開始時と終了時に行う処理はすべて にあります。および a>listener.isHasDispatchStartlistener.dispatchStart()listener.dispatchEnd()listener.dispatchStart()listener.dispatchEnd()
  • コード 2: このコード行は、個人的には最後の投稿にあると理解しています。listener.isValid() == false && isBegin == false && listener.isHasDispatchStart == true のとき、最後の投稿に対して listener.dispatchEnd() メソッドが呼び出されます。時間。
  • コード 3: LooperMonitor.register(LooperDispatchListener listener) メソッドを使用して LooperDispatchListener listener を LooperMonitor に設定できます。次に、 を設定するためにこのメソッドを呼び出す場所を見てみましょう。 < a i=3> 聞いてみると、メッセージ処理の最初と最後にある特定のロジックがわかりますLooperDispatchListener listener

public class LooperMonitor implements MessageQueue.IdleHandler {

    private static final HashSet<LooperDispatchListener> listeners = new HashSet<>();     

    ......

    public abstract static class LooperDispatchListener {

        boolean isHasDispatchStart = false;

        boolean isValid() {
            return false;
        }

        @CallSuper
        void dispatchStart() {
            this.isHasDispatchStart = true;
        }

        @CallSuper
        void dispatchEnd() {
            this.isHasDispatchStart = false;
        }
    }

    ......

    public static void register(LooperDispatchListener listener) {                          // 代码 3         
        synchronized (listeners) {
            listeners.add(listener);
        }
    }

    private static void dispatch(boolean isBegin) {                                                                             

        for (LooperDispatchListener listener : listeners) {                       
            if (listener.isValid()) {                                                                                         // 代码 1
                if (isBegin) {
                    if (!listener.isHasDispatchStart) {
                        listener.dispatchStart();
                    }
                } else {
                    if (listener.isHasDispatchStart) {
                        listener.dispatchEnd();
                    }
                }
            } else if (!isBegin && listener.isHasDispatchStart) {                 // 代码 2
                listener.dispatchEnd();
            }
        }

    }  

    ......

}

3.2 メッセージ処理

下の図に示すように、AppMethodBeatUIThreadMonitor があります。 3> メソッド LooperMonitor で を設定します。この記事に関連するのは クラスです。このクラスの分析に焦点を当てましょうLooperMonitor.register(LooperDispatchListener listener)LooperDispatchListener listenerUIThreadMonitor

最初にUIThreadMonitorクラス初期化メソッドを分析します。このメソッドは主にリフレクションを通じて Choreographer のいくつかのプロパティを取得します。 LooperMonitor.register(LooperDispatchListener listener) メソッド を通じて LooperMonitor に設定します。LooperDispatchListener listener

  • コード 1: リフレクションを通じて Choreographer インスタンスの mCallbackQueues 属性を取得しました。mCallbackQueues はコールバック キュー配列ですCallbackQueue[] mCallbackQueues , 4 つのコールバック キューが含まれており、1 つ目は入力イベント コールバック キューCALLBACK_INPUT = 0、2 つ目はアニメーション コールバック キューCALLBACK_ANIMATION = 1、3 つ目はトラバーサル描画コールバックです。キュー CALLBACK_TRAVERSAL = 2 、4 番目は送信コールバック キュー CALLBACK_COMMIT = 3 です。これら 4 つのステージは、各フレームの UI レンダリングで順番に実行され、各フレームの各ステージの開始時に、mCallbackQueues 内の対応するコールバック キューのコールバック メソッドがコールバックされます。 Choreographer に関する推奨記事: Android Choreographer のソース コード分析
  • コード 2: リフレクションを通じて入力イベント コールバック キューの addCallbackLocked メソッドを取得する
  • コード 3: リフレクションを通じてアニメーション コールバック キューのaddCallbackLockedメソッドを取得する
  • コード 4: リフレクションを通じて描画コールバック キューを走査するメソッドを取得するaddCallbackLocked
  • コード 5: LooperMonitor.register(LooperDispatchListener listener) メソッドを通じて を LooperMonitor に設定しますLooperDispatchListener listener
  • コード 6: Looper.loop() でのメッセージ処理の開始時のコールバック
  • リスト 7: Looper.loop() でのメッセージ処理の終了時のコールバック

public class UIThreadMonitor implements BeatLifecycle, Runnable {
    private static final String ADD_CALLBACK = "addCallbackLocked";

    public static final int CALLBACK_INPUT = 0;
    public static final int CALLBACK_ANIMATION = 1;
    public static final int CALLBACK_TRAVERSAL = 2;

    private final static UIThreadMonitor sInstance = new UIThreadMonitor();
    private Object callbackQueueLock;
    private Object[] callbackQueues;
    private Method addTraversalQueue;
    private Method addInputQueue;
    private Method addAnimationQueue;
    private Choreographer choreographer;
    private long frameIntervalNanos = 16666666;

    ......

    public static UIThreadMonitor getMonitor() {
        return sInstance;
    }

    public void init(TraceConfig config) {
        if (Thread.currentThread() != Looper.getMainLooper().getThread()) {
            throw new AssertionError("must be init in main thread!");
        }
        this.isInit = true;
        this.config = config;
        choreographer = Choreographer.getInstance();
        callbackQueueLock = reflectObject(choreographer, "mLock");              
        callbackQueues = reflectObject(choreographer, "mCallbackQueues");                                                                    // 代码 1

        addInputQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_INPUT], ADD_CALLBACK, long.class, Object.class, Object.class);             // 代码 2
        addAnimationQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_ANIMATION], ADD_CALLBACK, long.class, Object.class, Object.class);     // 代码 3
        addTraversalQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_TRAVERSAL], ADD_CALLBACK, long.class, Object.class, Object.class);     // 代码 4  
        frameIntervalNanos = reflectObject(choreographer, "mFrameIntervalNanos");

        LooperMonitor.register(new LooperMonitor.LooperDispatchListener() {                                                                  // 代码 5  
            @Override
            public boolean isValid() {
                return isAlive;
            }

            @Override
            public void dispatchStart() {
                super.dispatchStart();
                UIThreadMonitor.this.dispatchBegin();                                                                                       // 代码 6
            }

            @Override
            public void dispatchEnd() {
                super.dispatchEnd();
                UIThreadMonitor.this.dispatchEnd();                                                                                      // 代码 7
            }

        });

                ......

    }  
}

セクション 3.1 で説明したように、Looper がメッセージの処理を開始および終了すると、LooperDispatchListener の dispatchStart() メソッドと dispatchEnd() メソッドがそれぞれコールバックされます。 、上記と同様、 コードはコード 6 とコード 7 に示されています

dispatchStart() メソッドは比較的単純です。UIThreadMonitor.this.dispatchBegin() を呼び出して 2 つの開始時刻 (1 つはスレッドの開始時刻、もう 1 つは CPU の開始時刻) を記録し、順番にコールバックします。 LooperObserver#dispatchBegin() メソッド(以下に示す)

public class UIThreadMonitor implements BeatLifecycle, Runnable {
        private long[] dispatchTimeMs = new long[4];
    private volatile long token = 0L;

    ......

    private void dispatchBegin() {
        token = dispatchTimeMs[0] = SystemClock.uptimeMillis();
        dispatchTimeMs[2] = SystemClock.currentThreadTimeMillis();
        AppMethodBeat.i(AppMethodBeat.METHOD_ID_DISPATCH);

        synchronized (observers) {
            for (LooperObserver observer : observers) {
                if (!observer.isDispatchBegin()) {
                    observer.dispatchBegin(dispatchTimeMs[0], dispatchTimeMs[2], token);
                }
            }
        }
    }  

    ......

}

dispatchEnd() メソッドは比較的複雑です。変数 isBelongFrame に基づいて doFrameEnd(long token) を呼び出すかどうかを決定し、スレッドの終了時刻と CPU の終了時刻を記録します。最後にLooperObserver#dispatchEnd() メソッド を呼び出します。

  • コード 1、UIThreadMonitorqueueStatusqueueCost には長さ 3 の 2 つの配列があり、それぞれ各フレームの入力イベント ステージとアニメーションに対応します。描画フェーズを通過する際のフェーズ、ステータス、消費時間。queueStatus には DO_QUEUE_DEFAULT、DO_QUEUE_BEGIN、DO_QUEUE_END の 3 つの値があります
  • コード 2、変数の初期値isBelongFramefalse で、doFrameBegin(long token) メソッドで設定されます< /span> a> オブジェクトはいつ実行されるのでしょうか。以下の分析 メソッドをオーバーライドします。では、スレッドによって インターフェイスは当然 は実装されます メソッドで呼び出され、 truedoFrameBegin(long token)run()UIThreadMonitorRunnablerun()UIThreadMonitor
  • コード 3、メッセージ処理の最後に dispatchEnd() メソッドが呼び出されます。このメソッドでは、変数 isBelongFrame を使用して、メソッドを呼び出すかどうかが決定されます。 doFrameEnd(long token)
  • コード 4、run() メソッドでは、まず doFrameBegin(long token) を呼び出して変数 isBelongFrame を に設定します。 true を使用し、 doQueueBegin() メソッドを使用して、入力イベント コールバック CALLBACK_INPUT が開始されたときのステータスと時刻を記録し、< a i=7> メソッド のアニメーション コールバック と、トラバーサル描画コールバック のコールバック メソッドを設定します。addFrameCallback()ChoreographerCALLBACK_ANIMATIONCALLBACK_TRAVERSAL
  • コード 5、アニメーション コールバックのコールバック メソッドで、 の終了ステータスと終了時刻を記録します。 、 の開始ステータスと時刻も記録します。CALLBACK_ANIMATIONrun()CALLBACK_INPUTCALLBACK_ANIMATION
  • コード 6、トラバーサル描画CALLBACK_TRAVERSALのコールバック メソッドrun()で、CALLBACK_ANIMATION の終了ステータスと終了時刻を記録します。 、CALLBACK_TRAVERSAL の開始ステータスと時刻も記録します。
  • 実際、UIThreadMonitor をインターフェイスとして使用するために Runnable インターフェイスを実装していると推測できます。入力イベント コールバック a> のコールバック メソッドは に設定されます。UIThreadMonitorCALLBACK_INPUTChoreographer

public class UIThreadMonitor implements BeatLifecycle, Runnable {
    public static final int CALLBACK_INPUT = 0;
    public static final int CALLBACK_ANIMATION = 1;
    public static final int CALLBACK_TRAVERSAL = 2;
    private static final int CALLBACK_LAST = CALLBACK_TRAVERSAL;

    private boolean isBelongFrame = false;
    private int[] queueStatus = new int[CALLBACK_LAST + 1];                                   // 代码 1                               
    private long[] queueCost = new long[CALLBACK_LAST + 1];

    private static final int DO_QUEUE_DEDO_QUEUE_DEFAULT、FAULT = 0;
    private static final int DO_QUEUE_BEGIN = 1;
    private static final int DO_QUEUE_END = 2;

    private void doFrameBegin(long token) {                                                                 // 代码 2
        this.isBelongFrame = true;
    }

    private void dispatchEnd() {                                                                                            // 代码 3
        if (isBelongFrame) {
            doFrameEnd(token);
        }

        dispatchTimeMs[3] = SystemClock.currentThreadTimeMillis();
        dispatchTimeMs[1] = SystemClock.uptimeMillis();

        AppMethodBeat.o(AppMethodBeat.METHOD_ID_DISPATCH);

        synchronized (observers) {
            for (LooperObserver observer : observers) {
                if (observer.isDispatchBegin()) {
                    observer.dispatchEnd(dispatchTimeMs[0], dispatchTimeMs[2], dispatchTimeMs[1], dispatchTimeMs[3], token, isBelongFrame);
                }
            }
        }

    }  

    @Override
    public void run() {                                                                                                             // 代码 4
        final long start = System.nanoTime();
        try {
            doFrameBegin(token);                                                                                            // 将 isBelongFrame 置位 true                              
            doQueueBegin(CALLBACK_INPUT);                                                                           // 通过 doQueueBegin(int type) 记录输入事件回调 CALLBACK_INPUT 开始状态和时间

            addFrameCallback(CALLBACK_ANIMATION, new Runnable() {                           // 通过 addFrameCallback() 方法向 Choreographer 中添加动画 CALLBACK_ANIMATION 回调 
                                                                                                                                                            // 每个回调其实都是一个 Runnable,执行时会主动调用 `run()` 方法
                @Override
                public void run() {                                                                                     // 代码 5
                    doQueueEnd(CALLBACK_INPUT);                                                             // 输入事件回调 CALLBACK_INPUT 结束
                    doQueueBegin(CALLBACK_ANIMATION);                                                   // 动画回调 CALLBACK_ANIMATION 开始
                }
            }, true);

            addFrameCallback(CALLBACK_TRAVERSAL, new Runnable() {                           // 通过 addFrameCallback() 方法向 Choreographer 中添加遍历绘制 CALLBACK_TRAVERSAL 回调

                @Override
                public void run() {                                                                                     // 代码 6
                    doQueueEnd(CALLBACK_ANIMATION);                                                     // 动画回调 CALLBACK_ANIMATION 结束
                    doQueueBegin(CALLBACK_TRAVERSAL);                                                   // 遍历绘制回调 CALLBACK_TRAVERSAL 开始
                }
            }, true);

        } finally {
            if (config.isDevEnv()) {
                MatrixLog.d(TAG, "[UIThreadMonitor#run] inner cost:%sns", System.nanoTime() - start);
            }
        }
    }

    private synchronized void addFrameCallback(int type, Runnable callback, boolean isAddHeader) {

        if (callbackExist[type]) {
            MatrixLog.w(TAG, "[addFrameCallback] this type %s callback has exist!", type);
            return;
        }

        if (!isAlive && type == CALLBACK_INPUT) {
            MatrixLog.w(TAG, "[addFrameCallback] UIThreadMonitor is not alive!");
            return;
        }
        try {
            synchronized (callbackQueueLock) {
                Method method = null;
                switch (type) {
                    case CALLBACK_INPUT:
                        method = addInputQueue;
                        break;
                    case CALLBACK_ANIMATION:
                        method = addAnimationQueue;
                        break;
                    case CALLBACK_TRAVERSAL:
                        method = addTraversalQueue;
                        break;
                }
                if (null != method) {
                    method.invoke(callbackQueues[type], !isAddHeader ? SystemClock.uptimeMillis() : -1, callback, null);
                    callbackExist[type] = true;
                }
            }
        } catch (Exception e) {
            MatrixLog.e(TAG, e.toString());
        }
    }

    private void doQueueBegin(int type) {
        queueStatus[type] = DO_QUEUE_BEGIN;
        queueCost[type] = System.nanoTime();
    }

    private void doQueueEnd(int type) {
        queueStatus[type] = DO_QUEUE_END;
        queueCost[type] = System.nanoTime() - queueCost[type];
        synchronized (callbackExist) {
            callbackExist[type] = false;
        }
    }

    ......

}

入力イベント コールバック CALLBACK_INPUT が Choreographer に設定されたときを見てみましょう

上の図に示すように、addFrameCallback() メソッドには、アニメーション コールバックCALLBACK_ANIMATIONとトラバーサル描画コールバックコールバックを追加しましたCALLBACK_TRAVERSALCALLBACK_INPUT

  • コード 1、UIThreadMonitor#onStart() メソッドが初めて呼び出されるとき、addFrameCallback(CALLBACK_INPUT, this, true) メソッドが 1 回呼び出され、< a i=3> は入力イベントとして使用されます のコールバックが Choreographyer に追加されますRunnableCALLBACK_INPUT
  • コード 2、doFrameEnd(long token) メソッドでは、addFrameCallback(CALLBACK_INPUT, this, true) メソッドは各フレームの完了後に 1 回呼び出され、次のフレームのコールバックのために再度追加されます。 . 3 つのコールバック。 doFrameEnd(long token) メソッドは、メッセージの処理の最後に dispatchEnd() メソッドでコールバックされます
  • コード 3。上記のコード分析では、doQueueBegin(CALLBACK_TRAVERSAL) のみが最後に呼び出され、doFrameEnd(long token) メソッド 3>、トラバーサル描画コールバックの監視が完了しましたdoQueueEnd(CALLBACK_TRAVERSAL)CALLBACK_TRAVERSAL
  • コード 4、長さ 3 の新しい配列を作成し、それを queueStatus に割り当てます。実際、ここで質問があります。 doFrameEnd(long token)各フレームのメソッド描画は毎回コールバックされます。このメソッドで配列オブジェクトを作成するとメモリ ジッターが発生しますか?
  • コード 5、LooperObserver の HashSet オブジェクトを走査し、入力イベント、アニメーション、走査描画時間をパラメータとして受け取る doFrame(String focusedActivityName, long start, long end, long frameCostMs, long inputCostNs, long animationCostNs, long traversalCostNs) メソッドをコールバックします。着信

public class UIThreadMonitor implements BeatLifecycle, Runnable {

    @Override
    public synchronized void onStart() {
        if (!isInit) {
            throw new RuntimeException("never init!");
        }
        if (!isAlive) {
            MatrixLog.i(TAG, "[onStart] %s", Utils.getStack());
            this.isAlive = true;
            addFrameCallback(CALLBACK_INPUT, this, true);                                           // 代码 1
        }
    }

    private void doFrameEnd(long token) {

        doQueueEnd(CALLBACK_TRAVERSAL);                                                                             // 代码 3

        for (int i : queueStatus) {
            if (i != DO_QUEUE_END) {
                queueCost[i] = DO_QUEUE_END_ERROR;
                if (config.isDevEnv) {
                    throw new RuntimeException(String.format("UIThreadMonitor happens type[%s] != DO_QUEUE_END", i));
                }
            }
        }
        queueStatus = new int[CALLBACK_LAST + 1];                                                     // 代码 4

        long start = token;
        long end = SystemClock.uptimeMillis();
        synchronized (observers) {                                                                                  // 代码 5
            for (LooperObserver observer : observers) {
                if (observer.isDispatchBegin()) {
                    observer.doFrame(AppMethodBeat.getFocusedActivity(), start, end, end - start, queueCost[CALLBACK_INPUT], queueCost[CALLBACK_ANIMATION], queueCost[CALLBACK_TRAVERSAL]);
                }
            }
        }

        addFrameCallback(CALLBACK_INPUT, this, true);                                               // 代码 2

        this.isBelongFrame = false;
    }  
}

3.3 フレームレートの計算

上記のコードから、各フレームの時間情報が HashSet<LooperObserver> observers を通じてコールバックされることがわかります。observers LooperObserver コールバック(以下の図に示す)

ここでは主にFrameTracer、フレーム レート FPS の計算を含むこのクラスについて説明します。他のクラスに興味がある友人は、自分で分析できます

FrameTracer のコードは次のとおりです。よく見てみると理解するのは難しくないと思います。主なロジックは notifyListener(final String focusedActivityName, final long frameCostMs) メソッドにあり、変数 frameIntervalMs = 16.67 ミリ秒

  • コード 1: HashSet<IDoFrameListener> listeners を通過し、 時間に基づいてドロップされたフレームの数を計算します。 > 、最後に インターフェイスdoFrame() の 2 つのメソッドを通じて をコールバックします。long frameCostframeCostMsdropFrameIDoFrameListener
  • ドロップされたフレームの数は次のように計算されます。final int dropFrame = (int) (frameCostMs / frameIntervalMs)frameCostMs はこのフレームの描画にかかった時間、frameIntervalMs 実際には 16.67 ミリ秒です。

public class FrameTracer extends Tracer {
    private final long frameIntervalMs;

    private HashSet<IDoFrameListener> listeners = new HashSet<>();

     public FrameTracer(TraceConfig config) {
        this.frameIntervalMs = TimeUnit.MILLISECONDS.convert(UIThreadMonitor.getMonitor().getFrameIntervalNanos(), TimeUnit.NANOSECONDS) + 1;
        ......
    }

    public void addListener(IDoFrameListener listener) {
        synchronized (listeners) {
            listeners.add(listener);
        }
    }

    public void removeListener(IDoFrameListener listener) {
        synchronized (listeners) {
            listeners.remove(listener);
        }
    }

    @Override
    public void onAlive() {
        super.onAlive();
        UIThreadMonitor.getMonitor().addObserver(this);
    }

    @Override
    public void onDead() {
        super.onDead();
        UIThreadMonitor.getMonitor().removeObserver(this);
    }

    @Override
    public void doFrame(String focusedActivityName, long start, long end, long frameCostMs, long inputCostNs, long animationCostNs, long traversalCostNs) {
        notifyListener(focusedActivityName, frameCostMs);
    }  

    private void notifyListener(final String focusedActivityName, final long frameCostMs) {
        long start = System.currentTimeMillis();
        try {
            synchronized (listeners) {
                for (final IDoFrameListener listener : listeners) {
                    final int dropFrame = (int) (frameCostMs / frameIntervalMs);                                // 代码 1
                    listener.doFrameSync(focusedActivityName, frameCostMs, dropFrame);
                    if (null != listener.getHandler()) {
                        listener.getHandler().post(new Runnable() {
                            @Override
                            public void run() {
                                listener.doFrameAsync(focusedActivityName, frameCostMs, dropFrame);
                            }
                        });
                    }
                }
            }
        } finally {
            long cost = System.currentTimeMillis() - start;
            if (config.isDevEnv()) {
                MatrixLog.v(TAG, "[notifyListener] cost:%sms", cost);
            }
            if (cost > frameIntervalMs) {
                MatrixLog.w(TAG, "[notifyListener] warm! maybe do heavy work in doFrameSync,but you can replace with doFrameAsync! cost:%sms", cost);
            }
            if (config.isDebug() && !isForeground()) {
                backgroundFrameCount++;
            }
        }
    }  
}

呼び出される場所を確認してくださいaddListener(IDoFrameListener listener)FrameTracer に追加IDoFrameListenerします。下の図に示すように、主に FrameDecorator と FrameTracer の 2 つの場所があります。これら 2 つの場所のロジックは実際には似ています。主に addListener(new FPSCollector()); を見てください。

FPSCollectorFrameTracer の内部クラスで、 IDoFrameListener インターフェイスを実装します。メイン ロジックは doFrameAsync() メソッドにあります< /span> a>

  • コード 1: 対応する FrameCollectItem オブジェクトが現在の ActivityName に基づいて作成され、HashMap に保存されます。
  • コード 2: FrameCollectItem#collect() を呼び出して、フレーム レート FPS およびその他の情報を計算します
  • コード 3: このアクティビティの合計描画時間が timeSliceMs (デフォルトは 10 秒) を超えた場合、FrameCollectItem#report() を呼び出して統計データを報告し、現在の ActivityName と対応する FrameCollectItem をハッシュマップ オブジェクト

    private class FPSCollector extends IDoFrameListener {

        private Handler frameHandler = new Handler(MatrixHandlerThread.getDefaultHandlerThread().getLooper());
        private HashMap<String, FrameCollectItem> map = new HashMap<>();

        @Override
        public Handler getHandler() {
            return frameHandler;
        }

        @Override
        public void doFrameAsync(String focusedActivityName, long frameCost, int droppedFrames) {
            super.doFrameAsync(focusedActivityName, frameCost, droppedFrames);
            if (Utils.isEmpty(focusedActivityName)) {
                return;
            }

            FrameCollectItem item = map.get(focusedActivityName);       // 代码 1
            if (null == item) {
                item = new FrameCollectItem(focusedActivityName);
                map.put(focusedActivityName, item);
            }

            item.collect(droppedFrames);                                                                // 代码 2

            if (item.sumFrameCost >= timeSliceMs) { // report                       // 代码 3
                map.remove(focusedActivityName);
                item.report();
            }
        }

    }

FrameCollectItem も FrameTracer の内部クラスであり、最も重要なものは 2 つのメソッド collect(int droppedFrames)report() メソッド です。

  • コード 1: 各フレームの所要時間を加算し、合計の所要時間を sumFrameCost で表します。
  • コード 2: sumDroppedFrames はドロップされたフレームの総数をカウントします。
  • コード 3: sumFrame はフレームの総数を表します
  • コード 4: ドロップされたフレームの数に基づいて、フレーム ドロップ動作の程度を判断し、その数を記録します。
  • コード 5: float fps = Math.min(60.f, 1000.f * sumFrame / sumFrameCost) に基づいて fps 値を計算します。
  • コード 6: 統計情報を Issue オブジェクトにカプセル化し、TracePlugin#onDetectIssue() メソッド を通じてコールバックします。

    private class FrameCollectItem {
        String focusedActivityName;
        long sumFrameCost;
        int sumFrame = 0;
        int sumDroppedFrames;
        // record the level of frames dropped each time
        int[] dropLevel = new int[DropStatus.values().length];
        int[] dropSum = new int[DropStatus.values().length];

        FrameCollectItem(String focusedActivityName) {
            this.focusedActivityName = focusedActivityName;
        }

        void collect(int droppedFrames) {
            long frameIntervalCost = UIThreadMonitor.getMonitor().getFrameIntervalNanos();
            sumFrameCost += (droppedFrames + 1) * frameIntervalCost / Constants.TIME_MILLIS_TO_NANO;            // 代码 1
            sumDroppedFrames += droppedFrames;                                                                                                                      // 代码 2
            sumFrame++;                                                                                                                                                                     // 代码 3

            if (droppedFrames >= frozenThreshold) {                                                                                                             // 代码 4
                dropLevel[DropStatus.DROPPED_FROZEN.index]++;
                dropSum[DropStatus.DROPPED_FROZEN.index] += droppedFrames;
            } else if (droppedFrames >= highThreshold) {
                dropLevel[DropStatus.DROPPED_HIGH.index]++;
                dropSum[DropStatus.DROPPED_HIGH.index] += droppedFrames;
            } else if (droppedFrames >= middleThreshold) {
                dropLevel[DropStatus.DROPPED_MIDDLE.index]++;
                dropSum[DropStatus.DROPPED_MIDDLE.index] += droppedFrames;
            } else if (droppedFrames >= normalThreshold) {
                dropLevel[DropStatus.DROPPED_NORMAL.index]++;
                dropSum[DropStatus.DROPPED_NORMAL.index] += droppedFrames;
            } else {
                dropLevel[DropStatus.DROPPED_BEST.index]++;
                dropSum[DropStatus.DROPPED_BEST.index] += (droppedFrames < 0 ? 0 : droppedFrames);
            }
        }

        void report() {
            float fps = Math.min(60.f, 1000.f * sumFrame / sumFrameCost);                                                                           // 代码 5
            MatrixLog.i(TAG, "[report] FPS:%s %s", fps, toString());

            try {
                TracePlugin plugin = Matrix.with().getPluginByClass(TracePlugin.class);                                             // 代码 6
                JSONObject dropLevelObject = new JSONObject();
                dropLevelObject.put(DropStatus.DROPPED_FROZEN.name(), dropLevel[DropStatus.DROPPED_FROZEN.index]);
                dropLevelObject.put(DropStatus.DROPPED_HIGH.name(), dropLevel[DropStatus.DROPPED_HIGH.index]);
                dropLevelObject.put(DropStatus.DROPPED_MIDDLE.name(), dropLevel[DropStatus.DROPPED_MIDDLE.index]);
                dropLevelObject.put(DropStatus.DROPPED_NORMAL.name(), dropLevel[DropStatus.DROPPED_NORMAL.index]);
                dropLevelObject.put(DropStatus.DROPPED_BEST.name(), dropLevel[DropStatus.DROPPED_BEST.index]);

                JSONObject dropSumObject = new JSONObject();
                dropSumObject.put(DropStatus.DROPPED_FROZEN.name(), dropSum[DropStatus.DROPPED_FROZEN.index]);
                dropSumObject.put(DropStatus.DROPPED_HIGH.name(), dropSum[DropStatus.DROPPED_HIGH.index]);
                dropSumObject.put(DropStatus.DROPPED_MIDDLE.name(), dropSum[DropStatus.DROPPED_MIDDLE.index]);
                dropSumObject.put(DropStatus.DROPPED_NORMAL.name(), dropSum[DropStatus.DROPPED_NORMAL.index]);
                dropSumObject.put(DropStatus.DROPPED_BEST.name(), dropSum[DropStatus.DROPPED_BEST.index]);

                JSONObject resultObject = new JSONObject();
                resultObject = DeviceUtil.getDeviceInfo(resultObject, plugin.getApplication());

                resultObject.put(SharePluginInfo.ISSUE_SCENE, focusedActivityName);
                resultObject.put(SharePluginInfo.ISSUE_DROP_LEVEL, dropLevelObject);
                resultObject.put(SharePluginInfo.ISSUE_DROP_SUM, dropSumObject);
                resultObject.put(SharePluginInfo.ISSUE_FPS, fps);

                Issue issue = new Issue();
                issue.setTag(SharePluginInfo.TAG_PLUGIN_FPS);
                issue.setContent(resultObject);
                plugin.onDetectIssue(issue);

            } catch (JSONException e) {
                MatrixLog.e(TAG, "json error", e);
            }
        }

        @Override
        public String toString() {
            return "focusedActivityName=" + focusedActivityName
                    + ", sumFrame=" + sumFrame
                    + ", sumDroppedFrames=" + sumDroppedFrames
                    + ", sumFrameCost=" + sumFrameCost
                    + ", dropLevel=" + Arrays.toString(dropLevel);
        }
    }

ここでは主に、各フレームに費やされた合計時間に基づいて、ドロップされたフレームの数、FPS、その他のデータをカウントし、最終的にこれらのデータを特定の形式でコールバックします。

4. まとめ

この記事では、Matrix-TraceCanary モジュールにおけるフレーム レート FPS やドロップ フレーム数などのデータの原理と一般的なプロセスを主に分析します。原理は、、コールバック キューにコールバックを追加するは、リフレクションを通じて UIThreadMonitor に実装されます。 Looper.getMainLooper().setMessageLogging(Printer printer)mCallbackQueuesmCallbackQueues

Looper.getMainLooper().setMessageLogging(Printer printer) による FPS 統計の実装には常に問題がありました。セクション 2.1 と 2.3 で説明したように、 を使用して FPS 統計を実装する方法があります。 2> メカニズム 1 つの欠点: メッセージ処理開始ログとメッセージ処理終了ログを出力するときに文字列の結合が行われるため、文字列の結合が頻繁に行われるとパフォーマンスにも影響します。 Looper

おすすめ

転載: blog.csdn.net/jdsjlzx/article/details/134180291