【再出发】Android11源码分析: UI到底为什么会卡顿?

3,021 阅读8分钟

系列文章索引

并发系列:线程锁事

  1. 篇一:为什么CountDownlatch能保证执行顺序?

  2. 篇二:并发容器为什么能实现高效并发?

  3. 篇三:从ReentrientLock看锁的正确使用姿势

新系列:Android11系统源码解析

  1. Android11源码分析:Mac环境如何下载Android源码?

  2. Android11源码分析:应用是如何启动的?

  3. Android11源码分析:Activity是怎么启动的?

  4. Android11源码分析:Service启动流程分析

  5. Android11源码分析:静态广播是如何收到通知的?

  6. Android11源码分析:binder是如何实现跨进程的?(创作中)

  7. 番外篇 - 插件化探索:插件Activity是如何启动的?

  8. Android11源码分析: UI到底为什么会卡顿?

  9. Android11源码分析:SurfaceFlinger是如何对vsync信号进行分发的?(创作中)

经典系列:Android10系统启动流程

  1. 源码下载及编译

  2. Android系统启动流程纵览

  3. init进程源码解析

  4. zygote进程源码解析

  5. systemServer源码解析

前言

前几年,Android经常被吐槽不如IOS系统流畅,给人的感觉就是一卡一卡的

直到近一两年Android高刷屏开始普及,才从体验上真正有了一个质的提升

针对UI刷新的优化,系统层也一直在做相关的工作

今天我们从应用层的是使用出发,去深挖下源码,看看UI卡顿的根源到底是什么

下面,正文开始!

注: 本文源码基于android11.0.0-r2,保证逻辑清晰,部分源码有删减

UI是怎么刷新的?从requestLayout()说起

在应用层想要刷新UI,触发重绘,只需要调用requestLayout()函数即可

最终会调用的ViewRootImprequestLayout()函数,执行scheduleTraversals()方法

这里就涉及到一个问题,假如短时间重复的调用requestLayout(),会使屏幕不断刷新吗?

在这个函数里,其实做了过滤处理,设置mTraversalScheduled标识是否执行了Traversal的流程,如果没有执行完成,则一直是ture的状态,直到接收到VSYNC信号,对当前的Traversal流程执行完毕,才会将其设置为false

也就是说,在一个VSYNC信号的周期范围内,只会触发一次重绘的流程

具体代码如下

/frameworks/base/core/java/android/view/ViewRootImpl.java

void scheduleTraversals() {
        if (!mTraversalScheduled) { //判断是否执行scheduleTraversals内的逻辑,确保在一个vsync时钟周期内只出发一次scheduleTraversals
            mTraversalScheduled = true; //如果执行过则置为ture,避免多次调用requestlayout时重复出发的情况
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();//发送屏障消息(对异步消息没有影响)
            mChoreographer.postCallback( //post一个异步消息,等vsyn信号来的时候优先进行处理
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);//mTraversalRunnable会在下一个vysc信号来的时候执行
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

在这个函数中,调用了ChoregrapherpostCallback()函数注册VSYNC信号的回调消息,其中真正处理VSNC事件的是mTraversalRunnable,其中经过doTraversal()--> performTraversals()的函数调用对view进行重新绘制

此处我们先暂时忽略绘制的细节,继续跟进postCallback()函数看看他是怎么注册Vsync信号回调的

Choregrapher中,最终调用到了postCallbackDelayedInternal()

其中使用维护了一个集合mCallbackQueues用来存储不同类型(callbackType)的CallbackQueue

其中CallbackQueue使用`单向链表存储,并按照需要执行的先后顺序进行排列

如果callback的执行时间戳小于当前时间,则调用scheduleFrameLocked()去请求vsync信号触发绘制

如果大于当前时间则通过handler发送异步延迟消息稍后处理

具体代码如下

/frameworks/base/core/java/android/view/Choreographer.java

private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) {
        //...
        synchronized (mLock) {
            final long now = SystemClock.uptimeMillis();
            final long dueTime = now + delayMillis;
			//通过callbackType获取到对应的mCallbackQueues,再根据dueTime插入到合适的位置,按时间顺序进行排序
            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

            if (dueTime <= now) {
                scheduleFrameLocked(now);//最终执行到DisplayEventReceiver的requestNextVsync函数,注册vsync信号
            } else {
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, dueTime);
            }
        }
    }

我们先跟踪需要立即执行的分支,看看它是如何请求vsync信号的

这里需要先提一下Choregrapher的创建,是通过getInstance()函数获取的,但其中并不是单利的实现,而是用ThreadLocal存储的线程本地变量

在初始化时,会调用Looper.myLooper()对Looper进行初始化,如果获取到的Looper对象是主线程的Looper,则将其Choregrapher实例保存在mMainInstance

也就是说,不同的线程,创建的Choregraher对象也不一样

Choregrapher的初始化代码如下

// Thread local storage for the choreographer.  choregrapher为线程本地变量
    private static final ThreadLocal<Choreographer> sThreadInstance =
            new ThreadLocal<Choreographer>() {
        @Override
        protected Choreographer initialValue() {
            Looper looper = Looper.myLooper(); //创建当前线程的Looper对象
            //...
            Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);//给应用层处理的VSYNC信号
            if (looper == Looper.getMainLooper()) { //如果looper为主线程looper,则将choreographer设置mMainInstance
                mMainInstance = choreographer;
            }
            return choreographer;
        }
    };

再回到vysnc信号的注册流程中来,这里会判断Chorographer所在的线程是否是它的工作线程(根据是否是同一个Looper判断,注意是工作线程,不是主线程),如果是在异步线程中,则需要通过hander切换到工作线程去执行

代码如下

/frameworks/base/core/java/android/view/Choreographer.java

private void scheduleFrameLocked(long now) {
        if (!mFrameScheduled) {
            mFrameScheduled = true;
            if (USE_VSYNC) {
                if (isRunningOnLooperThreadLocked()) {//如果当前线程是Chorographer的工作线程(根据是否是同一个线程的Looper来判断)
                    scheduleVsyncLocked(); 
                } else {
                    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC); //发送消息到主线程执行
                    msg.setAsynchronous(true); //发送异步消息
                    mHandler.sendMessageAtFrontOfQueue(msg);
                }
            }
            //略...
        }
    }

我们这里暂时关注在工作线程的情况,这里会调用到scheduleVsyncLocked()函数,交给mDisplayEventReceiverscheduleVsync()函数

此处的mDisplayEventReceiver是在Choreographer构造初始化的时候创建的,对应的类FrameDisplayEventReceiverDisplayEventReceiver的子类对象

其初始化过程又调用了nativeInit()创建出对应的native层的DisplayEventReceiver对象,真正的函数调用是在这个类中实现的,scheduleVsync()函数也是交给native层去实现的,

其中会调用native层中会调用requestNextVsync()函数,请求下一个vsync信号,这里又涉及到另一个数据结构,DisplayEventConnection

这里面用到的mEventConnection是在DisplayEventReceiver的构造中通过SurfaceFlinger的binderProxy对象调用到createDisplayEventConnection()创建的

SurfaceFlinger中,又调用到了SchedulercreateDisplayEventConnection(),最终的创建交给了EventThread来执行,代码如下

/frameworks/native/services/surfaceflinger/Scheduler/EventThread.cpp

sp<EventThreadConnection> EventThread::createEventConnection( //创建Connection对象(其实是一个bpbinder)
        ResyncCallback resyncCallback, ISurfaceComposer::ConfigChanged configChanged) const {
    return new EventThreadConnection(const_cast<EventThread*>(this), std::move(resyncCallback),
                                     configChanged);
}

Connection对应到的数据结构是EventThreadConnection,这个connection其实是一个binder的实体对象,用来跨进程传输的(在系统层和应用层之间传输), EventThread会将connection维护在一个mDisplayEventConnections集合中

EventThread是在SurfaceFlinger中创建的,创建后会在在其工作线程(threadMain)循环获取mDisplayEventConnections中需要处理的connection回调,并调用dispatchEvent()函数向DisplayEventReceiver分发vsync事件,这里通过调用postEvent()-->sendEvents()进行事件的分发,此处使用BitTue进行写操作,持有receiveFd的一端即可收到消息进行读操作

/frameworks/native/libs/gui/DisplayEventReceiver.cpp

ssize_t DisplayEventReceiver::sendEvents(gui::BitTube* dataChannel,
        Event const* events, size_t count)
{   //此处使用BitTube通信,类似于管道,一个读描述符,一个写描述符,此处通过bitTube写入event数据,读的一端即可收到event数据
    return gui::BitTube::sendObjects(dataChannel, events, count);
}

此时,receiver便能收到注册的vsync信号的回调,执行到应用端FrameDisplayEventReceiveronVsync()函数进行绘制了

总结

前面分析了vsync信号的分发和接收的原理,在一个sync时钟周期内,只会触发一次重绘操作

整体上看,vsync的事件分发涉及应用端(Client)和服务端(SurfaceFlinger)两个进程的交互,SurfaceFlinger接收到硬件的vsync信号后,通过应用端注册的eventConnection,将其分发给应用端,处理具体的绘制流程

在应用端(Client),又涉及到java层和native层的通信,这里主要是在DisplayEventReceiver的实现中有涉及,通过native层调用去请求sync信号

在服务端(SurfaceFlinger)会不断进行loop循环取到应该执行的回调函数进行vsync信号的分发,最终分发给客户端

UI为什么会卡顿?

分析完屏幕的重绘流程,我们再回过头来看,UI卡顿的原因,其实是因为在一个vsync时钟周期内没能完成绘制导致的

在开发过程中,一般是由于在主线程执行了大量的block操作导致的;另外也可能由于频繁创建内存导致频繁GC而导致主线程无法执行绘制的卡顿情况

在系统层,如果某一帧执行的绘制执行时间过长,就会产生跳帧的现象,如果绘制时间超过了30帧*16.6mills,系统会打印日志提示可能在主线程执行了过多的耗时操作

在activity中执行耗时操作超过5s会触发ANR(在ActivityThread中进行处理,弹出Dialog框进行提示)

如何解决UI卡顿?

以上说了卡顿在应用层面的两点原因

  1. 主线程block导致卡顿

  2. 频繁GC导致卡顿

针对到具体的优化业务中时,首先要分析是哪里产生了block导致卡顿。

比如四大组件的各个生命周期都是在主线程执行的,就要注意不要在主线程执行耗时操作,而是放到异步线程中执行

针对频繁GC导致的卡顿,一般是重复调用某个占用内存大的函数导致的,比如view的ondraw方法中不要频繁创建大内存的对象

如何监测UI卡顿?

针对卡顿监测,大概有以下几种思路可以做

  1. 通过替换主线程Looper的日志打印,计算事件的完成事件,如果超过了16mills,说明发生了卡顿问题

  2. 通过ChoregraphergetFps()获取到当前的帧率

  3. 要测量某个函数在主线程中耗时的时常,可以使用AOP切面进行无侵入的监测,计算各个函数的耗时

  4. 在发生ANR后,可以通过traces.text日志进行分析

写在最后

今天针对屏幕刷新机制进行了详细的源码探究,针对设备的不同,block的节点可能也会不同

在发生卡顿问题时,要分析性能的瓶颈点是在什么地方,先针对大量耗时的地方进行优化,再去针对细致的地方进行分析

我是释然,我们下期文章再见!

如果本文对你有所启发,请多多点赞支持!