Android 绘制原理浅析「干货」

背景

对于Android开发,在面试的时候,经常会被问到,说一说View的绘制流程?我也经常问面试者,View的绘制流程.

对于3年以上的开发人员来说,就知道onMeasure/onLayout/onDraw基本,知道他们呢是干些什么的,这样就够了吗?

如果你来我们公司,我是你的面试官,可能我会考察你这三年都干了什么,对于View你都知道些什么,会问一些更细节的问题,比如LinearLayout的onMeasure,onLayout过程?他们都是什么时候被发起的,执行顺序是什么?

如果以上问题你都知道,可能你进来我们公司就差不多了(如果需要内推,可以联系我,Android/IOS 岗位都需要),可能我会考察你draw的 canvas是哪里来的,他是怎么被创建显示到屏幕上呢?看看你的深度有多少?

对于现在的移动开发市场逐渐趋向成熟,趋向饱和,很多不缺人的公司,都需要高级程序员.在说大家也都知道,面试要造飞机大炮,进去后拧螺丝,对于一个3年或者5年以上Android开发不稍微了解一些Android深一点的东西,不是很好混.扯了这么多没用的东西,还是回到今天正题,Android的绘图原理浅析.

本文介绍思路

从面试题中几个比较容易问的问题,逐层深入,直至屏幕的绘图原理.

在讲Android的绘图原理前,先介绍一下Android中View的基本工作原理,本文暂不介绍事件的传递流程。

View 绘制工作原理

我们先理解几个重要的类,也是在面试中经常问到的

Activity,Window(PhoneWindow),DecorView之间的关系

理解他们三者的关系,我们直接看代码吧,先从Activity开始的setContentView开始(注:代码删除了一些不是本次分析流程的代码,以免篇幅过长)

//Activity
 /**
 * Set the activity content from a layout resource. The resource will be
 * inflated, adding all top-level views to the activity.
 *
 * @param layoutResID Resource ID to be inflated.
 *
 * @see #setContentView(android.view.View)
 * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
 */
 public void setContentView(@LayoutRes int layoutResID) {
 getWindow().setContentView(layoutResID);
 initWindowDecorActionBar();
 }
 
 public Window getWindow() {
 return mWindow;
 }

里面调用的getWindow的setContentView,这个接下来讲,那么这个mWindow是何时被创建的呢?

//Activity
private Window mWindow;
final void attach(Context context, ActivityThread aThread,····) {
 attachBaseContext(context);
 mFragments.attachHost(null /*parent*/);
 mWindow = new PhoneWindow(this, window, activityConfigCallback);
}

在Activity的attach中创建了PhoneWindow,PhoneWindow是Window的实现类.

继续刚才的setContentView

//PhoneWindow
 @Override
 public void setContentView(int layoutResID) {
 if (mContentParent == null) {
 installDecor();
 } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
 mContentParent.removeAllViews();
 }
 if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
 final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
 getContext());
 transitionTo(newScene);
 } else {
 mLayoutInflater.inflate(layoutResID, mContentParent);
 }
 }

在setContentView中,如果mContentParent为空,会去调用installDecor,最后将布局infalte到mContentParent.在来看一下installDecor

//PhoneWindow
 // This is the view in which the window contents are placed. It is either
 // mDecor itself, or a child of mDecor where the contents go.
 ViewGroup mContentParent;
 
 private DecorView mDecor;
 
 private void installDecor() {
 mForceDecorInstall = false;
 if (mDecor == null) {
 mDecor = generateDecor(-1);
 } else {
 mDecor.setWindow(this);
 }
 if (mContentParent == null) {
 mContentParent = generateLayout(mDecor);
 }
 }
 protected DecorView generateDecor(int featureId) {
 return new DecorView(context, featureId, this, getAttributes());
 }

在installDecor,创建了一个DecorView.看mContentParent的注释我们可以知道,他本身就是mDecor或者是mDecor的contents部分.

综上,我们大概知道了三者的关系,

  • Activity包含了一个PhoneWindow,
  • PhoneWindow就是继承于Window
  • Activity通过setContentView将View设置到了PhoneWindow上
  • PhoneWindow里面包含了DecorView,最终布局被添加到Decorview上.

理解ViewRootImpl,WindowManager,WindowManagerService(WMS)之间的关系

看了上述三者的关系后,我们知道布局最终被添加到了DecorView上.那么DecorView是怎么被添加到系统的Framework层.

当Activity准备好后,最终会调用到Activity中的makeVisible,并通过WindowManager添加View,代码如下

//Activity 
 void makeVisible() {
 if (!mWindowAdded) {
 ViewManager wm = getWindowManager();
 wm.addView(mDecor, getWindow().getAttributes());
 mWindowAdded = true;
 }
 mDecor.setVisibility(View.VISIBLE);
 }

那他们到底是什么关系呢? (下面提到到客户端服务端是Binder通讯中的客户端服务端概念. )

以下内容是重点需要理解的部分

  • ViewRootImpl(客户端):View中持有与WMS链接的mAttachInfo,mAttachInfo持有ViewRootImpl.ViewRootImpl是ViewRoot的的实现,WMS管理窗口时,需要通知客户端进行某种操作,比如事件响应等.ViewRootImpl有个内部类W,W继承IWindow.Stub,实则就是一个Binder,他用于和WMS IPC交互。ViewRootHandler也是其内部类继承Handler,用于与远程IPC回来的数据进行异步调用.
  • WindowManger(客户端):客户端需要创建一个窗口,而具体创建窗口的任务是由WMS完成,WindowManger就像一个部门经理,谁有什么需求就告诉它,它和WMS交互,客户端不能直接和WMS交互.
  • WindowManagerService(WMS)(服务端):负责窗口的创建,显示等.

View的重绘

从上述关系中,ViewRootImpl是用于接收WMS传递来的消息.那么我们来看一下ViewRootImpl里面的几个关于View绘制的代码.

在这里在强调一下,ViewRootImpl 两个重要的内部类

  • W类 继承Binder 用于接收WMS 传递来的消息
  • ViewRootHandler类继承Handler 接收W类的异步消息

下面看一下ViewRootHandler类.(以View的setVisible为例.)

// ViewRootHandler(ViewRootImpl的内部类,用于异步消息处理,和Acitivity的启动很像)
//第一步 Handler接收W(Binder)传递来的消息
@Override
public void handleMessage(Message msg) {
 switch (msg.what) {
 case MSG_INVALIDATE:
 ((View) msg.obj).invalidate();
 break;
 case MSG_INVALIDATE_RECT:
 final View.AttachInfo.InvalidateInfo info = (View.AttachInfo.InvalidateInfo) msg.obj;
 info.target.invalidate(info.left, info.top, info.right, info.bottom);
 info.recycle();
 break;
 case MSG_DISPATCH_APP_VISIBILITY://处理Visible
 handleAppVisibility(msg.arg1 != 0);
 break;
 } 
}
 
void handleAppVisibility(boolean visible) {
 if (mAppVisible != visible) {
 mAppVisible = visible;
 scheduleTraversals();
 if (!mAppVisible) {
 WindowManagerGlobal.trimForeground();
 }
 }
}
 
 void scheduleTraversals() {
 if (!mTraversalScheduled) {
 mTraversalScheduled = true;
 mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
 //开启下次刷新,就遍历View树
 mChoreographer.postCallback(
 Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
 if (!mUnbufferedInputDispatch) {
 scheduleConsumeBatchedInput();
 }
 notifyRendererOfFramePending();
 pokeDrawLockIfNeeded();
 }
}

看一下mTraversalRunnable

 final class TraversalRunnable implements Runnable {
 @Override
 public void run() {
 doTraversal();
 }
 }
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
 
 void doTraversal() {
 if (mTraversalScheduled) {
 mTraversalScheduled = false;
 mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
 performTraversals();
 }
 } 

在TraversalRunnable中,执行doTraversal.并在doTraversal执行performTraversals(),是不是看到了我们熟悉的performTraversals()了?是的,在这里才开始View的绘制工作.

在ViewRootImpl中的performTraversals(),这个方法代码很长(大约800行代码),大致流程是

  1. 判断是否需要重新计算视图大小,如果需要就执行performMeasure()
  2. 是否需要重新安置所在的位置,performLayout()
  3. 是否需要重新绘制performDraw()

那么是什么导致View的重绘呢?这里总结了3个主要原因

  • 视图本身内部状态(enable,pressed等)变化,可能引起重绘
  • View内部添加或者删除了View
  • View本身的大小和可见性发生了变化

View的绘制流程

在上一小节了,讲述了performTraversals()的是被WMS IPC调用执行的.View的绘制流程一般是

从performTraversals -> performMeasure() -> performLayout() -> performDraw().

下面看一下performMeasure()

//ViewRootImpl
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
 if (mView == null) {
 return;
 }
 Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
 try {
 mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
 } finally {
 Trace.traceEnd(Trace.TRACE_TAG_VIEW);
 }
 }
 
 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
 MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
 && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
 final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
 && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
 final boolean needsLayout = specChanged
 && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
 if (forceLayout || needsLayout) {
 mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
 resolveRtlPropertiesIfNeeded();
 int cacheIndex = forceLayout ? -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
//1
 private void performDraw() {
 try {
 draw(fullRedrawNeeded);
 } finally {
 mIsDrawing = false;
 Trace.traceEnd(Trace.TRACE_TAG_VIEW);
 }
 }
 
 //2
 private void draw(boolean fullRedrawNeeded) {
 Surface surface = mSurface;
 if (!surface.isValid()) {
 return;
 }
 
 if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
 return;
 }
 }
 
 //3
 private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
 boolean scalingRequired, Rect dirty) {
 Canvas canvas = mSurface.lockCanvas(dirty);
 } 

看代码执行流程,1—>2->3, 最终拿到了Java层的canvas,然后进行一系列绘制操作.而canvas是通过Suface.lockCanvas()得到的.

那么Surface又是一个什么呢?在这里Surface只是一个抽象,在APP创建窗口时,会调用WindowManager向WMS服务发起一个请求,携带上surface对象,只有他被分配完一段屏幕缓冲区才能真正对应屏幕上的一个窗口.

来看一下Framework中的绘图架构.更好的理解Surface

Surface本质上仅仅代表了一个平面,绘制不同图案显然是一种操作,而不是一段数据,Android使用了Skia绘图驱动库来进行平面上的绘制,在程序中使用canvas来表示这个功能.

双缓冲技术的介绍

在ViewRootImpl中,我们看到接收到绘制消息后,不是立刻绘制而是调用scheduleTraversals,在scheduleTraversals调用Choreographer.postCallback(),这又是因为什么呢?这其实涉及到屏幕绘制原理(除了Android其他平台也是类似的).

Android 绘制原理浅析「干货」

我们都知道显示器以固定的频率刷新,比如 iPhone的 60Hz、iPad Pro的 120Hz。当一帧图像绘制完毕后准备绘制下一帧时,显示器会发出一个垂直同步信号(VSync),所以 60Hz的屏幕就会一秒内发出 60次这样的信号。

并且一般地来说,计算机系统中,CPU、GPU和显示器以一种特定的方式协作:CPU将计算好的显示内容提交给 GPU,GPU渲染后放入帧缓冲区,然后视频控制器按照 VSync信号从帧缓冲区取帧数据传递给显示器显示.

但是如果屏幕的缓冲区只有一块,那么这个VSync同步信号发出时, 开始刷新屏幕,那么你看到的屏幕就是一条一条的数据在变化.为了让屏幕看上去是一帧一帧的数据,一般都有两块缓冲区(也被成为双缓冲区).当数据要刷新时,直接替换另一个缓冲区的数据.

双缓冲技术里面,如果不能特定时间刷新完的话(如果60HZ的话,就是16ms内)把这个缓冲区数据刷新完成,屏幕发出VSync同步信号,无法完成两个缓冲区的切换,那么就会造成卡顿现象.

回到scheduleTraversals()上,这个地方就是使用了双缓冲技术(或者三缓冲技术),Choreographer接收VSync的同步信号,当屏幕刷新来时,开始屏幕的刷新操作。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容