Android 7系统国际化(七)Resources 与 AssetManager 源码剖析

系列目录 :第一篇:全景图与调用链路概览 | 第二篇:Locale 与 LocaleList | 第三篇:Configuration | 第四篇:LocalePicker | 第五篇:ActivityManagerService | 第六篇:ActivityThread | 第七篇:Resources 与 AssetManager | 第八篇:总结与实战建议


一、为什么这是整个系列的核心

前六篇追踪了语言偏好如何从用户指尖传到 AMS,再传回应用进程。但"传回去之后呢?"------应用代码里写了 getString(R.string.hello),Configuration 的 locale 从 zh-CN 变成了 en-US,返回的字符串怎么就从"你好"变成了"Hello"?

答案就在 Resources / ResourcesImpl / ResourcesManager / AssetManager 这一套资源管理体系中。这是整个国际化链路的"最后一公里"------也是最硬核的一公里。


二、四层资源管理架构

复制代码
┌──────────────────────────────────────────────────────────┐
│  Resources.java          ← 公共 API(开发者直接使用的类) │
│  getString(), getDrawable(), getDimension()...            │
├──────────────────────────────────────────────────────────┤
│  ResourcesImpl.java      ← 核心逻辑实现                   │
│  配置差异计算、缓存清除、AssetManager 配置下发             │
├──────────────────────────────────────────────────────────┤
│  ResourcesManager.java   ← 全局单例,管理 Resources 缓存  │
│  Map<ResourcesKey, WeakReference<ResourcesImpl>> 缓存池   │
├──────────────────────────────────────────────────────────┤
│  AssetManager.java       ← Java 到 Native 的桥梁          │
├──────────────────────────────────────────────────────────┤
│  AssetManager.cpp        ← Native 资源索引与查询          │
│  ResourceTypes.cpp       ← resources.arsc 二进制解析      │
├──────────────────────────────────────────────────────────┤
│  resources.arsc           ← APK 中的资源索引表             │
│  res/values/strings.xml, res/values-zh-rCN/strings.xml... │
└──────────────────────────────────────────────────────────┘

源码路径:

  • frameworks/base/core/java/android/content/res/Resources.java
  • frameworks/base/core/java/android/content/res/ResourcesImpl.java
  • frameworks/base/core/java/android/app/ResourcesManager.java
  • frameworks/base/core/java/android/content/res/AssetManager.java
  • frameworks/base/libs/androidfw/AssetManager.cpp

三、ResourcesManager ------ 全局缓存池

ResourcesManager 是应用进程中的全局单例,管理所有 ResourcesImpl 实例的缓存。当语言切换后,它负责创建或复用对应语言配置的 ResourcesImpl。

源码路径 :frameworks/base/core/java/android/app/ResourcesManager.java

java 复制代码
public class ResourcesManager {
    // 以 ResourcesKey 为 key 的缓存池(WeakReference 允许 GC 回收)
    private final ArrayMap<ResourcesKey, WeakReference<ResourcesImpl>> mResourceImpls =
            new ArrayMap<>();

    /**
     * 获取或创建 Resources 对象。
     * 先查缓存,缓存未命中时创建新的 ResourcesImpl。
     */
    public Resources getOrCreateResources(IBinder activityToken,
            ResourcesKey key, ClassLoader classLoader) {
        synchronized (this) {
            // 1. 查缓存
            ResourcesImpl impl = findResourcesImplForKeyLocked(key);
            if (impl != null) {
                return getOrCreateResourcesForActivityLocked(
                        activityToken, classLoader, impl);
            }

            // 2. 缓存未命中:创建新的 ResourcesImpl
            impl = createResourcesImpl(key);
            mResourceImpls.put(key, new WeakReference<>(impl));
            return getOrCreateResourcesForActivityLocked(
                    activityToken, classLoader, impl);
        }
    }

    /**
     * 配置变更时,更新所有缓存的 ResourcesImpl。
     * 这是 ActivityThread.handleConfigurationChanged 调用的方法。
     */
    public final boolean applyConfigurationToResourcesLocked(
            @NonNull Configuration config, @Nullable CompatibilityInfo compat) {
        // 遍历所有缓存的 ResourcesImpl,逐个更新
        for (int i = mResourceImpls.size() - 1; i >= 0; i--) {
            ResourcesKey key = mResourceImpls.keyAt(i);
            ResourcesImpl r = mResourceImpls.valueAt(i).get();
            if (r != null) {
                r.updateConfiguration(config, null, compat);
            } else {
                mResourceImpls.removeAt(i);  // 已被 GC 回收的清理掉
            }
        }
        return true;
    }
}

关键设计 :ResourcesKey 包含 mOverrideConfiguration(其中含有 locale 信息)。当用户切换语言后,新的 Configuration 产生新的 key,findResourcesImplForKeyLocked() 查不到缓存 → createResourcesImpl() 创建新的 ResourcesImpl → 内部 AssetManager 用新 locale 重新索引资源。由于 WeakReference 的存在,旧 ResourcesImpl 在无引用后会被 GC 回收。


四、Resources ------ 开发者视角的门面

源码路径 :frameworks/base/core/java/android/content/res/Resources.java

java 复制代码
public class Resources {
    private ResourcesImpl mResourcesImpl;  // 委托给 ResourcesImpl

    public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
        mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
    }

    @NonNull
    public String getString(@StringRes int id) throws NotFoundException {
        return getText(id).toString();  // 委托给 getText()
    }

    public void updateConfiguration(Configuration config, DisplayMetrics metrics,
            CompatibilityInfo compat) {
        mResourcesImpl.updateConfiguration(config, metrics, compat);
    }
}

关键设计 :Resources 是门面(Facade),ResourcesImpl 是核心实现。从 Android 7 开始这个分离逐渐强化------Resources 保持稳定的公共 API,ResourcesImpl 独立演化。getString() 最终通过 mResourcesImpl.getAssets().getResourceText(id) 走到 Native 层。


五、ResourcesImpl ------ 配置更新的终点

ResourcesImpl 是资源管理体系中真正"干活"的类。配置变更到达这里后,完成缓存清除、AssetManager 配置下发等关键操作。

源码路径 :frameworks/base/core/java/android/content/res/ResourcesImpl.java

java 复制代码
public class ResourcesImpl {
    private final AssetManager mAssets;         // Native 资源管理器
    private final Configuration mConfiguration;  // 当前配置快照
    // 各种资源缓存
    private DrawableCache mDrawableCache;
    private ConfigurationBoundResourceCache<Animator> mAnimatorCache;
    private ConfigurationBoundResourceCache<StateListAnimator> mStateListAnimatorCache;

    public void updateConfiguration(Configuration config, DisplayMetrics metrics,
            CompatibilityInfo compat) {
        synchronized (mAccessLock) {
            // 1. 计算实际配置变更
            final @Config int configChanges = calcConfigChanges(config);

            // 2. 多语言匹配:从多语言列表中选取最佳匹配
            LocaleList locales = mConfiguration.getLocales();
            if (locales.isEmpty()) {
                locales = LocaleList.getDefault();
                mConfiguration.setLocales(locales);
            }
            if ((configChanges & ActivityInfo.CONFIG_LOCALE) != 0) {
                if (locales.size() > 1) {
                    String[] availableLocales = mAssets.getNonSystemLocales();
                    if (LocaleList.isPseudoLocalesOnly(availableLocales)) {
                        availableLocales = mAssets.getLocales();
                    }
                    if (availableLocales != null) {
                        final Locale bestLocale = locales.getFirstMatchWithEnglishSupported(
                                availableLocales);
                        if (bestLocale != null && bestLocale != locales.get(0)) {
                            mConfiguration.setLocales(
                                    new LocaleList(bestLocale, locales));
                        }
                    }
                }
            }

            // 3. ⭐ 关键:将配置下发给 Native 层 AssetManager
            mAssets.setConfiguration(
                    mConfiguration.mcc, mConfiguration.mnc,
                    adjustLanguageTag(
                            mConfiguration.getLocales().get(0).toLanguageTag()),
                    mConfiguration.orientation,
                    mConfiguration.touchscreen,
                    mConfiguration.densityDpi, mConfiguration.keyboard,
                    keyboardHidden, mConfiguration.navigation, width, height,
                    mConfiguration.smallestScreenWidthDp,
                    mConfiguration.screenWidthDp, mConfiguration.screenHeightDp,
                    mConfiguration.screenLayout, mConfiguration.uiMode,
                    Build.VERSION.RESOURCES_SDK_INT);

            // 4. 清除与配置变更相关的缓存
            mDrawableCache.onConfigurationChange(configChanges);
            mColorDrawableCache.onConfigurationChange(configChanges);
            mAnimatorCache.onConfigurationChange(configChanges);
            mStateListAnimatorCache.onConfigurationChange(configChanges);
        }
    }
}

关键设计 :calcConfigChanges() 会调用 mConfiguration.updateFrom(mTmpConfig) 同时完成两件事------计算差异位掩码和更新配置快照。这意味着 mConfiguration 在 updateConfiguration() 返回前就已经被更新为新配置,后续的 setConfiguration() 使用的是新配置值。
关键设计 :多语言匹配逻辑在这里再次出现------getFirstMatchWithEnglishSupported() 从用户的多语言列表中选取应用实际有翻译资源的最佳匹配语言。如果 App 只翻译了 zh-CN 而用户首选是 zh-TW,系统会降级到 zh-CN。


六、AssetManager ------ Java 到 Native 的桥梁

源码路径 :frameworks/base/core/java/android/content/res/AssetManager.java

java 复制代码
public final class AssetManager implements AutoCloseable {
    private long mObject;  // Native AssetManager 的 C++ 指针

    public AssetManager() {
        mObject = nativeCreate();
    }

    // 将配置下发给 Native 层(16 个参数逐一传递)
    public native final void setConfiguration(int mcc, int mnc, String locale,
            int orientation, int touchscreen, int density, int keyboard,
            int keyboardHidden, int navigation, int screenWidth, int screenHeight,
            int smallestScreenWidthDp, int screenWidthDp, int screenHeightDp,
            int screenLayout, int uiMode, int majorVersion);

    // 查询资源文本
    /*package*/ native final CharSequence getResourceText(int ident);

    // 获取系统 ROM 中所有已翻译的语言列表
    public native final String[] getLocales();

    private native long nativeCreate();
}

关键设计 :setConfiguration() 是 Native 方法(JNI),16 个参数逐一传递。getResourceText() 内部将资源 ID 传给 C++ 层查询,返回的 CharSequence 直接从字符串池中取出。


七、Native 层:AssetManager 的资源表索引

C++ 层 AssetManager 是资源匹配的真正引擎。

源码路径 :frameworks/base/libs/androidfw/AssetManager.cpp

7.1 核心数据结构

cpp 复制代码
class AssetManager : public AaptAssetManager {
    ResTable* mResources;            // 资源表
    ResTable_config mConfig;         // 当前配置(Native 版 Configuration)
    Vector<asset_path> mAssetPaths;  // 已加载的资源包列表
};

ResTable_config 是 Configuration 的 Native 对应结构体:

源码路径 :frameworks/base/libs/androidfw/ResourceTypes.h

cpp 复制代码
struct ResTable_config {
    uint32_t size;
    union {
        struct {
            uint16_t mcc;
            uint16_t mnc;
            char language[2];        // ⭐ "zh", "en", "ja"
            char country[2];         // ⭐ "CN", "US", "JP"
            uint8_t orientation;
            uint8_t touchscreen;
            uint16_t density;
            uint8_t keyboard;
            uint8_t navigation;
            uint16_t screenWidth;
            uint16_t screenHeight;
            uint16_t sdkVersion;
            uint16_t minorVersion;
            uint8_t screenLayout;
            uint8_t uiMode;
            uint16_t smallestScreenWidthDp;
            uint16_t screenWidthDp;
            uint16_t screenHeightDp;
            char localeScript[4];    // ⭐ "Hans", "Hant"
            char localeVariant[8];   // ⭐ 变体
        };
    };
};

关键设计 :language[2] 是 2 字节------对 "en"、"zh" 两字节语言刚好。三字节语言(如 Filipino "fil")使用 localeScript 字段存储。

7.2 setConfiguration()

源码路径 :frameworks/base/libs/androidfw/AssetManager.cpp

cpp 复制代码
void AssetManager::setConfiguration(const ResTable_config& config) {
    const_cast<ResTable_config&>(mConfig) = config;

    const size_t N = mAssetPaths.size();
    for (size_t i = 0; i < N; i++) {
        mAssetPaths[i].resTable->setParameters(&mConfig);
    }
}

关键设计 :遍历所有已加载的资源包(APK),将新配置传给每个 ResTable,使其在下次查询时用新配置进行匹配。


八、资源匹配算法 ------ 以 getString 为例

复制代码
R.string.hello (资源ID = 0x7f040001)
        │
        ▼
    ResTable::getEntry(entryIndex)
        │  根据资源 ID 找到 ResTable_entry
        │  该 entry 包含多个 config 变体:
        │
        ▼
    遍历该 entry 的所有 config 变体(按优先级排列):
        ├── config_0: language="",   country=""      → values/strings.xml
        ├── config_1: language="en", country=""      → values-en/strings.xml
        ├── config_2: language="en", country="US"    → values-en-rUS/strings.xml
        ├── config_3: language="zh", country=""      → values-zh/strings.xml
        └── config_4: language="zh", country="CN"    → values-zh-rCN/strings.xml
                │
                ▼
    ResTable_config::match(mConfig) 匹配算法:
        对每个候选 config,计算与当前 mConfig 的匹配度
                │
                ├── language 完全匹配 → +高优先分
                │   language 为空(通配)→ +低优先分
                │   language 不匹配 → 排除
                │
                ├── country 完全匹配 → +分
                │   country 为空 → +低分
                │   country 不匹配 → 排除
                │
                ├── script 匹配 → +分
                │
                └── 任何维度的值不兼容且非空 → 直接排除
                │
                ▼
    选择匹配分最高的 config
        │  并列最高分 → 选择在 resources.arsc 中先出现的
        │
        ▼
    从该 config 对应的资源池中取出实际值
        │  config_4 (zh-CN) 的字符串池中 → "你好"
        │
        ▼
    返回 "你好" → CharSequence

8.1 匹配规则的精髓

关键在于排除 vs 降级的区别:

  • 如果候选 config 的 language 是 "en" 而请求是 "zh":直接排除
  • 如果候选 config 的 country 是 ""(空)而请求是 "CN":不排除,降级匹配
  • 如果候选 config 的 language 是 ""(通配默认值):永远可匹配,优先级最低

关键设计 :这解释了为什么 values/strings.xml(无语言后缀)永远能匹配上------它是最终兜底的默认值。


九、resources.arsc ------ 编译后的资源索引表

resources.arsc 由 AAPT 在编译期生成,是二进制格式的资源索引:

复制代码
resources.arsc
├── ResTable_header          # 文件头(包数量)
├── StringPool               # 全局字符串池
│   ├── "hello"
│   ├── "app_name"
│   └── ...
│
├── ResTable_package (packageId = 0x7f)  # 应用资源包
│   ├── packageName = "com.example.app"
│   │
│   ├── TypeSpec (typeId = 0x04) → strings
│   │   ├── entry(0x0001) "hello" → 指向多个 variant
│   │   │   ├── variant_0: config(values)     → StringPool offset → "Hello"
│   │   │   ├── variant_1: config(values-zh-rCN) → StringPool offset → "你好"
│   │   │   └── variant_2: config(values-ja)  → StringPool offset → "こんにちは"
│   │   └── ...
│   │
│   ├── TypeSpec (typeId = 0x02) → drawables
│   └── ...
│
├── ResTable_package (packageId = 0x01)  # 系统资源包
│   └── ...
│
└── StringPool 其他字符串池...

编译前(XML)→ 编译后(arsc)的对应关系:

复制代码
编译前:                             编译后(resources.arsc):
─────────                            ─────────────────────────
res/values/strings.xml               StringPool offset 42: "Hello"
  <string name="hello">Hello</string>

res/values-zh-rCN/strings.xml        ResTable_entry "hello" {
  <string name="hello">你好</string>   configs: [
                                          {locale:"",       value@42},
                                          {locale:"zh-CN",  value@128},
res/values-ja/strings.xml               {locale:"ja",      value@256}
  <string name="hello">こんにちは</string> ]
                                      }

9.1 资源 ID 编码

每个资源 ID(如 0x7f040001)编码了完整信息:

复制代码
0x 7f 04 0001
   │  │  └── entry index(条目编号)
   │  └── type index(04=string, 02=drawable...)
   └── package id(0x7f=应用, 0x01=系统 framework-res)

十、完整数据流回顾

复制代码
用户点击"English"
    │
    ▼
[第四篇] LocalePicker.updateLocales(LocaleList("en-US"))
    │  config.setLocales(locales); config.userSetLocale = true
    │
    ▼
[第五篇] AMS.updatePersistentConfiguration(config)
    │  权限检查 → updateConfigurationLocked(persistent=true)
    │  SystemProperties.set("persist.sys.locale", "en-US")
    │  mSystemThread.applyConfigurationToResources() ← system_server 自身先更新
    │  遍历 mLruProcesses → scheduleConfigurationChanged(configCopy)
    │
    ▼
[第六篇] ActivityThread.handleConfigurationChanged(config)
    │  mResourcesManager.applyConfigurationToResourcesLocked(config, compat)
    │
    ▼
[第七篇] ResourcesManager.applyConfigurationToResourcesLocked(config, compat)
    │  遍历所有缓存的 ResourcesImpl → 逐个调用 updateConfiguration()
    │
    ▼
ResourcesImpl.updateConfiguration(config, ...)
    │  calcConfigChanges(config)    ← 计算差异并更新配置快照
    │  getFirstMatchWithEnglishSupported  ← 最佳语言匹配
    │  mAssets.setConfiguration(mcc, mnc, "en-US", ...)  ← 下发配置给 Native
    │  mDrawableCache.onConfigurationChange(configChanges)  ← 清除缓存
    │
    ▼
[Native] AssetManager.setConfiguration(config)
    │  ResTable::setParameters(&config)  ← 更新所有资源包的匹配参数
    │
    ▼
下次 getString(R.string.hello) 或 inflate XML 时:
    │  ResTable::getEntry() → 根据新 locale 重新匹配 config variant
    │  → 从新语言的字符串池中取出 "Hello"
    │  → 返回给 Java 层
    │
    ▼
getString(R.string.hello) → "Hello"(而非"你好")

十一、小结

要点 说明
资源管理架构 Resources(门面)→ ResourcesImpl(实现)→ AssetManager(JNI 桥)→ C++ 引擎
缓存策略 ResourcesManager 以 ResourcesKey(含 Configuration)为 key 缓存,WeakReference 允许 GC 回收
配置变更响应 ResourcesManager.applyConfigurationToResourcesLocked() 遍历所有缓存的 ResourcesImpl 逐个更新
配置传递终点 ResourcesImpl.updateConfiguration() → mAssets.setConfiguration()
多语言匹配 getFirstMatchWithEnglishSupported() 从多语言列表中选取最佳匹配
缓存清除 onConfigurationChange(configChanges) 传入差异位掩码,按变更类型选择性清除
Native 匹配 ResTable_config::match() 算法:language → country → script 逐级打分
兜底机制 values/ 目录(无语言后缀)永远可匹配,优先级最低

下一篇预告:第八篇(本系列最后一篇)将回顾完整调用链,总结各层关键设计思想,并给出应用层的实战建议。


本系列源码基于 AOSP 7.x(API 24/25),各版本可能存在差异,请以实际代码为准。

相关推荐
聚美智数6 小时前
智能结构化识别-图片结构解析-图像结构化解析-照片结构化解析
android·java·数据库
mmsx11 小时前
Android 插件化架构:40 个 Gradle 模块的拆分、通信与共存
android·kotlin
mmsx11 小时前
Android 不改一行源码:用 AspectJ 做三类拦截
android·kotlin
TST-魚爷11 小时前
易视讯会议摄像头 + 全向麦克风实操讲解稿
android·电脑·钉钉·腾讯会议·zoom会议
嘟哩DuliDuli12 小时前
AI创作工作区的项目上下文与多模态任务边界
android·人工智能·安全·ai·软件工程
imDwAaY13 小时前
MySQL 存储引擎是什么?
android·数据库·mysql
Meteors.14 小时前
Android Compose学习02:基础组件与 Modifier
android·学习
其实防守也摸鱼15 小时前
存储管理:页面置换算法学习任务(测验题要求给出置换页面顺序,缺页次数,缺页率)
android·前端·数据库·学习·oracle·自动化
mmsx17 小时前
Android 地图上绘制 KML:解析、样式与渲染
android·xml
运行时异常17 小时前
【无标题】
android