完美代码的网站,制作app页面的软件,模板王怎么下载字体,郑州专业公司网站建设公司安卓开发中#xff0c;Java/Kotlin等高级语言被编译成.class字节码#xff0c;之后通过dx/d8、r8等工具编译成dex文件#xff08;Dalvik字节码#xff09;#xff0c;打包到APK中。安卓通过ART或者DalvikVM加载运行Dalvik字节码。因此#xff0c;对于安卓编码#xff0c…安卓开发中Java/Kotlin等高级语言被编译成.class字节码之后通过dx/d8、r8等工具编译成dex文件Dalvik字节码打包到APK中。安卓通过ART或者DalvikVM加载运行Dalvik字节码。因此对于安卓编码Dalvik字节码层面相比.class字节码层面更有指导意义。静态属性与this指针静态属性用static关键字修饰包括字段以及方法是类的属性而非静态属性则是实例的属性。由于非静态属性往往跟实例绑定静态属性的访问不存在实例需要的参数更少。DalvikVM是基于寄存器的指令集每个方法内的pn寄存器都是参数寄存器而vn都是本地寄存器非静态方法中的p0表示this指针。如下代码是读取类a.b.C的非静态字段bool和静态字段sbool可以看到在读取非静态字段时会从p0this进行引用而读取sbool则不需要。iget-boolean v0, p0 La/b/C;-bool:Zsget-boolean v1 La/b/C-sbool:Z另外对于代码调用静态方法使用invoke-static不需要传入实例本身而非静态方法使用invoke-virtual调用即便是从p0(this)调用自己的方法也需要传入实例。.method call()V.registers 2invoke-virtual {p0}, La/b/C;-getBool()Zmove-result-object v0invoke-static La/b/C;-sgetBool()Zmove-result-object v0return-void.end method.method getBool()Z.registers 2sget-boolean v0, La/b/C;-sbool:Zreturn v0.end method.method static sgetBool()Z.registers 1sget-boolean v0, La/b/C;-sbool:Zreturn v0.end method可以看到在调用getBool和sgetBool时由于方法是否静态的差别其调用参数也有差别虽然二者的字节码指令一致仅使用寄存器v0但非静态方法调用时仍需传参p0。另外考虑以下调用a.b.C obj null;obj.sgetBool();obj.getBool(); // NullPointerException其中obj为一个空值其在调用非静态方法getBool时必然抛出NullPointerException异常但却可以安全调用静态方法sgetBool因为编译器编译后会直接换作obj的类进行invoke-static静态调用与实例本身无关。因此在不考虑子类重写以及使用this时尽量用static修饰方法。字段与局部变量前面说到Dalvik字节码是基于寄存器的指令集经过ART的AOT/JIT后也更方便生成机器码这与基于堆栈的.class字节码不同。局部变量在生成Dalvik字节码时往往都用寄存器表示因此在安卓开发中使用局部变量时直接当作寄存器即可不必像堆栈型JVM那样考虑堆栈操作的开销。考虑以下代码int a;void compute1() {a a * a - 1;}void compute2() {int i a;a i * i - 1;}对应的Dalvik字节码为.field a:I.method compute1()V.locals 2iget v0, p0, LMain;-a:Iiget v1, p0, LMain;-a:Imul-int/2addr v0, v1add-int/lit8 v0, v0, -0x1iput v0, p0, LMain;-a:Ireturn-void.end method.method compute2()V.locals 1iget v0, p0, LMain;-a:Imul-int/2addr v0, v0add-int/lit8 v0, v0, -0x1iput v0, p0, LMain;-a:Ireturn-void.end method对于compute1读了两次字段a、写了一次对于compute2读了一次写了一次。因此虽然后者的Java代码更多但是生成的Dalvik字节码要少一句读a的操作而局部变量i则用寄存器v0表示。另外这种情况下两次对a的访问操作也不能通过编译优化为一次因为Java多线程情况下如果由于线程调度使两次获取a的值不一致如果编译优化则会使其一致影响了代码逻辑。很多人写代码时针对全局变量或者字段进行访问时为图代码规整、简洁大量重复访问同一字段既使Dalvik字节码变得冗余又增加了多线程不一致的隐患。建议在需要多次访问一个字段且保持一致性时先赋值给一个局部变量后续的访问仅针对这个局部变量进行操作或者可以用final修饰字段因为final修饰时多次访问可以被编译优化为一次。final属性与编译优化常量替换与函数内联是一种常见的编译优化手段用于将一些常量的值或短函数的代码直接嵌入到引用处减少了寻址、调用栈变动的开销。考虑到JVM的继承与重写特性可被子类修改的字段以及方法无private且无final修饰往往不会被常量替换与方法内联。因此如果需要对这类字段或方法进行优化尽量加上final修饰如下boolean testa(int n) {return n ! 0;}public final boolean testb(int n) {return n ! 0;}private boolean testc(int n) {return n ! 0;}void test() {int n (int)(Math.random() * 100);System.out.println(testa(n));System.out.println(testb(n)); // System.out.println(n ! 0);System.out.println(testc(n)); // System.out.println(n ! 0);}如果编译器支持内联优化那么testb与testc均会被内联优化为n ! 0而testa不会因为private和final都保证了这些方法不能被子类重写在编译阶段就能确认它们具体调用的代码而testa可以被子类重写因此此处调用testa的具体代码是什么还要看子类是否会重写因此不能确认具体调用的代码无法进行内联优化。同理仅有被final修饰的全局字面值常量才能进行常量替换。当然也有一些局部变量也可以被替换但这应归于一种更广义的编译优化预计算。内部类与桥接方法Dalvik字节码中的成员类是如何访问主类的私有属性的考虑以下代码class Main{private int pA;private void pM() {}class Member{Member() {pA 0;pM();}}}如果是在Hotspot中这不成问题private属性对成员类自然开放可直接由invoke族、getfield、putfield等指令进行操作。但是对于Dalvik字节码中成员类不能直接访问主类的private方法需要由编译器生成一些桥接方法Bridge Method实现。Hotspot本身支持桥接方法主要用于子类对父类重写一些方法并改变一些签名类型时使用比如父类方法签名()Ljava/lang/CharSequence;而子类重写时改为()Ljava/lang/String;此时编译器会给子类生成一个桥接方法来实现签名兼容。而在生成Dalvik字节码通过对主类生成一些访问权限更宽的桥接方法来实现成员类对主类私有方法、字段的访问。上述代码生成的部分Dalvik字节码如下# 成员类 .class LMain$Member;.method constructor init(LMain;)V.registers 3invoke-direct {p0}, Ljava/lang/Object;-init()V # super();const/4 v0, 0x0invoke-static {p1, v0}, LMain;--$$Nest$fputpA(LMain;I)V # Main.this.pA 0;invoke-static {p1}, LMain;--$$Nest$mpM(LMain;)V # Main.this.pM();return-void.end method# 主类 .class LMain;.field private pA:I# 桥接方法修改pA.method static bridge synthetic -$$Nest$fputpA(LMain;I)V.registers 2iput p1, p0, LMain;-pA:Ireturn-void.end method# 桥接方法调用pM.method static bridge synthetic -$$Nest$mpM(LMain;)V.registers 1invoke-direct {p0}, LMain;-pM()Vreturn-void.end method.method private pM()V.registers 1return-void.end method可以看到编译器为主类Main生成了一个桥接方法-$$Nest$fputpA用于修改pA以及一个桥接方法-$$Nest$mpM用于调用pM这些桥接方法均为包内可见的静态方法因此成员类Main.Member可以直接访问之。因此当成员类通过桥接方法访问主类时必然造成调用栈变长。如果主类的某个私有字段已经创造了一些getter/setter方法但是Java代码中成员类仍显式操作主类字段时生成的桥接方法就显得多余重复。为减少不必要的桥接方法尽量不显式访问主类的私有字段与方法。除了用getter/setter间接操作主类字段也可以用protected代替private避免桥接方法生成因为在DalvikVM中protected对子类与成员类均可见可以实现一种较弱的私有化。匿名类与Lambda匿名类可以实现闭包编译期会生成一个.class文件而Lambda编译期不会生成.class文件而是直接用invokedynamic指令生成一个Lambda实例。但是DalvikVM不支持invokedynamic指令在生成dex时Lambda仍会转为匿名类。因此在安卓上Lambda只能算一种语法糖。在Lambda作为语法糖的情况下此处仅讨论匿名类。考虑闭包匿名类属于非静态成员类在编译器生成的构造函数中会传入主类的this引用因此才可以在匿名类中通过Main.this实现对主类字段的访问主类this前缀可省也是一种语法糖另外为了实现对本地变量的闭包构造函数中也会自动生成引用的局部变量作为形参。如果不希望再匿名类的形参中出现this建议直接通过有名的静态成员类或者封装一个静态方法来创建匿名类。大多数情况下我们只对接口类创建匿名类而接口是可以被多实现的因此可以通过单个类来实现多个接口来减少.class的数量甚至直接用主类实现。对比以下代码interface IA { public void a(); }interface IB { public void b(); }class A implements IA, IB {void callThis() {callbackA(this);callbackB(this);}void callLambda() {callbackA(this::a); // Lambda实际转为匿名类下同callbackB(this::b);}void callbackA(IA ia) {...}void callbackB(IB ib) {...}public void a() {...}public void b() {...}}其中callThis和callLambda生成的Dalvik字节码如下.method callThis()V.registers 1invoke-virtual {p0, p0}, LA;-callbackA(LIA;)V # this.callbackA(this);invoke-virtual {p0, p0}, LA;-callbackB(LIB;)V # this.callbackB(this);return-void.end method.method callLambda()V.registers 2new-instance v0, LA$$ExternalSyntheticLambda0;invoke-direct {v0, p0}, LA$$ExternalSyntheticLambda0;-init(LA;)V # v0 new A$$ExternalSyntheticLambda0(this);invoke-virtual {p0, v0}, LA;-callbackA(LIA;)V # this.callbackA(v0);new-instance v0, LA$$ExternalSyntheticLambda1;invoke-direct {v0, p0}, LA$$ExternalSyntheticLambda1;-init(LA;)V # v0 new A$$ExternalSyntheticLambda1(this);invoke-virtual {p0, v0}, LA;-callbackB(LIB;)V # this.callbackB(v0);return-void.end method可以看到callThis由于直接传入this对象作为callback方法的参数因此并没有出现匿名类的情况反观callLambda由于使用Lambda进行传参实际生成了A$$ExternalSyntheticLambda0和A$$ExternalSyntheticLambda1两个匿名类并分别创建了两个实例。相较而言通过单个类甚至是主类实现多个接口有利于减少匿名接口类的数量以及实例的数量对减少字节码规模、减少堆内存使用、避免内存泄漏某些回调接口实例都是有帮助的。当然通过单个类/主类实现多个接口也是有缺点的使主类携带过多接口信息影响封装性主类中用于闭包的字段需要手动回收需要为同一接口创建不同代码的实例时往往只能取其一。