AndroidManifest中meta-data动态修改的问题

潘俊楚
2023-12-01

本文记录一下我在研究Android的meta-data所得的一些收获和坑,希望大家少走弯路。

项目中集成一些第三方的API时,按其官方文档,经常需要在AndroidManifest文件中的application标签下加上meta-data配置。
比如如下友盟的配置:

一、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,或者多渠道打包时,想不同渠道配置不同的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

二、meta-data可以通过代码动态更改吗?

上面的实现中,通过改变gradle.properties的"env"为"test"或者"release"来切换测试环境和生产环境,如果需要测试环境和生产环境的包,就得打两个包。
现在我要实现动态切换环境这样的需求,这个功能很常见了,也不复杂,但是meta-data可以动态更改吗?

看源码发现,meta-data实际上就是Bundle,有get方法,也有put方法。于是实践使用了一下put方法,结果如下:

  1. 如果像下面这样,动态设置其值后立即获取,确实得到的是新值。
        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"。

  1. 如果是下面这样,动态设置其值后,重新获取ApplicationInfo 再获取meta-data,得到的还是原来的旧值。
        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的值吗?待研究……

 类似资料: