我奶奶都能懂的UI绘制流程(上)

时间:2022-04-26
本文章向大家介绍我奶奶都能懂的UI绘制流程(上),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

前言

从今天开始,慢慢整理Android高级UI的知识,涉及到各种酷炫狂拽吊炸天的特效。

之前写过一篇Window一本满足算是这个专题的预备知识,本文就基于这篇文章,继续往下探索UI的绘制流程。按照国际惯例,我们开始源码的分析,毕竟只有把原理给搞清楚了,才能进行各种天马行空的创作。

setContentView

在关于Window的学习中,我们知道每个Activity都有自己的一个Window负责界面的显示。PhoneWindow是抽象Window唯一的实现类。调用Activity的setContentView()实际上是调用了PhoneWindow的setContentView()

最开始的时候,判断mContentParent是否为空,为空则执行installDecor(),从名字上可以看出这个方法与DecorView的初始化有关。接下来通过FEATURE_CONTENT_TRANSITIONS判断是否需要执行过场动画,需要则执行,不需要则直接通过mLayoutInflater将XML资源加载到mContentParent中。

关于mContentParent和mDecor的关系,直接看官方注释,我就不翻译了。

接着来看看先前猜测的installDecor()方法到底做了些啥

当mDecor为空时generateDecor()会直接新建一个DecorView对象并将其返回,注意,DecorView本质上就是一个FrameLayout。

接着当mContentParent为空时,执行generateLayout(mDecor)并将返回值赋给mContentParent,这是一个重量级的方法,主要包含5块内容

第一步,通过getWindowStyle()获取当前Window的TypedArray 。熟悉自定义控件的同学对TypedArray一定是相当熟悉的,他可以用来获取布局xml中的信息 。

TypedArray a = getWindowStyle();

第二步,通过获取到的TypedArray对Feature状态位进行设置,比如判断当前Window是否为悬浮状态,是否全屏,是否显示ActionBar,是否透明等等

第三步,通过设置好的Feature获取对应的layoutResource,这些layoutResource都是Android系统原先就提供好的。

我们来看看最简单的R.layout.screen_simple布局:

ViewStub是用来延迟加载的一种组件,是用来动态显示bar的,而id为content的这个FrameLayout就是我们真正加载布局的地方了。一定要记住android:id="@android:id/content",其他类型的布局或许样式不同,但真正加载用户布局的id始终都为content

第四步,将获取到的layoutResource进行渲染,添加到decor中。要注意,这个时候用户的布局还没有加载到content中,此时只是将原始的layoutResource加载到decor中

第五步,获取layoutResource中id为ID_ANDROID_CONTENT的ViewGroup,并将其返回。这个ViewGroup就是真正加载用户布局的地方。

到此为止,generateLayout(mDecor)完成了自己的历史使命,mContentParent 成为了真正加载用户布局的FrameLayout。回到setContentView()方法中,现在容器已经准备好了,我们可以放心的开始加载用户布局。

LayoutInflater

setContentView()的最后,用户布局开始进行加载

mLayoutInflater.inflate(layoutResID, mContentParent);

inflate()方法第一次重载后如下

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        return inflate(resource, root, root != null);
    }

注意第三个参数为root != null,由于我们将mContentParent作为root传入,所以此时第三个参数为true

下一次重载中,会获取一个XmlResourceParser用于解析用户传入的布局资源,之后将这个XmlPullParser 作为参数进行最后一次重载。这个方法内容比较多,我们一点一点看

首先,根据XmlResourceParser获取到AttributeSet ,这个set中保存了xml布局中的配置信息。

 final AttributeSet attrs = Xml.asAttributeSet(parser);

接着,通过XML解析获取根节点,此时name就是根节点标签的名字。不熟悉XML解析的同学自行百度。(主要有PULL,SAX,DOM三种解析方式)

获取到根节点的标签后,首先要判断是否为TAG_MERGE。如果是且root为空则抛出异常,否则进行合并渲染。

这里稍微解释一下TAG_MERGE。在我们写布局的时候,会使用<include/>标签来引入某个布局,<merge/>标签的作用就体现在此,因为父布局已经存在一个ViewGroup了,所以使用<merge/>时,子布局可以不写最外层的ViewGroup。这样就做到了减少图层的效果。

如果根节点标签不是TAG_MERGE,那么此时获取到的Tag就是xml中真正的根View。

我们将其创建出来。此时View类已经实例化了,但是在xml设置的属性还没有添加进去。

xml中的属性通过XmlResourceParser解析到attrs中,所以此时要通过root.generateLayoutParams(attrs)将attrs转化成LayoutParams 。还记得root是什么吗?在上文中,root就是id为content的那个FrameLayout。是加载用户布局的地方。

我们获取到了LayoutParams,最后只要通过temp.setLayoutParams(params)将params属性设置到View中就OK了。继续往下看代码:

 // Inflate all children under temp against its context.
                    rInflateChildren(parser, temp, attrs, true);
                    ...
                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

现在已经获得了用户布局中的根View以及它的属性,接下来就通过rInflateChildren(parser, temp, attrs, true)来渲染其子view,其会重载rInflate(),这个方法长度适中,为了大家能全方位的理解,就一口气展示出来

可以看到,整个过程是处于while循环中,这也是xml解析的一种基本方式。

首先还是通过XmlPullParser 获取到子布局的名称,接着开始判断子布局的类型。如果类型为TAG_INCLUDE并且深度为0,说明<include />是根节点,抛出异常。如果发现类型为TAG_MERGE且深度不为0,说明<merge />不是根节点,抛出异常。

异常判断结束后,重复之前绘制根节点的操作,将子View与子View的子View都一一绘制并添加到他们的父View中。

经过上面这些操作后,用户界面XML中的元素就全部解析并且封装了起来,最后就可以调用root.addView(temp, params)将这个封装完毕的View添加到root中。

到此为止,LayoutInflater.inflate()方法完成了它的历史使命,我们用一张图来总结

AppComPatActivity

文章前面已经将Activity的setContentView()介绍完毕了,但是现在使用AndroidStudio开发时,咱们默认的Activity是谁?是AppCompatActivity,这是一个为了填补Google曾经挖下的各种坑而出现的超级无敌自适应Activity。下面,大家一起来看看AppCompatActivity的setContentView()是怎么操作的。

   public void setContentView(@LayoutRes int layoutResID) {
        getDelegate().setContentView(layoutResID);
    }

这一上来就不一样啊。 getDelegate()是获取代理的方法,会通过mDelegate = AppCompatDelegate.create(this, this)来创建代理对象

private static AppCompatDelegate create(Context context, Window window,
            AppCompatCallback callback) {
        final int sdk = Build.VERSION.SDK_INT;
        if (BuildCompat.isAtLeastN()) {
            return new AppCompatDelegateImplN(context, window, callback);
        } else if (sdk >= 23) {
            return new AppCompatDelegateImplV23(context, window, callback);
        } else if (sdk >= 14) {
            return new AppCompatDelegateImplV14(context, window, callback);
        } else if (sdk >= 11) {
            return new AppCompatDelegateImplV11(context, window, callback);
        } else {
            return new AppCompatDelegateImplV9(context, window, callback);
        }
    }

可以看到,不同的版本会返回不同的代理对象,这些代理对象都继承自AppCompatDelegateImplV9,我们重点看这个类的setContentView()方法。

其他内容都和Activity中的差不多,就是第一行多了ensureSubDecor(),这个方法会调用createSubDecor()来创建一个ViewGroup对象,这是AppCompatActivity中十分关键的一个方法。

createSubDecor()和Activity中的generateLayout(mDecor)十分类似,因为比较重量级,具体的可以结合源码与文章前面对generateLayout(mDecor)的分析来看,在这里我们就分析几处关键的地方。

首先,获取到TypedArray 对象。

 TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);

要注意,此处的主题是R.styleable.AppCompatTheme。系统需要通过这个主题来对一些View进行兼容性的改造。这也就是为什么在使用AppCompatActivity时,主题必须设置为AppCompat类型,否则就会抛出异常。

接下来,获取DecorView

// Now let's make sure that the Window has installed its decor by retrieving it
        mWindow.getDecorView();

getDecorView()会调用PhoneWindow的installDecor(),这个方法之前详细介绍过,很重要,忘记了就往前翻翻。

继续下潜,有很长一段代码都是用来判断subDecor需要加载什么系统布局,这个过程和Activity中的类似,我们依然以simple布局为例

 subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);

这些组件都是在v7包中用来做版本适配的,再来看看Include进来的这个布局

这个ContentFrameLayout就相当于Activity中的FrameLayout,所以我们一定要把它的id记住,action_bar_activity_content,action_bar_activity_content,action_bar_activity_content,说三遍。

终于,材料已经准备完毕,是时候来享受真正的大餐了。

final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
                R.id.action_bar_activity_content);

final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);

contentView 是subDecor中id为action_bar_activity_content的ContentFrameLayout, windowContentView是mWindow中id为content的FrameLayout。

将windowContentView的id设为NO_ID,再将contentView的id设为content。这个时候,R.id.action_bar_activity_content就完成了它的任务。

最后,将subDecor添加到mWindow中,大功告成!

是不是感觉茅塞顿开了?这招偷梁换柱简直漂亮!我们上一张图来感受此时下整体的结构。

ViewRootLmpl

仔细回忆下之前的过程,在setContentView()方法中,界面布局的xml资源已经解析并生成了view,而view也添加到了window上,但此时view并没有绘制出来,对用户而言还是不可见的。

接下来,我们就来学习View的绘制流程。在开始前,强烈建议大家先去复习下有关Window的爱恨情仇!以及Activity启动流程简直丧心病狂!,不然等会懵逼的可能性会很大。

故事要从Activity启动流程简直丧心病狂!的结尾开始,上回说到,在ActivityThread中调用了handleLaunchActivity()开始真正启动一个活动,今天咱们就来仔细分析下这个方法。

Activity a = performLaunchActivity(r, customIntent);

首先,调用performLaunchActivity()实例化了Activity,这个方法主要做了三件事 第一,通过反射获取到Activity的实例

 ClassLoader cl = r.packageInfo.getClassLoader();
 activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);

第二,调用Activity.attach()初始化window

第三,通过mInstrumentation最终回调了Activity的onCreate方法

由于setContentView()是在onCreate()中执行的,所以现在我们就获取了view并添加到了window上,接下来要开始绘制了,很显然,留给我们进行绘制的只剩下onResume

现在回到handleLaunchActivity()方法中,继续往下看,果然这里会调用handleResumeActivity()

 handleResumeActivity(r.token, false, r.isForward,
                    !r.activity.mFinished && !r.startsNotResumed);

进入这个方法,看看会发生什么。

  r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                ViewManager wm = a.getWindowManager();
                ...
                wm.addView(decor, l);

也就是说,在handleResumeActivity()中,我们获取到了DecorView以及WindowManager,并将decor添加到了wm中。

也就是说,在handleResumeActivity()中,我们获取到了DecorView以及WindowManager,并将decor添加到了wm中。

WindowManager.addView()的作用就是通过AIDL将window显示到屏幕上,再调用ViewRootImpl进行view的绘制

addView()中,会实例化ViewRootImpl对象并调用它的setView()方法

root = new ViewRootImpl(view.getContext(), display);
...
root.setView(view, wparams, panelParentView);

ViewRootImpl.setView()主要做了三件事,第一是通过下面的代码将window添加到屏幕上

res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);

第二是调用requestLayout()进行view的绘制 第三是调用view.assignParent(this)将decorView的parent设置为当前的ViewRootImpl

事件一在有关Window的爱恨情仇!介绍过了,过程比较复杂,请移步。

今天我们主要介绍事件二、三。 首先看比较简单的事件三,这里就是直截了当的将ViewRootImpl设置为decorView的parent

 void assignParent(ViewParent parent) {
        if (mParent == null) {
            mParent = parent;
        }
         ...
    }

这么做的意义是什么呢?大家知道,在View中调用requestLayout()会使得界面重绘,来看看这个方法

 public void requestLaut() {
        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
         ...
    }

原来如此,View.requestLayout()会不断回调其parent的requestLayout()方法,最后到达decorView时,就会调用ViewRootImpl的requestLayout()

也就是说,ViewRootImpl.requestLayout()是view绘制的起源,我们来事件二仔细感受一下

  public void requestLayout()  {
      if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

该方法会调用scheduleTraversals()

void scheduleTraversals() {
           ...
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
       ...  
         }

接着在mChoreographer中执行mTraversalRunnable,这是一个Runnable 对象,唯一的作用就是调用doTraversal()

final class TraversalRunnable implements Runnable {
        public void run() {
            doTraversal();
        }  
   }

doTraversal()又会调用performTraversals(),这个方法那是相当长,一看就是有特殊癖好的变态工程师写的,我们主要看其中与UI绘制有关的部分。从前往后慢慢找,依次可以看到他们:

performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
performDraw();

哇!终于看到这三兄弟了!大家一起来松口气,咱们今天就说到这,虽然还没开始View的绘制,但前面的准备工作都完成啦!最后方式一张流程图进行来梳理一下吧。