Android热修--构造方法热修

353 阅读3分钟

大家好,我是瑞英。

本篇会讲述如何进行构造方法热修。能够实现修复构造方法中,super()调用之后的逻辑。

如前文所述,热修一个方法的前提是,要在方法体内预先插入代理执行逻辑。如下示例:

image.png

但构造方法与非构造方法的区别在于,进入方法体的第一条语句时,“this”对象是否被初始化。热修代理方法调用,需要传递的一个参数是,当前类的实例对象。

构造方法内的第一条语句

java中

构造方法必须进行直接或间接(调用当前类其他构造方法)调用父类构造方法。在编写代码时,会要求若构造方法体内存在super调用,必须放在第一条语句。如果不是则会报错,如下:

image.png

若没有显示调用父类构造方法的语句,则编译器会自动补充调用父类无参构造方法super(),若父类没有无参构造方法,那么子类必须显示调用父类构造方法,可以通过间接调用当前类某个构造方法,或者直接调用父类构造方法两种途径实现。

class Base {
    public Base(int a) {
    }
}

class SubClass extends Base {
    public SubClass() {
        super(3); // 直接显示调用父类构造方法
    }
    
    public SubClass(int a){
        this(); // 调用当前类构造方法实现间接调用父类构造方法
        
        // super(4); // 也可以直接调用父类构造方法
    }
}

kotlin中

本质上同java一样,构造方法必须要直接或间接调用父类构造方法,但是kotlin中将构造方法分为主构造方法和次构造方法。一个类可以有0或1个主构造方法,若干个次构造方法。同样若父类有无参构造方法,开发者没有显示调用构造方法会自动补充调用无参构造方法。

  • 若类没有主构造方法
    • 那么所有的次构造方法,都必须直接或间接(调用当前类的其他次构造方法)调用父类构造方法
  • 若类有主构造方法
    • 主构造方法必须显示调用父类构造方法
    • 那么所有的次构造方法,都必须直接或间接(调用当前类的其他次构造方法)调用主构造方法,子类的次构造方法不能直接调用父类构造方法
open class Parent(val name: String)

// 有主构造方法的子类
class Child(name: String, val age: Int) : Parent(name) { //主构造方法显示调用父类构造方法
    init {
        println("Child initialized with name: $name and age: $age")
    }
    
    ```
    // 次构造方法,调用主构造方法
    constructor(name: String) : this(name, 0) {
        println("Child initialized with name: $name and default age: 0")
    }
}

// 没有主构造方法的子类
class Child2 : Parent {
    // 次构造方法,调用父类构造方法
    constructor(name: String, age: Int) : super(name) {
        println("Child initialized with name: $name and age: $age")
    }

    // 次构造方法,调用当前类其他次构造方法
    constructor(name: String) : this(name, 0) {
        println("Child initialized with name: $name and default age: 0")
    }
    // 次构造方法,调用父类构造方法
    constructor(age: Int) : super("DefaultName") {
        println("Child initialized with default name and age: $age")
    }
}

热修构造方法的插入点

在父类构造方法/当前类其他构造方法调用语句之后,进行热修proxy插桩。换言之,构造方法热修,修复的是,父类初始化完成后的逻辑。若直接在构造方法第一条语句插入热修代理调用会抛出java.lang.VerifyError异常。如下所示:

使用了一个没有被初始化的引用 image.png

ASM如何落实热修构造方法的插入

定义一个ConstructorVisitor,在ClassVisitor访问类时,对构造方法进行访问,并完成热修代理调用语句插入

class ConstructorVisitor(private val superClassName: String, private val currentClassName: String) : MethodVisitor(ASM_API) {
    private var flag = false //标识是否已经插入了代码

    override fun visitMethodInsn(opcode: Int, owner: String, name: String, descriptor: String, isInterface: Boolean) {
        super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
        if (!flag && (name == "<init>" && (owner == superClassName || owner == currentClassName ) )) {
        }
    }
}

关于通过热修对类新增构造方法,将会在后续篇章中讲述。