面试官:你开发的app卡顿过吗?这样回答让面试官很满意

本文分析了Android应用卡顿的原因,从屏幕刷新机制入手,详细讲解了从View#requestLayout到Choreographer的执行流程,揭示了主线程耗时操作如何导致掉帧。此外,还介绍了如何监控应用卡顿,如基于消息队列的BlockCanary原理,以及插桩和系统工具如TraceView、Systrace的使用。文章适合有一定经验的Android开发者阅读,以提升对卡顿优化的理解。
摘要由CSDN通过智能技术生成

故事开始

面试官:平时开发中有遇到卡顿问题吗?你一般是如何处理的?
来面试的小伙:额…没有遇到过卡顿问题,我平时写的代码质量比较高,不会出现卡顿。
面试官:…

上面对话像是开玩笑,但是前段时间真的遇到一个来面试的小伙这样答,问他有没有遇到过卡顿问题,一般怎么处理的?他说没遇到过,说他写的代码不会出现卡顿。这回答似乎没啥问题,但是我会认为你在卡顿优化这一块是0经验。

卡顿这个话题,相信大部分两年或以上工作经验的同学都应该能说出个大概。 一般的回答可能类似这样:

卡顿是由于主线程有耗时操作,导致View绘制掉帧,屏幕每16毫秒会刷新一次,也就是每秒会刷新60次,人眼能感觉到卡顿的帧率是每秒24帧。所以解决卡顿的办法就是:耗时操作放到子线程、View的层级不能太多、要合理使用include、ViewStub标签等等这些,来保证每秒画24帧以上。

如果稍微问深一点, 卡顿的底层原理是什么?如何理解16毫秒刷新一次?假如界面没有更新操作,View会每16毫秒draw一次吗?

这个问题相信会难倒一片人,包括大部分3年以上经验的同学,如果没有去阅读源码,未必能答好这个问题。当然,我希望你刚好是小部分人~

接下来将从源码角度分析屏幕刷新机制,深入理解卡顿原理,以及介绍卡顿监控的几种方式,希望对你有帮助。


一、屏幕刷新机制

View#requestLayout 开始分析,因为这个方法是主动请求UI更新,从这里分析完全没问题。

1. View#requestLayout

    protected ViewParent mParent;
	...
    public void requestLayout() {
	...

    if (mParent != null && !mParent.isLayoutRequested()) {
        mParent.requestLayout(); //1
    }
    }

主要看注释1,这里的 mParent.requestLayout(),最终会调用 ViewRootImplrequestLayout 方法。你可能会问,为什么是ViewRootImpl?因为根View是DecorView,而DecorView的parent就是ViewRootImpl,具体看ViewRootImplsetView方法里调用view.assignParent(this);,可以暂且先认为就是这样的,之后整理View的绘制流程的时候会详细分析。

2. ViewRootImpl#requestLayout

    public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
		//1 检测线程
        checkThread();
        mLayoutRequested = true;
		//2 
        scheduleTraversals();
    }
}

注释1 是检测当前是不是在主线程

2.1 ViewRootImpl#checkThread
    void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

这个异常很熟悉吧,我们平时说的子线程不能更新UI,会抛异常,就是在这里判断的,ViewRootImpl#checkThread

接着看注释2

2.2 ViewRootImpl#scheduleTraversals
    void scheduleTraversals() {
	//1、注意这个标志位,多次调用 requestLayout,要这个标志位false才有效
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
		// 2\. 同步屏障
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
		// 3\. 向 Choreographer 提交一个任务
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
		//绘制前发一个通知
        notifyRendererOfFramePending();
		//这个是释放锁,先不管
        pokeDrawLockIfNeeded();
    }
}

主要看注释的3点:

注释1:防止短时间多次调用 requestLayout 重复绘制多次,假如调用requestLayout 之后还没有到这一帧绘制完成,再次调用是没什么意义的。

注释2: 涉及到Handler的一个知识点,同步屏障: 往消息队列插入一个同步屏障消息,这时候消息队列中的同步消息不会被处理,而是优先处理异步消息。这里很好理解,UI相关的操作优先级最高,比如消息队列有很多没处理完的任务,这时候启动一个Activity,当然要优先处理Activity启动,然后再去处理其他的消息,同步屏障的设计堪称一绝吧。 同步屏障的处理代码在MessageQueuenext方法:

Message next() {
...
        for (;;) {
           ...
          synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) { //如果msg不为空并且target为空
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
      ...
}

逻辑就是:如果msg不为空并且target为空,说明是一个同步屏障消息,进入do while循环,遍历链表,直到找到异步消息msg.isAsynchronous()才跳出循环交给Handler去处理这个异步消息。

回到上面的注释3:mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);,往Choreographer 提交一个任务 mTraversalRunnable,这个任务不会马上就执行,接着看~

3. Choreographer

看下 mChoreographer.postCallback

3.1 Choreographer#postCallback
public void postCallback(int callbackType, Runnable action, Object token) {
    postCallbackDelayed(callbackType, action, token, 0);
}

public void postCallbackDelayed(int callbackType,
        Runnable action, Object token, long delayMillis) {
    if (action == null) {
        throw new IllegalArgumentException("action must not be null");
    }
    if (callbackType < 0 || callbackType > CALLBACK_LAST) {
        throw new IllegalArgumentException("callbackType is invalid");
    }

    postCallbackDelayedInternal(callbackType, action, token, delayMillis);
}

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;
		//1.将任务添加到队列
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

		//2\. 正常延时是0,走这里
        if (dueTime <= now) {
            scheduleFrameLocked(now);
        } else {
			//3\. 什么时候会有延时,绘制超时,等下一个vsync?
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callba
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值