美文网首页
setContentView()及LayoutInflater布

setContentView()及LayoutInflater布

作者: 碧云天EthanLee | 来源:发表于2021-07-29 15:46 被阅读0次
    概述

    这回我们来分析一下 Activity布局加载的setContentView()的整个流程。这一流程将细化为3个部分来分析:setContentView系统布局加载流程、LayoutInflater初始化分析、LayoutInflater布局加载流程

    一、setContentView系统布局加载流程

    我们来看看 Activity的 setContentView()方法:

        // Activity.java
        // mWindow 实例
        mWindow =new PhoneWindow(this,window, activityConfigCallback);
       // ......
        public Window getWindow() {
            return mWindow;
        }
        public void setContentView(@LayoutRes int layoutResID) {
            //注释 1 mWindow是 PhoneWindow。我们到 PhoneWindow去看 setContentView() 方法
            getWindow().setContentView(layoutResID);
            initWindowDecorActionBar();
        }
    
       // PhoneWindow.java
        @Override
        public void setContentView(int layoutResID) {
            if (mContentParent == null) {
                //注释 2, 如果 mContentParent等于空,创建一个 DecorView(mContentParent 就是 ContentView)
                installDecor();
            } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
                mContentParent.removeAllViews();
            }
            // 注释 3, 将我们页面的布局记载道 id为 R.id.content(mContentParent) 的布局里
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
    

    上面我们看到,Activity的 setContentView方法调用了变量 mWindow的方法,而mWindow的实现类是PhoneWindow,看上面注释。

    我们看下上面注释2、3,PhoneWindow的 setContentView方法先会判断布局变量mContentParent 是否等于空。如果等于空,在上面注释 2的地方初始化 DecorView。然后在注释 3的地方将我们在activity_main_layout自己定义的布局加载到 mContentParent布局里,而这个 mContentParent布局就是这个 id为 R.id.content 的Layout,这个下面会看到。现在我们来看看注释 2处 DecorView创建的过程:

    private void installDecor() {
            mForceDecorInstall = false;
            if (mDecor == null) {
                //注释 4, 如果 mDecor等于null,创建一个 DecorView
                mDecor = generateDecor(-1);
                ......
            } else {
                mDecor.setWindow(this);
            }
            if (mContentParent == null) {
                //注释 5 创建一个id为 R.id.content 的布局(也就是contentView),这就是我们 activity_main_layout的父布局
                mContentParent = generateLayout(mDecor);
            }
    }
    
     protected DecorView generateDecor ( int featureId){
                // 创建一个 DecorView
                return new DecorView(context, featureId, this, getAttributes());
            }
    

    上面注释 4处创建了一个 DecorView 对象赋给了变量 mDecor。然后注释 5是 mContentParent 的初始化过程,下面看一下:

         protected ViewGroup generateLayout (DecorView decor){
                //注释 6, int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
                // 获取到 id为 R.id.content的Layout
                ViewGroup contentParent = (ViewGroup) findViewById(ID_ANDROID_CONTENT);
                // .......
                if (
                    //注释 7, 一些列 if else判断
                ) {......} else {
                    // Embedded, so no decoration is needed.
                    layoutResource = R.layout.screen_simple;
                    // System.out.println("Simple!");
                }
                //注释 8, 在 DecorView里再添加一层 id为layoutResource 的系统布局
                mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
                //注释 9, 返回 content的Layout,赋值给上面的 mContentParent变量
                return contentParent;
            }
    
     //  DecorView.java
            void onResourcesLoaded (LayoutInflater inflater,int layoutResource){
                ......
                // 创建系统布局
                final View root = inflater.inflate(layoutResource, null);
                ......
                // Put it below the color views.
                // 在 DecorView里再添加一层 id为layoutResource 的系统布局
                addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
                ......
            }
    

    上面注释 6的地方开始创建这个 id为R.id.content的布局,并最终返回给上面的mContentParent 变量。然后注释 7这个地方有一系列判断,最终会选择一个系统布局,在注释 8的地方把这个系统布局加载到我们的 DecorView里。而且需要注意的是这个系统布局里会有一个叫 R.id.content的 View。

    • 小结

    总结一下,Activity的setContentView是通过 其持有的 PhoneWindow来加载布局的。PhoneWindow会先创建一个 DecorView加载进来,而DecorView在创建时又会加载一个系统布局添加给 DecorView,而这个系统布局又包含了一个叫做 R.id.content的布局,此布局最终会加载并赋值给 mContentParent 变量,最后我们自己在 activity里定义的activity_main_layout会加载进 mContentParent。

    下面是一个简单的层级图:


    setContentView.png
    二、LayoutInflater初始化分析

    下面到 LayoutInflater布局加载分析。分析布局加载之前我们先看一下 LayoutInflater的初始化过程。

    1.View view = View.inflate(this,id_view, viewParent);
    2.View view = LayoutInflater.from(this).inflate(id_view, viewParent);
    3.View view = LayoutInflater.from(this).inflate(id_view, viewParent, false);
    

    我们常用的布局加载有上面几种用法,其实最终都会走到第3种方式。第3种方式最后一个 boolean型参数表示是否将创建的布局添加到父布局。下面我们先看看 LayoutInflater.from(this)这个方法,LayoutInflater是怎么初始化的:

    //LayoutInflater .java
     public static LayoutInflater from(Context context) {
            LayoutInflater LayoutInflater =
                // 这里点进去是抽象方法,找到 context的实现类 ContextImpl的 getSystemService方法
               (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
                    // ......
                    return LayoutInflater;
                }
    

    上面是调用了 context的 getSystemService方法,我们找到实现类 ContextImpl看一下:

     // ContextImpl.java
                public Object getSystemService (String name){
                    // ......
                    // 在 SystemServiceRegistry.java里拿
                    return SystemServiceRegistry.getSystemService(this, name);
                }
    // SystemServiceRegistry.java
                public final class SystemServiceRegistry {
                    // 单例,用于缓存系统服务
                    private static final Map<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS =
                            new ArrayMap<String, ServiceFetcher<?>>();
                    static {
                        // 注册系统布局加载 Service
                        registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
                                new CachedServiceFetcher<LayoutInflater>() {
                                    @Override
                                    public LayoutInflater createService(ContextImpl ctx) {
                                        return new PhoneLayoutInflater(ctx.getOuterContext());
                                    }
                                });
                        // 这个静态块里注册了 N多个系统服务
                        // 省略......
                    }
    
                    public static Object getSystemService(ContextImpl ctx, String name) {
                        // 从缓存内中获取系统服务
                        final ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
                        final Object ret = fetcher.getService(ctx);
                        // ......
                        return ret;
                    }
                }
    

    上面一直找,找到了 SystemServiceRegistry类里面。原来 LayoutInflater在SystemServiceRegistry 初始化块里面会被注册成一个系统服务,而且以单例的形式保存在 ArrayMap列表里。

    三、LayoutInflater布局加载流程

    下面我们来分析LayoutInflater加载布局的流程,看它的inflate方法:

    public View inflate ( @LayoutRes int resource, @Nullable ViewGroup root){
                return inflate(resource, root, root != null);
            }
    
            public View inflate ( @LayoutRes int resource, @Nullable ViewGroup root,
            boolean attachToRoot){
                // 获取用于资源加载的 Resources对象
                final Resources res = getContext().getResources();
    
                //注释 10, 用来根据布局文件的xml预编译生成dex,然后通过反射来生成对应的View,
               //从而减少XmlPullParser解析Xml的时间。
                View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
                if (view != null) {
                    return view;
                }
    
                // 获取 XmlResourceParser解析器,用于解析布局
                XmlResourceParser parser = res.getLayout(resource);
                try {
                    //注释 11, 开始加载布局并返回
                    return inflate(parser, root, attachToRoot);
                } finally {
                    parser.close();
                }
            }
    

    上面注释 10,首先根据布局文件的xml预编译生成dex,然后通过反射来生成对应的View。如果没有,则往下用 XmlResourceParser 解析加载。这个预编译的开关默认是false,只有在内部测试时打开,这里不再讲。我们继续往下看注释 11处inflate的重载方法:

    public View inflate (XmlPullParser parser, @Nullable ViewGroup root,boolean attachToRoot){
                synchronized (mConstructorArgs) {
                    // 获取属性集
                    final AttributeSet attrs = Xml.asAttributeSet(parser);
                    View result = root;
                    try {
                        // parser 移动到布局根节点处,获取根标签名称
                        advanceToRootNode(parser);
                        final String name = parser.getName();
                        //注释12, 判断 根标签是不是 “merge”
                        if (TAG_MERGE.equals(name)) {
                            // 是 merge标签,那就遍历布局并创建。通过递归解析加载布局
                            rInflate(parser, root, inflaterContext, attrs, false);
                        } else {
                            //注释 13, 不是 merge,则创建根标签 View
                            final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                            //省略......
    
                            //注释 14, 遍历布局文件,生成所有自View。通过递归解析加载布局
                            rInflateChildren(parser, temp, attrs, true);
                            //注释 15, 如果需要的话,把布局添加到父布局
                            if (root != null && attachToRoot) {
                                root.addView(temp, params);
                            }
                            // 返回 View
                            if (root == null || !attachToRoot) {
                                result = temp;
                            }
                        }
                        // 省略 ......
                        return result;
                    }
                }
    

    上面在创建布局的时候,会现在注释 12处判断布局的根节点标签是不是“merge”,如果是就会调用rInflate方法遍历标签下的所有布局,并递归地解析和加载布局。

    如果根标签不是“merge”,就会到注释13处开始创建布局,并在注释 14处同样采用递归的方式解析和加载子布局。然后在注释15的地方判断创建的布局是否加载到父布局中。也就是我们在调用布局加载参数时可以传入一个boolean参数,决定是否加入父布局。

    下面再继续看一下上面注释 13处,View是怎么创建的:

       View createViewFromTag (View parent, String name, Context context, AttributeSet attrs,
                boolean ignoreThemeAttr){
                    // ......
                    try {
                        // 注释16,尝试从 Factory中获取View
                        View view = tryCreateView(parent, name, context, attrs);
                        if (view == null) {
    
                            try {
                                //注释17 如果类名里存在'.',则说明布局里用的是全类名,说明这是自定义 View
                                // 否则 不带'.',说明用的不是全类名,是系统提供的View(默认前缀 android.widget)
                                if (-1 == name.indexOf('.')) {
                                    view = onCreateView(context, parent, name, attrs);
                                } else {
                                    view = createView(context, name, null, attrs);
                                }
                            } finally {
                                mConstructorArgs[0] = lastContext;
                            }
                        }
                        return view;
                        // ......
                    }
    
     public final View createView (@NonNull Context viewContext, @NonNull String name,
                            @Nullable String prefix, @Nullable AttributeSet attrs){
                        // ......
                        // 先从缓存中拿构造函数
                        Constructor<? extends View> constructor = sConstructorMap.get(name);
                        Class<? extends View> clazz = null;
                        try {
                            if (constructor == null) {
                                //注释 18 缓存中没有,那就通过反射创建构造函数,并把构造函数加入缓存
                                clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                                        mContext.getClassLoader()).asSubclass(View.class);
                                // ......
                                constructor = clazz.getConstructor(mConstructorSignature);
                                constructor.setAccessible(true);
                                sConstructorMap.put(name, constructor);
                            } else {
                                // If we have a filter, apply it to cached constructor
                            }
                                // .......
                            try {
                                // 用构造函数通过反射创建 View并返回
                                final View view = constructor.newInstance(args);
                                // ......
                                return view;
                            } finally {
                            }
                        }
                    }
    

    上面注释 16的地方,会首先尝试从Factory中获取View的对象,这个Factory可以人为地传入,这样我们就可以拦截到 View的创建过程。再往下看注释 18 的地方,到了View最终创建的地方。View的创建是通过反射的方式,并将构造器保存在缓存中,下次创建时直接在缓存中拿。

    我们再看看上面注释 16的地方 Factory的传入,我们在 activity的onCreate方法里可以以下面的方式传入Factory,这样就可以拦截到 View的创建:

            LayoutInflater layoutInflater = LayoutInflater.from(this);
            LayoutInflaterCompat.setFactory2(layoutInflater, new LayoutInflater.Factory2() {
                @Nullable
                @Override
                public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
                    // 可以创建View并返回
                    return null;
                }
    
                @Nullable
                @Override
                public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
                    // 可以创建View并返回
                    return null;
                }
            });
    

    好了布局的加载流程基本就这样了。稍微总结一下吧:

    • Activity的 setContentView方法不只是负责用 LayoutInflater 加载我们定义的布局,还加载了多层系统布局,比如 DecorView、ContentView等。

    • LayoutInflater 是系统服务。

    • rInflate和rInflateChildren方法都是通过递归解析并加载xml中的子布局。

    • 我们在使用 LayoutInflater 加载布局的时候,可以通过参数 attachToRoot选择是否将该布局加载到某个父布局。

    • 我们可以在 Activity的 onCreate方法里,在setContentView执行前,传入 Factory对象,这样可以拦截 View的创建。

    相关文章

      网友评论

          本文标题:setContentView()及LayoutInflater布

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