题外话

王朝学院·作者佚名  2016-08-27
窄屏简体版  字體: 小  |  中  |  大  |  超大  

虽然文章标题是java, 但几乎所有面向对象设计的语言都遵守这个初始化流程, 感谢廖祜秋liaohuqiu_秋百万指出, 之前忘记提这个了.

前言drakeet写了个和RecyclerView相关的GenerousRecyclerView, 原文提到了写这个的目的. 因为需要知道ViewGroup的clipToPadding属性, 所以调用了ViewGroup.getClipToPadding, 但这个方法是API level 21引入的. 我看了一下代码, ViewGroup是通过调用setClipToPadding完成相关内容初始化的, setClipToPadding在API level 1就有了, 也就是说我们只要监视setClipToPadding的调用, 就能知道ViewGroup的clipToPadding状态. 如此巧妙, 如果我告诉drakeet, 说不定就能引起他的注意, 出任CEO, 走上人生巅峰.

如果你已经知道我要说什么了, 可以鄙视我.

问题简单还原一下问题, 我们有一个类SuperClass

1

2

3

4

5

6

7

8

9

10

11

12

publicclassSuperClass {

PRivateintmSuperX;

publicSuperClass() {

setX(99);

}

publicvoidsetX(intx) {

mSuperX = x;

}

}

现在我们想随时知道mSuperX是什么值, 不用反射, 因为父类从不直接修改mSuperX的值, 总是通过setX来改, 那么最简单的方法就是继承SuperClass, 重写setX方法, 监听它的改变就好.下面是我们的子类SubClass:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

publicclassSubClassextendsSuperClass {

privateintmSubX =1;

publicSubClass() {}

@Override

publicvoidsetX(intx) {

super.setX(x);

mSubX = x;

System.out.println("SubX is assigned "+ x);

}

publicvoidprintX() {

System.out.println("SubX = "+ mSubX);

}

}

我使用mSubX来跟踪mSuperX

因为在ViewGroup中, clipToPadding默认值是true(为了简化问题, 把它当成boolean, 实际并不是), 而ViewGroup初始化有可能不调用setClipToPadding, 此时是默认值, 为了模拟这种情况, 将mSubX初始化为1.

最后在main里调用:

1

2

3

4

5

6

publicclassMain {

publicstaticvoidmain(String[] args) {

SubClass sc =newSubClass();

sc.printX();

}

}

很多人, 包括我, 认为终端输出的结果应该是:

1

2

SubX is assigned99

SubX =99

然而真正运行后输出的是:

1

2

SubX is assigned99

SubX =1

实际分析要想知道发生了什么, 最简单的方法就是看看到底程序到底是怎么执行的, 比如单步调试, 或者直接一点, 看看Java字节码.

下面是Main的字节码

1

2

3

4

5

6

7

8

9

10

Compiled from"Main.java"

publicclassbugme.Main {

......

publicstaticvoidmain(java.lang.String[]);

Code:

0:new #2 // class bugme/SubClass

3: dup

4: invokespecial #3 // Method bugme/SubClass."<init>":()V

......

}

这是直接用javap反编译.class文件得到的. 虽说同样是Java写的, 用apktool反编译APK文件(其中的dex文件)得到的smali代码和Java Bytecode明显长得不一样.

字节码乍一看怪怪的, 只要知道它隐含了一个栈和局部变量表就好懂了.

这段代码首先new一个SubClass实例, 把引用入栈, dup是把栈顶复制一份入栈, invokespecial #3将栈顶元素出栈并调用它的某个方法, 这个方法具体是什么要看常量池里第3个条目是什么, 但是javap生成的字节码直接给我们写在旁边了, 即SubClass.<init>.

接下来看SubClass.<init>,

1

2

3

4

5

6

publicclassbugme.SubClassextendsbugme.SuperClass {

publicbugme.SubClass();

Code:

0: aload_0

1: invokespecial #1 // Method bugme/SuperClass."<init>":()V

......

这里面并没有方法叫<init>, 是因为javap为了方便我们阅读, 直接把它改成类名bugme.SubClass, 顺便一提, bugme是包名. <init>方法并非通常意义上的构造方法, 这是Java帮我们合成的一个方法, 里面的指令会帮我们按顺序进行普通成员变量初始化, 也包括初始化块里的代码, 注意是按顺序执行, 这些都执行完了之后才轮到构造方法里代码生成的指令执行. 这里aload_0将局部变量表中下标为0的元素入栈, 其实就是Java中的this, 结合invokespecial #1, 是在调用父类的构造函数, 也就是我们常见的super().

所以我们再看SuperClass.<init>

1

2

3

4

5

6

7

8

9

10

11

12

publicclassbugme.SuperClass {

publicbugme.SuperClass();

Code:

0: aload_0

1: invokespecial #1 // Method java/lang/Object."<init>":()V

4: aload_0

5: bipush 99

7: invokevirtual #2 // Method setX:(I)V

10:return

......

}

同样是先调了父类Object的构造方法, 然后再将this, 99入栈, invokevirtual #2旁边注释了是调用setX, 参数分别是this和99也就是this.setX(99), 然而这个方法被重写了, 调用的是子类的方法, 所以我们再看SubClass.setX:

1

2

3

4

5

6

7

8

9

publicclassbugme.SubClassextendsbugme.SuperClass {

......

publicvoidsetX(int);

Code:

0: aload_0

1: iload_1

2: invokespecial #3 // Method bugme/SuperClass.setX:(I)V

......

}

这里将局部变量表前两个元素都入栈, 第一个是this, 第二个是括号里的参数, 也就是99,invokespecial #3调用的是父类的setX, 也就是我们代码中写的super.setX(int)

SuperClass.setX就很简单了:

1

2

3

4

5

6

7

8

9

publicclassbugme.SuperClass {

......

publicvoidsetX(int);

Code:

0: aload_0

1: iload_1

2: putfield #3 // Field mSuperX:I

5:return

}

这里先把this入栈, 再把参数入栈, putfield #3使得前两个入栈的元素全部出栈, 而成员mSuperX被赋值, 这四条指令只对应代码里的一句this.mSuperX = x;

接下来控制流回到子类的setX:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

publicclassbugme.SubClassextendsbugme.SuperClass {

......

publicvoidsetX(int);

Code:

0: aload_0

1: iload_1

2: invokespecial #3 // Method bugme/SuperClass.setX:(I)V

->5: aload_0 // 即将执行这句

6: iload_1

7: putfield #2 // Field mSubX:I

10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;

13:new #5 // class java/lang/StringBuilder

16: dup

17: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V

20: ldc #7 // String SubX is assigned

22: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;

25: iload_1

26: invokevirtual #9 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;

29: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;

32: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

35:return

}

从5处开始继续分析, 5,6,7将参数的值赋给mSubX, 此时mSubX是99了, 下面那一堆则是在执行System.out.println("SubX is assigned " + x);并返回, 还可以看到Java自动帮我们使用StringBuilder优化字符串拼接, 就不分析了.

说了这么多, 我们的代码才刚把下面箭头指着的这句执行完:

1

2

3

4

5

6

7

8

9

10

11

12

publicclassbugme.SubClassextendsbugme.SuperClass {

publicbugme.SubClass();

Code:

0: aload_0

->1: invokespecial #1 // Method bugme/SuperClass."<init>":()V

4: aload_0

5: iconst_1

6: putfield #2 // Field mSubX:I

9:return

......

}

此时mSubX已经是99了, 再执行下面的4,5,6, 这一部分是SubClass的初始化, 代码将把1赋给mSubX,99被1覆盖了.

方法返回后, 相当于我们执行完了箭头指的这一句代码:

1

2

3

4

5

6

publicclassMain {

publicstaticvoidmain(String[] args) {

->SubClass sc =newSubClass();

sc.printX();

}

}

接下来执行的代码将打印mSubX的值, 自然就是1了.

以前就听说过JVM是基于栈的, Dalvik是基于寄存器的, 现在看了Java字节码, 回想一下smali, 自然就能明白. 我在Android无需权限显示悬浮窗, 兼谈逆向分析app中有分析smali代码, smali里面经常看到类似v0, v1这类东西, 是在操作寄存器, 而刚才分析的bytecode, 指令常常伴随着入栈出栈.

理论解释我们都知道Java是面向对象的语言, 面向对象三大特性之一多态性. 假如父类构造方法中调用了某个方法, 这个方法恰好被子类重写了, 会发生什么?

根据多态性, 实际被调用的是子类的方法, 这个没错. 再考虑有继承时, 初始化的顺序. 如果是new一个子类, 那么初始化顺序是:

父类static成员 -> 子类static成员 -> 父类普通成员初始化和初始化块 -> 父类构造方法 -> 子类普通成员初始化和初始化块 -> 子类构造方法

父类构造方法中调用了一次setX, 此时mSubX中已经是我们要跟踪的值, 但之后子类普通成员初始化将mSubX又初始化了一遍, 覆盖了前面我们跟踪的值, 自然得到的值就是错的.

Java中, 在构造方法中唯一能安全调用的是基类中的final方法, 自己的final方法(自己的private方法自动final), 如果类本身是final的, 自然就能安全调用自己所有的方法.

完全遵守这个准则, 可以保证不会出这个bug. 实际上我们常常不能遵守, 所以要时刻小心这个问题.

这个东西在Java编程思想(第四版) (机械工业出版社 2012年11月第1版) 的8.3.3小节有写过, 但是这种东西除非自己遇到bug了, 基本看过不会有印象.

这篇文章所有的知识点基本都是很基础的, 我自己也都记得, 但当这些知识合在一起的时候, 他们之间产生的反应却是我没有注意过的. 这也是我写这篇文章的原因.

如果以后有人面试拿这个问题考你, 你可能是遇上drakeet了.

题外话关于默认初始化, 比如这样写:

1

2

3

4

5

6

publicclassSubClassextendsSuperClass {

privateintmSubX;

publicSubClass() {}

......

}

如果父类保证一定会在初始化时调用setX, 程序是不会出现上面说的bug的, 因为默认初始化并不是靠生成下面这样的代码默认初始化.

1

2

3

4: aload_0

5: iconst_1

6: putfield #2 // Field mSubX:I

所谓的默认初始化, 其实是我们要实例化一个对象之前, 需要一块内存放我们的数据, 这块内存被全部置为0, 这就是默认初始化了.

下面这两句话, 虽然效果一样, 但实际是有区别的.

1

2

3

privateintmSubX;

privateintmSubX =0;

一般情况下, 这两句代码对程序没有任何影响(除非你遇到这个bug), 上面一句和下面一句的区别在于, 下面一句会导致<init>方法里面生成3条指令, 分别是aload_0, iconst_0, putfield #**, 而上面一句则不会.

所以如果你的成员变量使用默认值初始化, 就没必要自己赋那个默认值, 而且还能省3条指令.

全能程序员交流QQ群290551701,群内程序员都是来自,百度、阿里、京东、小米、去哪儿、饿了吗、蓝港等高级程序员 ,拥有丰富的经验。加入我们,直线沟通技术大牛,最佳的学习环境,了解业内的一手的资讯。如果你想结实大牛,那 就加入进来,让大牛带你超神!

 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
 
© 2005- 王朝網路 版權所有 導航