故事:Resources的“寻宝”之旅

想象一下,你是一位乐高大师(Activity ),要搭建一个酷炫的模型(UI界面 )。你手里有一本厚厚的说明书(APK文件),里面包含了所有乐高积木的清单和拼装步骤。

  1. 宝藏地图(Resources): 这本说明书不是随便翻的,它需要一个专业的"地图管理员"来帮你查找。这个管理员就是 Resources 类。它不是一个简单的目录,而是一个超级智能的系统,知道去哪里找到你需要的任何积木(字符串、图片、布局等)。
  2. 积木编号(R.layout.xxx): 你不能直接对管理员说"我要那个红色的长方形积木",你得使用一个唯一的编号,比如 R.layout.activity_main。这个编号就像是说明书里的精确页码。
  3. 仓库总管(AssetManager): 地图管理员(Resources)自己并不直接拿着说明书,他有一个更底层的"仓库总管"叫 AssetManager。这个总管才真正拥有打开APK文件(说明书)并读取里面原始数据的能力。
  4. 变形金刚(LayoutInflater): 当你通过地图管理员和仓库总管拿到了"拼装步骤图"(layout的XML文件)时,它只是一张纸(文本),还不是真正的乐高模型。这时就需要一个"变形金刚"------LayoutInflater 。它的魔法就是把纸上的描述(<TextView>)变成实实在在的、可以活动的乐高积木(View对象)。

详细探险:代码级别的寻宝之旅

现在,让我们穿上代码的探险服,跟着 setContentView(R.layout.activity_main) 这句咒语,开始冒险!

第一站:Activity的指令

java 复制代码
// 在Activity中
public void setContentView(int layoutResID) {
    getWindow().setContentView(layoutResID);
}
  • getWindow() 返回的是 PhoneWindow 对象,它是 Window 抽象类的具体实现。你可以把 PhoneWindow 想象成你工作用的"桌面"。

第二站:PhoneWindow的准备

PhoneWindow.setContentView(layoutResID) 做了几件关键事:

  1. 检查桌面是否干净: 它首先检查是否已经有一个叫 DecorView 的"基础底板"了。如果没有,就创建一个。DecorView 是整个窗口的根视图,它包含了系统UI(如标题栏)和我们自己的内容。
  2. 寻找内容区域: 它在 DecorView 里找到一个特定的区域叫 mContentParent(一个 ViewGroup,通常是 FrameLayout),我们的布局最终就要放在这里。
  3. 呼叫变形金刚!: 最关键的一步来了!
java 复制代码
// 简化版的PhoneWindow代码
public void setContentView(int layoutResID) {
    // 1. 创建或获取DecorView
    if (mContentParent == null) {
        installDecor();
    }
    // 2. 清空内容区域(如果需要)
    mContentParent.removeAllViews();
    // 3. 核心魔法:将布局资源ID"膨胀"成真正的View树,并添加到mContentParent
    mLayoutInflater.inflate(layoutResID, mContentParent);
}

第三站:LayoutInflater的魔法核心

LayoutInflater.inflate(resId, root) 是整个流程的灵魂。我们深入看看 inflate 方法:

java 复制代码
public View inflate(int resource, ViewGroup root) {
    return inflate(resource, root, root != null);
}

public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
    // 重点!通过Resources获取XML的解析器(XmlResourceParser)
    XmlResourceParser parser = getContext().getResources().getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}

看!getContext().getResources().getLayout(resource)!这里,我们终于见到了故事里的"地图管理员"------Resources

第四站:Resources与AssetManager的联手

Resources.getLayout(int id) 方法:

java 复制代码
public XmlResourceParser getLayout(int id) throws NotFoundException {
    return loadXmlResourceParser(id, "layout");
}

它调用了 loadXmlResourceParser,这里发生了更神奇的事情:

java 复制代码
XmlResourceParser loadXmlResourceParser(int id, String type) throws NotFoundException {
    // 1. 创建一个"价值容器" TypedValue,用于存放查询结果
    TypedValue value = new TypedValue();
    // 2. 核心查询!通过id获取资源的具体信息
    getValue(id, value, true);
    // 3. 检查找到的是不是我们想要的XML文件
    if (value.type == TypedValue.TYPE_STRING) {
        // 4. 让AssetManager去打开这个具体的文件!
        return loadXmlResourceParser(value.string.toString(), id, value.assetCookie, type);
    }
    throw new NotFoundException("Resource ID does not point to an XML file");
}
  • getValue(id, value, true):这是资源查找的核心。Resources对象内部维护着一个复杂的资源表结构(在APK打包时由AAPT2工具生成)。它通过 id 这个索引,快速查找到资源在APK中的具体位置、文件名、配置等信息,并填充到 TypedValue 对象中。这里的 assetCookie 可以理解为对应APK文件的句柄。

最终,Resources 会调用到它的得力助手------AssetManager 的Native方法(如 nativeOpenXmlBlockAsset),去真正地打开APK文件(ZIP格式),解析并读取对应的XML文件(如 res/layout/activity_main.xml),返回一个 XmlResourceParser 对象。

简单比喻:

  • Resources 像是一个聪明的图书管理员,他知道每本书(资源)在哪个书架(APK)、第几层(资源类型)、第几本(资源ID)。
  • AssetManager 则是图书馆的仓库机器人,拥有打开仓库门(APK文件)并取出具体书籍(资源文件)的能力。

第五站:LayoutInflater的解析与创建

现在,LayoutInflater 拿到了 XmlResourceParser(一个XML解析器)。它开始像读流水线一样解析XML:

java 复制代码
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
    // ... 循环解析XML的每一个节点 ...
    while (parser.next() != XmlPullParser.END_TAG) {
        if (parser.getEventType() != XmlPullParser.START_TAG) {
            continue;
        }
        final String name = parser.getName();
        // 1. 如果是 <merge> 标签,特殊处理
        if (TAG_MERGE.equals(name)) {
            // ... 处理merge ...
        } else {
            // 2. 核心:根据标签名创建View对象!
            final View view = createViewFromTag(root, name, inflaterContext, attrs);
            final ViewGroup.LayoutParams params = root.generateLayoutParams(attrs);
            // 3. 递归调用!如果当前View是ViewGroup,继续inflate它的子View
            if (view instanceof ViewGroup) {
                inflateChildren(parser, (ViewGroup) view, attrs, true);
            }
            // 4. 将创建好的View添加到父容器中
            root.addView(view, params);
        }
    }
    // ...
}

createViewFromTag 是另一个魔法点:

java 复制代码
View createViewFromTag(/* ... */) {
    // ... 尝试通过Factory创建,这是插件化、换肤的关键hook点 ...
    View view;
    if (mFactory2 != null) {
        view = mFactory2.onCreateView(name, context, attrs);
    }
    // ... 其他Factory尝试 ...

    if (view == null) {
        // 没有Factory干预,使用最原始的方式创建
        if (-1 == name.indexOf('.')) {
            // 如果是系统控件,如 <TextView>
            view = onCreateView(context, name, attrs);
        } else {
            // 如果是自定义控件,如 <com.example.MyView>
            view = createView(context, name, null, attrs);
        }
    }
    return view;
}

最终,createView 会通过反射调用类的构造函数(如 new TextView(context, attrs))来创建View对象!这样,一个冰冷的XML标签就变成了一个有血有肉、可以在屏幕上绘制的Java对象。


寻宝全过程时序图

下面这张时序图清晰地展示了整个调用链条:

总结

整个 Resources 获取 layout 的过程,就像一场精心策划的寻宝行动:

  1. 发起请求: Activity 通过 setContentView 发出指令。
  2. 准备舞台: PhoneWindow 准备好承载内容的根容器 DecorViewmContentParent
  3. 资源定位: LayoutInflater 求助 ResourcesResources 利用内部的资源映射表,通过ID快速定位到XML文件的具体信息。
  4. 文件读取: Resources 派遣 AssetManager,通过Native方法深入到APK包中,将XML文件读取出来,变成一个可解析的流。
  5. 视图创造: LayoutInflater 开始解析XML流,遇到一个标签,就通过反射 实例化对应的View类,并设置属性。如果这个View是容器(ViewGroup),就递归地重复这个过程,最终构建出一棵完整的View树。
  6. 最终呈现: 这棵View树被添加到 PhoneWindowmContentParent 中,经过测量(Measure)、布局(Layout)、绘制(Draw)后,就呈现在了用户面前。

理解这个过程,对于处理界面卡顿(优化布局层级)、实现插件化(hook LayoutInflaterFactory)、换肤等高级技术至关重要。希望这个有趣的故事和详细的代码分析能帮助你彻底掌握它!

相关推荐
雮尘2 小时前
一文读懂Android Fragment栈管理
android·前端
带娃的IT创业者2 小时前
从零构建智能HTML转Markdown转换器:Python GUI开发实战
android·python·html
灿烂阳光g4 小时前
HWC的软硬件架构
android
2501_915921434 小时前
Charles 抓包 HTTPS 原理详解,从 CONNECT 到 SSL Proxying、常见问题与真机调试实战(含 Sniffmaster 补充方案)
android·网络协议·小程序·https·uni-app·iphone·ssl
冬天vs不冷5 小时前
Java基础(十三):内部类详解
android·java·python
AKA5 小时前
Bugly的使用
android·android studio
落叶霞枫6 小时前
【uniapp安卓原生语言插件】之华为统一扫码插件【保姆级】开发教程。
android·uni-app·android studio
wodongx1236 小时前
从零开始部署Android环境的Jenkins CI/CD流水线(docker环境,Win系统)
android·docker·jenkins
loitawu6 小时前
Rockchip平台 Android 11 到 Android 16 系统占用内存对比分析
android·ddr·内存占用·rockhip·android内存