Android高工面试:APP画面卡顿的根本原因是什么?卡顿优化你是怎么做的

本文详细剖析了Android中的关键绘制函数scheduleTraversals(),展示了Vsync信号如何驱动界面刷新,以及Choreographer、TraversalRunnable和SurfaceFlinger在UI渲染过程中的作用。同时,文章强调了性能优化的重要性,如视图树复杂度、requestLayout()的使用和UI线程的阻塞等问题。
摘要由CSDN通过智能技术生成

@Override
public void doFrame(long frameTimeNanos) {

scheduleTraversals();
//请求一个Vsync信号,后面还会提到这个方法
mChoreographer.postFrameCallback(mRenderProfiler);
//每次回调时,重新将FrameCallback post到Choreographer中

}
};

mChoreographer.postFrameCallback(mRenderProfiler);
//将FrameCallback post到Choreographer中

}

上面代码出现了一个重要方法scheduleTraversals()。下面我们看看它究竟为何重要。
ViewRootImpl.java

void scheduleTraversals() {

mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
//向Choreographer中post一个TraversalRunnable
//这又是一个十分重要的对象

}

可以看出scheduleTraversals()每次调用时会向Choreographer中post一个TraversalRunnable,它会促使Choreographer去请求一个Vsync信号。所以这个方法的作用就是用来请求一次Vsync信号刷新界面的。事实上,你可以看到,在**invalidate()、requestLayout()**等操作中,都能够看到它被调用。原因就是这些操作需要刷新界面,所以需要请求一个Vsync信号来出发新界面的绘制。

ViewRootImpl.java

final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
//开始遍历视图树,这意味着开始绘制一帧内容了
}
}

从图中可以看到,每当doTraversal()被调用时,一系列的测量、布局和绘制操作就开始了。在绘制时,会通过Surface来获取一个Canvas内存块交给DecorView,用于视图的绘制。整个View视图的内容都是被绘制到这个Canvas中。

Choreographer中的风起云涌

前面反复提到向Choreographer中post回调,那么post过去发生了些什么呢?从图中可以看到,所有的post操作最终都进入到postCallbackDelayedInternal()中。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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;
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
//将Callback添加到CallbackQueue[]中
if (dueTime <= now) {
scheduleFrameLocked(now);
//如果回调时间到了,请求一个Vsync信号
//在接收到后会调用doFrame()回调这个Callback。
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
//异步消息,避免被拦截器拦截
mHandler.sendMessageAtTime(msg, dueTime);
//如果还没到回调的时间,向FrameHandelr中发送
//MSG_DO_SCHEDULE_CALLBACK消息
}
}

}

上面这段代码会把post到Choreographer中的Callback添加到Callback[]中,并且当它因该被回调时,请求一个Vsync信号,在接收到下一个Vsync信号时回调这个Callback。如果没有到回调的时间,则向FrameHandler中发送一个MSG_DO_SCHEDULE_CALLBACK消息,但最终还是会请求一个Vsync信号,然后回调这个Callback。

**简单提一下CallbackQueue:**简单说一下CallbackQueue。它和MessageQueue差不多,都是单链表结构。在我的这篇【惊天秘密!从Thread开始,揭露Android线程通讯的诡计和主线程的阴谋http://www.jianshu.com/p/8862bd2b6a29】文章中,你能够看到更多关于MessageQueue和Handler机制的内容。不同的是它同时还是一个一维数组,下标表示Callback类型。事实上,算上每种类型的单链表结构,它更像是二维数组的样子。简单点描述,假设有一个MessageQueue[]数组,里面存了几个MessageQueue。来看看它的创建你可能就明白,它是在Choreographer初始化时创建的。

private Choreographer(Looper looper) {
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
//CALLBACK_LAST值为3。
for (int i = 0; i <= CALLBACK_LAST; i++) {
mCallbackQueues[i] = new CallbackQueue();
}
}

现在来看看前面代码中调用的scheduleFrameLocked()是如何请求一个Vsync信号的。

private void scheduleFrameLocked(long now) {

//先判断当前是不是在UI线程
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
//是UI线程就请求一个Vsync信号
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
//不在UI线程向FrameHandler发送一个MSG_DO_SCHEDULE_VSYNC消息
//来请求一个Vsync信号
}
}

private void scheduleVsyncLocked() {
mDisplayEventReceiver.scheduleVsync();
//通过DisplayEventReceiver请求一个Vsync信号
//这是个恨角色,待会儿会聊聊它。
//MSG_DO_SCHEDULE_VSYNC消息也是通过调用这个方法请求Vsync信号的。
}

上面我们提到过,Choreographer在一个线程中只有一个。所以,如果在其它线程,需要通过Handler来切换到UI线程,然后再请求Vsync信号。

下面看看刚刚出场的mDisplayEventReceiver是个什么鬼?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {

//这个方法用于接收Vsync信号
public void onVsync(){

Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
//这里并没有设置消息的类型
//其实就是默认为0,即MSG_DO_FRAME类型的消息
//它其实就是通知Choreographer开始回调CallbackQueue[]中的Callback了
//也就是开始绘制下一帧的内容了
}

//这个方法是在父类中的,写在这方便看
public void scheduleVsync() {

nativeScheduleVsync(mReceiverPtr);
//请求一个Vsync信号
}
}

这给类功能比较明确,而且很重要!

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

上面一直在说向FrameHandler中发消息,搞得神神秘秘的。接下来就来看看FrameHandler本尊。请在图中找到对应位置哦。

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:
//开始回调Callback,以开始绘制下一帧内容
doFrame(System.nanoTime(), 0);
break;
case MSG_DO_SCHEDULE_VSYNC:
//请求一个Vsync信号
doScheduleVsync();
break;
case MSG_DO_SCHEDULE_CALLBACK:
//实际也是请求一个Vsync信号
doScheduleCallback(msg.arg1);
break;
}
}
}

FrameHandler主要在UI线程处理3种类型的消息。

  • MSG_DO_FRAME:值为0。当接收到一个Vsync信号时会发送该种类型的消息,然后开始回调CallbackQueue[]中的Callback。比如上面说过,在ViewRootImpl有两个重要的Callback,FrameCallback(请求Vsync并再次注册回调)和TraversalRunnable(执行doTraversal()开始绘制界面)频繁被注册。
  • MSG_DO_SCHEDULE_VSYNC:值为1。当需要请求一个Vsync消息(即屏幕上的内容需要更新时)会发送这个消息。接收到Vsync后,同上一步。
  • MSG_DO_SCHEDULE_CALLBACK:值为2。请求回调一个Callback。实际上会先请求一个Vsync信号,然后再发送MSG_DO_FRAME消息,然后再回调。

FrameHandler并不复杂,但在UI的绘制过程中具有重要的作用,所以一定要结合图梳理下这个流程。

SurfaceFlinger和Surface简单说

在介绍Vsync的时候,我们可能已经看到了,现在Android系统会将HW_VSYNC虚拟化为两个Vsync信号。一个是VSYNC,被发送给上面一直在讲的Choreographer,用于触发视图树的绘制渲染。另一个是SF_VSYNC,被发送给我接下来要讲的SurfaceFlinger,用于触发Surface的合成,即各个Window窗口画面的合成。接下来我们就简单的看下SurfaceFlinger和Surface。由于这部分基本是c++编写的,我着重讲原理。

隐藏在背后的Surface

平时同学们都知道,我们的视图需要被绘制。那么它们被绘制到那了呢?也许很多童鞋脑海里立即浮现出一个词:Canvas。但是,~没错!就是绘制到了Canvas上。那么Canvas又是怎么来的呢?是的,它可以New出来的。但是前面提到过,我们Window中的视图树都是被绘制到一个由Surface提供的Canvas上。忘了的童鞋面壁思过😄。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Canvas实际代表了一块内存,用于储存绘制出来的数据。在Canvas的构造器中你可以看到:

public Canvas() {

mNativeCanvasWrapper = initRaster(null);
//申请一块内存,并且返回该内存的一个long类型的标记或者索引。

}

可以看到,Canvas实际主要就是持有了一块用于绘制的内存块的索引long mNativeCanvasWrapper。每次绘制时就通过这个索引找到对应的内存块,然后将数据绘制到内存中。比如:

public void drawRect(@NonNull RectF rect, @NonNull Paint paint) {
native_drawRect(mNativeCanvasWrapper,
rect.left, rect.top, rect.right, rect.bottom, paint.getNativeInstance());
//在mNativeCanvasWrapper标记的内存中绘制一个矩形。
}

简单的说一下。Android绘制图形是通过图形库Skia(主要针对2D)或OpenGL(主要针对3D)进行。图形库是个什么概念?就好比你在PC上用画板画图,此时画板就相当于Android中的图形库,它提供了一系列标准化的工具供我们画图使用。比如我们drawRect()实际就是操作图形库在内存上写入了一个矩形的数据。

扯多了,我们继续回到Surface上。当ViewRootImpl执行到draw()方法(即开始绘制图形数据了),会根据是否开启了硬件(从Android 4.0开始默认是开启的)加速来决定是使用CPU软绘制还是使用GPU硬绘制。如果使用软绘制,图形数据会绘制在Surface默认的CompatibleCanvas上(和普通Canvas的唯一区别就是对Matrix进行了处理,提高在不同设备上的兼容性)。如果使用了硬绘制,图形数据会被绘制在DisplayListCanvas上。DisplayListCanvas会通过GPU使用openGL图形库进行绘制,因此具有更高的效率。

前面也简单说了一下,每一个Window都会有一个自己的Surface,也就是说一个应用程序中会存在多个Surface。通过上面的讲解,童鞋们也都知道了Surface的作用就是管理用于绘制视图树的Canvas的。这个Surface是和SurfaceFlinger共享,从它实现了Parcelable接口也可以才想到它会被序列化传递。事实上,Surface中的绘制数据是通过匿名共享内存的方式和SurfaceFlinger共享的,这样SurfaceFlinger可以根据不同的Surface,找到它所对应的内存区域中的绘制数据,然后进行合成。

合成师SurfaceFlinger

SurfaceFlinger是系统的一个服务。前面也一直在提到它专门负责把每个Surface中的内容合成缓存,以待显示到屏幕上。SurfaceFlinger在合成Surface时是根据Surface的Z-order顺序一层一层进行。比如一个Dialog的Surface就会在Activity的Surface上面。然后这个东西不多提了。

终于可以说说你的App为什么这么卡的原因了

通过对Android绘制机制的了解,我们知道造成应用卡顿的根源就在于16ms内不能完成绘制渲染合成过程,因为Android平台的硬件刷新率为60HZ,大概就是16ms刷新一次。如果没能在16ms内完成这个过程,就会使屏幕重复显示上一帧的内容,即造成了卡顿。在这16ms内,需要完成视图树的所有测量、布局、绘制渲染及合成。而我们的优化工作主要就是针对这个过程的。

复杂的视图树

如果视图树复杂,会使整个Traversal过程变长。因此,我们在开发过程中要控制视图树的复杂程度。减少不必要的层级嵌套。比如使用RelativeLayout可以减少复杂布局的嵌套。比如使用【震惊!这个控件绝对值得收藏。轻松实现圆角、文字描边、状态指示等效果http://www.jianshu.com/p/cfe18cbc6924】😄,这个控件可以减少既需要显示文字,又需要图片和特殊背景的需求的布局复杂程度,所有的东西由一个控件实现。

频繁的requestlayout()

如果频繁的触发requestLayout(),就可能会导致在一帧的周期内,频繁的发生布局计算,这也会导致整个Traversal过程变长。有的ViewGroup类型的控件,比如RelativeLayout,在一帧的周期内会通过两次layout()操作来计算确认子View的位置,这种少量的操作并不会引起能够被注意到的性能问题。但是如果在一帧的周期内频繁的发生layout()计算,就会导致严重的性能,每次计算都是要消耗时间的!而requestLayout()操作,会向ViewRootImpl中一个名为mLayoutRequesters的List集合里添加需要重新Layout的View,这些View将在下一帧中全部重新layout()一遍。通常在一个控件加载之后,如果没什么变化的话,它不会在每次的刷新中都重新layout()一次,因为这是一个费时的计算过程。所以,如果每一帧都有许多View需要进行layout()操作,可想而知你的界面将会卡到爆!卡到爆!需要注意,setLayoutParams()最终也会调用requestLayout(),所以也不能烂用!同学们在写代码的过程中一定要谨慎注意那些可能引起requestLayout()的地方啊!

UI线程被阻塞

如果UI线程受到阻塞,显而易见的是,我们的Traversal过程也将受阻塞!画面卡顿是妥妥的发生啊。这就是为什么大家一直在强调不要在UI线程做耗时操作的原因。通常UI线程的阻塞和以下原因脱不了关系。

  • 在UI线程中进行IO读写数据的操作。这是一个很费时的过程好吗?千万别这么干。如果不想获得一个卡到爆的App的话,把IO操作统统放到子线程中去。
  • 在UI线程中进行复杂的运算操作。运算本身是一个耗时的操作,当然简单的运算几乎瞬间完成,所以不会让你感受到它在耗时。但是对于十分复杂的运算,对时间的消耗是十分辣眼睛的!如果不想获得一个卡到爆的App的话,把复杂的运算操作放到子线程中去。
  • 在UI线程中进行复杂的数据处理。我说的是比如数据的加密、解密、编码等等。这些操作都需要进行复杂运算,特别是在数据比较复杂的时候。如果不想获得一个卡到爆的App的话,把复杂数据的处理工作放到子线程中去。
  • 频繁的发生GC,导致UI线程被频繁中断。在Java中,发生GC(垃圾回收)意味着Stop-The-World,就是说其它线程全部会被暂停啊。好可怕!正常的GC导致偶然的画面卡顿是可以接受的,但是频繁发生就让人很蛋疼了!频繁GC的罪魁祸首是内存抖动,这个时候就需要看下我的这篇【Android内存基础——内存抖动http://www.jianshu.com/p/69e6f894c698】文章了。简单的说就是在短时间内频繁的创建大量对象,导致达到GC的阀值,然后GC就发生了。如果不想获得一个卡到爆的App的话,把内存的管理做好,即使这是Java。
  • 故意阻塞UI线程。好吧,相信没人会这么干吧。比如sleep()一下?

总结

  • 抽出空余时间写文章分享需要动力,还请各位看官动动小手点个赞,鼓励下喽😄
  • 我一直在不定期的创作新的干货,想要上车只需进到我的个人主页点个关注就好了哦。发车喽~

整篇下来,相信童鞋对Android的绘制机制也有了一个比较全面的了解。现在回过头来再写代码时是不是有种知根知底的自信呢?😄

扩展阅读

一线大厂资深APP性能优化系列(一)卡顿定位
一线大厂资深APP性能优化系列(二)异步优化与拓扑排序
一线大厂大型APP性能优化系列(三)自定义启动器
一线大厂大型APP性能优化系列(四)更优雅的延迟方案
究极深入Android网络优化——网络筑基(一)
究极深入Android网络优化——网络筑基(二)
网络优化最佳实践!百度App网络深度优化系列《一》DNS优化
Android Framework学习笔记(一)Android 系统架构
Android 高级 UI 进阶之路 (一) View 的基础知识你必须知道
Android 音视频学习系列(一) JNI 从入门到精通

参考链接

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
img

最后

针对Android程序员,我这边给大家整理了一些资料,包括不限于高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!

  • Android前沿技术大纲

  • 全套体系化高级架构视频

资料领取:点赞+点击GitHub免费获取

往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、混合式开发(ReactNative+Weex)全方面的Android进阶实践技术,群内还有技术大牛一起讨论交流解决问题。

Android前沿技术大纲*

[外链图片转存中…(img-gNrP2H0m-1711379366593)]

  • 全套体系化高级架构视频

    [外链图片转存中…(img-4saq15GH-1711379366593)]

资料领取:点赞+点击GitHub免费获取

往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、混合式开发(ReactNative+Weex)全方面的Android进阶实践技术,群内还有技术大牛一起讨论交流解决问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值