背景Android开发,面试的时候经常被问到,说说View的绘图过程?也经常问面试官,View的绘制过程。3年以上的开发者,他们只知道onMeasure/onLayout/onDraw的基础知识,以及他们的所作所为。这够了吗?如果你来我们公司,我就是你的面试官,也许我会考察你这三年做了什么,你对View了解多少,你会问一些比较细化的问题,比如onMeasure和onLayout的流程线性布局?他们是什么时候开始的,执行的顺序是什么?如果你有以上我所知道的所有问题,也许你来我们公司就差不多了(如果你需要内部推荐,你可以联系我,需要Android/IOS职位),也许我会调查你的drawcanvas来自哪里来自,又是如何创建并显示在屏幕上的呢?看看你有多深?当前的移动开发市场正在逐渐走向成熟和饱和。很多不缺人的公司都需要高级程序员。大家都知道面试要造飞机造炮。拧完螺丝,对于一个已经开发了3、5年,对Android了解不多的Android开发者来说,并不容易。说了这么多无用的东西,我们回到今天的话题,简单分析一下Android的绘图原理。本文从面试题中比较容易出的几个问题开始介绍思路,逐层深入,直到画面的绘制原理。在说Android的绘图原理之前,先介绍一下Android中View的基本工作原理。本文暂时不介绍事件。转移过程。View绘制的工作原理先来了解几个重要的类,也就是面试中经常被问到的Activity、Window(PhoneWindow)、DecorView的关系。要理解它们之间的关系,我们直接看代码,先从ActivitysetContentView开始(注意:代码删除了一些不在本次分析过程中的代码,以免太长)//Activity/***从布局资源中设置活动内容。资源将*膨胀,将所有顶级视图添加到活动中。**@paramlayoutResIDResourceID膨胀。**@see#setContentView(android.view.View)*@see#setContentView(android.view.View,android.view.ViewGroup.LayoutParams)*/publicvoidsetContentView(@LayoutResintlayoutResID){getWindow().setContentView(layoutResID);initWindowDecorActionBar();}publicWindowgetWindow(){returnmWindow;}中调用的getWindow的setContentView,接下来,这个mWindow是什么时候创建的?//ActivityprivateWindowmWindow;finalvoidattach(Contextcontext,ActivityThreadaThread,····){attachBaseContext(context);mFragments.attachHost(null/*parent*/);mWindow=newPhoneWindow(this,window,activityConfigCallback);}fActivity,PhoneWindow是Window的实现类。继续刚才的setContent查看//PhoneWindow@OverridepublicvoidsetContentView(intlayoutResID){if(mContentParent==null){installDecor();}elseif(!hasFeature(FEATURE_CONTENT_TRANSITIONS)){mContentParent.removeAllViews();}if(hasFeature(FEATURE_CONTENT_TRANSITIONS)){finalScenenewScene=Scene.getSceneForLayout(mContentParent,layoutResID,getContext());transitionTo(newScene);}else{mLayoutInflater.inflate(layoutResID,mContentParent);}}在setContentView中,如果mContentParent为空,会去调用installDecor,最后将发布到infaltemContentParent.在看一下installDecor//PhoneWindow//Thisistheviewinwhichthewindowcontentsareplaced.Itiseither//mDecoritself,orchildofmDecorwherethecontentsgo.ViewGroupmContentParent;privateDecorViewmDecor;privatevoidinstallDecor(){mForceDecorInstall=false;if(mDecor==null){mDecor=generateDecor(-1);}else{mDecor.setWindow(this);}if(mContentParent==null){mContentParent=generateLayout(mDecor);}}protectedDecorViewgenerateDecor(intfeatureId){returnnewDecorView(context,featureId,this,getAttributes());}在installDecor中,创建了一个DecorView。查看mContentParent的注释,可以知道是mDecor或者mDecor的contents部分。综上所述,我们大概知道三者的关系。Activity中包含一个PhoneWindow,PhoneWindow是继承WindowActivity通过setContentView将View设置为PhoneWindow。PhoneWindow包含DecorView,最终布局添加到Decorview。理解ViewRootImpl、WindowManager、WindowManagerService(WMS)的关系看到上面三者的关系,我们就知道layout是最后添加到DecorView中的了。那么DecorView是如何加入到系统的Framework层的呢?当Activity准备好后,最终会在Activity中调用makeVisible,通过WindowManager添加View。代码如下);}它们之间的关系是什么?(下面提到client-server就是Binder通信中client-server的概念。)下面的内容是ViewRootImpl(client)需要理解的重点部分:View持有与WMS挂钩的mAttachInfo,mAttachInfo持有ViewRootImpl。ViewRoot的实现,在WMS管理窗口的时候,需要通知客户端进行某些操作,比如事件响应等。ViewRootImpl有一个内部类W,W继承了IWindow.Stub,其实就是一个Binder,也就是用于与WMSIPC交互ViewRootHandler也是一个继承Handler的内部类,用于对远程IPC返回的数据进行异步调用。WindowManger(client):客户端需要创建窗口,创建窗口的具体任务由WMS完成。WindowManger就像一个部门经理,谁需要谁就告诉它,它与WMS交互,客户端不能直接与WMS交互。WindowManagerService(WMS)(服务器):负责窗口的创建和显示。视图重绘从上面的关系来看,ViewRootImpl是用来接收WMS消息的。那么我们再来看看ViewRootImpl中关于View绘制的几段代码。这里强调一下,ViewRootImpl的两个重要的内部类都继承了Binder来接收WMS的消息。ViewRootHandler类继承Handler来接收W类的异步消息。让我们看一下ViewRootHandler类。(以View的setVisible为例)//ViewRootHandler(ViewRootImpl的内部类,用于异步消息处理,类似于Acitivity的启动)//PartOne-stepHandler从W(Binder)接收消息@OverridepublicvoidhandleMessage(Messagemsg){switch(msg.what){caseMSG_INVALIDATE:((View)msg.obj).invalidate();break;caseMSG_INVALIDATE_RECT:finalView.AttachInfo.InvalidateInfoinfo=(View.AttachInfo.InvalidateInfo)msg.obj;信息。target.invalidate(info.left,info.top,info.right,info.bottom);info.recycle();break;caseMSG_DISPATCH_APP_VISIBILITY://processingVisiblehandleAppVisibility(msg.arg1!=0);break;}}voidhandleAppVisibility(booleanvisible){if(mAppVisible!=可见){mAppVisible=可见;scheduleTraversals();if(!mAppVisible){WindowManagerGlobal.trimForeground();}}}voidscheduleTraversals(){if(!mTraversalScheduled){mTraversalScheduled=true;mTraversalBarrier=mHandler.getLooper().getQueue().postSyncBarrier();//启用下一次刷新时遍历View树=false;mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);performTraversals();}}在TraversalRunnable中,执行doTraversal。并在doTraversal中执行performTraversals()。你见过熟悉的performTraversals()吗?是的,View的绘制工作就从这里开始。在ViewRootImpl()中performTraversals,这样代码很长(约800行代码),大致流程是判断是否需要重新计算视图尺寸,必要时执行performMeasure(),是否需要重新定位,performLayout()是否需要重绘performDraw(),那么是什么导致了View的重绘呢?这里主要是三个原因导致View本身内部状态发生变化(enable、pressed等),可能会导致View重绘添加或删除View。View本身的大小和可见性发生了变化。View的绘制过程上一节介绍了performTraversals()是由WMSIPC调用执行的。View的绘制过程一般是从performTraversals->performMeasure()->performLayout()->performDraw()。我们来看看performMeasure()//ViewRootImplprivatevoidperformMeasure(intchildWidthMeasureSpec,intchildHeightMeasureSpec){if(mView==null){return;}Trace.traceBegin(Trace.TRACE_TAG_VIEW,"measure");try{mView.measure(childWidthMeasureSpec,childHeightMeasureSpec));}finally{Trace.trace(Trace.TRACE_TAG_VIEW);}}publicfinalvoidmeasure(intwidthMeasureSpec,intheightMeasureSpec){MeasureSpec.getMode(widthMeasureSpec)==MeasureSpec.EXACTLY&&MeasureSpec.getMode(heightMeasureSpec)==MeasureSpec.EXACTLY;finalbooleanmatchesSpecSize=getMeasuredWidth()==MeasureSpec.getSize(宽度测量Spec)&&getMeasuredHeight()==MeasureSpec.getSize(heightMeasureSpec);finalbooleanneedsLayout=specChanged&&(sAlwaysRemeasureExactly||!isSpecExactly||!matchesSpecSize);如果(forceLayout||needsLayout){mPrivateFlags&=~PFLAG_MEASURED_DIMENSION_SET;resolveRtlPropertiesIfNeedxforceLayout=();int-1:mMeasureCache.indexOfKey(key);if(cacheIndex<0||sIgnoreMeasureCache){//这里调用了onMeasure方法onMeasure(widthMeasureSpec,heightMeasureSpec);mPrivateFlags3&=~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;}}}最后调用了View的measure方法,并且View中的measure()方法被定义为final类型,以保证整个流程的执行。performLayout()和performDraw()也是类似的过程。对于程序员来说,自定义View只需要关注它提供的几个对应的方法,onMeasure/onLayout/onDraw。关于这方面的知识网上有很多资料,大家很容易看到View和ViewGroup中的代码。这部分知识建议阅读LinerLayout的源码来了解,这里不做详细展开。Android的绘图原理分析Android屏幕绘图关于绘图,就要从performDraw()说起。我们来看看这个过程是如何绘制的//ViewRootImpl//1privatevoidperformDraw(){try{draw(fullRedrawNeeded);}finally{mIsDrawing=false;Trace.traceEnd(Trace.TRACE_TAG_VIEW);}}//2privatevoiddraw(booleanfullRedrawNeeded){Surfacesurface=mSurface;if(!surface.isValid()){return;}if(!drawSoftware(surface,mAttachInfo,xOffset,yOffset,scalingRequired,dirty)){return;}}//3privatebooleandrawSoftware(Surfacesurface,AttachInfoattachInfo,intxoff,intyoff,booleanscalingRequired,Rectdirty){Canvascanvas=mSurface.lockCanvas(dirty);}看代码执行过程,1—>2->3,最终得到Java层的画布,然后执行一系列绘图操作。画布是通过Suface.lockCanvas()获取的。那么什么是Surface?在这里,Surface只是一个抽象。APP创建窗口时,会调用WindowManager向WMS服务发起请求,携带表面对象,只有他分配了一段屏幕缓冲区,才能真正对应到屏幕上的一个窗口。我们来看看Framework中的绘图架构。更好的理解Surface表面本质上只代表一个平面。绘制不同的图案显然是一个操作,而不是一个部分。资料,Android使用Skia绘图驱动库在平面上进行绘图,在程序中使用canvas来表示该功能。双缓冲技术的引入是在ViewRootImpl中。我们看到收到绘制消息后,并没有立即绘制而是调用了scheduleTraversals,在scheduleTraversals中调用了Choreographer.postCallback(),这是什么原因呢?这其实涉及到屏幕绘制的原理(除Android外其他平台类似)。我们都知道显示器以固定的频率刷新,比如iPhone的60Hz,当iPadPro的120Hz在绘制完一帧图像后准备绘制下一帧时,显示器会发出一个垂直同步信号(VSync),所以60Hz的屏幕在一秒内会发出60次这样的信号。而一般来说,在一个计算机系统中,CPU、GPU、显示器以一种特定的方式进行协作:CPU将计算出的显示内容提交给GPU,GPU将其渲染到帧缓冲区中,然后视频控制器跟随VSync信号帧数据从帧缓冲区中取出并传递给显示器进行显示。但是如果屏幕只有一个buffer,那么当发送VSync同步信号的时候,屏幕刷新,你看到的屏幕是一个一个变化的。为了使屏幕看起来像一帧一帧的数据,一般有两个缓冲区(也称双缓冲区)。当要刷新数据时,直接替换另一个缓冲区中的数据。在双缓冲技术中,如果不能指定时间刷新后(如果是60HZ,16ms以内),缓冲区数据刷新,屏幕发送VSync同步信号,两个缓冲区之间无法完成切换,这将造成冻结现象。回到scheduleTraversals(),这个地方使用了双缓冲技术(或者三缓冲技术)。Choreographer接收到VSync的同步信号,当屏幕刷新时,开始屏幕刷新操作。
