《Android 源码设计模式解析与实战》——第2章,第2.8节无名英雄——深入理解LayoutInflater

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介:

本节书摘来自异步社区《Android 源码设计模式解析与实战》一书中的第2章,第2.8节无名英雄——深入理解LayoutInflater,作者 何红辉 , 关爱民,更多章节内容可以访问云栖社区“异步社区”公众号查看

2.8 无名英雄——深入理解LayoutInflater
LayoutInflater在我们的开发中扮演着重要的角色,但很多时候我们都不知道它的重要性,因为它的重要性被隐藏在了Activity、Fragment等组件的光环之下。

LayoutInflater是一个抽象类,具体代码如下:

public abstract class LayoutInflater {
    // 代码省略
}

既然是抽象不是具体的,那我们必须把这个深藏功与名的“家伙”找出来!需要先从layoutInflater的起源开始。在上文中知道,在加载ContenxtImpl时会通过如下代码将LayoutInflater的ServiceFetcher注入到容器中,具体代码如下:

registerService(LAYOUT_INFLATER_SERVICE, new ServiceFetcher() {
     public Object createService(ContextImpl ctx) {
        return PolicyManager.makeNewLayoutInflater(ctx.getOuterContext());
        }
});

这里调用了PolicyManager. makeNewLayoutInflater方法,继续往下看看:

public final class PolicyManager {
    // Policy实现类
    private static final String POLICY_IMPL_CLASS_NAME =
        "com.android.internal.policy.impl.Policy";
    private static final IPolicy sPolicy;
    static {
        // 通过反射构造Policy对象
        try {
            Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME);
            sPolicy = (IPolicy)policyClass.newInstance();
        } 
        // catch 代码段
}
    private PolicyManager() {}
    // 这里就是创建PhoneWindow对象的地方
    public static Window makeNewWindow(Context context) {
        return sPolicy.makeNewWindow(context);
    }
    // 通过sPolicy创建LayoutInflater
    public static LayoutInflater makeNewLayoutInflater(Context context) {
        return sPolicy.makeNewLayoutInflater(context);
    }
}

PolicyManager中通过反射构造了Policy实现类,这个类实现了IPolicy接口,通过这种形式将具体的Policy类对外进行隐藏实现。PolicyManager实际上是一个代理类,具体的功能通过sPolicy对象进行实现,我们看看sPolicy对应的Policy类,也就是com.android.internal.policy.impl.Policy:

public class Policy implements IPolicy {
    // 代码省略
    // 创建PhoneWindow,这就是Activity中Window的具体实现类
    public Window makeNewWindow(Context context) {
        return new PhoneWindow(context);
    }
    // 创建LayoutInflater,具体类为PhoneLayoutInflater,这才是我们要关注的地方
    public LayoutInflater makeNewLayoutInflater(Context context) {
        return new PhoneLayoutInflater(context);
    }
}
此时,已经很清楚了,真正LayoutInflater的实现类就是PhoneLayoutInflater。我们继续深入看看PhoneLayoutInflater的源代码:

public class PhoneLayoutInflater extends LayoutInflater {
    // 内置View类型的前缀,如TextView的完整路径是android.widget.TextView
    private static final String[] sClassPrefixList = {
        "android.widget.",
        "android.webkit."
    };

    // 代码省略
    @Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNot FoundException {
        // 在View名字的前面添加前缀来构造View的完整路径,例如,类名为TextView,那么TextView完整  
        //路径是android.widget.TextView
        for (String prefix : sClassPrefixList) {
            try {
                View view = createView(name, prefix, attrs);
                if (view != null) {
                    return view;
                }
            } catch (ClassNotFoundException e) {
                // 省略
            }
        }
        return super.onCreateView(name, attrs);
    }
}

代码不多,核心的程序语句就是覆写了LayoutInflater的onCreateView方法,该方法就是在传递进来的View名字前面加上“android.widget.”或者“android.webkit.”前缀用以得到该内置View类(如TextView、Button等都在android.widget包下)的完整路径。最后,根据类的完整路径来构造对应的View对象。

具体是一个怎样的流程呢?以Activity的setContentView为例,先来看看这个函数的实现:

public void setContentView(View view) {
    getWindow().setContentView(view);
    initActionBar();
}

Activity的setContentView方法实际上调用的是Window的setContentView,而Window是一个抽象类,上文提到Window的具体实现类是PhoneWindow,我们看看PhoneWindow中对应的方法:

 @Override
public void setContentView(int layoutResID) {
        // 1. 当mContentParent为空时先构建DecorView
        // 并且将DecorView包裹到mContentParent中
        if (mContentParent == null) {
            installDecor();
        } else {
            mContentParent.removeAllViews();
        }
        // 2. 解析layoutResID
        mLayoutInflater.inflate(layoutResID, mContentParent);
    // 代码省略
    }

在分析之前,我们来看看一个Window的View层级图,如图2-2所示。


e6baf26e091fc0bd0141621b81474a92735e0934

从图2-2中,我们看到mDecor中会加载一个系统定义好的布局,这个布局中又包裹了mContentParent,而这个mContentParent就是我们设置的布局,并将添加到parent区域。在PhoneWindow的setContentView方法中也验证了这点,首先会构建mContentParent这个对象,然后通过LayoutInflater的inflate函数将指定布局的视图添加到mContentParent中。那么就先来看看inflate方法:

public View inflate(int resource, ViewGroup root) {
        // root不为空,则会从resource布局解析到View,并添加到root中
        return inflate(resource, root, root != null);
}

public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
        // 获取xml资源解析器
    XmlResourceParser parser = getContext().getResources().getLayout(resource);
    try {
            return inflate(parser, root, attachToRoot);
     } finally {
            parser.close();
        }
    }

// 参数1为xml解析器,参数2为要解析布局的父视图,参数3为是否将要解析的视图添加到父视图中
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context)mConstructorArgs[0];
            // Context对象
            mConstructorArgs[0] = mContext;
            // 存储父视图
            View result = root;
            try {
                // Look for the root node.
                int type;
                // 找到root元素
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }
                // 代码省略
                final String name = parser.getName();
                // 1. 解析merge标签
                if (TAG_MERGE.equals(name)) {
                    rInflate(parser, root, attrs, false);
                } else {
                    // 2. 不是merge标签那么直接解析布局中的视图
                    View temp;
                    if (TAG_1995.equals(name)) {
                        temp = new BlinkLayout(mContext, attrs);
                    } else {
                        // 3. 这里就是通过xml的tag来解析layout根视图
                        // name就是要解析的视图的类名,如RelativeLayout
                        temp = createViewFromTag(root, name, attrs);
                    }

                    ViewGroup.LayoutParams params = null;
                    if (root != null) {
                        // 生成布局参数
                        params = root.generateLayoutParams(attrs);
                        // 如果attachToRoot为false,那么将给temp设置布局参数
                        if (!attachToRoot) {
                            temp.setLayoutParams(params);
                        }
                    }
                    // 解析temp视图下的所有子View
                    rInflate(parser, temp, attrs, true);
                    // 如果Root不为空,且attachToRoot为true,那么将temp添加到父视图中
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }
                    // 如果root为空或者attachToRoot为false,那么返回的结果就是temp
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }
            } 
            // 省略catch, finaly代码
            return result;
        }

上述的inflate方法中,主要有下面几步:

(1)解析xml中的根标签(第一个元素);

(2)如果根标签是merge,那么调用rInflate进行解析,rInflate会将merge标签下的所有子View直接添加到根标签中;

(3)如果标签是普通元素,那么运行到代码3,调用createViewFromTag对该元素进行解析;

(4)调用rInflate解析temp根元素下的所有子View,并且将这些子View都添加到temp下;

(5)返回解析到的根视图。

我们先从简单的地方理解,即解析单个元素的createViewFromTag,看看如下代码:

 View createViewFromTag(View parent, String name, AttributeSet attrs) {
        if (name.equals("view")) {
            name = attrs.getAttributeValue(null, "class");
        }
        try {
            View view;
            // 1. 用户可以通过设置LayoutInflater的factory来自行解析View,默认这些Factory都为  
            //空, 可以忽略这段
            if (mFactory2 != null) view = mFactory2.onCreateView(parent, name, mContext, attrs);
            else if (mFactory != null) view = mFactory.onCreateView(name, mContext, attrs);
            else view = null;
            // 代码省略
            // 2. 没有Factory的情况下通过onCreateView或者createView创建View
            if (view == null) {
                // 3. 内置View控件的解析
                if (-1 == name.indexOf('.')) {
                    view = onCreateView(parent, name, attrs);
                } else {
                    // 4. 自定义控件的解析
                    view = createView(name, null, attrs);
                }
            }
            return view;

        } 
        // 省略catch块
    }

本程序重点就在代码2,以及以后的代码,createViewFromTag会将该元素的parent及名字传递过来。当这个tag的名字中没有包含“.”(在名字中查找“.”返回-1)时,LayoutInflater会认为这是一个内置的View,例如,我们在xml中声明一个内置View时大概是这样的:

<TextView
        android:id="@+id/my_textview "
        android:layout_width="60dp"
    android:layout_height="60dp" />

这里的TextView就是xml元素的名字,因此,在执行infate时就会调用代码3处的onCreateView来解析这个TextView标签。当我们自定义View时,在xml中必须写View的完整路径,例如:

<com.dp.custom.MyView
            android:id="@+id/my_custom_view "
            android:layout_width="fill_parent"
       android:layout_height="fill_parent" />

此时,就会调用代码注释4的createView来解析该View。为什么要这么处理,它们之间又有什么不同呢?

在上文的PhoneLayoutInflater中我们知道,PhoneLayoutInflater覆写了onCreateView方法,也就是代码3处的onCreateView,该方法就是在View标签名的前面设置一个“android.widget.”前缀,然后再传递给createView进行解析。也就是说内置View和自定义View最终都调用了createView进行解析,只是Google为了让开发者在xml中更方便定义View,只写View名称而不需要写完整的路径。在LayoutInflater解析时如果遇到只写类名的View,那么认为是内置的View控件,在 onCreateView方法中会将“android.widget.”前缀传递给createView方法,最后,在createView中构造View的完整路径来进行解析。如果是自定义控件,那么必须写完整的路径,此时调用createView且前缀为null进行解析。

关于createView的解释已经有很多,我们还是看下面的代码吧:

// 根据完整路径的类名通过反射机制构造View对象
    public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        // 1. 从缓存中获取构造函数
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        Class<? extends View> clazz = null;
        try {
            // 2. 没有缓存构造函数
            if (constructor == null) {
                // 如果prefix不为空,那么构造完整的View路径,并且加载该类
                clazz = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);
                // 代码省略
                // 3. 从Class对象中获取构造函数
                constructor = clazz.getConstructor(mConstructorSignature);
                // 4. 将构造函数存入缓存中
                sConstructorMap.put(name, constructor);
            } else {
                // 代码省略
            }
            Object[] args = mConstructorArgs;
            args[1] = attrs;
            // 5. 通过反射构造View
            final View view = constructor.newInstance(args);
            if (view instanceof ViewStub) {
                // always use ourselves when inflating ViewStub later
                final ViewStub viewStub = (ViewStub) view;
                viewStub.setLayoutInflater(this);
            }
            return view;

        } 
        // 省略各种catch、finaly代码
    }

createView相对比较简单,如果有前缀,那么构造View的完整路径,并且将该类加载到虚拟机中,然后获取该类的构造函数并且缓存起来,再通过构造函数来创建该View的对象,最后将View对象返回,这就是解析单个View的过程。而我们的窗口中是一棵视图树,LayoutInflater需要解析完这棵树,这个功能就交给了rInflate方法,具体代码如下:

void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs,
            boolean finishInflate) throws XmlPullParserException, IOException {
        // 1. 获取树的深度,深度优先遍历
        final int depth = parser.getDepth();
        int type;
        // 2. 挨个元素解析
        while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) 
         && type != XmlPullParser.END_DOCUMENT) {
            if (type != XmlPullParser.START_TAG) {
                continue;
            }
            final String name = parser.getName();
            if (TAG_REQUEST_FOCUS.equals(name)) {
                parseRequestFocus(parser, parent);
            } else if (TAG_INCLUDE.equals(name)) {  // 解析include标签
                parseInclude(parser, parent, attrs);
            } else if (TAG_MERGE.equals(name)) { // 解析merge标签,抛出异常,因为merge标签  
                                                        //必须为根视图
                throw new InflateException("<merge /> must be the root element");
            } else if (TAG_1995.equals(name)) {// 闪烁视图,这里可以不用管
// 代码省略
            } else {
                // 3. 根据元素名进行解析
                final View view = createViewFromTag(parent, name, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams (attrs);
                // 递归调用进行解析,也就是深度优先遍历
                rInflate(parser, view, attrs, true);
                // 将解析到的View添加到viewGroup中,也就是它的parent
                viewGroup.addView(view, params);
            }
        }
        if (finishInflate) parent.onFinishInflate();
    }

rInflate通过深度优先遍历来构造视图树,每解析到一个View元素就会递归调用rInflate,直到这条路径下的最后一个元素,然后再回溯过来将每个View元素添加到它们的parent中。通过rInflate的解析之后,整棵视图树就构建完毕。当调用了Activity的onResume之后,我们通过setContentView设置的内容就会出现在我们的视野中。

相关文章
|
23天前
|
Java 开发工具 Android开发
Android与iOS开发环境搭建全解析####
本文深入探讨了Android与iOS两大移动操作系统的开发环境搭建流程,旨在为初学者及有一定基础的开发者提供详尽指南。我们将从开发工具的选择、环境配置到第一个简单应用的创建,一步步引导读者步入移动应用开发的殿堂。无论你是Android Studio的新手还是Xcode的探索者,本文都将为你扫清开发道路上的障碍,助你快速上手并享受跨平台移动开发的乐趣。 ####
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
68 2
|
1天前
|
存储 缓存 算法
HashMap深度解析:从原理到实战
HashMap,作为Java集合框架中的一个核心组件,以其高效的键值对存储和检索机制,在软件开发中扮演着举足轻重的角色。作为一名资深的AI工程师,深入理解HashMap的原理、历史、业务场景以及实战应用,对于提升数据处理和算法实现的效率至关重要。本文将通过手绘结构图、流程图,结合Java代码示例,全方位解析HashMap,帮助读者从理论到实践全面掌握这一关键技术。
28 13
|
9天前
|
数据采集 DataWorks 搜索推荐
阿里云DataWorks深度评测:实战视角下的全方位解析
在数字化转型的大潮中,高效的数据处理与分析成为企业竞争的关键。本文深入评测阿里云DataWorks,从用户画像分析最佳实践、产品体验、与竞品对比及Data Studio公测体验等多角度,全面解析其功能优势与优化空间,为企业提供宝贵参考。
65 13
|
6天前
|
数据采集 存储 JavaScript
网页爬虫技术全解析:从基础到实战
在信息爆炸的时代,网页爬虫作为数据采集的重要工具,已成为数据科学家、研究人员和开发者不可或缺的技术。本文全面解析网页爬虫的基础概念、工作原理、技术栈与工具,以及实战案例,探讨其合法性与道德问题,分享爬虫设计与实现的详细步骤,介绍优化与维护的方法,应对反爬虫机制、动态内容加载等挑战,旨在帮助读者深入理解并合理运用网页爬虫技术。
|
16天前
|
存储 Linux API
深入探索Android系统架构:从内核到应用层的全面解析
本文旨在为读者提供一份详尽的Android系统架构分析,从底层的Linux内核到顶层的应用程序框架。我们将探讨Android系统的模块化设计、各层之间的交互机制以及它们如何共同协作以支持丰富多样的应用生态。通过本篇文章,开发者和爱好者可以更深入理解Android平台的工作原理,从而优化开发流程和提升应用性能。
|
12天前
|
存储 监控 调度
云服务器成本优化深度解析与实战案例
本文深入探讨了云服务器成本优化的策略与实践,涵盖基本原则、具体策略及案例分析。基本原则包括以实际需求为导向、动态调整资源、成本控制为核心。具体策略涉及选择合适计费模式、优化资源配置、存储与网络配置、实施资源监控与审计、应用性能优化、利用优惠政策及考虑多云策略。文章还通过电商、制造企业和初创团队的实际案例,展示了云服务器成本优化的有效性,最后展望了未来的发展趋势,包括智能化优化、多云管理和绿色节能。
|
13天前
|
PyTorch Shell API
Ascend Extension for PyTorch的源码解析
本文介绍了Ascend对PyTorch代码的适配过程,包括源码下载、编译步骤及常见问题,详细解析了torch-npu编译后的文件结构和三种实现昇腾NPU算子调用的方式:通过torch的register方式、定义算子方式和API重定向映射方式。这对于开发者理解和使用Ascend平台上的PyTorch具有重要指导意义。
|
18天前
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
47 12
|
16天前
|
Java 调度 Android开发
安卓与iOS开发中的线程管理差异解析
在移动应用开发的广阔天地中,安卓和iOS两大平台各自拥有独特的魅力。如同东西方文化的差异,它们在处理多线程任务时也展现出不同的哲学。本文将带你穿梭于这两个平台之间,比较它们在线程管理上的核心理念、实现方式及性能考量,助你成为跨平台的编程高手。

推荐镜像

更多