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方法绘制。
相关推荐
程序边界22 分钟前
MongoDB迁移到KES实战全纪录(下):性能优化与实践总结
数据库·mongodb·性能优化
武子康23 分钟前
Java-160 MongoDB副本集部署实战 单机三实例/多机同法 10 分钟起集群 + 选举/读写/回滚全流程
java·数据库·sql·mongodb·性能优化·系统架构·nosql
GISer_Jing3 小时前
不定高虚拟列表性能优化全解析
前端·javascript·性能优化
消失的旧时光-19433 小时前
Flutter 响应式 + Clean Architecture / MVU 模式 实战指南
android·flutter·架构
你听得到114 小时前
卷不动了?我写了一个 Flutter 全链路监控 SDK,从卡顿、崩溃到性能,一次性搞定!
前端·flutter·性能优化
404未精通的狗4 小时前
(数据结构)栈和队列
android·数据结构
恋猫de小郭4 小时前
今年各大厂都在跟进的智能眼镜是什么?为什么它突然就成为热点之一?它是否是机会?
android·前端·人工智能
l1t4 小时前
利用DeepSeek改写递归CTE SQL语句为Python程序及优化
数据库·人工智能·python·sql·算法·性能优化·deepseek
游戏开发爱好者86 小时前
iOS 混淆工具链实战 多工具组合完成 IPA 混淆与加固 无源码混淆
android·ios·小程序·https·uni-app·iphone·webview
豆豆豆大王11 小时前
Android 数据持久化(SharedPreferences)
android