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.javaframeworks/base/core/java/android/content/res/ResourcesImpl.javaframeworks/base/core/java/android/app/ResourcesManager.javaframeworks/base/core/java/android/content/res/AssetManager.javaframeworks/base/libs/androidfw/AssetManager.cpp
三、ResourcesManager —— 全局缓存池
ResourcesManager是应用进程中的全局单例,管理所有ResourcesImpl实例的缓存。当语言切换后,它负责创建或复用对应语言配置的ResourcesImpl。
源码路径:frameworks/base/core/java/android/app/ResourcesManager.java
publicclassResourcesManager{// 以 ResourcesKey 为 key 的缓存池(WeakReference 允许 GC 回收)privatefinalArrayMap<ResourcesKey,WeakReference<ResourcesImpl>>mResourceImpls=newArrayMap<>();/** * 获取或创建 Resources 对象。 * 先查缓存,缓存未命中时创建新的 ResourcesImpl。 */publicResourcesgetOrCreateResources(IBinderactivityToken,ResourcesKeykey,ClassLoaderclassLoader){synchronized(this){// 1. 查缓存ResourcesImplimpl=findResourcesImplForKeyLocked(key);if(impl!=null){returngetOrCreateResourcesForActivityLocked(activityToken,classLoader,impl);}// 2. 缓存未命中:创建新的 ResourcesImplimpl=createResourcesImpl(key);mResourceImpls.put(key,newWeakReference<>(impl));returngetOrCreateResourcesForActivityLocked(activityToken,classLoader,impl);}}/** * 配置变更时,更新所有缓存的 ResourcesImpl。 * 这是 ActivityThread.handleConfigurationChanged 调用的方法。 */publicfinalbooleanapplyConfigurationToResourcesLocked(@NonNullConfigurationconfig,@NullableCompatibilityInfocompat){// 遍历所有缓存的 ResourcesImpl,逐个更新for(inti=mResourceImpls.size()-1;i>=0;i--){ResourcesKeykey=mResourceImpls.keyAt(i);ResourcesImplr=mResourceImpls.valueAt(i).get();if(r!=null){r.updateConfiguration(config,null,compat);}else{mResourceImpls.removeAt(i);// 已被 GC 回收的清理掉}}returntrue;}}关键设计:
ResourcesKey包含mOverrideConfiguration(其中含有 locale 信息)。当用户切换语言后,新的 Configuration 产生新的 key,findResourcesImplForKeyLocked()查不到缓存 →createResourcesImpl()创建新的ResourcesImpl→ 内部AssetManager用新 locale 重新索引资源。由于WeakReference的存在,旧ResourcesImpl在无引用后会被 GC 回收。
四、Resources —— 开发者视角的门面
源码路径:frameworks/base/core/java/android/content/res/Resources.java
publicclassResources{privateResourcesImplmResourcesImpl;// 委托给 ResourcesImplpublicResources(AssetManagerassets,DisplayMetricsmetrics,Configurationconfig){mResourcesImpl=newResourcesImpl(assets,metrics,config,newDisplayAdjustments());}@NonNullpublicStringgetString(@StringResintid)throwsNotFoundException{returngetText(id).toString();// 委托给 getText()}publicvoidupdateConfiguration(Configurationconfig,DisplayMetricsmetrics,CompatibilityInfocompat){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
publicclassResourcesImpl{privatefinalAssetManagermAssets;// Native 资源管理器privatefinalConfigurationmConfiguration;// 当前配置快照// 各种资源缓存privateDrawableCachemDrawableCache;privateConfigurationBoundResourceCache<Animator>mAnimatorCache;privateConfigurationBoundResourceCache<StateListAnimator>mStateListAnimatorCache;publicvoidupdateConfiguration(Configurationconfig,DisplayMetricsmetrics,CompatibilityInfocompat){synchronized(mAccessLock){// 1. 计算实际配置变更final@ConfigintconfigChanges=calcConfigChanges(config);// 2. 多语言匹配:从多语言列表中选取最佳匹配LocaleListlocales=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){finalLocalebestLocale=locales.getFirstMatchWithEnglishSupported(availableLocales);if(bestLocale!=null&&bestLocale!=locales.get(0)){mConfiguration.setLocales(newLocaleList(bestLocale,locales));}}}}// 3. ⭐ 关键:将配置下发给 Native 层 AssetManagermAssets.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
publicfinalclassAssetManagerimplementsAutoCloseable{privatelongmObject;// Native AssetManager 的 C++ 指针publicAssetManager(){mObject=nativeCreate();}// 将配置下发给 Native 层(16 个参数逐一传递)publicnativefinalvoidsetConfiguration(intmcc,intmnc,Stringlocale,intorientation,inttouchscreen,intdensity,intkeyboard,intkeyboardHidden,intnavigation,intscreenWidth,intscreenHeight,intsmallestScreenWidthDp,intscreenWidthDp,intscreenHeightDp,intscreenLayout,intuiMode,intmajorVersion);// 查询资源文本/*package*/nativefinalCharSequencegetResourceText(intident);// 获取系统 ROM 中所有已翻译的语言列表publicnativefinalString[]getLocales();privatenativelongnativeCreate();}关键设计:
setConfiguration()是 Native 方法(JNI),16 个参数逐一传递。getResourceText()内部将资源 ID 传给 C++ 层查询,返回的CharSequence直接从字符串池中取出。
七、Native 层:AssetManager 的资源表索引
C++ 层AssetManager是资源匹配的真正引擎。
源码路径:frameworks/base/libs/androidfw/AssetManager.cpp
7.1 核心数据结构
classAssetManager:publicAaptAssetManager{ResTable*mResources;// 资源表ResTable_config mConfig;// 当前配置(Native 版 Configuration)Vector<asset_path>mAssetPaths;// 已加载的资源包列表};ResTable_config是 Configuration 的 Native 对应结构体:
源码路径:frameworks/base/libs/androidfw/ResourceTypes.h
structResTable_config{uint32_tsize;union{struct{uint16_tmcc;uint16_tmnc;charlanguage[2];// ⭐ "zh", "en", "ja"charcountry[2];// ⭐ "CN", "US", "JP"uint8_torientation;uint8_ttouchscreen;uint16_tdensity;uint8_tkeyboard;uint8_tnavigation;uint16_tscreenWidth;uint16_tscreenHeight;uint16_tsdkVersion;uint16_tminorVersion;uint8_tscreenLayout;uint8_tuiMode;uint16_tsmallestScreenWidthDp;uint16_tscreenWidthDp;uint16_tscreenHeightDp;charlocaleScript[4];// ⭐ "Hans", "Hant"charlocaleVariant[8];// ⭐ 变体};};};关键设计:
language[2]是 2 字节——对 “en”、“zh” 两字节语言刚好。三字节语言(如 Filipino “fil”)使用localeScript字段存储。
7.2 setConfiguration()
源码路径:frameworks/base/libs/androidfw/AssetManager.cpp
voidAssetManager::setConfiguration(constResTable_config&config){const_cast<ResTable_config&>(mConfig)=config;constsize_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) 的字符串池中 → "你好" │ ▼ 返回 "你好" → CharSequence8.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),各版本可能存在差异,请以实际代码为准。
