02 性能优化-布局优化-布局的加载流程与绘制原理

时间:2021-6-12 作者:qvyue

本篇属于三个部分中的理论基础部分。

目录

  • Android系统的绘图机制
  • Activity的组成
  • 布局文件的加载流程
  • View的绘制流程
  • 布局优化的简单建议
  • 总结

正文

一、Android系统的绘图机制

Android系统每隔16ms就重新绘制一次Activity,这就要求UI界面必须在16ms内完成屏幕刷新的全部逻辑操作,这样才能达到每秒60fps,然而这个fps是由手机硬件所决定,现在大多数手机屏幕刷新率是60Hz(赫兹是国际单位制中频率的单位,它是每秒中的周期性变动重复次数的计量),也就是说我们有16ms(1000ms/60fps=16.66ms)的时间去完成每帧的绘制逻辑操作,如果超过了就会出现所谓的丢帧。实际开发中复杂的界面往往在16ms内完成全部绘制,但是尽量降级UI的绘制时间,总是可以有效的降低卡顿感。

对于Android系统的硬件绘图机制,并非布局优化的重点,有兴趣的可以翻看文末的参考资料。

二、Activity的组成

一个Activity层级结构图,如下所示

02 性能优化-布局优化-布局的加载流程与绘制原理
image

它有点像洋葱圈一层包裹着一层,下面我们就来逐个介绍一下。

  • PhoneWindow

    PhoneWindow是Window的子类,Window是顶级窗口外观和行为策略的抽象基类。它提供标准的UI策略,例如背景,标题区域,默认密钥处理等。它的唯一实现就是PhoneWindow

  • DecorView

    DecorView是一个ViewGroup类,继承自FrameLayout,是Activity在绘制布局文件时的宿主,也可以把它理解为绘制布局文件时的“画布”。

  • TitleActionBar

    Android提供一个默认的ActionBar,我们在写demo时经常会看到这个ActionBar,一般正式开发时,会在Style.xml中把它去掉.

    
    
    
  • ContentView

    ContentView就是我们在setContentView时传入的xml布局文件绘制出来的ViewGroup,在Activity(kotlin语言)中我们可以通过如下代码获取到各个ContentView

    //kotlin
    window.decorView.findViewById(android.R.id.content).getChildAt(0)
    //java
    getWindow().getDecorView().findViewById(android.R.id.content).getChildAt(0)
    
    

通过这张层级关系图,我们就大致明白了Activity层级结构,理解Activity的页面层级结构非常的重要,它不仅与性能优化息息相关,而且也可以帮助我们理解Android触摸事件的分发机制。

触摸事件的分发机制,经常涉及到自定义的View,自定义View其实也是我们在布局优化时常用的手段之一。

这里重新画了一张“洋葱圈”一样的层级结构图,来帮助你理解触摸事件的向上传递机制。这张图很形象的解释了触摸事件是如何从Activity中开始传递,又是如何回到Activity中的。关于触摸事件的分发具体的分发机制,请参阅其他文章,这里就不再细说了。

02 性能优化-布局优化-布局的加载流程与绘制原理
image

三、布局文件的加载流程

在Android开发中setContentView是我们最常用的将xml格式的布局文件绘制到activity中的方法。那么布局文件是如何绘制到Activity当中的呢?通过阅读setcontentView的源代码,可以发现布局文件的加载大致分为,读取xml创建View对象两个流程。

02 性能优化-布局优化-布局的加载流程与绘制原理
image.png
  • 布局加载源码分析

我们先从Activity.setContentView开始分析布局是如何被加载的,这里主要为了分析布局加载相关的原理,所以省略了一些逻辑。想了解View绘制流程,可以看最全的View绘制流程(上)— Window、DecorView、ViewRootImp的关系

  1. Activity.setContentView(@LayoutRes int layoutResID)
public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

  1. PhoneWIndow.setContentView(int layoutResID)
@Override
public void setContentView(int layoutResID) {
    if (mContentParent == null) {
        //初始化DecorView和mContentParent
        installDecor();
    }
    ...
        //加载资源文件,创建view树装载到mContentParent
        mLayoutInflater.inflate(layoutResID, mContentParent);
    ...
}

  1. LayoutInflate.inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();
    //1.加载解析xml文件
    final XmlResourceParser parser = res.getLayout(resource);
    try {
        //2.填充View树
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}

可以看出布局加载流程主要分为加载解析xml文件填充View树两部分

3.1 加载xml文件

Resources.getLayout(@LayoutRes int id)

 public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException {
        return loadXmlResourceParser(id, "layout");
 }

Resources.loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie,@NonNul l String type)

XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie,@NonNul l String type) throws NotFoundException {
        if (id != 0) {
            try {
                synchronized (mCachedXmlBlocks) {
                    final int[] cachedXmlBlockCookies = mCachedXmlBlockCookies;
                    final String[] cachedXmlBlockFiles = mCachedXmlBlockFiles;
                    final XmlBlock[] cachedXmlBlocks = mCachedXmlBlocks;
                    // First see if this block is in our cache.
                    final int num = cachedXmlBlockFiles.length;
                    for (int i = 0; i 

我们不用非常深入这个方法的具体实现细节,我们只需要知道,这个方法的作用就是将我们写的xml文件读取到内存中,并进行一些数据解析和封装。所以这个方法本质上就是一个IO操作,我们知道,IO操作往往是比较耗费性能的

3.2 填充View树

LayoutInflate.inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        final Context inflaterContext = mContext;
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        Context lastContext = (Context) mConstructorArgs[0];
        mConstructorArgs[0] = inflaterContext;
        View result = root;
            int type;
            final String name = parser.getName();
            if (TAG_MERGE.equals(name)) {
                if (root == null || !attachToRoot) {
                    throw new InflateException(" can be used only with a valid "
                            + "ViewGroup root and attachToRoot=true");
                }

                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                // Temp is the root view that was found in the xml
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                ViewGroup.LayoutParams params = null;

                if (root != null) {
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        temp.setLayoutParams(params);
                    }
                }

                rInflateChildren(parser, temp, attrs, true);
              
                if (root != null && attachToRoot) {
                    root.addView(temp, params);
                }

                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }

        return result;
    }
}

上面这个方法中我们最主要关注createViewFromTag(View parent, String name, Context context, AttributeSet attrs)

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
        boolean ignoreThemeAttr) {
  
    //解析view标签
    if (name.equals("view")) {
        name = attrs.getAttributeValue(null, "class");
    }

    //如果需要该标签与主题相关,需要对context进行包装,将主题信息加入context包装类ContextWrapper
    if (!ignoreThemeAttr) {
        final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
        final int themeResId = ta.getResourceId(0, 0);
        if (themeResId != 0) {
            context = new ContextThemeWrapper(context, themeResId);
        }
        ta.recycle();
    }

    if (name.equals(TAG_1995)) {
       //BlinkLayout是一种闪烁的FrameLayout,它包裹的内容会一直闪烁,类似QQ提示消息那种。
        return new BlinkLayout(context, attrs);
    }

            //设置Factory,来对View做额外的拓展,这块属于可定制的内容
        View view;
        if (mFactory2 != null) {
            view = mFactory2.onCreateView(parent, name, context, attrs);
        } else if (mFactory != null) {
            view = mFactory.onCreateView(name, context, attrs);
        } else {
            view = null;
        }

        if (view == null && mPrivateFactory != null) {
            view = mPrivateFactory.onCreateView(parent, name, context, attrs);
        }

          //如果此时不存在Factory,不管Factory还是Factory2,还是mPrivateFactory都不存在,
            //那么会直接对name直接进行解析
        if (view == null) {
            final Object lastContext = mConstructorArgs[0];
            mConstructorArgs[0] = context;
            try {
                //如果name中包含"."即为自定义View,否则为原生的View控件
                if (-1 == name.indexOf('.')) {
                    view = onCreateView(parent, name, attrs);
                } else {
                    view = createView(name, null, attrs);
                }
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        }

        return view;
}
  • 根据源码可以将createViewFromTag分为三个流程:
  1. 对一些特殊标签,做分别处理,例如:view,TAG_1995(blink)

  2. 进行对Factory、Factory2的设置判断,如果设置那么就会通过设置Factory、Factory2进行生成View

  3. 如果没有设置Factory或Factory2,那么就会使用LayoutInflater默认的生成方式,进行View的生成

  • createViewFromTag过程分析:
  1. 处理view标签
    如果标签的名称是view,注意是小写的view,这个标签一般大家不太常用

在使用时,相当于所有控件标签的父类一样,可以设置class属性,这个属性会决定view这个节点会变成什么控件

  1. 如果该节点与主题相关,则需要特殊处理
    如果该节点与主题(Theme)相关,需要将context与theme信息包装至ContextWrapper类

  2. 处理TAG_1995标签
    这就有意思了,TAG_1995指的是blink这个标签,这个标签感觉使用的很少,以至于大家根本不知道。

这个标签最后会被解析成BlinkLayout,BlinkLayout其实就是一个FrameLayout,这个控件最后会将包裹内容一直闪烁(就和电脑版QQ消息提示一样)

  1. 判断其是否存在Factory或者Factory2
    在这里先对Factory进行判空,这里不管Factory还是Factory2(mPrivateFactory 就是Factory2),本质上都是一种扩展操作,提前解析name,然后直接将解析后的View返回

Factory

public interface Factory {
    public View onCreateView(String name, Context context, AttributeSet attrs);
}

Factory2

public interface Factory2 extends Factory {
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}

从这里可以看出,Factory2和Factory都是一个接口,需要自己实现,而Factory2和Factory的区别是Factory2继承Factory,从而扩展出一个参数,就是增加了该节点的父View。设置Factory和Factory2需要通过setFactory()或者setFactory2()来实现

setFactory()

public void setFactory(Factory factory) {
    //如果已经设置Factory,不可以继续设置Factory
    if (mFactorySet) {
        throw new IllegalStateException("A factory has already been set on this LayoutInflater");
    }
    if (factory == null) {
        throw new NullPointerException("Given factory can not be null");
    }
    //设置Factory会添加一个标记
    mFactorySet = true;
    if (mFactory == null) {
        mFactory = factory;
    } else {
        mFactory = new FactoryMerger(factory, null, mFactory, mFactory2);
    }
}

setFactory2()

public void setFactory2(Factory2 factory) {
    if (mFactorySet) {
        throw new IllegalStateException("A factory has already been set on this LayoutInflater");
    }
    if (factory == null) {
        throw new NullPointerException("Given factory can not be null");
    }
    //注意设置Factory和Factory2的标记是共用的
    mFactorySet = true;
    if (mFactory == null) {
        mFactory = mFactory2 = factory;
    } else {
        mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
    }
}

通过上面代码可以看出,Factory和Factory2只能够设置一次,并且Factory和Factory2二者互斥,只能存在一个。所以一般setFactory()或者setFactory2(),一般在cloneInContext()之后设置,这样生成一个新的LayoutInflater,标记默认是false,才能够设置

  1. createView(String name, String prefix, AttributeSet attrs)
 public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        //判断构造器是否存在    
        Constructor extends View> constructor = sConstructorMap.get(name);
        if (constructor != null && !verifyClassLoader(constructor)) {
            constructor = null;
            sConstructorMap.remove(name);
        }
        Class extends View> clazz = null;

        try {
        //如果构造器不存在,这个就相当于Class之前是否被加载过,sConstructorMap就是缓存这些Class的Map
            if (constructor == null) {
                //通过前缀+name的方式去加载
                clazz = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);
                //通过过滤去设置一些不需要加载的对象
                if (mFilter != null && clazz != null) {
                    boolean allowed = mFilter.onLoadClass(clazz);
                    if (!allowed) {
                        failNotAllowed(name, prefix, attrs);
                    }
                }
                constructor = clazz.getConstructor(mConstructorSignature);
                constructor.setAccessible(true);
                //缓存Class
                sConstructorMap.put(name, constructor);
            } else {
            //如果Class存在,并且加载Class的ClassLoader合法
                //这里先判断该Class是否应该被过滤
                if (mFilter != null) {
                    //过滤器也有缓存之前的Class是否被允许加载,判断这个Class的过滤状态
                    Boolean allowedState = mFilterMap.get(name);
                    if (allowedState == null) {
                        //加载Class对象操作
                        clazz = mContext.getClassLoader().loadClass(
                                prefix != null ? (prefix + name) : name).asSubclass(View.class);
                        //判断Class是否可被加载
                        boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
                        mFilterMap.put(name, allowed);
                        if (!allowed) {
                            failNotAllowed(name, prefix, attrs);
                        }
                    } else if (allowedState.equals(Boolean.FALSE)) {
                        failNotAllowed(name, prefix, attrs);
                    }
                }
            }

            Object[] args = mConstructorArgs;
            args[1] = attrs;
            
                    //如果过滤器不存在,直接实例化该View
            final View view = constructor.newInstance(args);
            //如果View属于ViewStub那么需要给ViewStub设置一个克隆过的LayoutInflater
            if (view instanceof ViewStub) {
                final ViewStub viewStub = (ViewStub) view;
                viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
            }
            return view

从上面的代码可以看出,我们是通过反射的方式去创建View实例的

目前为止Activity还是看不任何东西的,因为创建的View还没有开始绘制。接下来我们就来看看View的绘制流程。

四、View的绘制流程

View绘制流程主要分为三个部分:measure、layout、draw,分别对应测量、布局和绘制,其中measure确定View的测量宽高,layout确定View最终宽高和四个顶点的位置,draw负责将view最终绘制到屏幕上。

ViewGroup的绘制流程与View大体相同,唯一的区别就是,View只需要绘制它自己,而ViewGroup不仅要绘制它自己还要绘制它的子View。下面我们就以ViewGroup为例,简单从源代码的角度来看一下这三个流程:

1.Measure与MeasureSpec

测量过程通过measure()来实现,是View树自顶向下的遍历,每个View在循环过程中将尺寸细节向下传递,当测量过程完成之后,所有的View也就都存储了自己尺寸。

ViewGroup是一个抽象类,它并没有重写View的measure()方法,它在内部会调用measureChildren(),然后再去循环调用View的measure()方法。

measure()方法需要传入两个参数widthMeasureSpec和heightMeasureSpec。

protected void measure(int widthMeasureSpec, int heightMeasureSpec)

表面上看widthMeasureSpec和heightMeasureSpec是int的数字,它们是父类传过来的给当前View的一个建议值(这个建议值是我们在XML中设定的),实际上是由mode+size组成的。将widthMeasureSpec转换为二进制后,它是一个32位的数字,前两位表示模式(mode),后30位表示数值(size)。

mode共有三种模式,分别是

  • UNSPECIFIED(未指定)

    不做任何限制,View可以获得任意大小。它一般用于系统的内部测量过程。

  • EXACTLY(完全)

    由父View决定子View的确切大小,子View将被限定在给定边界里而忽略它自身的大小。对应match_parent和具体的dp值

  • AT_MOST(至多)

    View最多达到指定大小的值,对应wrap_content

上述3中模式在自定义view时非常有用,当模式是EXACTLY时,我们是直接使用父类的建议值,当模式是AT_MOST时,我们则需要自己设定View的大小,因为用户没有规定这个View有多大。

2.layout

Layout的作用是ViewGroup用来确定子View的位置。在ViewGroup中调用layout方法确定位置确定后,它会在onLayout中遍历所有子View的layout方法,子View的layout又会调用onLayout方法,确定自己的位置。

layout的大致流程如下:

首先通过setFrame设定View的四个顶点位置;

然后调用onLayout方法,在这里面调用每个子View的layout

3.draw

draw的过程是最简单的,它的作用就是把View绘制到屏幕上,

 public void draw(Canvas canvas) {}

在draw方法中主要完成了一下几个任务:

  • 使用drawBackground方法绘制背景
  • 在onDraw中绘制自己
  • 在dispatch中绘制子View
  • 在onDrawScrollBars中绘制装饰

在Android中draw方法会被频繁的调用,例如:按home键app进入后台,当我们在回到APP时,即使APP没有被销毁,当前界面下View组件的draw方法也会被调用。

简单了解了View的绘制流程后,不难看出这里面也存在至少两个性能瓶颈,一个是measure和layout过程中会循环调用子View的方法,其实这就决定了布局文件不能嵌套过深,否则循环的时间复杂度会很高。另一个是View的draw方法会被频繁的调用,对于这类频繁调用的方法,我们不能在其中创建对象或执行耗时操作,否则会产生剧烈的内存抖动和页面卡顿。

五、布局优化的简单建议

通过上面的分析,我们对布局的组成,加载以及绘制有了一定的了解,现在再来看看常见的布局优化建议,相信你一定对这些建议有了进一步的认识。

  • 使用ConstraintLayout减少布局嵌套

    ConstraintLayout是Google推出一种可以有效减少嵌套问题的布局,它可以让你的布局更加的扁平化,如果你没有使用过ConstraintLayout,强烈推荐使用。

  • 使用和标签来减少布局嵌套

    标签可以将一个指定的布局引入到当前的布局中,通过这种方式可以复用项目中已经存在的布局。有时候被引用的布局顶级节点与外部布局存在重复的情况,这时就可以使用将多余的顶级节点去掉。关于

  • 使用ViewStub延迟加载布局

    ViewStub继承了View,它的宽高都是0,因此它不参与任何布局与绘制的过程。在开发中有的布局正常情况下并不显示,这时候就可以使用ViewStub,在布局初始化的时候可以避免加载这类并不需要立即显示的布局。

  • 不要在onDraw()创建对象或执行耗时操作

    具体原因在上面已经说过了,这里就不赘述了。

  • 不使用xml布局

    使用xml布局文件,Android需要通过IO操作把xml布局文件加载到内存中,然后通过反射创建view对象,如果不使用xml就可以完全避免这些影响的性能操作。使用这类思想创建布局文件框架有的iReader的X2C和FaceBook的Litho,不过这是一类很极端的做法,并不推荐。

  • 复杂布局使用自定义View

    当App设计图非常复杂,我们需要使用非常多的系统组件组合才能实现相似的功能时,建议使用自定义View,保持界面的扁平化。

六、总结

本篇文章梳理了一下Activity的组成,一个xml的布局是如何加载到界面中,以及是如果绘制出来的,最后总结了一下目前的布局优化建议。

参考资料

《Android开发艺术探索》 任玉刚著

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。