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

Posted Ever69

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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);
    

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

以上是关于Android反射修改buildConfigField生成的属性失效问题的主要内容,如果未能解决你的问题,请参考以下文章

Java反射机制的原理及在Android下的简单应用

实例:Android中运用反射机制

Android Kotlin 反射使用 (null receiver 异常问题)

6.5 Android硬件访问服务使用反射

Android 灵活切换下拉刷新(策略模式+反射)

Android 灵活切换下拉刷新(策略模式+反射)