虽然文章的标题是Java,但是几乎所有面向对象的设计语言都遵循这个初始化过程。感谢廖虎丘liaohuqiu_QiWanwan指出我之前忘记说了。前言drakeet写了一个RecyclerView相关的GenerousRecyclerView,原文中提到写这个的目的就达到了。因为我们需要知道ViewGroup的clipToPadding属性,所以我们调用了ViewGroup.getClipToPadding,但是这个方法是APIlevel21引入的,我看了一下代码,ViewGroup是通过调用setClipToPadding来初始化的,而setClipToPadding在APIlevel1是available,也就是说我们只需要监听setClipToPadding的调用就可以知道ViewGroup的clipToPadding状态。太巧妙了,如果我告诉drakeet,说不定就能引起他的注意,成为CEO,开始人生**。如果你已经知道我要说什么,你可以鄙视我。这个问题简单还原问题,我们有一个classSuperClasspublicclassSuperClass{privateintmSuperX;publicSuperClass(){setX(99);}publicvoidsetX(intx){mSuperX=x;}}现在我们想随时知道mSuperX的值不用反射,因为父类从不直接修改mSuperX的值,总是通过setX来改变,所以最简单的方法就是继承SuperClass,重写setX方法,监听它的变化即可。下面是我们的子类SubClass:publicclassSubClassextendsSuperClass{privateintmSubX=1;publicSubClass(){}@OverridepublicvoidsetX(intx){super.setX(x);mSubX=x;System.out.println("SubXisassigned"+x);}publicvoidprintX(){System.out.println("SubX="+mSubX);}}我使用mSubX来跟踪mSuperX因为在ViewGro在up中,clipToPadding的默认值为true(为了简化问题,认为是boolean,其实不是),ViewGroup的初始化可以不调用setClipToPadding,此时是默认值。为了模拟这种情况,将mSubX初始化为1。***在main中调用:publicclassMain{publicstaticvoidmain(String[]args){SubClasssc=newSubClass();sc.printX();}}包括我在内的很多人认为那终端输出的结果应该是:SubXisassigned99SubX=99然而,运行后实际输出是:SubXisassigned99SubX=1实际分析如果想知道发生了什么,最简单的方法就是看程序是如何执行的,比如单步调试,或者直接,看JavawordSection代码。下面是Main的字节码编译自"Main.java"publicclassbugme.Main{...publicstaticvoidmain(java.lang.String[]);Code:0:new#2//classbugme/SubClass3:dup4:invokespecial#3//Methodbugme/SubClass."":()V......}这个是用javap反编译.class文件直接得到的。虽然也是Java写的,但是用apktool逆向编译APK文件(dex文件)得到的smali代码,和JavaBytecode有明显区别。字节码乍一看很奇怪,只要知道它隐含了一个栈和一个局部变量表,就很好理解了。这段代码段首先创建一个新的SubClass实例,将引用压入栈中,dup将栈顶复制到栈中,invokespecial#3弹出栈顶元素并调用它的某个方法,这个方法是什么是依赖于常量池中的第三个入口是什么,但是javap生成的字节码直接写在我们旁边,也就是SubClass.。接下来看SubClass.,publicclassbugme.SubClassextendsbugme.SuperClass{publicbugme.SubClass();Code:0:aload_01:invokespecial#1//Methodbugme/SuperClass."":()V......没有调用的方法,因为javap是为它准备的为了方便我们阅读,直接改成类名bugme.SubClass,对了,bugme是包名。方法并不是通常意义上的构造方法,它是Java为我们合成的方法,里面的指令会帮助我们按顺序初始化普通成员变量,包括初始化块中的代码。注意是按顺序执行的。这些执行完之后,就轮到执行构造方法中代码生成的指令了。这里将局部变量表中aload_0下标为0的元素压入栈中,在Java中其实就是this。结合invokespecial#1,就是调用父类的构造函数,也就是我们常见的super()。那么我们看一下SuperClass.publicclassbugme.SuperClass{publicbugme.SuperClass();代码:0:aload_01:invokespecial#1//Methodjava/lang/Object."":()V4:aload_05:bipush997:invokevirtual#2//MethodsetX:(I)V10:return......}也是先调整父类Object的构造函数,然后把这个,99压栈。invokevirtual#2旁边的注释是调用setX,参数是this和99,也就是this.setX(99),不过这??个方法改写成调用子类的方法了,再看SubClass。setX:publicclassbugme.SubClassextendsbugme.SuperClass{...publicvoidsetX(int);Code:0:aload_01:iload_12:invokespecial#3//Methodbugme/SuperClass.setX:(I)V......}这里,第一个局部变量表的两个元素被压入堆栈,***就是这个,第二个是括号里的参数,也就是99,invokespecial#3调用父类的setX,也就是我们代码中写的super.setX(int)SuperClass.setX很简单:publicclassbugme。SuperClass{...publicvoidsetX(int);Code:0:aload_01:iload_12:putfield#3//FieldmSuperX:I5:return}这里先把这个入栈,再把参数入栈,putfield#3使得之前入栈的两个元素全部出栈,给成员mSuperX赋值。这四个指令只对应代码中的this.mSuperX=x这句话;然后控制流返回子类的setX:publicclassbugme.SubClassextendsbugme.SuperClass{......publicvoidsetX(int);Code:0:aload_01:iload_12:invokespecial#3//Methodbugme/SuperClass.setX:(i)V->5:aload_0//这句话会被执行6:iload_17:putfield#2//FieldmSubX:I10:getstatic#4//Fieldjava/lang/System.out:Ljava/io/PrintStream;13:new#5//classjava/lang/StringBuilder16:dup17:invokespecial#6//Methodjava/lang/StringBuilder."":()V20:ldc#7//StringSubXisassigned22:invokevirtual#8//Methodjava/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;25:iload_126:invokevirtual#9//Methodjava/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;29:invokevirtual#10//Methodjava/lang/StringBuilder.toString:()Ljava/lang/String;32:invokevirtual#11//Methodjava/io/PrintStream.println:(Ljava/lang/String;)V35:return}从5开始继续来分析一下,5、6、7把参数的值赋值给mSubX,此时mSubX为99,下面这堆在执行System.out.println("SubXisassigned"+x);并返回,可以看到java自动帮我们使用StringBuilder优化字符串拼接,就不分析了。说了这么多,我们的代码只是执行了下面箭头所指的那句话:publicclassbugme.SubClassextendsbugme.SuperClass{publicbugme.SubClass();Code:0:aload_0->1:invokespecial#1//Methodbugme/SuperClass."":()V4:aload_05:iconst_16:putfield#2//FieldmSubX:I9:return...}这时候mSubX已经是99了,然后执行下面的4、5、6,这部分就是SubClass的初始化,代码会给mSubX赋1,99被1覆盖。方法返回后,相当于我们执行完了箭头所指这行代码:publicclassMain{publicstaticvoidmain(String[]args){->SubClasssc=newSubClass();sc.printX();}}接下来执行的代码会打印mSubX的值,自然是1。之前听说过JVM是基于栈的,Dalvik是基于寄存器。现在看了Java字节码,回想smali,自然能看懂。Android上显示悬浮窗不需要权限,说说app中smali代码的逆向分析,在smali中,我们经常看到v0、v1之类的东西,它们是操作寄存器的,而刚才分析的字节码,指令中往往伴随着出栈和出栈。我们都知道Java是一种面向对象的语言。多态性,面向对象的三大特征之一。如果在父类构造方法中调用了一个方法,而这个方法恰好被子类重写了,会发生什么情况呢?根据多态性,子类的方法实际上是被调用的,这是正确的。考虑有继承时的初始化顺序。如果是新的子类,那么初始化顺序是:父类静态成员->子类静态成员->父类普通成员初始化和初始化块->父类构造方法->子类普通成员初始化和初始化块->子类构造方法在父类构造方法中调用了一次setX,此时mSubX已经是我们要跟踪的值,但是子类普通成员初始化后会将mSubX重新初始化,覆盖掉我们之前跟踪的值,自然得到的值是错误的。在Java中,构造方法中唯一可以安全调用的就是基类中的final方法,还有你自己的final方法(你自己的私有方法Automaticfinal),如果类本身是final的,自然就可以安全地调用它自己的所有方法。完全遵守这个规则可以保证不会出现这个bug。事实上,我们经常不遵守,所以我们必须时刻注意这个问题。这个东西在Java编程思想(第四版)(机械工业出版社,第1版,2012年11月)的8.3.3节已经写过了,但是除非遇到bug,否则你不会对这种东西印象深刻。这篇文章的所有知识点基本上都是很基础的,我自己也记住了,但是当这些知识放在一起的时候,它们之间的反应是我没有注意到的。这就是我写这篇文章的原因。如果以后有人用这个问题面试你,你可能遇到了drakeet。关于默认初始化的题外话,例如:publicclassSubClassextendsSuperClass{privateintmSubX;publicSubClass(){}......}如果父类保证在初始化时会调用setX,程序就不会出现上面说的bug,因为默认初始化不是通过生成下面的代码来默认初始化的。4:aload_05:iconst_16:putfield#2//FieldmSubX:I所谓默认初始化其实就是我们要实例化一个对象之前,需要一块内存来存放我们的数据,而这块内存全部设置为0,即默认初始化。下面两句话,虽然效果是一样的,但实际上还是有区别的。私人整数毫秒ubX;私有intmSubX=0;一般情况下,这两行代码对程序没有影响(除非你遇到这个bug),上面一行和下面一行的区别是下面一行会导致在方法中生成3指令,分别是aload_0,iconst_0,putfield#**,但是上面这句不会。所以如果你的成员变量初始化了默认值,就不用自己赋默认值了,可以省去3条指令。