用两张图告诉你,为什么你的App会卡顿?

有什么料?

从这篇文章中你能获得这些料:

  • 知道setContentView()之后发生了什么?
  • 知道Android究竟是如何在屏幕上显示我们期望的画面的?
  • 对Android的视图架构有整体把握。
  • 学会从根源处分析画面卡顿的原因。
  • 掌握如何编写一个流畅的App的技巧。
  • 从源码中学习Android的细想。
  • 收获两张自制图,帮助你理解Android的视图架构。

从setContentView()说起


  1. public class AnalyzeViewFrameworkActivity extends Activity { 
  2.   @Override 
  3.   protected void onCreate(Bundle savedInstanceState) { 
  4.     super.onCreate(savedInstanceState); 
  5.     setContentView(R.layout.activity_analyze_view_framwork); 
  6.   } 
  7. }  

上面这段代码想必Androider们大都已经不能再熟悉的更多了。但是你知道这样写了之后发生什么了吗?这个布局到底被添加到哪了?我的天,知识点来了!

可能很多同学也知道这个布局是被放到了一个叫做DecorView的父布局里,但是我还是要再说一遍。且看下图

这个图可能和伙伴们在书上或者网上常见的不太一样,为什么不太一样呢?因为是我自己画的,哈哈哈...

下面就来看着图捋一捋Android最基本的视图框架。

PhoneWindow

估计很多同学都知道,每一个Activity都拥有一个Window对象的实例。这个实例实际是PhoneWindow类型的。那么PhoneWindow从名字很容易看出,它应该是Window的儿子(即子类)!

知识点:每一个Activity都有一个PhoneWindow对象。

那么,PhoneWindow有什么用呢?它在Activity充当什么角色呢?下面我就姑且把PhoneWindow等同于Window来称呼吧。

Window从字面看它是一个窗口,意思和PC上的窗口概念有点像。但也不是那么准确。看图说。可以看到,我们要显示的布局是被放到它的属性mDecor中的,这个mDecor就是DecorView的一个实例。下面会专门撸DecorView,现在先把关注点放到Window上。Window还有一个比较重要的属性mWindowManager,它是WindowManager(这是个接口)的一个实现类的一个实例。我们平时通过getWindowManager()方法获得的东西就是这个mWindowManager。顾名思义,它是Window的管理者,负责管理着窗口及其中显示的内容。它的实际实现类是WindowManagerImpl。可能童鞋们现在正在PhoneWindow中寻找着这个mWindowManager是在哪里实例化的,是不是上下来回滚动着这个类都找不见?STOP!mWindowManager是在它爹那里就实例化好的。下面代码是在Window.java中的。


  1. public void setWindowManager(WindowManager wm,  
  2.     IBinder appToken,  
  3.     String appName,  
  4.     boolean hardwareAccelerated) { 
  5.         ... 
  6.         if (wm == null) { 
  7.             wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE); 
  8.             //获取了一个WindowManager 
  9.         } 
  10.         mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this); 
  11.         //通过这里我们可以知道,上面获取到的wm实际是WindowManagerImpl类型的。 
  12.     }  

通过上面的介绍,我们已经知道了Window中有负责承载布局的DecorView,有负责管理的WindowManager(事实上它只是个代理,后面会讲它代理的是谁)。

DecorView

前面提到过,在Activity的onCreate()中通过setContentView()设置的布局实际是被放到DecorView中的。我们在图中找到DecorView。

从图中可以看到,DecorView继承了FrameLayout,并且一般情况下,它会在先添加一个预设的布局。比如DecorCaptionView,它是从上到下放置自己的子布局的,相当于一个LinearLayout。通常它会有一个标题栏,然后有一个容纳内容的mContentRoot,这个布局的类型视情况而定。我们希望显示的布局就是放到了mContentRoot中。

知识点:通过setContentView()设置的布局是被放到DecorView中,DecorView是视图树的最顶层。

WindowManager

前面已经提到过,WindowManager在Window中具有很重要的作用。我们先在图中找到它。这里需要先说明一点,在PhoneWindow中的mWindowManager实际是WindowManagerImpl类型的。WindowManagerImpl自然就是接口WindowManager的一个实现类喽。这一点是我没有在图中反映的。

WindowManager是在Activity执行attach()时被创建的,attach()方法是在onCreate()之前被调用的。关于Activity的创建可以看看我的这篇:【可能是史上最简单的!一张图3分钟让你明白Activity启动流程,不看后悔!http://www.jianshu.com/p/9ecea420eb52】。

Activity.java


  1. final void attach(Context context, ActivityThread aThread, 
  2.     Instrumentation instr, IBinder token, int ident, 
  3.     Application application, Intent intent, ActivityInfo info, 
  4.     CharSequence title, Activity parent, String id, 
  5.     NonConfigurationInstances lastNonConfigurationInstances, 
  6.     Configuration config, String referrer, IVoiceInteractor voiceInteractor, 
  7.     Window window){ 
  8.         ... 
  9.         mWindow = new PhoneWindow(this, window); 
  10.         //创建Window 
  11.         ... 
  12.         mWindow.setWindowManager( 
  13.          (WindowManager)context.getSystemService(Context.WINDOW_SERVICE), 
  14.          mToken, mComponent.flattenToString(), 
  15.          (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0); 
  16.         //注意!这里就是在创建WindowManager。 
  17.         //这个方法在前面已经说过了。 
  18.         if (mParent != null) { 
  19.            mWindow.setContainer(mParent.getWindow()); 
  20.         } 
  21.         mWindowManager = mWindow.getWindowManager(); 
  22.             }  

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

WimdowManagerImpl.java


  1. public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) { 
  2.     ... 
  3.     mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow); 
  4.     //实际是通过WindowManagerGlobal实现的。 
  5. }  

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

ActivityThread.java


  1. final void handleResumeActivity(IBinder token, 
  2.     boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) { 
  3.     ... 
  4.     ViewManager wm = a.getWindowManager(); 
  5.     //获得WindowManager,实际是WindowManagerImpl 
  6.     ... 
  7.     wm.addView(decor, l); 
  8.     //添加视图 
  9.     ... 
  10.     wm.updateViewLayout(decor, l); 
  11.     //需要刷新的时候会走这里 
  12.     ... 
  13. }  

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

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

WindowManagerGlobal.java


  1. public void addView(View view, ViewGroup.LayoutParams params, 
  2.     Display display, Window parentWindow) { 
  3.     ... 
  4.     root = new ViewRootImpl(view.getContext(), display); 
  5.     //重要角色登场 
  6.     view.setLayoutParams(wparams); 
  7.     mViews.add(view); 
  8.     mRoots.add(root); 
  9.     //保存起来维护 
  10.     mParams.add(wparams); 
  11.     ... 
  12.     root.setView(view, wparams, panelParentView); 
  13.     //设置必要属性view是DecorView,panelParentView是PhoneWindow 
  14.     ... 
  15. }  

可以看出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


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

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

ViewRootImpl.java


  1. //这个方法只有在ViewRootImpl初始化时才会被调用 
  2. private void profileRendering(boolean enabled) { 
  3.     ... 
  4.     mRenderProfiler = new Choreographer.FrameCallback() { 
  5.     @Override 
  6.     public void doFrame(long frameTimeNanos) { 
  7.         ... 
  8.         scheduleTraversals(); 
  9.         //请求一个Vsync信号,后面还会提到这个方法 
  10.         mChoreographer.postFrameCallback(mRenderProfiler); 
  11.         //每次回调时,重新将FrameCallback post到Choreographer中 
  12.         ... 
  13.     } 
  14.     }; 
  15.     ... 
  16.     mChoreographer.postFrameCallback(mRenderProfiler); 
  17.     //将FrameCallback post到Choreographer中 
  18.     ... 
  19. }  

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


  1. void scheduleTraversals() { 
  2.     ... 
  3.     mChoreographer.postCallback( 
  4.         Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); 
  5.     //向Choreographer中post一个TraversalRunnable 
  6.     //这又是一个十分重要的对象 
  7.     ... 
  8.     }  

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

ViewRootImpl.java


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

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

Choreographer中的风起云涌

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

Choreographer.java


  1. private void postCallbackDelayedInternal(int callbackType, 
  2.     Object action, Object token, long delayMillis) { 
  3.     ... 
  4.     synchronized (mLock) { 
  5.         final long now = SystemClock.uptimeMillis(); 
  6.         final long dueTime = now + delayMillis; 
  7.         mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token); 
  8.         //将Callback添加到CallbackQueue[]中 
  9.         if (dueTime <= now) { 
  10.             scheduleFrameLocked(now); 
  11.             //如果回调时间到了,请求一个Vsync信号 
  12.             //在接收到后会调用doFrame()回调这个Callback。 
  13.         } else { 
  14.             Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action); 
  15.             msg.arg1 = callbackType; 
  16.             msg.setAsynchronous(true); 
  17.             //异步消息,避免被拦截器拦截 
  18.             mHandler.sendMessageAtTime(msg, dueTime); 
  19.             //如果还没到回调的时间,向FrameHandelr中发送 
  20.             //MSG_DO_SCHEDULE_CALLBACK消息 
  21.         } 
  22.     } 
  23.     ... 
  24. }  

上面这段代码会把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初始化时创建的。


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

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


  1. private void scheduleFrameLocked(long now) { 
  2.     ... 
  3.     //先判断当前是不是在UI线程 
  4.     if (isRunningOnLooperThreadLocked()) { 
  5.         scheduleVsyncLocked(); 
  6.         //是UI线程就请求一个Vsync信号 
  7.     } else { 
  8.         Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC); 
  9.         msg.setAsynchronous(true); 
  10.         mHandler.sendMessageAtFrontOfQueue(msg); 
  11.         //不在UI线程向FrameHandler发送一个MSG_DO_SCHEDULE_VSYNC消息 
  12.         //来请求一个Vsync信号 
  13.     } 
  14.  
  15. private void scheduleVsyncLocked() { 
  16.     mDisplayEventReceiver.scheduleVsync(); 
  17.     //通过DisplayEventReceiver请求一个Vsync信号 
  18.     //这是个恨角色,待会儿会聊聊它。 
  19.     //MSG_DO_SCHEDULE_VSYNC消息也是通过调用这个方法请求Vsync信号的。 
  20. }  

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

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


  1. private final class FrameDisplayEventReceiver extends DisplayEventReceiver 
  2.     implements Runnable { 
  3.  
  4.     //这个方法用于接收Vsync信号 
  5.     public void onVsync(){ 
  6.         ... 
  7.         Message msg = Message.obtain(mHandler, this); 
  8.         msg.setAsynchronous(true); 
  9.         mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS); 
  10.         //这里并没有设置消息的类型 
  11.         //其实就是默认为0,即MSG_DO_FRAME类型的消息 
  12.         //它其实就是通知Choreographer开始回调CallbackQueue[]中的Callback了 
  13.         //也就是开始绘制下一帧的内容了 
  14.     } 
  15.  
  16.     //这个方法是在父类中的,写在这方便看 
  17.     public void scheduleVsync() { 
  18.         ... 
  19.         nativeScheduleVsync(mReceiverPtr); 
  20.         //请求一个Vsync信号 
  21.     } 
  22. }  

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

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


  1. private final class FrameHandler extends Handler { 
  2.     public FrameHandler(Looper looper) { 
  3.         super(looper); 
  4.     } 
  5.  
  6.     @Override 
  7.     public void handleMessage(Message msg) { 
  8.         switch (msg.what) { 
  9.             case MSG_DO_FRAME: 
  10.                 //开始回调Callback,以开始绘制下一帧内容 
  11.                 doFrame(System.nanoTime(), 0); 
  12.                 break; 
  13.             case MSG_DO_SCHEDULE_VSYNC: 
  14.                 //请求一个Vsync信号 
  15.                 doScheduleVsync(); 
  16.                 break; 
  17.             case MSG_DO_SCHEDULE_CALLBACK: 
  18.                 //实际也是请求一个Vsync信号 
  19.                 doScheduleCallback(msg.arg1); 
  20.                 break; 
  21.             } 
  22.         } 
  23.     }  

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的构造器中你可以看到:


  1. public Canvas() { 
  2.     ... 
  3.     mNativeCanvasWrapper = initRaster(null); 
  4.     //申请一块内存,并且返回该内存的一个long类型的标记或者索引。 
  5.     ... 
  6. }  

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


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

简单的说一下。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的绘制机制也有了一个比较全面的了解。现在回过头来再写代码时是不是有种知根知底的自信呢?

本文作者:佚名

来源:51CTO

时间: 2024-10-30 06:02:37

用两张图告诉你,为什么你的App会卡顿?的相关文章

一张图告诉你PS CS6到底有啥好

PS CS6 新功能有哪些? 很多同学都在孜孜不倦的用着CS5, 现在用一张图告诉你CS6到底有啥好? 下面举了几个例子~~ 现在大家知道PS CS6有多好了吧?相信通过这种图大家对PS CS6有了更深的了解了~         注:更多精彩教程请关注三联photoshop教程栏目,三联PS群:75789345欢迎你的加入 分类: PS入门教程

“金砖”峰会即将举行!一张图告诉你金砖国家什么产品畅销中国

金砖国家领导人第九次会晤将于9月3日至5日在福建厦门举行.电子商务为金砖国家经济和社会发展带来了重大的历史机遇,成为金砖国家发展的新引擎. 阿里研究院即将发布金砖国家电子商务研究报告--<BRICS:携手迈向3万亿美元网络零售的未来>. 中国与金砖国家的经贸往来十分密切.中国商务部数据显示,2017年上半年,中国自金砖国家进口金额超过701.6亿美元,增速达 33.6%,占中国外贸进口总额的8.1%.金砖国家经贸潜力巨大,在跨境电商领域合作空间广阔. 那么,中国人喜欢网购金砖国家什么产品?而中

eclipse的安卓虚拟机建立不了啊,,问题如下两张图,头疼死我了!

问题描述 eclipse的安卓虚拟机建立不了啊,,问题如下两张图,头疼死我了! 解决方案 解决方案二: target无法选择,,,我已经在sdk下了好几个API了,,eclipse也更新了 解决方案三: sdk下载是否正确,eclipse中sdk路径是否设置正确 解决方案四: 楼主用的是google提供的ide吧,当然不能.因为google是与某adt绑定在一起的.正确的是,从eclipse官网下载独立的eclipse,下载后再装adt,任何版本都适应. 解决方案五: SDK下载的不正确. 解决

Javscript轮播 支持平滑和渐隐两种效果(可以只有两张图)

原文:Javscript轮播 支持平滑和渐隐两种效果(可以只有两张图) 先上两种轮播效果:渐隐和移动   效果一:渐隐 1 2 3 4 效果二:移动 1 2 3 4   接下来,我们来大致说下整个轮播的思路: 一.先来看简单的,移动的,先上来一个图----移动效果图: 说明:       基本原则就是顺序是按照当前显示的为基准:如当前为2,那么顺序就是2,3,4,1:如当前为3,那么顺序就是3,4,1,2.以此类推.       整个移动划分为三种:1.下一个  2.上一个  3.任意个    

马云为何能成为中国首富?一张图告诉你!

马云为何能成为中国首富?一张图告诉你! 时间:2014-10-24 15:25 来源: 21世纪网 作者:佚名

两张图看透IT人的逻辑思维(r12笔记第21天)

今天我们来看两张图,为什么要看,看完就就知道了. 我们来猜猜看,通过这个图能够读懂什么,准备好了吗? go 首先这是一张手机拍摄的图,看起来是一张很随意的照片,但是细看图片却发现有很多蹊跷之处,这是哪里,为什么要拍这么一张图片,拍照片的人想通过这张图片表达什么意思?从图片的拍摄角度来看,没有掌握好拍摄的技巧,所以很可能是位生活比较随意的男士拍的,从图片拍摄的场景来看,场地像是公园,环境看起来很舒适,也很安静,远处还有提供餐饮的场地,从师傅忙碌的背影和顾客量,能够判断出这个时间应该离饭点很近,看着

两张图说明三层的奥义!

直入正题先看两张图:     第一张图呢类似理论上的三层,第二层负责很多事情,要挑起打大梁.而且要做到可以更换上下两层.   但是实际的项目呢?一部分项目做来做去,就变成第二张图了.第二层由制造者变成了传声筒和搬运工!   当然高手做的项目是很厉害的,达到了三层的预期目的.   如何更换UI?   如果是基于webform(.aspx)的,要实现起来还真的挺有难度.因为.aspx把页面和数据绑的有点紧,虽然用起来确实挺方便的.    如果采用 js + json + service 的方式,就比

两张图总结 Neutron 架构 - 每天5分钟玩转 OpenStack(74)

前面我们详细讨论了 Neutron 架构,包括 Neutron Server,Core 和 Service Agent.现在用两张图做个总结.先看第一张: 与 OpenStack 其他服务一样,Neutron 采用的是分布式架构,包括 Neutorn Server.各种 plugin/agent.database 和 message queue. Neutron server 接收 api 请求. plugin/agent 实现请求. database 保存 neutron 网络状态. mess

8张图告诉你如何运营微信公众号

本文摘自亲钢的微信公众平台上的文章,原文标题"如何运营微信公众号,8张思维导图告诉你,拿走,不谢" 微信公众号已经过了红利期了.   什么叫红利期?   就是只要你做个公众号,随便发发文章,也能够每天有几百人关注,那个就是红利期.   之所以有微信红利期,是因为当时公众号实在太少,用户也贪新鲜.   不过对于真正用心运营公众号的朋友来说,没有红利期反而对他们有利.   因为任何项目,如果靠投机取巧就能够获得成功的话,对踏踏实实,认真做事情的人一点都不公平.   今天不多说什么了,给大家