美文网首页安卓开发博客AndroidAndroid学习
Android布局优化(一)LayoutInflate — 从布

Android布局优化(一)LayoutInflate — 从布

作者: Geekholt | 来源:发表于2019-12-03 16:45 被阅读0次

    如需转载请评论或简信,并注明出处,未经允许不得转载

    系列文章

    目录

    前言

    最近打算写一些Android布局优化相关的文章,既然要进行布局优化,就要先了解布局加载原理,才能知道有哪些地方可以作为优化的切入点。开发同学做任何事情最好都能够知其所以然

    布局加载源码分析

    这里主要为了分析布局加载相关的原理,所以省略了一些逻辑。想了解View绘制流程,可以看最全的View绘制流程(上)— Window、DecorView、ViewRootImp的关系

    我们先从Activity.setContentView开始分析布局是如何被加载的

    Activity.setContentView(@LayoutRes int layoutResID)

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

    PhoneWIndow.setContentView(int layoutResID)

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

    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树两部分

    加载解析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 < num; i++) {
                            if (cachedXmlBlockCookies[i] == assetCookie && cachedXmlBlockFiles[i] != null
                                    && cachedXmlBlockFiles[i].equals(file)) {
                                return cachedXmlBlocks[i].newParser();
                            }
                        }
    
                        // Not in the cache, create a new block and put it at
                        // the next slot in the cache.
                        final XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file);
                        if (block != null) {
                            final int pos = (mLastCachedXmlBlockIndex + 1) % num;
                            mLastCachedXmlBlockIndex = pos;
                            final XmlBlock oldBlock = cachedXmlBlocks[pos];
                            if (oldBlock != null) {
                                oldBlock.close();
                            }
                            cachedXmlBlockCookies[pos] = assetCookie;
                            cachedXmlBlockFiles[pos] = file;
                            cachedXmlBlocks[pos] = block;
                            return block.newParser();
                        }
                    }
                } catch (Exception e) {
                    final NotFoundException rnf = new NotFoundException("File " + file
                            + " from xml type " + type + " resource ID #0x" + Integer.toHexString(id));
                    rnf.initCause(e);
                    throw rnf;
                }
            }
    
            throw new NotFoundException("File " + file + " from xml type " + type + " resource ID #0x"
                    + Integer.toHexString(id));
        }
    

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

    填充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("<merge /> 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. 对一些特殊标签,做分别处理,例如:viewTAG_1995(blink)

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

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

    createViewFromTag过程分析:

    1. 处理view标签

    如果标签的名称是view,注意是小写的view,这个标签一般大家不太常用

    <view
        class="RelativeLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"></view>
    

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

    1. 如果该节点与主题相关,则需要特殊处理

    如果该节点与主题(Theme)相关,需要将context与theme信息包装至ContextWrapper

    1. 处理TAG_1995标签

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

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

    <blink
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="这个标签会一直闪烁"/>
        
    </blink>
    
    1. 判断其是否存在Factory或者Factory2

    在这里先对Factory进行判空,这里不管Factory还是Factory2mPrivateFactory 就是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);
    }
    

    从这里可以看出,Factory2Factory都是一个接口,需要自己实现,而Factory2Factory的区别是Factory2继承Factory,从而扩展出一个参数,就是增加了该节点的父View。设置FactoryFactory2需要通过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);
        }
    }
    

    通过上面代码可以看出,FactoryFactory2只能够设置一次,并且FactoryFactory2二者互斥,只能存在一个。所以一般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实例的

    总结

    经过对布局加载原理的分析,我们可以看出布局加载的主要性能瓶颈主要在两个方面

    1. 加载xml文件是一个IO过程,如果xml文件过大,就会比较耗时

    2. View实例是通过反射进行创建的,通过反射创建对象相对会更耗费性能

    android布局加载过程

    相关文章

      网友评论

        本文标题:Android布局优化(一)LayoutInflate — 从布

        本文链接:https://www.haomeiwen.com/subject/sethgctx.html