Tencentインタビュアー:AndroidのUI更新メカニズムについて教えてください。

DachangのAndroidインタビューは、実際には誰もが想像したほど難しくはありません。多くの質問は、薬を変えるのではなく、スープを変えることについてです。

著者:偶尔皮一下的Raina
リンク:https://juejin.im/post/5e64390bf265da575f4e7de8

この記事は主に次の問題を解決します。

  • Androidのリフレッシュレートが毎秒60フレームであることは誰もが知っていますが、これはonDrawメソッドが16ミリ秒ごとに呼び出されることを意味しますか?
  • インターフェイスを再描画する必要がない場合16ms、画面は後で更新されますか?
  • 呼び出したinvalidate()直後に画面が更新されますか?
  • フレームロスは、メインスレッドが時間のかかる操作を行うためだと言いますが、なぜメインスレッドがフレームロスを引き起こす時間のかかる操作を行うのですか?
  • 画面が更新されようとしているときにOnDraw()描画すると、フレームがドロップされますか?

さて、上記の質問で、答えを見つけるためにソースコードを入力しましょう。

1.画面描画プロセス

画面描画メカニズムの基本原理は次のようにまとめることができます。

画面全体を描画する基本的なプロセスは次のとおりです。

  • アプリケーションがシステムサービスにバッファを要求します
  • システムサービスがバッファを返す
  • アプリケーションが描画された後、バッファをシステムサービスに送信します

それがAndroidに入れられている場合、それは次のとおりです。

Androidでは、Surfaceの一部がメモリの一部に対応します。メモリアプリケーションが成功した後、アプリ側に描画する場所があります。今日はAndroidのビュー描画が焦点ではないので、ここでやめましょう〜

2.画面更新分析

画面の更新のタイミングは、図に示すように、Vsync信号が到着したときです。

Android側では、誰がVsync制作を管理していますか?アプリを更新するように通知するのは誰ですか?Androidでは、Vysncシグナルは基盤となるHWComposerによって生成され、更新する通知アプリケーションはJavaレイヤーですChoreographer。Androidの画面全体の更新の中核はこれにありChoreographerます。一緒にコードを見てみましょう。UIを再描画するたびに、requestLayout()を呼び出すので、次のメソッドから始めます。

2.1 requestLayout()
----》类名:ViewRootImpl

    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            //重点
            scheduleTraversals();
        }
    }
2.2 scheduleTraversals()
----》类名:ViewRootImpl

    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
           ......
        }
    }

ご覧のとおり、ここではすぐに再描画することはできませんが、次の2つのことが行われます。

  • SyncBarrier(同期バリア)をメッセージキューに挿入します
  • 合格Cherographer postAcallback

次に、このSyncBarrier(同期バリア)について簡単に説明します。非同期バリアの役割は次のとおりです。

  • 同期メッセージの実行を防止します
  • 非同期メッセージを優先する

なぜこれを設計するのSyncBarrierですか?主な理由は、Androidでは、一部のメッセージが非常に緊急であり、すぐに実行する必要があるためです。メッセージキューに通常のメッセージが多すぎる場合は、実行されるまでに時間が経過している可能性があります。

この時点で、誰かが私のようになっている可能性があります。メッセージに優先順位を付けて、優先順位で並べ替えてみませんか?ホールドを取得PriorityQueueまだ終わっていませんか?

私の理解では、Androidでは、メッセージキューのデザインは単一リンクリストであり、リンクリスト全体が時間に従って並べ替えられます。このときに優先度の並べ替えルールを追加すると、並べ替えルールが複雑になります。一方では、メッセージを制御できなくなります。優先順位はユーザー自身が記入できるので、面倒ではありませんか?ユーザーが常に最高の優先度を入力すると、システムメッセージが長時間消費され、システム全体の動作に問題が発生し、最終的にユーザーエクスペリエンスに影響を与えるため、Androidのデザインは同期バリアは非常に賢いです。〜

要約するとscheduleTraversals() 、実行後、非同期メッセージの優先実行を保証するためにバリアが挿入されます。

小さな質問を挿入します:メソッドでrequestLayout()を複数回呼び出す場合、質問したいと思います:システムは複数のバリアを挿入しますか、それとも複数のコールバックを投稿しますか?答えはノーです、なぜですか?変数mTraversalScheduledが表示されますか?答えです〜

2.3 Choreographer.postCallback()

最初に簡単に説明しましょうChoreographerChoreographer中国語の翻訳は振付師と呼ばれ、その主な機能はシステムを調整することです。(グーグルの下で実際の仕事の振付師に行くことができます、このクラス名は本当に適切です〜)振付師クラスのアプリケーションはどのように初期化されますか?ているgetInstance()方法:

    public static Choreographer getInstance() {
        return sThreadInstance.get();
    }
    
        // Thread local storage for the choreographer.
    private static final ThreadLocal<Choreographer> sThreadInstance =
            new ThreadLocal<Choreographer>() {
        @Override
        protected Choreographer initialValue() {
            Looper looper = Looper.myLooper();
            if (looper == null) {
                throw new IllegalStateException("The current thread must have a looper!");
            }
            Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
            if (looper == Looper.getMainLooper()) {
                mMainInstance = choreographer;
            }
            return choreographer;
        }
    };

これはChoreographer、シングルトンはなく、スレッドごとに個別のコピーであることを全員に思い出させるためにここに投稿されています。

さて、コードに戻りましょう。

 ----》类名:Choreographer
 //1
    public void postCallback(int callbackType, Runnable action, Object token) {
        postCallbackDelayed(callbackType, action, token, 0);
    }
  //2  
     public void postCallbackDelayed(int callbackType,
            Runnable action, Object token, long delayMillis) {
       ....
        postCallbackDelayedInternal(callbackType, action, token, delayMillis);
    }
    //3
      private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) {
                ...
                mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
                if (dueTime <= now) {
                scheduleFrameLocked(now);
            } else {
                ...
              }
            }

Choreographerpostcallback置くCallbackQueueことを、これはCallbackQueue単独でリンクされたリストです。

最初にcallbackTypeCallbackQueue単一リンクリスト取得され、次にコールバックが時系列に従って単一リンクリストに挿入されます。

2.4 scheduleFrameLocked()
 ----》类名:Choreographer
  private void scheduleFrameLocked(long now) {
       ...
       // If running on the Looper thread, then schedule the vsync immediately,
                // otherwise post a message to schedule the vsync from the UI thread
                // as soon as possible.
                if (isRunningOnLooperThreadLocked()) {
                    scheduleVsyncLocked();
                } else {
                    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                    msg.setAsynchronous(true);
                    mHandler.sendMessageAtFrontOfQueue(msg);
                }
            } else {
               ...
            }
        }
    }

scheduleFrameLocked役割は次のとおりです。

現在のスレッドがCherographerワーカースレッドの場合は、直接実行しますscheduleVysnLocked

それ以外の場合は、非同期メッセージをメッセージキューに送信します。この非同期メッセージは同期バリアの影響を受けないため、メッセージをメッセージキューの先頭に挿入する必要があります。このメッセージは非常に緊急であることがわかります。

ソースコードを追跡したところ、実際、MSG_DO_SCHEDULE_VSYNCこのメッセージは最終的にscheduleFrameLockedこのメソッドによって実行されたことがわかったためこのメソッドを直接追跡しscheduleVsyncLocked()ました。

2.5 scheduleVsyncLocked()
 ----》类名:Choreographer
 
    private void scheduleVsyncLocked() {
        mDisplayEventReceiver.scheduleVsync();
    }
    
 ----》类名:DisplayEventReceiver
 
        public void scheduleVsync() {
        if (mReceiverPtr == 0) {
            Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
                    + "receiver has already been disposed.");
        } else {
        //mReceiverPtr是Native层一个类的指针地址
        //这里这个类指的是底层NativeDisplayEventReceiver这个类
        //nativeScheduleVsync底层会调用到requestNextVsync()去请求下一个Vsync,
        //具体不跟踪了,native层代码更长,还涉及到各种描述符监听以及跨进程数据传输
            nativeScheduleVsync(mReceiverPtr);
        }
    }

ここで、新しいクラスを確認できます。DisplayEventReceiverこのクラスの役割は、Vsync信号の監視を登録することであり、次のVsync信号が到着したときに通知されますDisplayEventReceiver

どこに通知しますか?ソースコードのコメントは非常に明確です:

 ----》类名:DisplayEventReceiver
 
    // Called from native code.  <---注释还是很良心的
    private void dispatchVsync(long timestampNanos, int builtInDisplayId, int frame) {
        onVsync(timestampNanos, builtInDisplayId, frame);
    }

次のVysnc信号が来ると、onVsyncメソッドは最終的に呼び出されます。

    public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
    }

クリックして確認してください。これは空の実装であり、クラス定義に戻ります。抽象クラスであることが判明し、その実装クラスはFrameDisplayEventReceiver次のCherographerとおりです。で定義されています。

 ----》类名:Choreographer
 
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {
            ....
            }

2.6 FrameDisplayEventReceiver.onVysnc()
 ----》类名:Choreographer
 
 private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {

        @Override
        public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
             ....
            mTimestampNanos = timestampNanos;
            mFrame = frame;
            Message msg = Message.obtain(mHandler, this);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }

        @Override
        public void run() {
            ....
            doFrame(mTimestampNanos, mFrame);
        }
    }

onVsyncこのメソッドは、Cherographerが配置されているスレッドのメッセージキューにメッセージを送信します。このメッセージはそれ自体(Runnableを実装)であるため、最終的にdoFrame()メソッドが呼び出されます。

2.7 doFrame(mTimestampNanos、mFrame)

doFrame()処理は2つの段階に分かれています。

   void doFrame(long frameTimeNanos, int frame) {
        final long startNanos;
        synchronized (mLock) {
           //1、阶段一
            long intendedFrameTimeNanos = frameTimeNanos;
            startNanos = System.nanoTime();
            final long jitterNanos = startNanos - frameTimeNanos;
            if (jitterNanos >= mFrameIntervalNanos) {
                final long skippedFrames = jitterNanos / mFrameIntervalNanos;
                if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
                    Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                            + "The application may be doing too much work on its main thread.");
                }
                ...
            }
            ...
        }

frameTimeNanos現在のタイムスタンプです。現在の時刻と開始時刻を引いて、このフレームの処理にかかった時間を取得します。それより長い場合はmFrameIntervalNano、処理に時間がかかり、毎日表示される内容を印刷します。The application may be doing too much work on its main thread。

フェーズ2:

 void doFrame(long frameTimeNanos, int frame) {
 ...
try {
//阶段2
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
            AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);

            mFrameInfo.markInputHandlingStart();
            doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

            mFrameInfo.markAnimationsStart();
            doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);

            mFrameInfo.markPerformTraversalsStart();
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
        } 
        ...
        }

doFrame()第二段階はcallbackCallbackQueue内部からcallback処理の実行時間まで、あらゆる種類の処理を処理callbackすることですが、これはどうですか?

ここで前のpostCallback()操作を思い出してください:

このコールバックは実際には1つだけmTraversalRunnableであり、Runnableであり、最終的にrun()メソッドを呼び出して、インターフェイスの実際の更新を実現します。

 ----》类名:ViewRootImpl

    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    
    void doTraversal() {
        if (mTraversalScheduled) {
          ...
            performTraversals();
         ...
        }
    }
    
    private void performTraversals() {
      ...
      //开始真正的界面绘制
       performDraw();
      ...
    }

3、まとめ

長いコード追跡の後、インターフェイスの更新プロセス全体が終了しました。以下に要約します。

4、質問応答

Q: Androidのリフレッシュレートが毎秒60フレームであることは誰もが知っていますonDrawが、これはメソッドが16ミリ秒ごとに呼び出されることを意味しますか?

A: ここでは、60フレーム/秒が画面の更新頻度ですが、onDraw()メソッドが呼び出されるかどうかは、アプリケーションがrequestLayout()登録監視のために呼び出されるかどうかによって異なります

Q: インターフェイスを再描画する必要がない場合、16ミリ秒後に画面が更新されますか?

A: 再描画する必要がない場合、アプリケーションはVsync信号を受信しませんが、それでも更新されますが、描画されたデータは変更されません。

Q: invalidate()を呼び出した直後に画面が更新されますか?

A: いいえ、次のVsync信号が到着するまで

Q: メインスレッドが時間のかかる操作を行うためにフレームがドロップされると言いますが、メインスレッドが時間のかかる操作を行う理由はフレームをドロップする原因になります

A: その理由は、メインスレッドで時間のかかる操作を行うと、次のフレームの描画に影響を与え、このVsync時点でインターフェイスを更新できなくなり、フレームが失われるためです。

Q: 画面が更新されようとしているときにOnDraw()描画すると、フレームがドロップされますか?

Vsync信号は周期的であるため、これはそれほど重要ではありません。onDraw()を開始しても、インターフェイスの更新には影響しません。

5、参照ドキュメント

六、ついに

この期間に編集したAndroidで最も重要で人気のある学習指導資料を、何千人もの人々のAndroidテクノロジーエクスチェンジサークル共有フォルダーに入れました(表示するにはここをクリック)。自己学習プログラミングルートとインタビューもあります。さまざまな方向性。質問の収集/顔の古典、一連の技術記事など。

リソースは継続的に更新されており、誰もが一緒に学び、議論することを歓迎します。

おすすめ

転載: blog.csdn.net/weixin_49559515/article/details/114640019