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) 同时完成两件事------计算差异位掩码和更新配置快照。这意味着 mConfigurationupdateConfiguration() 返回前就已经被更新为新配置,后续的 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++ 引擎
缓存策略 ResourcesManagerResourcesKey(含 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),各版本可能存在差异,请以实际代码为准。

相关推荐
Android-Flutter5 小时前
Android 内存泄漏详解
android·kotlin
Android-Flutter5 小时前
android Handler , Looper , Message , MessageQueue 详解
android·kotlin
雨声不在6 小时前
docker-android 重启后镜像起不来
android·docker·容器
阿pin6 小时前
Android随笔-Framework
android·framework
达达尼昂6 小时前
Flutter AI Harness 如何让 Agent 参与软件开发全流程
android·人工智能·后端
sRect6 小时前
用ESP32-S3开发板开发一个接收消息的BB机
android·serverless·嵌入式
AFinalStone7 小时前
Android 7系统无障碍服务(二)AccessibilityManagerService 启动与初始化
android·无障碍服务
小孔龙8 小时前
BufferQueue 多缓冲与背压
android
阿pin8 小时前
Android随笔-kotlin Flow
android·kotlin·flow