Android 系统启动时,整个系统内的界面显示都是保持同一种语言。在不做任何修改的情况下,系统的默认语言是英文。当有需要切换语言时,系统设置开放了语言切换的界面以及所有系统可切换的语言包。
如果需要系统启动时就完成默认语言切换,可以通过以下方式进行配置。
1 系统语言默认配置方式
1.1 最佳实践 :mk 指定 locale prop
在定制系统时需要指定系统的默认语言,这个定制可以通过 prop 的配置完成。通常在某个产品的 device.mk 或者其他配置 mk 中直接指定即可,如:
PRODUCT_PROPERTY_OVERRIDES += \
persist.sys.locale=zh-CN
1.2 其他方式
在网上看到其他几种解决方式,可以具体分为以下几类,简单列在下面。
以下的做法选一即可,如果多种方式一起使用,可能会有优先级问题导致修改失败。
1.2.1 配置 prop
配置的 prop 属性主要有两种搭配:persist.sys.locale 或者 persist.sys.language + persist.sys.country。下面的修改位置配合任意一种 prop 方式均可生效,原因可查看后续 2.3 章节的流程解析。
1. 修改 init.rc,设置 locale prop
修改 init.rc 或者其他被链接到的 rc 文件,启动时执行设置 prop 指令
setprop persist.sys.locale zh-CN
本质上也是通过 setprop 修改默认语言。但是系统每次启动都会执行 rc,导致系统启动都必定是固定语言。因此表现为:在设置应用中切换后马上生效,重启失效会恢复固定语言。适用于不记忆用户切换语言的场景。
2. mk 指定语言和国家 prop
在某个产品的 mk 中,加入如下配置:
PRODUCT_PROPERTY_OVERRIDES += \
persist.sys.language=zh \
persist.sys.country=CN
同样可以修改系统默认语言。但是在系统启动后,通过 getprop 查看,会发现这两个 prop 是空(不是没有设置,而是被设置为空)。原因在后续的 2.3 章节的流程解析中会说明。
3. prop文件指定语言和国家 prop
找到某个产品的 system.prop / build.prop / default.prop,确保修改的 prop 文件会 copy 到对应的分区中,加入如下配置:
persist.sys.language=zh
persist.sys.country=CN
表现同上一小节。
4. builinfo.sh 中指定 locale prop
修改 build/tools/buildinfo.sh,加入如下配置:
echo "persist.sys.locale=zh-CN"
5. 一些注意事项
-
以上的做法示例采用了两种 prop 方式,locale 或者 language + country。实际上这两种配置差别不大,都可以满足需求。在系统运行中,language + country 的组合会被修改成 locale 这种方式(具体的可以看 2.3 章节的流程解析),因此建议直接使用 persist.sys.locale 这个配置。
-
如果采用 language + country 的配置方式,可以单独配置 language,而不配置 country,这样子系统的 locale 最终会采用 language 的配置,然后由系统自动匹配几种地区中最合适的一个;反过来,单独配置 country 则不行。
1.2.2 调整 languages_default.mk 中的语言顺序
修改 build/target/product/languages_default.mk, 将希望设置的默认语言的顺序移至第一位:
# This is a build configuration that just contains a list of languages, with
# en_US set as the default language.
# 注意以上的注释:PRODUCT_LOCALES 列表中的第一个会是系统的默认语言,因此我们把目标语言放置第一位即可
PRODUCT_LOCALES := \
+ zh_CN \
en_US \
af_ZA \
需要注意的是,这个修改不一定生效。是否生效取决于 mk 的编译顺序,从而决定的 PRODUCT_LOCALES 的环境变量的值(在多个 mk 均有定义同一环境变量的状态下,可能会出现相互覆盖的情况)。当最终确定的 PRODUCT_LOCALES 的第一个值为 zh_CN,则系统判定的默认语言才是中文。
判断最终 PRODUCT_LOCALES 的取值,可以查看 out/soong/soong.variable 文件中的 AAPTConfig 字段间接获取。具体原因可以查看后续章节的流程解析。
1.2.3 配置环境变量
在某个产品的 mk 中,加入以下编译变量配置:
PRODUCT_LOCALES := zh_CN
同上一节调整 language_default.mk 的调整方式,此配置不一定生效。
关于这个环境变量的其他作用,可以查看 2.2.2 章节的内容。
2 系统语言相关流程解析
2.1 环境变量的定义
和语言相关的环境变量主要包括 PRODUCT_LOCALES 和 CUSTOM_LOCALES。这里我们只做 PRODUCT_LOCALES 的分析。
2.1.1 PRODUCT_LOCALES 初定义
系统语言相关的配置主要在于 PRODUCT_LOCALES 这个环境变量。基于手上的代码,简单追踪了一下定义和引用链如下(不是实际的赋值顺序):
full_base.mk:定义 PRODUCT_LOCALSE := en_US。
languages_full.mk:在PRODUCT_LOCALES 后追加 en_XC 。
languages_defautl.mk:(覆盖)定义原生列出的所有支持的语言。其中有注释:
# This is a build configuration that just contains a list of languages, with
# en_US set as the default language.
# 注意以上的注释:PRODUCT_LOCALES 列表中的第一个会是系统的默认语言
PRODUCT_LOCALES := \
en_US \
af_ZA \
am_ET \
这是原生基础的支持语言定义,最终的 PRODUCT_LOCALS 环境变量值以实际为准。
2.2 编译时
编译相关的几个 mk 和引用链如下:
2.2.1 PRODUCT_LOCALES 的补充
build/make/core/product_config.mk:
# Figure out which resoure configuration options to use for this
# product.
# If CUSTOM_LOCALES contains any locales not already included
# in PRODUCT_LOCALES, add them to PRODUCT_LOCALES.
extra_locales := $(filter-out $(PRODUCT_LOCALES),$(CUSTOM_LOCALES))
ifneq (,$(extra_locales))
ifneq ($(CALLED_FROM_SETUP),true)
# Don't spam stdout, because envsetup.sh may be scraping values from it.
$(info Adding CUSTOM_LOCALES [$(extra_locales)] to PRODUCT_LOCALES [$(PRODUCT_LOCALES)])
endif
PRODUCT_LOCALES += $(extra_locales)
extra_locales :=
endif
# Add PRODUCT_LOCALES to PRODUCT_AAPT_CONFIG
PRODUCT_AAPT_CONFIG := $(PRODUCT_LOCALES) $(PRODUCT_AAPT_CONFIG)
在 2.1 中,有提到 CUSTOM_LOCALES 这个环境变量,但没有做分析,原因就在这里。product_config.mk 对 LOCALES 的处理:对 CUSTOM_LOCALES 做 PRODUCT_LOCALES 的差集,然后经过判断后,追加到 PRODUCT_LOCALES 中。
一般来说,我们不会使用 CUSTOM_LOCALES 这个变量,但是这个确实可能在编译期影响 PRODUCT_LOCALES 的值。
2.2.2 系统支持语言列表的定义
在 2.2.1 中,我们获取到最终的 PRODUCT_LOCALES 的值。在 product_config.mk 中,除了修正 PRODUCT_LOCALES 的值,还有一段和 AAPT 相关的语句:
# Add PRODUCT_LOCALES to PRODUCT_AAPT_CONFIG
PRODUCT_AAPT_CONFIG := $(PRODUCT_LOCALES) $(PRODUCT_AAPT_CONFIG)
这一句的影响非常重要!将 PRODUCT_LOCALES 的语言列表赋值给了 PRODUCT_AAPT_CONFIG,相当于指定了 aapt 工具的对语言的配置,会影响代码内所有以源码方式集成的应用打包出来的资源!
举个简单的例子:
当我们 PRODUCT_LOCALES 最终的变量值为原生 languages_default.mk 中所有的语言,意味着 aapt 配置了需要支持这些语言,因此在编译 apk 时,会将资源包中这些语言的目录全部加入编译。以原生资源包 framework-res.apk 为例,查看 arsc 索引表可以看到如下:
当我们修改 PRODUCT_LOCALES(比如重新赋值),使得最终的变量值只包含了中文 zh_CN 时,意味着 aapt 配置了只需要中文语言包,因此在编译 apk 时,仅会过滤中文词条目录加入编译。以原生资源包 framework-res.apk 为例,查看 arsc 索引表可以看到如下:
这种方式也就是 1.2.3 节中修改方式的具体实现。直接定义 PRODUCT_LOCALES 可以让默认语言生效,实际上就是减少了系统支持的语言。
这种修改的好处在于:如果确认不需要其他语言(没有海外版,只有固定语言的需求),可以通过 aapt 的配置减少源码集成应用参与编译的资源包,从而减少包体积。以某个产品的 framework-res.apk 为例,打包全语言的包体积为 31M,仅打包中文简体、中文繁体和英文(默认)的包体积为 6M。坏处在于:仅支持了这些语言,如果有切换语言的场景,可能因为不支持而导致显示错误。
在编译时,PRODUCT_AAPT_CONFIG 这个环境变量的值会输出在 out/soong/soong.variable 这个文件中。由于上面脚本可知他是由 PRODUCT_LOCALES 得出来的,因此可以通过查看 soong.variable 中的 AAPTConfig 间接知道 PRODUCT_LOCALES 的变量值。
"AAPTConfig": ["en_US,en_US,af_ZA,am_ET,ar_EG,ar_XB,as_IN,az_AZ,be_BY,bg_BG,bn_BD,bs_BA,ca_ES,cs_CZ,da_DK,de_DE,el_GR,en_AU,en_CA,en_GB,en_IN,en_XA,es_ES,es_US,et_EE,eu_ES,fa_IR,fi_FI,fr_CA,fr_FR,gl_ES,gu_IN,hi_IN,hr_HR,hu_HU,hy_AM,in_ID,is_IS,it_IT,iw_IL,ja_JP,ka_GE,kk_KZ,km_KH,kn_IN,ko_KR,ky_KG,lo_LA,lt_LT,lv_LV,mk_MK,ml_IN,mn_MN,mr_IN,ms_MY,my_MM,nb_NO,ne_NP,nl_NL,or_IN,pa_IN,pl_PL,pt_BR,pt_PT,ro_RO,ru_RU,si_LK,sk_SK,sl_SI,sq_AL,sr_Latn_RS,sr_RS,sv_SE,sw_TZ,ta_IN,te_IN,th_TH,tl_PH,tr_TR,uk_UA,ur_PK,uz_UZ,vi_VN,zh_CN,zh_HK,zh_TW,zu_ZA,en_XC,"],
2.2.3 系统默认语言 prop:ro.product.locale
build/core/Makefile:
BUILDINFO_SH := build/make/tools/buildinfo.sh
# Accepts a whitespace separated list of product locales such as
# (en_US en_AU en_GB...) and returns the first locale in the list with
# underscores replaced with hyphens. In the example above, this will
# return "en-US".
define get-default-product-locale
$(strip $(subst _,-, $(firstword $(1))))
endef
$(hide) TARGET_BUILD_TYPE="$(TARGET_BUILD_VARIANT)" \
……
PRODUCT_DEFAULT_LOCALE="$(call get-default-product-locale,$(PRODUCT_LOCALES))" \
……
bash $(BUILDINFO_SH) >> $@
这个 Makefile 中以上相关语句的意思是:
-
定义了 PRODUCT_DEFAULT_LOCALE 环境变量,他的值是函数 get-default-product-locale 的返回值,传参 PRODUCT_LOCALES。
-
get-default-product-locale:从参数列表中获取第一个元素单词,并把其中的下划线替换为横杠。如 en_US -> en-US。
-
执行 build/make/tools/buildinfo.sh 脚本,并将结果输出到某个文件。这个文件实际上是 out/target/product/(具体产品)/system/build.prop。
接下来看看执行脚本相关内容:
build/make/tools/buildinfo.sh:
#!/bin/bash
……
if [ -n "$PRODUCT_DEFAULT_LOCALE" ] ; then
echo "ro.product.locale=$PRODUCT_DEFAULT_LOCALE"
fi
……
buildinfo.sh 是负责将一些生成一些产品相关的只读的 prop。其中会拿到上一步 Makefile 中生成的环境变量 PRODUCT_DEFAULT_LOCALE 的值,并输出 ro.product.locale 的 prop 到系统中。
2.2.4 总结
到目前为止,总结一下我们已经有的环境变量和 prop:
-
PRODUCT_LOCALES : 系统内支持的所有语言列表。
-
PRODUCT_AAPT_CONFIG:由 PRODUCT_LOCALES 得来,编译时应用会只打包指定的语言资源包。
-
ro.default.locale:由 PRODUCT_LOCALES 得来,是列表中排名第一位的项目。
-
可能存在的其他的客制化改动:包括修改 persist.sys.language、persist.sys.locale 等。
2.3 运行期
老生常谈 Android 系统启动:boot 阶段加载内核;启动 init 进程(首个进程);由 init 启动 Zygote 进程;Zygote 孵化其他 Android 世界进程。
2.3.1 启动 Zygote 进程,配置虚拟机 locale 启动参数
Zygote 进程的启动是通过执行 /system/bin/app_process 命令完成的。这个命令实际上调用了 app_main.cpp 中的 main 方法,创建一个 AppRuntime 对象,并调用 start 方法。其中 AppRuntime 又继承自 AndroidRuntime 类,因此也调用到 AndroidRuntime.cpp 的 start 方法,从而调用到 startVm 方法。
int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote, bool primary_zygote)
{
/* Set the properties for locale */
{
strcpy(localeOption, "-Duser.locale=");
const std::string locale = readLocale();
strncat(localeOption, locale.c_str(), PROPERTY_VALUE_MAX);
addOption(localeOption);
}
}
/*
* Read the persistent locale. Inspects the following system properties
* (in order) and returns the first non-empty property in the list :
*
* (1) persist.sys.locale
* (2) persist.sys.language/country/localevar (country and localevar are
* inspected iff. language is non-empty.
* (3) ro.product.locale
* (4) ro.product.locale.language/region
*
* Note that we need to inspect persist.sys.language/country/localevar to
* preserve language settings for devices that are upgrading from Lollipop
* to M. The same goes for ro.product.locale.language/region as well.
*/
const std::string readLocale()
{
const std::string locale = GetProperty("persist.sys.locale", "");
if (!locale.empty()) {
return locale;
}
const std::string language = GetProperty("persist.sys.language", "");
if (!language.empty()) {
const std::string country = GetProperty("persist.sys.country", "");
const std::string variant = GetProperty("persist.sys.localevar", "");
std::string out = language;
if (!country.empty()) {
out = out + "-" + country;
}
if (!variant.empty()) {
out = out + "-" + variant;
}
return out;
}
const std::string productLocale = GetProperty("ro.product.locale", "");
if (!productLocale.empty()) {
return productLocale;
}
// If persist.sys.locale and ro.product.locale are missing,
// construct a locale value from the individual locale components.
const std::string productLanguage = GetProperty("ro.product.locale.language", "en");
const std::string productRegion = GetProperty("ro.product.locale.region", "US");
return productLanguage + "-" + productRegion;
}
以上的优先级如注释:
-
读取 persist.sys.locale。如果没有进行客制化,这里是空的;
-
读取 persist.sys.language,然后读取相关配置拼接。如果没有进行客制化,这里是空的;
-
读取 ro.product.locale。这个在 2.2.3 中已经进行赋值了;
-
读取 ro.product.locale.language 和 ro.product.locale.region,并拼接起来。
进行参数配置后,虚拟机会按照启动参数默认配置成某种语言。
2.3.2 启动 SystemServer
Zygote 进程会调用 SystemServer 的 main 方法启动 SystemServer。
public static void main(String[] args) {
new SystemServer().run();
}
private void run() {
……
// If the system has "persist.sys.language" and friends set, replace them with
// "persist.sys.locale". Note that the default locale at this point is calculated
// using the "-Duser.locale" command line flag. That flag is usually populated by
// AndroidRuntime using the same set of system properties, but only the system_server
// and system apps are allowed to set them.
//
// NOTE: Most changes made here will need an equivalent change to
// core/jni/AndroidRuntime.cpp
if (!SystemProperties.get("persist.sys.language").isEmpty()) {
final String languageTag = Locale.getDefault().toLanguageTag();
SystemProperties.set("persist.sys.locale", languageTag);
SystemProperties.set("persist.sys.language", "");
SystemProperties.set("persist.sys.country", "");
SystemProperties.set("persist.sys.localevar", "");
}
……
}
SystemServer 在这里有两步特殊操作:
-
读取默认 languageTag。这个 languageTag 实际上就是从 Dalvik 启动参数里的 -Duser.locale 获取的,也就是 2.3.1 中对应的设置逻辑。
-
判断 persist.sys.language 是否为空。如果不为空,则读取 Dalvik 启动参数配置,然后设置 persist.sys.locale,再清空其他所有相关的属性配置。
对于第二点的思考,个人认为应该是 Android 在版本迭代中的兼容。一开始并没有使用 locale 这个 prop 管理,而是使用的 language + country 的格式配置。后续为了简化语言切换,则统一使用 Locale 这个方式,因此对旧版本 language + country 的设定,在 SystemServer 的首次启动做一次转化,以便于后续统一使用 locale。
这也就是在 1.1 和 1.2 中提出最佳实践使用 locale ,但 language + country 也可以正常使用的原因。
2.3.3 运行中系统语言的切换
系统原生设置中开放了切换语言的界面,调用的是 LocalePicker.updateLocale 方法更新当前系统语言。
具体的执行就不细说了,基本上有以下几步:
-
调用 AMS 的 updatePersistentConfiguration 更新 configuration。
-
AMS 通知 ATMS 执行 updatePersistentConfiguration 方法。
-
ATMS 执行 updateGlobalConfigurationLocked 方法,完成以下工作
-
更新 persist.sys.locale 属性值;
-
调用 mSystemThread.applyConfigurationToResources(mTempConfig); mSystemThread 是 ActivityThread 类的对象,通知 ResourceManager 重新读取资源,更新当前应用界面。
-
遍历当前所有 ProcessRecord,更新配置。
-
3 调试问题相关
3.1 如何确认 PRODUCT_LOCALES 的值?
可以查看 out/soong/soong.variable 中找到 AAPTConfig 一项,间接知道 PRODUCT_LOCALES 的值。
3.2 设置语言界面中没有语言栏,或者没有可用语言
PRODUCT_LOCALES 配置了只支持一个或者两个语言,因此没有可选空间,设置会直接隐藏选择栏。
3.3 设置 persist.sys.locale 后不生效,原生设置语言栏显示 Und
persist.sys.locale 接受的值是横杠隔开,而不是下划线。如果是下划线则无法解析,会显示默认语言和 Und。
3.4 配置 persist.sys.country ,语言不生效
单独配置 persist.sys.country 是不起作用的,起关键作用的是 persist.sys.locale 属性或者 persist.sys.language 属性。优先选择设置 persist.sys.locale, 也可以通过设置 persist.sys.language + persist.sys.country 的组合。