该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,若是能给各位看官带来一丝启发或者帮助,那真是极好的。android
终于到了咱们的猪脚ViewRootImpl出场的时候了。ViewRootImpl类比较复杂,若是要把这个类所有解释清楚那须要不少章节,而且该类涉及了许多其余知识,如Android进程间通讯的Binder了,还有其余许多本文以及前文没有讲到的概念。因此咱们只分析其中的一部分。ide
public ViewRootImpl(Context context, Display display) { ... /**① 从WindowManagerGlobal 中获取一个IWindowSession的实例。 *它是ViewRootImpl和WindowManagerService(如下简称WMS)进行通讯的代理 */ mWindowSession = WindowManagerGlobal.getWindowSession(); //② FallbackEventHandler是一个处理未经任何人消费的输入事件的场所。 mFallbackEventHandler = new PhoneFallbackEventHandler(context); ... }
注:函数
它是一个Binder对象,真正的实现类是Session,也就是说下文setView方法中关于它的操做实际上是一次IPC过程。关于IPC(进程间通讯)的方式,以及Android操做系统中最主要的IPC方式Binder会在之后的文章中介绍。工具
关于FallbackEventHandler具体我会在下一章介绍。oop
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { synchronized (this) { if (mView == null) { //保存了控件的根 mView = view; ... mFallbackEventHandler.setView(view); ... /** *① 在添加窗口以前,先经过requestLayout方法在主线程上安排一次“遍历”。 * 所谓“遍历”是指ViewRootImpl中的核心方法performTraversal()。 * 这个方法实现对控件树进行测量、布局、向WMS申请修改窗口属性以及重绘的全部工做。 */ requestLayout(); /** * ② 初始化mInputChanel。InputChannel是窗口接收来自InputDispatcher的输入事件的管道 */ if ((mWindowAttributes.inputFeatures & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) { mInputChannel = new InputChannel(); } ... try { /** *上文刚讲过mWindowSession是个Binder类,它的实现类是Session, *将经过IPC远程调用(即调用另外一个进程中的)Session的addToDisplay方法把窗口添加进WMS中。 *完成这个操做后,mWindow已经被添加到指定对象中并且mInputChannel(若是不为空)已经准备好接收事件 */ res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mAttachInfo.mOutsets, mInputChannel); } catch (RemoteException e) { } finally { } ... // 错误处理。窗口添加失败的缘由一般是是权限问题、重复添加或者token无效 if (res < WindowManagerGlobal.ADD_OKAY) { } ... // ③ 若是mInputChannel不为空,则建立mInputEventReceiver用于接收输入事件。 if (mInputChannel != null) { mInputEventReceiver = new WindowInputEventReceiver(mInputChannel, Looper.myLooper()); } ... view.assignParent(this); ... } } }
接着咱们来一个个分析,先来最重要的,也是本章的最主要内容,另外两个将会在下一章分析。布局
@Override public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; scheduleTraversals(); } }
scheduleTraversals();函数声明以下post
void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); if (!mUnbufferedInputDispatch) { scheduleConsumeBatchedInput(); } notifyRendererOfFramePending(); pokeDrawLockIfNeeded(); } }
其中mTraversalRunnable的定义是这样的this
final class TraversalRunnable implements Runnable { @Override public void run() { doTraversal(); } }
doTraversal()函数声明以下;spa
void doTraversal() { if (mTraversalScheduled) { mTraversalScheduled = false; mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); if (mProfile) { Debug.startMethodTracing("ViewAncestor"); } performTraversals(); if (mProfile) { Debug.stopMethodTracing(); mProfile = false; } } }
注:如下文章屡次摘抄于张大伟老师的《深刻理解Android卷Ⅲ》,请支持原创,读者也可去看张大伟老师的这本书籍
终于看到了咱们的猪脚performTraversals();,ViewRootImpl中接收的各类变化,如来自WMS的窗口属性变化、来自控件树的尺寸变化以及重绘请求等都引起performTraversals();的调用,并在其中完成处理。View类及其子类的onMeasure()、onLayout()、onDraw()等回调也都是在该方法执行的过程当中直接或间接的引起。该函数可谓是是ViewRootImpl的“心跳”。咱们就来看一下这个方法把。操作系统
先上源码:(注:源码很长,具体的分析在下方)
private void performTraversals() { final View host = mView; /** 第1阶段 预测量 */ boolean windowSizeMayChange = false; boolean newSurface = false; boolean surfaceChanged = false; WindowManager.LayoutParams lp = mWindowAttributes; ...... //声明本阶段的猪脚,这两个变量将是mView的SPEC_SIZE份量的候选 int desiredWindowWidth; int desiredWindowHeight; ...... Rect frame = mWinFrame; ...... if (mFirst) { mFullRedrawNeeded = true; mLayoutRequested = true; final Configuration config = mContext.getResources().getConfiguration(); if (shouldUseDisplaySize(lp)) { //为状态栏设置desiredWindowWidth/height 其取值是屏幕尺寸 Point size = new Point(); mDisplay.getRealSize(size); desiredWindowWidth = size.x; desiredWindowHeight = size.y; } else { // ① 第1次“遍历”的测量,采用了应用可使用的最大尺寸做为SPEC_SIZE的候选 desiredWindowWidth = dipToPx(config.screenWidthDp); desiredWindowHeight = dipToPx(config.screenHeightDp); } ...... } else { // ② 在非第1次遍历的状况下,会采用窗口的最新尺寸做为SPEC_SIZE的候选 desiredWindowWidth = frame.width(); desiredWindowHeight = frame.height(); //若是窗口的最新尺寸与ViewRootImpl中的现有尺寸不一样,说明WMS单方面改变了窗口的尺寸,将致使一下三个结果 if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) { //须要完整的重绘以适应新的窗口尺寸 mFullRedrawNeeded = true; //须要对控件树从新布局 mLayoutRequested = true; //控件树可能拒绝接受新的窗口尺寸,可能须要窗口在布局阶段尝试设置新的窗口尺寸,,只是尝试 windowSizeMayChange = true; } } ...... boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw); if (layoutRequested) { final Resources res = mView.getContext().getResources(); if (mFirst) { ...... } else { ...... /** *检查WMS是否单方面改变了一些参数,标记下来,而后做为后文是否进行控件布局的条件之一 *若是窗口的width或height被指定为WRAP_CONTENT时。表示该窗口为悬浮窗口。 */ if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) { //悬浮窗口的尺寸取决于测量结果。所以有可能向WMS申请改变窗口的尺寸 windowSizeMayChange = true; if (shouldUseDisplaySize(lp)) { //同样的设置状态栏的desiredWindowWidth/height Point size = new Point(); mDisplay.getRealSize(size); desiredWindowWidth = size.x; desiredWindowHeight = size.y; } else { // ③ 设置悬浮窗口的SPEC_SIZE的候选为应用可使用的最大尺寸 Configuration config = res.getConfiguration(); desiredWindowWidth = dipToPx(config.screenWidthDp); desiredWindowHeight = dipToPx(config.screenHeightDp); } } } // ④ 进行测量 windowSizeMayChange |= measureHierarchy(host, lp, res, desiredWindowWidth, desiredWindowHeight); } ...... if (layoutRequested) { mLayoutRequested = false; } ...... //⑤ 判断窗口是否须要改变尺寸 boolean windowShouldResize = layoutRequested && windowSizeMayChange && ((mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) || (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT && frame.width() < desiredWindowWidth && frame.width() != mWidth) || (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT && frame.height() < desiredWindowHeight && frame.height() != mHeight)); ...... /** 第1阶段 预测量到这里结束 */ /** 第2阶段 窗口布局阶段从这里开始 */ if (/*进入窗口布局的几个条件*/) { ...... boolean hadSurface = mSurface.isValid(); ...... try { relayoutResult = relayoutWindow(params, viewVisibility, insetsPending); }catch(...){ ...... }finally{ ...... } /** 第2阶段 窗口布局阶段到这里结束。关于窗口布局的部分涉及太多,咱们不具体分析源码,后文会有总结 */ /** 第3阶段 最终测量阶段从这里开始 */ if (!mStopped || mReportNextDraw) { ...... int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); //① 能够看到与与测量中调用的performMeasure performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); int width = host.getMeasuredWidth(); int height = host.getMeasuredHeight(); boolean measureAgain = false; //② 判断LayoutParams.horizontalWeight和lp.verticalWeight ,以做为是否再次测量的依据 if (lp.horizontalWeight > 0.0f) { width += (int) ((mWidth - width) * lp.horizontalWeight); childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); measureAgain = true; } if (lp.verticalWeight > 0.0f) { height += (int) ((mHeight - height) * lp.verticalWeight); childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); measureAgain = true; } if (measureAgain) { if (DEBUG_LAYOUT) Log.v(mTag, "And hey let's measure once more: width=" + width + " height=" + height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); } layoutRequested = true; } } } else { } /** 第3阶段 最终测量阶段到这里结束 */ /** 第4阶段 控件布局阶段从这里开始 */ //① 布局阶段的判断条件 final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw); ...... if (didLayout) { ...... //② 经过performLayout对控件进行布局 performLayout(lp, mWidth, mHeight); ...... //③ 若是有必要,计算窗口的透明区域并把该区域设置给WMS if ((host.mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) { host.getLocationInWindow(mTmpLocation); mTransparentRegion.set(mTmpLocation[0], mTmpLocation[1], mTmpLocation[0] + host.mRight - host.mLeft, mTmpLocation[1] + host.mBottom - host.mTop); host.gatherTransparentRegion(mTransparentRegion); if (mTranslator != null) { mTranslator.translateRegionInWindowToScreen(mTransparentRegion); } if (!mTransparentRegion.equals(mPreviousTransparentRegion)) { mPreviousTransparentRegion.set(mTransparentRegion); mFullRedrawNeeded = true; try { mWindowSession.setTransparentRegion(mWindow, mTransparentRegion); } catch (RemoteException e) { } } } /** 第4阶段 控件布局阶段到这里结束 */ /** 第5阶段 绘制阶段从这里开始 */ ...... boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible; if (!cancelDraw && !newSurface) { if (mPendingTransitions != null && mPendingTransitions.size() > 0) { for (int i = 0; i < mPendingTransitions.size(); ++i) { mPendingTransitions.get(i).startChangingAnimations(); } mPendingTransitions.clear(); } performDraw(); } else { if (isViewVisible) { // Try again scheduleTraversals(); } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) { for (int i = 0; i < mPendingTransitions.size(); ++i) { mPendingTransitions.get(i).endChangingAnimations(); } mPendingTransitions.clear(); } } mIsInTraversal = false; }
因为该方法是Android源代码中最庞大的方法之一,因此咱们对其进行分阶段分析。在源码中有标注1,2,3,4,5,对每一阶段再细分为①②...,对照上文注释
这是进入performTraversals();的第一个阶段。它会对控件树进行第一次测量。在此阶段中将会计算出控件树为显示其内容所需的尺寸,即指望的窗口尺寸。在这个阶段中View及其子类的onMeasure()方法将会沿着控件树依次获得回调。
预测量和测量原理
预测量也是一次完整的测量过程,它与最终测量的区别仅在于参数不一样而已。实际的测量工做是在View或其子类的onMeasure()方法中完成,而且其测量结果须要受限于来自其父控件的指示。这个指示由onMeasure()方法中的两个参数进行传达:widthSpec和heightSpec。它们是被称为MeasureSpec的复合整型变量,用于指导控件对自身进行测量。她又两个份量,结构如图
由①、②、③可知预测量时的SPEC_SIZE按照以下原则进行取值:
在第1阶段第④步时,咱们看到了measureHierarchy方法,该方法用于测量整个控件树。传入的参数desiredWindowWidth,desiredWindowHeight在前述代码中作了精心的挑选。控件树本能够按照这两个参数完成测量,可是measureHierarchy有本身的考量,即如何将窗口布局的尽量优雅。measureHierarchy如何作到这一步呢,经过跟控件树的协商。可是协商只发生在LayoutParams.width被指定为WRAP_CONTENT时,若是LayoutParams.width被指定为MATCH_PARENT或者固定数值时。该协商过程不会发生。
咱们来看一下measureHierarchy的源码。
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp, final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) { int childWidthMeasureSpec; int childHeightMeasureSpec; boolean windowSizeMayChange = false;//表示是否可能致使窗口的尺寸变化 boolean goodMeasure = false;//表示侧脸是否能知足控件树充分显示内容的要求 if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) { /** ① 第一次协商 measureHierarchy使用它指望的宽度限制进行测量, */ final DisplayMetrics packageMetrics = res.getDisplayMetrics(); res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true); int baseSize = 0; //宽度限制保存在baseSize中 if (mTmpValue.type == TypedValue.TYPE_DIMENSION) { baseSize = (int)mTmpValue.getDimension(packageMetrics); } //若是宽度限制不为0而且传入的desiredWindowWidth 大于measureHierarchy指望的限制宽度, if (baseSize != 0 && desiredWindowWidth > baseSize) { childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); //② 第一次测量 使用measureHierarchy指望的限制宽度 并获得状态 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); //判断状态 if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) { goodMeasure = true;//控件树对测量结果满意 } else { //③ 控件树对测量结果不满意,进行第二次协商,此次把限制宽度放大为指望宽度baseSize和最大宽度desiredWindowWidth和的一半 baseSize = (baseSize+desiredWindowWidth)/2; childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); //④ 第2次测量 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) { goodMeasure = true; } } } } //若是两次协商测量均不能让控件树满意,那么measureHierarchy再也不对宽度进行限制,使用最大宽度进行测量 if (!goodMeasure) { childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); //若是测量获得的宽度或者高度与ViewRootImpl中的窗口不一致,,那么以后可能要改变窗口的尺寸了 if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) { windowSizeMayChange = true; } } return windowSizeMayChange; }
咱们再来看performMeasure方法,performMeasure方法的实现很是简单,它直接调用了mView.measure的方法
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); } }
终于到了View的measure方法,在该方法内部会调用咱们熟悉的onMeasure方法,咱们来看View.measure方法的实现
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { .......//初始化操做 if (forceLayout || needsLayout) { //① 准备工做 mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET; ....... //② 对本控价进行测量 onMeasure(widthMeasureSpec, heightMeasureSpec); ....... //③ 检查onMeasure的实现是否调用了setMeasuredDimension() if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) { throw new IllegalStateException("View with id " + getId() + ": " + getClass().getName() + "#onMeasure() did not set the" + " measured dimension by calling" + " setMeasuredDimension()"); } //④ 将PFLAG_LAYOUT_REQUIRED加入mPrivateFlags ,这一操做会对以后的布局操做放行 mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; } mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; }
前文屡次设置了windowSizeMayChange 为true,可是windowSizeMayChange 为true满是窗口是否须要改变尺寸的条件之一,咱们来看第1阶段⑤对应代码。
boolean windowShouldResize = layoutRequested && windowSizeMayChange && ((mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) || (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT && frame.width() < desiredWindowWidth && frame.width() != mWidth) || (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT && frame.height() < desiredWindowHeight && frame.height() != mHeight));
能够看到windowShouldResize 的判断较为复杂,咱们来总结一下
必要条件:
layoutRequested为true。表示ViewRootImpl的requestLayout方法被调用过。
在View中也有requestLayout方法。当控件内容发生变化从而须要调整其尺寸时,会调用自身的requestLayout(),而且此方法会沿着控件树向根部回溯,最终调用到ViewRootImpl的requestLayout,从而引起一次performTraversals()调用。
之因此这是一个必要条件,是由于performTraversals()还有可能由于重绘时调用,当控件仅须要重绘而不须要从新布局时(例如背景色或者前景色发生变化时)。会经过invalidate()方法回溯到ViewRootImpl,此时不会经过requestLayout触发performTraversals()调用,而是经过scheduleTraversals()方法进行触发。这种状况下不须要进行布局窗口阶段
windowSizeMayChange为true,该变量前文中已有详细描述。
在上述条件知足的条件下,如下条件知足其一即触发布局窗口阶段
根据预测量的结果,经过IWindowSession.relayout()方法向WMS请求调整窗口的尺寸等属性,这将引起WMS对窗口从新布局,并将布局结果返回给ViewRootImpl.
总结:布局窗口得以进行的缘由是控件系统有修改窗口属性的需求,如第一次“遍历”须要肯定窗口的尺寸以及一块Surface,预测量结果与窗口当前尺寸不一致须要进行窗口尺寸更改,mView可见性发生变化须要将窗口隐藏或显示等。
预测量的结果是控件树所指望的窗口尺寸。然而因为在WMS中影响布局的因素不少,WMS不必定会将窗口的准确的布局为控件树所要求的尺寸,而迫于WMS做为系统服务的强势地位,控件树不得不接受WMS的布局结果。在这个阶段中View及其子类的onMeasure()方法将会沿着控件树依次被回调。最终测量阶段直接调用performMeasure而不是measureHierarchy,是由于measureHierarchy有个协商过程,而到了最终测量阶段控件树已经没有了协商的余地,不管控件树乐意与否,他只能被迫接受WMS的布局结果
将上一步完成的最终测量的结果做为依据进行布局。测量肯定的是控件的尺寸,而布局肯定的是控件的位置。在这个阶段中View及其子类的onLayout()方法将会被回调。
整体来讲4. 布局控件树阶段(Layout)作了两件事。
调用了performLayout函数,虽然咱们还没看到该函数,但猜想想必和performMeasure差很少。咱们来看一下
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) { ...... try { //同样是调用View.layout函数 host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); ...... } finally { } ...... }
咱们再来看View.layout。布局阶段把测量结果转化为控件的实际位置与尺寸。而控件的实际位置与尺寸由Veiw的mLeft、mTop、mRight、mBottom 这4个成员变量存储的坐标值。即控件树的布局过程就是根据测量结果为每个控件设置这4个成员变量的过程。其中mLeft、mTop、mRight、mBottom 是相对于父控件的坐标值。
public void layout(int l, int t, int r, int b) { //若是设置了PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT标志位,那么在布局以前先进行测量,调用onMeasure函数 if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) { onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } //保存原始坐标 int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; //设置新坐标 boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); //应该还记得上文View.measure方法中的最后设置了PFLAG_LAYOUT_REQUIRED吧 if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { //调用onLayout方法。若是该Vie是个ViewGroup。onLayout中须要依次调用子控件的layout方法 onLayout(changed, l, t, r, b); ...... //清除PFLAG_LAYOUT_REQUIRED标记 mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; //通知每个对此控件布局变化有兴趣的Listener ListenerInfo li = mListenerInfo; if (li != null && li.mOnLayoutChangeListeners != null) { ArrayList<OnLayoutChangeListener> listenersCopy = (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone(); int numListeners = listenersCopy.size(); for (int i = 0; i < numListeners; ++i) { listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); } } } ...... } }
咱们来对比测量和布局阶段以便更好的理解
布局阶段的另外一个工做是计算并设置窗口的透明区域。这一功能主要是为SurfaceView服务。关于SurfaceView的相关知识咱们后文介绍
这是performTraversals();的最后阶段。肯定控件的尺寸和位置后。便进行对控件树的绘制。在这个阶段中View及其子类的onDraw()方法将会被回调。
咱们在开发Android自定义控件时,每每都须要重写View.onDraw()方法以绘制内容到一个给定的Canvas中。 咱们来看一下Canvas。Canvas是一个绘图工具类,其API提供了一系列绘图指定供开发者使用。这些指令能够分为两个部分:
本篇文章详细分析了ViewRootImpl的五大过程,ViewRootImpl比较复杂,尤为是它的“心跳”performTraversals();。但愿读者能多看几遍上面的分析。相信你必定会有收获的
在下一篇文章中咱们将进行实战项目,也是对咱们前几篇文章的实际应用。老话说的好,纸上得来终觉浅,绝知此事要躬行。下一篇甚至几篇咱们就来自定义View
此致,敬礼