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

继续看图。WindowManagerImpl持有了PhoneWindow的引用,因此它可以对PhoneWindow进行管理。同时它还持有一个非常重要的引用mGlobal。这个mGlobal指向一个WindowManagerGlobal类型的单例对象,这个单例每个应用程序只有唯一的一个。在图中,我说明了WindowManagerGlobal维护了本应用程序内所有Window的DecorView,以及与每一个DecorView对应关联的ViewRootImpl。这也就是为什么我前面提到过,WindowManager只是一个代理,实际的管理功能是通过WindowManagerGlobal实现的。我们来看个源码的例子就比较清晰了。开始啦!

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

WimdowManagerImpl.java

public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {

mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
//实际是通过WindowManagerGlobal实现的。
}

从上面的代码可以看出,WindowManagerImpl确实只是WindowManagerGlobal的一个代理而已。同时,上面这个方法在整个Android的视图框架流程中十分的重要。我们知道,在Activity执行onResume()后界面就要开始渲染了。原因是在onResume()时,会调用WindowManager的addView()方法(实际最后调用的是WindowManagerGlobal的addView()方法),把视图添加到窗口上。结合我的这篇【可能是史上最简单的!一张图3分钟让你明白Activity启动流程,不看后悔!http://www.jianshu.com/p/9ecea420eb52】看,可以帮助你更好的理解Android的视图框架。
ActivityThread.java

final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {

ViewManager wm = a.getWindowManager();
//获得WindowManager,实际是WindowManagerImpl

wm.addView(decor, l);
//添加视图

wm.updateViewLayout(decor, l);
//需要刷新的时候会走这里

}

从上面可以看到,当Activity执行onResume()的时候就会添加视图,或者刷新视图。需要解释一点:WindowManager实现了ViewManager接口。

如图中所说,WindowManagerGlobal调用addView()的时候会把DecorView添加到它维护的数组中去,并且会创建另一个关键且极其重要的ViewRootImpl(这个必须要专门讲一下)类型的对象,并且也会把它存到一个数组中维护。
WindowManagerGlobal.java

public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {

root = new ViewRootImpl(view.getContext(), display);
//重要角色登场
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
//保存起来维护
mParams.add(wparams);

root.setView(view, wparams, panelParentView);
//设置必要属性view是DecorView,panelParentView是PhoneWindow

}

可以看出ViewRootImpl是在Activity执行onResume()的时候才被创建的,并且此时才把DecorView传进去让它管理。

知识点:WindowManager是在onCreate()时被创建。它对窗口的管理能力实际是通过WindowManagerGlobal实现的。在onResume()是视图才通过WindowManager被添加到窗口上。

ViewRootImpl

ViewRootImpl能够和系统的WindowManagerService进行交互,并且管理着DecorView的绘制和窗口状态。非常的重要。赶紧在图中找到对应位置吧!

ViewRootImpl并不是一个View,而是负责管理视图的。它配合系统来完成对一个Window内的视图树的管理。从图中也可以看到,它持有了DecorView的引用,并且视图树它是视图树绘制的起点。因此,ViewRootImpl会稍微复杂一点,需要我们更深入的去了解,在图中我标出了它比较重要的组成Surface和Choreographer等都会在后面提到。

到此,我们已经一起把第一张图撸了一遍了,现在童鞋们因该对Android视图框架有了大致的了解。下面将更进一步的去了解Android的绘制机制。

App总是卡顿到底是什么原因?

下面将会详细的讲解为什么我们设置的视图能够被绘制到屏幕上?这中间究竟隐藏着怎样的离奇?看完之后,你自然就能够从根源知道为什么你的App会那么卡,以及开始有思路着手解决这些卡顿。

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

同样用一张图来展示这个过程。由于Android绘制机制确实有点复杂,所以第一眼看到的时候你的内心中可能蹦腾了一万只草泥马😂。不要怕!我们从源头开始,一点一点的梳理这个看似复杂的绘制机制。为什么说看似复杂呢?因为这个过程只需要几分钟。Just Do It!

CPU、GPU是搞什么鬼的?

整天听到CPU、GPU的,你知道他们是干什么的吗?这里简单的提一下,帮助理解后面的内容。

在Android的绘制架构中,CPU主要负责了视图的测量、布局、记录、把内容计算成Polygons多边形或者Texture纹理,而GPU主要负责把Polygons或者Textture进行Rasterization栅格化,这样才能在屏幕上成像。在使用硬件加速后,GPU会分担CPU的计算任务,而CPU会专注处理逻辑,这样减轻CPU的负担,使得整个系统效率更高。

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

RefreshRate刷新率和FrameRate帧率

RefreshRate刷新率是屏幕每秒刷新的次数,是一个与硬件有关的固定值。在Android平台上,这个值一般为60HZ,即屏幕每秒刷新60次。

FrameRate帧率是每秒绘制的帧数。通常只要帧数和刷新率保持一致,就能够看到流畅的画面。在Android平台,我们应该尽量维持60FPS的帧率。但有时候由于视图的复杂,它们可能就会出现不一致的情况。

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

如图,当帧率小于刷新率时,比如图中的30FPS < 60HZ,就会出现相邻两帧看到的是同一个画面,这就造成了卡顿。这就是为什么我们总会说,要尽量保证一帧画面能够在16ms内绘制完成,就是为了和屏幕的刷新率保持同步。

下面将会介绍Android是如何来确保刷新率和帧率保持同步的。

Vsync(垂直同步)是什么?

你可能在游戏的设置中见过Vsync,开启它通常能够提高游戏性能。在Android中,同样使用Vsync垂直同步来提高显示性能。它能够使帧率FrameRate和硬件的RefreshRate刷新强制保持一致。

HWComposer与Vsync不得不说的事

看图啦看图啦。首先在最左边我们看到有个叫HWComposer的类,这是一个c++编写的类。它Android系统初始化时就被创建,然后开始配合硬件产生Vsync信号,也就是图中的HW_Vsync信号。当然它不是一直不停的在产生,这样会导致Vsync信号的接收者不停的接收到绘制、渲染命令,即使它们并不需要,这样会带来严重的性能损耗,因为进行了很多无用的绘制。所以它被设计设计成能够唤醒和睡眠的。这使得HWComposer在需要时才产生Vsync信号(比如当屏幕上的内容需要改变时),不需要时进入睡眠状态(比如当屏幕上的内容保持不变时,此时屏幕每次刷新都是显示缓冲区里没发生变化的内容)。

如图,Vsync的两个接收者,一个是SurfaceFlinger(负责合成各个Surface),一个是Choreographer(负责控制视图的绘制)。我们稍后再介绍,现在先知道它们是干什么的就行了。

Vsync offset机制

为了提高效率,尽量减少卡顿,在Android 4.1时引入了Vsync机制,并在随后的4.4版本中加入Vsync offset偏移机制。

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

图1. 为4.1时期的Vsync机制。可以看到,当一个Vsync信号到来时,SurfaceFlinger和UI绘制进程会同时启动,导致它们竞争CPU资源,而CPU分配资源会耗费时间,着降低系统性能。同时当收到一个Vsync信号时,第N帧开始绘制。等再收到一个Vsync信号时,第N帧才被SurfaceFlinger合成。而需要显示到屏幕上,需要等都第三个Vsync信号。这是比较低效率。于是才有了图2. 4.4版本加入的Vsync offset机制。

图2. Google加入Vsync offset机制后,原本的HW_Vsync信号会经过DispSync会分成Vsync和SF_Vsync两个虚拟化的Vsync信号。其中Vsync信号会发送到Choreographer中,而SF_Vsync会发送到SurfaceFlinger中。理论上只要phase_app和phase_sf这两个偏移参数设置合理,在绘制阶段消耗的时间控制好,那么画面就会像图2中的前几帧那样有序流畅的进行。理想总是美好的。实际上很难一直维持这种有序和流畅,比如frame_3是比较复杂的一帧,它的绘制完成的时间超过了SurfaceFlinger开始合成的时间,所以它必须要等到下一个Vsync信号到来时才能被合成。这样便造成了一帧的丢失。但即使是这样,如你所见,加入了Vsync offset机制后,绘制效率还是提高了很多。

从图中可以看到,Vsync和SF_Vsync的偏移量分别由phase_app和phase_sf控制,这两个值是可以调节的,默认为0,可为负值。你只需要找到BoardConfig.mk文件,就可以对这两个值进行调节。

回到ViewRootImpl

前面介绍了几个关键的概念,现在我们回到ViewRootImpl中去,在图中找到ViewRootImpl的对应位置。

前面说过,ViewRootImpl控制着一个Window中的整个视图树的绘制。那它是如何进行控制的呢?一次绘制究竟是如何开始的呢?

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

在ViewRootImpl创建的时候,会获取到前面提到过过的一个关键对象Choreographer。Choreographer在一个线程中仅存在一个实例,因此在UI线程只有一个Choreographer存在。也就说,通常情况下,它相当于一个应用中的单例。

在ViewRootImpl初始化时,会实现一个Choreographer.FrameCallback(这是一个Choreographer中的内部类),并向Choreographer中post。顾名思义,FrameCallback会在每次接收到Vsync信号时被回调。
Choreographer.java

public interface FrameCallback {
public void doFrame(long frameTimeNanos);
//一旦注册到CallbackQueue中,那么
//每次Choreographer接收到Vsync信号时都会回调。
}

FrameCallback一旦被注册,那么每次收到Vsync信号时它都会被回调。利用它,我们可以实现会帧率的监听。

ViewRootImpl.java

//这个方法只有在ViewRootImpl初始化时才会被调用
private void profileRendering(boolean enabled) {

mRenderProfiler = new Choreographer.FrameCallback() {
@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图形库进行绘制,因此具有更高的效率。

最后

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

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

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

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
[外链图片转存中…(img-eY8OtPn6-1715496295699)]

[外链图片转存中…(img-S5lT8XE8-1715496295700)]

[外链图片转存中…(img-LbNHwoor-1715496295701)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 10
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值