Android反射修改buildConfigField生成的属性失效问题

在开发的时候,我们经常需要在项目的build.gradle文件中通过buildConfigField方法在BuildConfig类中生成一些常量属性供项目运行使用,比如一些第三方SDK的id和key,或是根据打包环境或者渠道对应的接口服务器地址等等。

现在问题来了,我们希望不止在打包时可以切换对应环境的接口服务器地址,在App运行时也可以切换,因为打包时的切换只算一种静态切换,它在编译时就已经将地址设定好了,如果想换一个地址,只能再从新打包,这在某些情况下就显得非常麻烦。

所以我们需要在支持“静态切换”的基础上再支持“动态切换”,那么怎么做呢,我们知道“静态切换”是通过在编译时生成修改BuildConfig类来完成的,这个BuildConfig类内部都是一些常量,并且不支持我们直接进行编辑,那么如何才能在程序运行时修改BuildConfig类中的属性呢?那自然是通过反射大法了,思路有了,直接上代码。

首先我在build.gradle文件中添加BASE_URL属性

 buildConfigField "String","BASE_URL","\"https://www.baidu.com\""

然后在java代码中去修改这个值

private void changeBuildConfigField(String fieldName) {
        try {
            Log.e("kkk", "修改前");
            printBaseUrl();
            BuildConfig config = new BuildConfig();
            Field declaredField = config.getClass().getDeclaredField(fieldName);
            declaredField.setAccessible(true);
            declaredField.set(config, "https://www.csdn.net");
            Log.e("kkk", "修改后");
            printBaseUrl();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    private void printBaseUrl() {
        Log.e("kkk", BuildConfig.BASE_URL);
    }

运行查看log发现

E/kkk: 修改前
E/kkk: https://www.baidu.com
E/kkk: 修改后
E/kkk: https://www.baidu.com

WTF!?反射后竟然没有修改?

这是为啥,是我写代码姿势不对吗?于是我翻前倒后的在百度出来的答案中寻找原因,

在一番艰苦的查找过后,皇天不负有心人,终于被我找到原因了。

其实不是反射没有成功,而是编译器在编译的时候做了一些优化,它直接将BuildConfig里的常量的值替换了你所用到其引用的地方,这么说是不是有点听不懂,直接看编译后的class文件就懂了。

//编译前
private void printBaseUrl() {
        Log.e("kkk", BuildConfig.BASE_URL);
    }
//编译后
private void printBaseUrl() {
        Log.e("kkk", "https://www.baidu.com");
    }

注意上面的printBaseUrl方法,我在java里面输出的是BuildConfig.BASE_URL这个引用的值,再看看编译后的class文件,它是直接将这个字符串输出了。所以即使我们再怎么反射修改BASE_URL这个值,在这个方法内输出的还是原来的内个值。

不信,我们直接将反射修改后的BASE_URL打印出来看看。

 //在changeBuildConfigField()方法内添加以下几行代码
 Object o = declaredField.get(config);
 Log.e("kkk","直接输出BASE_URL");
 Log.e("kkk",o.toString());
E/kkk: 修改前
E/kkk: https://www.baidu.com
E/kkk: 修改后
E/kkk: https://www.baidu.com
E/kkk: 直接输出BASE_URL
E/kkk: https://www.csdn.net

嗨,看看这编译器干的好事,那怎么才能解决编译器优化产生的这个值替换引用的问题呢?

我这有两种解决方案,

第一种,避免直接使用,在需要用到BuildConfig属性的地方通过反射获取属性再使用,就像上面直接输出那里一样,相比第二种方案使用上略显麻烦。

第二种,这个也是在网上得到的答案,就是让BuildConfig中的属性至少经过一次运算再赋值,这样编译器在编译的时候就不会再将值替换引用。

这里我就直接验证第二种方案了,第一种在上面的示例中已经证明是可行的了。

让属性至少经过一次运算再赋值,网上提供的方法不少,比如,

 private final String BASE_URL = new StringBuilder("https://www.baidu.com").toString();

或是修改属性类型,因为编译器只会对String和其他基础类型进行这种优化。

 private final StringBuilder BASE_URL = new StringBuilder("https://www.baidu.com");

再或者是使用null判断

 private final String BASE_URL = (null == null ? "https://www.baidu.com" : "");

方法倒不少,我们挨个都试一遍。

在build.gradle文件中添加三种不同方法生成的属性

//使用StringBuilder.toString()赋值String
buildConfigField "String", "BASE_URL", "new StringBuilder(\"https://www.baidu.com\").toString()"

//修改为StringBuilder类型
buildConfigField "StringBuilder", "BASE_URL2", " new StringBuilder(\"https://www.baidu.com\")"

//null判断
buildConfigField "String", "BASE_URL3", "null==null?\"https://www.baidu.com\":\"\""

添加完后编译一下,查看BuildConfig文件是否正确生成

public final class BuildConfig {
  
  ...

  // Fields from default config.
  public static final String BASE_URL = new StringBuilder("https://www.baidu.com").toString();
  public static final StringBuilder BASE_URL2 =  new StringBuilder("https://www.baidu.com");
  public static final String BASE_URL3 = null==null?"https://www.baidu.com":"";
}

ok,完美,看到这都感觉不用往下验证了,应该没问题。

private void changeBaseUrl() {
        try {
            Log.e("kkk", "\n通过StringBuilder.toString判断设置的");
            Log.e("kkk", "修改前");
            printBaseUrl();
            BuildConfig config = new BuildConfig();
            Field declaredField = config.getClass().getDeclaredField("BASE_URL");
            declaredField.setAccessible(true);
            declaredField.set(config, "https://www.csdn.net");
            Log.e("kkk", "修改后");
            printBaseUrl();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void changeBaseUr2() {
        try {
            Log.e("kkk", "\n通过修改属性类型为StringBuilder设置的");
            Log.e("kkk", "修改前");
            printBaseUrl2();
            BuildConfig config = new BuildConfig();
            Field declaredField = config.getClass().getDeclaredField("BASE_URL2");
            declaredField.setAccessible(true);
            declaredField.set(config, new StringBuilder("https://www.csdn.net"));
            Log.e("kkk", "修改后");
            printBaseUrl2();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void changeBaseUr3() {
        try {
            Log.e("kkk", "\n通过null判断设置的");
            Log.e("kkk", "修改前");
            printBaseUrl3();
            BuildConfig config = new BuildConfig();
            Field declaredField = config.getClass().getDeclaredField("BASE_URL3");
            declaredField.setAccessible(true);
            declaredField.set(config, "https://www.csdn.net");
            Log.e("kkk", "修改后");
            printBaseUrl3();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void printBaseUrl() {
        Log.e("kkk", BuildConfig.BASE_URL);
    }

    private void printBaseUrl2() {
        Log.e("kkk", BuildConfig.BASE_URL2.toString());
    }

    private void printBaseUrl3() {
        Log.e("kkk", BuildConfig.BASE_URL3);
    }

输出结果

E/kkk: 通过StringBuilder.toString判断设置的
E/kkk: 修改前
E/kkk: https://www.baidu.com
E/kkk: 修改后
E/kkk: https://www.csdn.net
E/kkk: 通过修改属性类型为StringBuilder设置的
E/kkk: 修改前
E/kkk: https://www.baidu.com
E/kkk: 修改后
E/kkk: https://www.csdn.net
E/kkk: 通过null判断设置的
E/kkk: 修改前
E/kkk: https://www.baidu.com
E/kkk: 修改后
E/kkk: https://www.csdn.net

nice,都修改成功了。

再看看编译后的class文件

 private void printBaseUrl() {
        Log.e("kkk", BuildConfig.BASE_URL);
    }

    private void printBaseUrl2() {
        Log.e("kkk", BuildConfig.BASE_URL2.toString());
    }

    private void printBaseUrl3() {
        Log.e("kkk", BuildConfig.BASE_URL3);
    }

没有替换成常量,完美解决~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值