Android进阶宝典 -- Choreographer实时监控App帧率变化,实现卡顿监控

前言

本文首发于掘金,有意向转发的同行可私信,想在Android领域有所进步的伙伴,可微信搜索个人的公众号「Android技术集中营」或扫码添加,有不定时福利等大家来拿。

1 从Activity启动流程引出Choreographer

在之前分析app启动耗时时,通过benchmark导出的trace文件时,如下图:

当Activity进入到onResume生命周期时,此时有一个类出现了,就是Choreographer,中文意思为编舞者,不知是否有伙伴们研究过这个类的作用,至少在我看到这个类的时候,第一反应就是与图像渲染展示相关,因为当Activity进入到onResume时,页面才会完全展示出来并且用户可交互,而且Choreographer出现之后,RenderThread也开始了绘制,所以我们从Activity启动流程中看下Choreographer这个类的作用。

1.1 重温View的添加流程

当Activity启动时,通过AMS一系列的进程间通信,最终会调用到ActivityThread的handleResumeActivity方法,在这个方法中,会执行View的添加流程,其实在之前关于WindowManager的文章中详细介绍过了,这里我们再重温一下。

java 复制代码
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
        boolean isForward, String reason) {
        
    //......

    final Activity a = r.activity;

    if (localLOGV) {
        Slog.v(TAG, "Resume " + r + " started activity: " + a.mStartedActivity
                + ", hideForNow: " + r.hideForNow + ", finished: " + a.mFinished);
    }

    final int forwardBit = isForward
            ? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;

    // If the window hasn't yet been added to the window manager,
    // and this guy didn't finish itself or start another activity,
    // then go ahead and add the window.
    boolean willBeVisible = !a.mStartedActivity;
    if (!willBeVisible) {
        willBeVisible = ActivityClient.getInstance().willActivityBeVisible(
                a.getActivityToken());
    }
    if (r.window == null && !a.mFinished && willBeVisible) {
        r.window = r.activity.getWindow();
        View decor = r.window.getDecorView();
        decor.setVisibility(View.INVISIBLE);
        ViewManager wm = a.getWindowManager();
        WindowManager.LayoutParams l = r.window.getAttributes();
        a.mDecor = decor;
        l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
        l.softInputMode |= forwardBit;
        if (r.mPreserveWindow) {
            a.mWindowAdded = true;
            r.mPreserveWindow = false;
            // Normally the ViewRoot sets up callbacks with the Activity
            // in addView->ViewRootImpl#setView. If we are instead reusing
            // the decor view we have to notify the view root that the
            // callbacks may have changed.
            ViewRootImpl impl = decor.getViewRootImpl();
            if (impl != null) {
                impl.notifyChildRebuilt();
            }
        }
        if (a.mVisibleFromClient) {
            if (!a.mWindowAdded) {
                a.mWindowAdded = true;
                wm.addView(decor, l);
            } else {
                // The activity will get a callback for this {@link LayoutParams} change
                // earlier. However, at that time the decor will not be set (this is set
                // in this method), so no action will be taken. This call ensures the
                // callback occurs with the decor set.
                a.onWindowAttributesChanged(l);
            }
        }

        // If the window has already been added, but during resume
        // we started another activity, then don't yet make the
        // window visible.
    } else if (!willBeVisible) {
        if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
        r.hideForNow = true;
    }

    // Get rid of anything left hanging around.
    cleanUpPendingRemoveWindows(r, false /* force */);

    // The window is now visible if it has been added, we are not
    // simply finishing, and we are not starting another activity.
    if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) {
        if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward=" + isForward);
        ViewRootImpl impl = r.window.getDecorView().getViewRootImpl();
        WindowManager.LayoutParams l = impl != null
                ? impl.mWindowAttributes : r.window.getAttributes();
        if ((l.softInputMode
                & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)
                != forwardBit) {
            l.softInputMode = (l.softInputMode
                    & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION))
                    | forwardBit;
            if (r.activity.mVisibleFromClient) {
                ViewManager wm = a.getWindowManager();
                View decor = r.window.getDecorView();
                wm.updateViewLayout(decor, l);
            }
        }

        r.activity.mVisibleFromServer = true;
        mNumVisibleActivities++;
        if (r.activity.mVisibleFromClient) {
            r.activity.makeVisible();
        }
    }

    r.nextIdle = mNewActivities;
    mNewActivities = r;
    if (localLOGV) Slog.v(TAG, "Scheduling idle handler for " + r);
    Looper.myQueue().addIdleHandler(new Idler());
}

在handleResumeActivity方法中,我们可以看到会将当前Activity的DecorView添加到WindowManager中,调用了WindowManager的addView方法,这个方法最终的调用就是在WidowManageGlobal中。

WidowManageGlobal # addView

java 复制代码
public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow, int userId) {
    if (view == null) {
        throw new IllegalArgumentException("view must not be null");
    }
    if (display == null) {
        throw new IllegalArgumentException("display must not be null");
    }
    if (!(params instanceof WindowManager.LayoutParams)) {
        throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
    }

    //......

    ViewRootImpl root;
    View panelParentView = null;

    synchronized (mLock) {
        // Start watching for system property changes.
        if (mSystemPropertyUpdater == null) {
            mSystemPropertyUpdater = new Runnable() {
                @Override public void run() {
                    synchronized (mLock) {
                        for (int i = mRoots.size() - 1; i >= 0; --i) {
                            mRoots.get(i).loadSystemProperties();
                        }
                    }
                }
            };
            SystemProperties.addChangeCallback(mSystemPropertyUpdater);
        }
        
        // 查找当前DecorView在mViews数组中的位置
        int index = findViewLocked(view, false);
        if (index >= 0) {
            if (mDyingViews.contains(view)) {
                // Don't wait for MSG_DIE to make it's way through root's queue.
                mRoots.get(index).doDie();
            } else {
                throw new IllegalStateException("View " + view
                        + " has already been added to the window manager.");
            }
            // The previous removeView() had not completed executing. Now it has.
        }

        // If this is a panel window, then find the window it is being
        // attached to for future reference.
        if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
                wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
            final int count = mViews.size();
            for (int i = 0; i < count; i++) {
                if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
                    panelParentView = mViews.get(i);
                }
            }
        }
        
        
        IWindowSession windowlessSession = null;
        // If there is a parent set, but we can't find it, it may be coming
        // from a SurfaceControlViewHost hierarchy.
        if (wparams.token != null && panelParentView == null) {
            for (int i = 0; i < mWindowlessRoots.size(); i++) {
                ViewRootImpl maybeParent = mWindowlessRoots.get(i);
                if (maybeParent.getWindowToken() == wparams.token) {
                    windowlessSession = maybeParent.getWindowSession();
                    break;
                }
            }
        }

        if (windowlessSession == null) {
            root = new ViewRootImpl(view.getContext(), display);
        } else {
            root = new ViewRootImpl(view.getContext(), display,
                    windowlessSession);
        }


        view.setLayoutParams(wparams);

        // 往数组中添加View
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);

        // do this last because it fires off messages to start doing things
        try {
            root.setView(view, wparams, panelParentView, userId);
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            if (index >= 0) {
                removeViewLocked(index, true);
            }
            throw e;
        }
    }
}

addView方法是一个线程安全的方法,首先我们将DecorView添加到WindowManager时,首先会从mViews数组中查找是否添加过这个View,如果添加过,那么就会抛出下面这个异常:

java 复制代码
"View xxx has already been added to the window manager."

相信伙伴们都碰到过这个问题,所以在使用WindowManager的时候,不能重复添加同一个View,在添加之前需要判断当前View是否已经被添加到Window上。

如果没有添加过,那么就会创建ViewRootImpl,同时将DecorView加入mViewsmRoots数组中,因此在WindowManagerGlobal中是维护了当前进程中所有的页面窗口,在页面刷新时,便可以从mViews中找到对应的窗口,做刷新处理即可。

最后是调用了ViewRootIml的setView方法,这个方法内容太多了,有兴趣的伙伴可以去看下源码,在这个方法中调用了requestLayout,这次调用是确保在系统接收到任意事件之前进行布局绘制。

java 复制代码
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();

1.2 第一次布局

前面我们提到了,调用ViewRootImpl的setView方法时,会执行requestLayout方法,在这个方法中,有一个非常重要的方法,就是scheduleTraversals.

java 复制代码
@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

ViewRootImpl # scheduleTraversals

其实不止ViewRootImpl的setView方法会调用到scheduleTraversals,所有View的刷新都会调用到scheduleTraversals,我们看下这个方法。

java 复制代码
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}
  • 首先有一个标志位mTraversalScheduled的判断,也就是说当调用scheduleTraversals方法时,此标志位就会被设置为true,同一时间只会执行一次,直到执行完成,举个例子,当TextView的setText方法同时执行两次,那么也只会触发一次渲染逻辑。

  • 通过Handler发送一个同步屏障,什么是同步屏障呢?就是通过Handler创建一个空消息,从View刷新这个时间点,到VSYNC信号来临这段时间内,屏蔽掉所有的同步消息,保证VSYNC消息来临时,立刻执行刷新,保证刷新的及时性。

  • 接下来,到了本篇文章的重点,出现了Choreographer。

2 Choreographer的作用

首先我们先看,Choreographer是什么时候初始化的。

java 复制代码
public ViewRootImpl(Context context, Display display) {
    this(context, display, WindowManagerGlobal.getWindowSession(),
            false /* useSfChoreographer */);
}

public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session) {
    this(context, display, session, false /* useSfChoreographer */);
}

public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session,
        boolean useSfChoreographer) {
    
    // ......
    
    // TODO(b/222696368): remove getSfInstance usage and use vsyncId for transactions
    mChoreographer = useSfChoreographer
            ? Choreographer.getSfInstance() : Choreographer.getInstance();
    // ......
}

在创建ViewRootImpl的时候,就将Choreographer实例化了,我们可以看到Choreographer是一个单例,会根据useSfChoreographer的值选择实例化什么类型的对象,因为useSfChoreographer = false,因此会通过getInstance获取。

java 复制代码
/**
 * Gets the choreographer for the calling thread.  Must be called from
 * a thread that already has a {@link android.os.Looper} associated with it.
 *
 * @return The choreographer for this thread.
 * @throws IllegalStateException if the thread does not have a looper.
 */
public static Choreographer getInstance() {
    return sThreadInstance.get();
}
java 复制代码
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是线程单例的,因为是通过ThreadLocal存储,初始化时选择的vsyncSource为VSYNC_SOURCE_APP。

2.1 Choreographer的构造方法

java 复制代码
private Choreographer(Looper looper, int vsyncSource) {
    mLooper = looper;
    mHandler = new FrameHandler(looper);
    mDisplayEventReceiver = USE_VSYNC
            ? new FrameDisplayEventReceiver(looper, vsyncSource)
            : null;
    mLastFrameTimeNanos = Long.MIN_VALUE;

    mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());

    mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
    for (int i = 0; i <= CALLBACK_LAST; i++) {
        mCallbackQueues[i] = new CallbackQueue();
    }
    // b/68769804: For low FPS experiments.
    setFPSDivisor(SystemProperties.getInt(ThreadedRenderer.DEBUG_FPS_DIVISOR, 1));
}
  • 首先创建了一个FrameHandler对象,主要用来接收消息处理事件,主要分为3类:MSG_DO_FRAME MSG_DO_SCHEDULE_VSYNC MSG_DO_SCHEDULE_CALLBACK
java 复制代码
private final class FrameHandler extends Handler {
    public FrameHandler(Looper looper) {
        super(looper);
    }

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_DO_FRAME:
                doFrame(System.nanoTime(), 0, new DisplayEventReceiver.VsyncEventData());
                break;
            case MSG_DO_SCHEDULE_VSYNC:
                doScheduleVsync();
                break;
            case MSG_DO_SCHEDULE_CALLBACK:
                doScheduleCallback(msg.arg1);
                break;
        }
    }
}
  • 通过系统的标志位USE_VSYNC,判断是否需要创建FrameDisplayEventReceiver,默认是true;那么FrameDisplayEventReceiver的作用是什么呢?主要是用来接收VSYNC事件的,当VSYNC信号来临时,通知Choreographer。

    可以将其理解为广播接收器的角色。

java 复制代码
// Enable/disable vsync for animations and drawing.
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123769497)
private static final boolean USE_VSYNC = SystemProperties.getBoolean(
        "debug.choreographer.vsync", true);
  • 创建CallbackQueue数组

2.2 Choreographer # postCallback 请求VSYNC

当初始化完成之后,ViewRootImpl中的scheduleTraversals方法中,调用了Choreographer的postCallback.

java 复制代码
public void postCallback(int callbackType, Runnable action, Object token) {
    postCallbackDelayed(callbackType, action, token, 0);
}
java 复制代码
private void postCallbackDelayedInternal(int callbackType,
        Object action, Object token, long delayMillis) {
    if (DEBUG_FRAMES) {
        Log.d(TAG, "PostCallback: type=" + callbackType
                + ", action=" + action + ", token=" + token
                + ", delayMillis=" + delayMillis);
    }

    synchronized (mLock) {
        final long now = SystemClock.uptimeMillis();
        final long dueTime = now + delayMillis;
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

        if (dueTime <= now) {
            scheduleFrameLocked(now);
        } else {
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callbackType;
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, dueTime);
        }
    }
}

最终调用到了postCallbackDelayedInternal方法中,会判断是否立即执行还是延期执行,如果立即执行,就会调用scheduleFrameLocked方法;如果是延期执行,那么最终还是会调用scheduleFrameLocked方法,mHandler就是我们介绍的在Choreographer构造方法中创建的FrameHandler。

java 复制代码
private void scheduleFrameLocked(long now) {
    if (!mFrameScheduled) {
        mFrameScheduled = true;
        if (USE_VSYNC) {
            if (DEBUG_FRAMES) {
                Log.d(TAG, "Scheduling next frame on vsync.");
            }

            // 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 {
            final long nextFrameTime = Math.max(
                    mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
            if (DEBUG_FRAMES) {
                Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
            }
            Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, nextFrameTime);
        }
    }
}

在scheduleFrameLocked方法中,因为USE_VSYNC在Android4.1之后默认开启,所以就会判断是否在主线程,如果在主线程直接执行scheduleVsyncLocked方法;如果不是,那么就通过FrameHandler post消息到主线程,最终还是执行scheduleVsyncLocked方法。

java 复制代码
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
private void scheduleVsyncLocked() {
    try {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#scheduleVsyncLocked");
        mDisplayEventReceiver.scheduleVsync();
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

在这个方法中,会调用FrameDisplayEventReceiver的scheduleVsync方法,此方法会向native层发起VSYNC信号请求任务。

java 复制代码
@UnsupportedAppUsage
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 {
        nativeScheduleVsync(mReceiverPtr);
    }
}

2.3 VSYNC信号的回调

前面我们讲到了,Choreographer调用postCallback,最终调用的是FrameDisplayEventReveiver的scheduleVsync方法,向VSYNC服务发起请求VSYNC信号刷新页面,那么VSYNC服务返回VSYNC信号是在FrameDisplayEventReveiver的onVsync方法中回调的。

java 复制代码
@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame,
        VsyncEventData vsyncEventData) {
    try {
        if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW,
                    "Choreographer#onVsync "
                            + vsyncEventData.preferredFrameTimeline().vsyncId);
        }
        // Post the vsync event to the Handler.
        // The idea is to prevent incoming vsync events from completely starving
        // the message queue.  If there are no messages in the queue with timestamps
        // earlier than the frame time, then the vsync event will be processed immediately.
        // Otherwise, messages that predate the vsync event will be handled first.
        long now = System.nanoTime();
        if (timestampNanos > now) {
            Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)
                    + " ms in the future!  Check that graphics HAL is generating vsync "
                    + "timestamps using the correct timebase.");
            timestampNanos = now;
        }

        if (mHavePendingVsync) {
            Log.w(TAG, "Already have a pending vsync event.  There should only be "
                    + "one at a time.");
        } else {
            mHavePendingVsync = true;
        }

        mTimestampNanos = timestampNanos;
        mFrame = frame;
        mLastVsyncEventData = vsyncEventData;
        Message msg = Message.obtain(mHandler, this);
        msg.setAsynchronous(true);
        mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

@Override
public void run() {
    mHavePendingVsync = false;
    doFrame(mTimestampNanos, mFrame, mLastVsyncEventData);
}

在onVsync方法中,是可以拿到VSYNC事件相关的数据信息,同时执行到run方法中,执行doFrame方法。

2.4 Choreographer # doFrame

doFrame方法执行是同步的,但是不要以为VSYNC'信号回调来的时候,doFrame就立即执行。 显然这种是最好的,能够最大限度的保证16.6ms内完成全部的渲染任务,但是如果主线程有耗时任务,那么doFrame就会被延迟,即便我们在刷新的时候设置了同步屏障,也不能解决此问题。

在doFrame方法中,有几个参数比较重要:

  • frameTimeNanos:VSYNC信号来临的时间;
  • vsyncEventData:VSYNC相关的数据,例如可以获取当前屏幕刷新率。
java 复制代码
void doFrame(long frameTimeNanos, int frame,
        DisplayEventReceiver.VsyncEventData vsyncEventData) {
    final long startNanos;
    // 获取屏幕刷新速率
    final long frameIntervalNanos = vsyncEventData.frameInterval;
    try {
        if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW,
                    "Choreographer#doFrame " + vsyncEventData.preferredFrameTimeline().vsyncId);
        }
        FrameData frameData = new FrameData(frameTimeNanos, vsyncEventData);
        synchronized (mLock) {
            if (!mFrameScheduled) {
                traceMessage("Frame not scheduled");
                return; // no work to do
            }

            if (DEBUG_JANK && mDebugPrintNextFrameTimeDelta) {
                mDebugPrintNextFrameTimeDelta = false;
                Log.d(TAG, "Frame time delta: "
                        + ((frameTimeNanos - mLastFrameTimeNanos) * 0.000001f) + " ms");
            }

            long intendedFrameTimeNanos = frameTimeNanos;
            startNanos = System.nanoTime();
            //当前时间到VSYNC信号来临时间间隔
            final long jitterNanos = startNanos - frameTimeNanos;
            
            if (jitterNanos >= frameIntervalNanos) {
                long lastFrameOffset = 0;
                if (frameIntervalNanos == 0) {
                    Log.i(TAG, "Vsync data empty due to timeout");
                } else {
                    lastFrameOffset = jitterNanos % frameIntervalNanos;
                    final long skippedFrames = jitterNanos / frameIntervalNanos;
                    if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
                        Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                                + "The application may be doing too much work on its main "
                                + "thread.");
                    }
                    if (DEBUG_JANK) {
                        Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms "
                                + "which is more than the frame interval of "
                                + (frameIntervalNanos * 0.000001f) + " ms!  "
                                + "Skipping " + skippedFrames + " frames and setting frame "
                                + "time to " + (lastFrameOffset * 0.000001f)
                                + " ms in the past.");
                    }
                }
                frameTimeNanos = startNanos - lastFrameOffset;
                frameData.updateFrameData(frameTimeNanos);
            }

            if (frameTimeNanos < mLastFrameTimeNanos) {
                if (DEBUG_JANK) {
                    Log.d(TAG, "Frame time appears to be going backwards.  May be due to a "
                            + "previously skipped frame.  Waiting for next vsync.");
                }
                traceMessage("Frame time goes backward");
                scheduleVsyncLocked();
                return;
            }

            if (mFPSDivisor > 1) {
                long timeSinceVsync = frameTimeNanos - mLastFrameTimeNanos;
                if (timeSinceVsync < (frameIntervalNanos * mFPSDivisor) && timeSinceVsync > 0) {
                    traceMessage("Frame skipped due to FPSDivisor");
                    scheduleVsyncLocked();
                    return;
                }
            }

            mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos,
                    vsyncEventData.preferredFrameTimeline().vsyncId,
                    vsyncEventData.preferredFrameTimeline().deadline, startNanos,
                    vsyncEventData.frameInterval);
            mFrameScheduled = false;
            mLastFrameTimeNanos = frameTimeNanos;
            mLastFrameIntervalNanos = frameIntervalNanos;
            mLastVsyncEventData = vsyncEventData;
        }

        AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);

        mFrameInfo.markInputHandlingStart();
        doCallbacks(Choreographer.CALLBACK_INPUT, frameData, frameIntervalNanos);

        mFrameInfo.markAnimationsStart();
        doCallbacks(Choreographer.CALLBACK_ANIMATION, frameData, frameIntervalNanos);
        doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameData,
                frameIntervalNanos);

        mFrameInfo.markPerformTraversalsStart();
        doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameData, frameIntervalNanos);

        doCallbacks(Choreographer.CALLBACK_COMMIT, frameData, frameIntervalNanos);
    } finally {
        AnimationUtils.unlockAnimationClock();
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }

    if (DEBUG_FRAMES) {
        final long endNanos = System.nanoTime();
        Log.d(TAG, "Frame " + frame + ": Finished, took "
                + (endNanos - startNanos) * 0.000001f + " ms, latency "
                + (startNanos - frameTimeNanos) * 0.000001f + " ms.");
    }
}
  • 首先会判断当前时间到VSYNC信号来临时的间隔jitterNanos,如果jitterNanos超过了屏幕刷新速率frameIntervalNanos,例如当前屏幕刷新率为60FPS,即16.67ms,那么就会打印出来丢帧的日志。

    如果丢帧的个数超过30帧(SKIPPED_FRAME_WARNING_LIMIT ),那么我们在控制台中会看到下面的日志:

java 复制代码
 Skipped 119 frames!  The application may be doing too much work on its main thread.
  • 如果没有超时,那么就正常执行doCallbacks
java 复制代码
void doCallbacks(int callbackType, FrameData frameData, long frameIntervalNanos) {
    CallbackRecord callbacks;
    long frameTimeNanos = frameData.mFrameTimeNanos;
    synchronized (mLock) {
        // We use "now" to determine when callbacks become due because it's possible
        // for earlier processing phases in a frame to post callbacks that should run
        // in a following phase, such as an input event that causes an animation to start.
        final long now = System.nanoTime();
        callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
                now / TimeUtils.NANOS_PER_MS);
        if (callbacks == null) {
            return;
        }
        mCallbacksRunning = true;

        // ......
    try {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
        for (CallbackRecord c = callbacks; c != null; c = c.next) {
            if (DEBUG_FRAMES) {
                Log.d(TAG, "RunCallback: type=" + callbackType
                        + ", action=" + c.action + ", token=" + c.token
                        + ", latencyMillis=" + (SystemClock.uptimeMillis() - c.dueTime));
            }
            c.run(frameData);
        }
    } finally {
        synchronized (mLock) {
            mCallbacksRunning = false;
            do {
                final CallbackRecord next = callbacks.next;
                recycleCallbackLocked(callbacks);
                callbacks = next;
            } while (callbacks != null);
        }
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

前面我们提到过,在初始化Choreographer时,我们创建了CallbacksQueue数组,在调用postCallback时,其实会把对应的Runnable对象封装成一个CallbackRecord放在数组中,最终会遍历这个数组并执行CallbackRecord的run方法。

java 复制代码
public void run(long frameTimeNanos) {
    if (token == FRAME_CALLBACK_TOKEN) {
        ((FrameCallback)action).doFrame(frameTimeNanos);
    } else {
        ((Runnable)action).run();
    }
}

void run(FrameData frameData) {
    if (token == VSYNC_CALLBACK_TOKEN) {
        ((VsyncCallback) action).onVsync(frameData);
    } else {
        run(frameData.getFrameTimeNanos());
    }
}

这里我们可以看到,执行run方法时还是会区分对应的action类型,也就意味着Choreographer中可能存在多种类型的Callback,对应不同的Runnable对象,其中postCallback方法,传入的就是Runnable对象。

java 复制代码
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

所以在执行Choreographer的doFrame方法时,最终会执行mTraversalRunnable的run方法,执行doTraversal方法。

2.5 ViewRootImpl # doTraversal

在执行doTraversal方法时,会移除同步屏障,并执行performTraversals方法,同时会把mTraversalScheduled这个标志位置反,意味着此时可以再次进行View的刷新。

java 复制代码
void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

至于performTraversals中干了什么事,相信伙伴们都知道吧,就是经典的measure、layout、draw三大View的绘制流程了。

3 实时监控App帧率变化

通过前面我们对于Choreographer的理解,我们看下在上层应用中,我们可以通过哪些方法来监控app的帧率变化。

这里我们根据系统源码的这种方式,来计算从发起下一次VSYNC到下一次VSYNC来临时,丢帧的次数。

Kotlin 复制代码
class FrameMonitorCallback(
    @get:JvmName("callVsyncTime")
    var callVsyncTime: Long
) : Choreographer.FrameCallback {

    /** 默认设置为60FPS,16.67ms刷新一次 */
    private val mFrameInterval = 1000000000 / 60f

    /**
     * VSYNC信号来临时,执行
     * @param frameTimeNanos 接收到VSYNC信号时的时间
     */
    override fun doFrame(frameTimeNanos: Long) {
        Log.d("FrameMonitor", "doFrame: $frameTimeNanos")
        if (callVsyncTime == 0L) {
            callVsyncTime = System.nanoTime()
        }
        //计算两者的时间偏差
        val jitter = frameTimeNanos - callVsyncTime
        Log.d("FrameMonitor", "jitter: $jitter")
        if (jitter >= mFrameInterval) {
            //计算丢帧次数
            val count = jitter / mFrameInterval
            Log.d("FrameMonitor", "doFrame: lost frame $count")
        }
//        callVsyncTime = frameTimeNanos
        //继续注册下一次vsync
//        Choreographer.getInstance().postFrameCallback(this)
    }
}

这里我们默认使用手机的屏幕刷新速率60FPS,模拟一次点击事件,首先发起VSYNC请求,因为一些任务导致主线程干了很多事,此时同步屏障是不能解决问题的,2s后刷新了UI。

Kotlin 复制代码
binding.btnLogin.setOnClickListener {
    Choreographer.getInstance().postFrameCallback(FrameMonitorCallback(System.nanoTime()))
    Thread.sleep(2000)
    binding.tv01.text = "好了"
}

从下面的日志看,与系统打印的丢帧次数是一致的,而且我们这次请求VSYNC就已经结束了,如果界面保持不变,那么doFrame就不会被执行,也就是意味着measure/layout/draw不会被执行,但是底层依然还是会保持16.6ms一次的刷新速率。

java 复制代码
2023-11-11 16:48:30.851  3977-3977  Choreographer           com.example.myapplication            I  Skipped 119 frames!  The application may be doing too much work on its main thread.
2023-11-11 16:48:30.851  3977-3977  FrameMonitor            com.example.myapplication            D  doFrame: 96936387090327
2023-11-11 16:48:30.851  3977-3977  FrameMonitor            com.example.myapplication            D  jitter: 1997299320
2023-11-11 16:48:30.851  3977-3977  FrameMonitor            com.example.myapplication            D  doFrame: lost frame 119.83796

只有当下一次页面开始刷新的时候,Choreographer会再次发起VSYNC请求,执行doFrame,所以我们要完成app的卡顿监控,需要密切观察屏幕的变化,在合适的时机注册VSYNC请求。

总结

当我们想要在TextView上显示文案,或者在ListView中显示列表数据时,都会涉及到View的刷新,那么整体的流程就是:

  • 当View需要刷新的时候,调用invalidate最终会走到ViewRootImpl的scheduleTraversals方法中,此时设置同步屏障,通过Choreographer向VSYNC服务请求VSYNC刷新;
  • 当VSYNC信号返回时,会执行Choreographer的doFrame方法,计算丢帧以及执行所有的CallbackRecord,也就是在调用postCallback或者postFrameCallback时设置的Runnable对象或者FrameCallback,执行其run方法或者doFrame方法;
  • 当执行postCallback中的Runnable对象时,最终调用了ViewRootImpl的doTraversals方法,此时会移除同步屏障,取反标志位,并执行performTraversals方法绘制。
相关推荐
JasonYin~1 小时前
HarmonyOS NEXT 实战之元服务:静态案例效果---手机查看电量
android·华为·harmonyos
zhangphil1 小时前
Android adb查看某个进程的总线程数
android·adb
抛空1 小时前
Android14 - SystemServer进程的启动与工作流程分析
android
Gerry_Liang3 小时前
记一次 Android 高内存排查
android·性能优化·内存泄露·mat
天天打码5 小时前
ThinkPHP项目如何关闭runtime下Log日志文件记录
android·java·javascript
爱数学的程序猿7 小时前
Python入门:6.深入解析Python中的序列
android·服务器·python
brhhh_sehe8 小时前
重生之我在异世界学编程之C语言:深入文件操作篇(下)
android·c语言·网络
zhangphil8 小时前
Android基于Path的addRoundRect,Canvas剪切clipPath简洁的圆形图实现,Kotlin(2)
android·kotlin
Calvin8808288 小时前
Android Studio 的革命性更新:Project Quartz 和 Gemini,开启 AI 开发新时代!
android·人工智能·android studio
敲代码敲到头发茂密9 小时前
【大语言模型】LangChain 核心模块介绍(Memorys)
android·语言模型·langchain