本文记录一下我在研究Android的meta-data所得的一些收获和坑,希望大家少走弯路。
项目中集成一些第三方的API时,按其官方文档,经常需要在AndroidManifest文件中的application标签下加上meta-data配置。
比如如下友盟的配置:
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/xtwx"
android:label="@string/app_name"
android:largeHeap="true"
android:supportsRtl="true"
android:usesCleartextTraffic="true"
android:theme="@android:style/Theme.NoTitleBar">
<!--友盟示例配置-->
<meta-data
android:name="UMENG_APPKEY"
android:value="123xyz" />
在Manifest中配置这个之后,这些第三方框架可以动态获取这些配置信息,从而进行检验,统计等目的。
一般我们在Application中初始化第三方框架时,它们便可以通过如下方式拿到meta-data的配置:
ApplicationInfo appInfo = null;
try {
appInfo = SoftApplication.getContext().getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
String value = appInfo.metaData.getString("UMENG_APPKEY"); // value为“123xyz”
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
这是meta-data定义在application标签下的情况。如果meta-data定义在activity,service,contentProvider等其它组件标签下面,则动态获取的方式会有所区别。
可参考:https://juejin.im/post/5c243454f265da6163021d5d
比如对于生产环境和测试环境,我们想配置不同的meta-data,或者多渠道打包时,想不同渠道配置不同的meta-data,如何实现呢?
gradle中有个manifestPlaceholders功能,可以用来实现此需求。
manifestPlaceholders相当于一个占位符。
把manifest中的meta-data配置改为如下:
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/xtwx"
android:label="@string/app_name"
android:largeHeap="true"
android:supportsRtl="true"
android:usesCleartextTraffic="true"
android:theme="@android:style/Theme.NoTitleBar">
<!--友盟示例配置-->
<meta-data
android:name="UMENG_APPKEY"
android:value="${UMENG_KEY}" />
经过上面的改动,相当于把上面的meta-data的value用一个叫UMENG_KEY的变量代替了。
然后在build.gradle中,根据不同条件给UMENG_KEY设置不同的值:
defaultConfig {
Properties properties = new Properties()
properties.load(project.rootProject.file("gradle.properties").newDataInputStream())
String env = properties.getProperty("env", "release") // "env"是定义在gradle.properties里的变量,用于切换环境
if("test".equals(env)){
manifestPlaceholders = [
// 测试环境
UMENG_KEY : "123abc",
OTHER_KEY : "aaaaaaaaaaaaaaaaaaaaaa"
]
} else if("release".equals(env)) {
manifestPlaceholders = [
// 正式环境
UMENG_KEY : "123xyz",
OTHER_KEY : "bbbbbbbbbbbbbbbbbbbbbbbb"
]
}
}
经过这样的设置,通过切换gradle.properties的"env"为"test"或者"release"来切换测试环境和生产环境,于是我们打包后,manifest里的UMENG_KEY的值是不同的。
多渠道打包,通过manifestPlaceholders设置meta-data为不同的数据,可以参考:https://juejin.im/post/5c243454f265da6163021d5d
上面的实现中,通过改变gradle.properties的"env"为"test"或者"release"来切换测试环境和生产环境,如果需要测试环境和生产环境的包,就得打两个包。
现在我要实现动态切换环境这样的需求,这个功能很常见了,也不复杂,但是meta-data可以动态更改吗?
看源码发现,meta-data实际上就是Bundle,有get方法,也有put方法。于是实践使用了一下put方法,结果如下:
ApplicationInfo appInfo = null;
try {
appInfo = SoftApplication.getContext().getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
appInfo.metaData.putString("UMENG_APPKEY", "123456");
String value = appInfo.metaData.getString("UMENG_APPKEY");
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
得到value为"123456"。
ApplicationInfo appInfo = null;
try {
appInfo = SoftApplication.getContext().getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
appInfo.metaData.putString("UMENG_APPKEY", "123456");
// 重新获取appInfo,再获取meta-data
appInfo = SoftApplication.getContext().getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
String value = appInfo.metaData.getString("UMENG_APPKEY");
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
得到value为设置前的旧值。
为什么会这样呢?参考网上的回答:https://stackoverflow.com/questions/9226691/change-package-manager-application-info-meta-data-in-android/9227097
原因就是说,通过getPackageManager().getApplicationInfo返回的applicationInfo始终是一个数据副本。如果动态设置的话,也只是改变了副本的值,并没有改变原数据。因此再次获取,又是个原数据的副本,仍然是旧值。
追了下代码,应该是在android.content.pm.PackageParser.java这个类里,生成副本的:
public static ApplicationInfo generateApplicationInfo(ApplicationInfo ai, int flags,
PackageUserState state, int userId) {
if (ai == null) return null;
if (!checkUseInstalledOrHidden(flags, state, ai)) {
return null;
}
// This is only used to return the ResolverActivity; we will just always
// make a copy.
ai = new ApplicationInfo(ai); // 这里创建了副本
ai.initForUser(userId);
if (state.stopped) {
ai.flags |= ApplicationInfo.FLAG_STOPPED;
} else {
ai.flags &= ~ApplicationInfo.FLAG_STOPPED;
}
updateApplicationInfo(ai, flags, state);
return ai;
}
目前没有用于更改applicationInfo原数据的api,那么还有办法动态更改meta-data的值吗?待研究……