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つに分けられますので、 簡単に紹介BlockCanaryEx、BlockCanary
2.1 ルーパー機構
メイン スレッドにMainLooper
があることは誰もが知っています。メイン スレッドが開始されると、MainLooper#loop()
メソッドが呼び出され、タスクが実行されます。メインスレッドでの実行は間接的となります (Handler のサブクラス) の によって実行されます3>. 分析してみましょう。 方法は次のとおりですActivityThread
H
handleMessage(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().mLogging
はLooper.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()
のmLogging
Looper.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 isBegin
、listener.isValid()
、< に基づきます。 a i=4 > 条件付き実行 メソッドと メソッド、メッセージ処理の開始時と終了時に行う処理はすべて にあります。および a> はlistener.isHasDispatchStart
listener.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 メッセージ処理
下の図に示すように、AppMethodBeat
と UIThreadMonitor
があります。 3> メソッド LooperMonitor で を設定します。この記事に関連するのは クラスです。このクラスの分析に焦点を当てましょうLooperMonitor.register(LooperDispatchListener listener)
LooperDispatchListener listener
UIThreadMonitor
最初に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、UIThreadMonitor
queueStatus
とqueueCost
には長さ 3 の 2 つの配列があり、それぞれ各フレームの入力イベント ステージとアニメーションに対応します。描画フェーズを通過する際のフェーズ、ステータス、消費時間。queueStatus
には DO_QUEUE_DEFAULT、DO_QUEUE_BEGIN、DO_QUEUE_END の 3 つの値があります - コード 2、変数の初期値
isBelongFrame
はfalse
で、doFrameBegin(long token)
メソッドで設定されます< /span> a> オブジェクトはいつ実行されるのでしょうか。以下の分析 メソッドをオーバーライドします。では、スレッドによって インターフェイスは当然 は実装されます メソッドで呼び出され、 はtrue
、doFrameBegin(long token)
run()
UIThreadMonitor
Runnable
run()
UIThreadMonitor
- コード 3、メッセージ処理の最後に
dispatchEnd()
メソッドが呼び出されます。このメソッドでは、変数isBelongFrame
を使用して、メソッドを呼び出すかどうかが決定されます。doFrameEnd(long token)
- コード 4、
run()
メソッドでは、まずdoFrameBegin(long token)
を呼び出して変数isBelongFrame
を に設定します。true
を使用し、doQueueBegin()
メソッドを使用して、入力イベント コールバックCALLBACK_INPUT
が開始されたときのステータスと時刻を記録し、< a i=7> メソッド のアニメーション コールバック と、トラバーサル描画コールバック のコールバック メソッドを設定します。addFrameCallback()
Choreographer
CALLBACK_ANIMATION
CALLBACK_TRAVERSAL
- コード 5、アニメーション コールバックのコールバック メソッドで、 の終了ステータスと終了時刻を記録します。 、 の開始ステータスと時刻も記録します。
CALLBACK_ANIMATION
run()
CALLBACK_INPUT
CALLBACK_ANIMATION
- コード 6、トラバーサル描画
CALLBACK_TRAVERSAL
のコールバック メソッドrun()
で、CALLBACK_ANIMATION
の終了ステータスと終了時刻を記録します。 、CALLBACK_TRAVERSAL
の開始ステータスと時刻も記録します。 - 実際、
UIThreadMonitor
は をインターフェイスとして使用するためにRunnable
インターフェイスを実装していると推測できます。入力イベント コールバック a> のコールバック メソッドは に設定されます。UIThreadMonitor
CALLBACK_INPUT
Choreographer
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_TRAVERSAL
CALLBACK_INPUT
- コード 1、
UIThreadMonitor#onStart()
メソッドが初めて呼び出されるとき、addFrameCallback(CALLBACK_INPUT, this, true)
メソッドが 1 回呼び出され、< a i=3> は入力イベントとして使用されます のコールバックが Choreographyer に追加されますRunnable
CALLBACK_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 frameCost
frameCostMs
dropFrame
IDoFrameListener
- ドロップされたフレームの数は次のように計算されます。
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());
を見てください。
FPSCollector
は FrameTracer
の内部クラスで、 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)
mCallbackQueues
mCallbackQueues
Looper.getMainLooper().setMessageLogging(Printer printer)
による FPS 統計の実装には常に問題がありました。セクション 2.1 と 2.3 で説明したように、 を使用して FPS 統計を実装する方法があります。 2> メカニズム 1 つの欠点: メッセージ処理開始ログとメッセージ処理終了ログを出力するときに文字列の結合が行われるため、文字列の結合が頻繁に行われるとパフォーマンスにも影響します。 Looper