Android-从常量的进一步认识来理解R文件

在C++中,经常用const来表示某个变量是个常量,在kotlin中也是,其他语言有用let来表示常量的,Java中一般用final来表示常量。更一般的,我们经常使用static final来表示,这样其实是为了防止被反射修改值。

import java.lang.reflect.Field;
public class FinalReflect {
    private final int A = 10;
    private static final int B = 20;
    public static void main(String[] args) throws Exception {
        FinalReflect finalReflect = new FinalReflect();
        Field aField = FinalReflect.class.getDeclaredField("A");
        Field bField = FinalReflect.class.getDeclaredField("B");
        System.out.println(finalReflect.A);
        aField.setAccessible(true);
        aField.set(finalReflect, 11);
        aField.setAccessible(false);
        System.out.println(finalReflect.A);
        System.out.println(FinalReflect.B);
        bField.setAccessible(true);
        bField.set(null, 21);
        bField.setAccessible(false);
        System.out.println(FinalReflect.B);
    }
}

运行后输出结果为

10

10

20

Exception in thread "main" java.lang.IllegalAccessException: Can not set static final int field com.bensonlab.java.study.FinalReflect.B to java.lang.Integer

at sun.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:76)

at sun.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:80)

at sun.reflect.UnsafeQualifiedStaticIntegerFieldAccessorImpl.set(UnsafeQualifiedStaticIntegerFieldAccessorImpl.java:77)

at java.lang.reflect.Field.set(Field.java:764)

at com.bensonlab.java.study.FinalReflect.main(FinalReflect.java:21)

Process finished with exit code 1

这里我们经常认为B就是一个常量了,但昨天一新来的同事向我反馈,他第一次将项目download下来编译出apk,发现我一个常量引用了R文件的layout,运行时报R.layout ClassNotDefException,我当时在代码中,由于多个地方需要使用同一个layout,也就是预加载布局和正常加载布局,要使用同一个layout,并且要修改也会同时修改,我就给定义到了一个常量中去了,类似public static final int LAYOUT_XXX = R.layout.xxx。我第一反应是要他把apk发我,我反编译了一下,原来是我们部门有人为了包体积,把R文件给删除了,但这不会导致R.layout类未定义。问了下另一个主攻编译优化的同事这是啥情况,有一个说static final的不能引用一个变量,因为我引用的地方在一个子module中,也就是会被打成aar,在子module或aar中,R.layout类中的layout的值其实是个变量,只有主模块,也就是application的模块R文件中的才全是常量。static final的常量不能引用一个变量?我对他说are you sure?,然后他随便在一个kotlin文件中演示了一遍给我看。代码类似如下

class KotlinStatic {
 
    companion object {
        private val A = 3
        private const val B = A
    }
 
}

这里在B = A的A位置会报Only 'const val' can be used in constant expressions的Error

还真是,然后我又想,为啥呢?我写的代码为啥能编译过,而且最后也能正常打成apk,还能正常运行呢?现在应该已经发灰度上百万的量了,也没出现啥问题。于是我又用Java写了一遍这个Demo

public class JavaStatic {
 
    private static int A = 10;
    private static final int B = A;
 
}

这次的Java代码没报任何错误。这里明明A是一个变量,而B是一个常量,怎么常量可以引用一个变量了?我又问了一下他,他说还真的有点神奇了。但以前我们遇到过一种情况,switch的case和注解的val不能引用变量,而且有时候连static final也不能引用。这里再来写个Demo验证下

import com.sun.xml.internal.ws.developer.Serialization;
@Serialization(encoding = RealConst.B)
public class RealConst {
    public static String A = "3";
    public static final String B = A;
 
    public static int C = 10;
    public static final int D = C;
 
    {
        switch (C) {
            case D:
                break;
            default:
                break;
        }
    }
}

这里在encoding=RealConst.B的地方会报Attribute value must be constant的错误,而且case D的地方也会报Constant expression required的错误。但如果我把B = A改成B = "3"这两个错误就都消失了。

这里把猜测总结了一下,其实编译的时候,有个静态检查的过程,编译器会把case和注解的地方判断一下B引用的真正变量类型。在编译原理课我们学过还有一种量叫作字面量,字面量和常量有点关系。像"3"和10这种就是字面量,也就是说他们不是变量,他们是一个个原生的值,也就是他们就是个值了,不是变量指向的一个引用或者内存指针。像注解的value或者case这种需要const的,应该可以理解成是需要一个字面量的引用,这里把常量讲成字面量引用可能更好理解一些。在编译完成后,其实很多常量都被优化成了字面量,比如说B = "3"这种的,当B被在注解的value或者case的地方时,编译完成后注解的value和case就不会再引用B这个常量了,而是会直接使用"3"。Java是有个常量池的,所以并不会因为出来多个"3",内存里就会有多份"3"。

再回想下kotlin的const为啥就不能这样干呢?猜测应该是kotlin在语法层面就对常量的引用值做了限制,只能引用字面量,而已还限制了const只能引用一些基本数据类型,这是为了防止常量指向的真正对象被修改,只有引用基本数据类型,才能保证是我们真正理解的常量。

public class JavaStatic {
    private static int A = 10;
    private static final int B = A;
    private static StringBuilder C = new StringBuilder("xxb");
    private static final StringBuilder D = C;
    public static void main(String[] args) {
        System.out.println(D);
        D.append(" +++");
        System.out.println(D);
    }
}

在这个Demo中,两个输出分别是xxb和xxb +++,这验证了刚才的猜测,static final并不是我们理解的常量,或者我们理解的常量更具体点应该是这个常量它是个量,是个指向的对象不能变的量,也就是说这个量指向了一个对象后,它就不能再指向其他对象了,但它指向的对象自身是可以变的。而kotlin把const指向的常量类型限制为基本数据类型,这就间接限制了常量指向的对象自己本身都不能再变了,因为如果一个常量指向一个3,再指向5,这其实是改变了指向的对象,而且编译器也不让常量有多次赋值的操作,再者,我们在kotlin中,也无法让基本数据类型的值,通过它自身的操作,改变其值。

最后再回到Android的R文件来,R文件的基本结构就是一个R类本身是一个final class,里面的资源类型比如id、layout、string等都是public static final class,这些资源静态内部类中我们的资源引用,全都是public static final int的类型。然后如果打开一个aar,里面是找不到R类文件的,但可以找到一个R.txt文件,在生成的apk文件中可以找到相应module的R类文件的,而且R.txt和apk中的R类文件内容基本上是一样的,那这样看来这里这个R.txt文件就是aar用来告诉最后生成apk,这个aar的R有哪些资源id。最后资源merge后,会把同名的资源给merge掉,最后生成一个最终的R类文件,这个时候,其中就全是static final int的类型了。而且生成的所有R类文件,其中的都是static final int类型,这也能说明子module中的资源被覆盖的情况了。

最后的最后我们再来理解理解public static final int LAYOUT_XXX = R.layout.xxx,这个static final int的常量的赋值操作。在aar或者子module中,其实LAYOUT_XXX引用的是一个static的变量,但最终编译的时候,这个R.layout.xxx会被编译成pubic static final int,也就是一个常量了。所以最终其实可以把LAYOUT_XXX理解成一个常量,但不能理解成是个const,把LAYOUT_XXX给注解用或者给switch-case用,也还是会报错误,因为本质上,LAYOUT_XXX引用的不是一个const或者说字面量,R.layout.xxx本身也不是一个const或者说字面量,这也是为啥Butterknife要搞个R2来解决注解上无法使用R的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值