问题背景
jar 包 A 使用了 jar 包 B 中 BClass 的实例方法 b,线上运行良好。
因业务迭代,jarB 中 BClass 的 b 由实例方法调整为静态方法,jarA 未重新打包,上线后发生如下崩溃:
java.lang.IncompatibleClassChangeError
The method 'void com.example.A.d(android.view.View, java.lang.String)' was expected to be of type virtual but instead was found to be of type static (declaration of 'com.example.B.a' appears in base.apk!classes5.dex)
经过排查,jarB 的方法调整后,在字节码层面发生的变化使 jarA 需要重新打包才能正常运行。
分析过程如下:
Demo
public class A {
public static void main(String[] args) {
B b = new B();
b.hello();
}
}
public class B {
public void hello(){
System.out.println("hello");
}
}
javac 编译 A.java 同时生成 A.class 和 B.class
>>> java A
hello
运行 A.class 成功输出 hello
复现
public class B {
public static void hello(){
System.out.println("hello");
}
}
调整 hello() 为静态方法。
javac 仅编译 B.java,编译过的 A.class 保持不变,再次运行 A.class 出现异常:
>>> java A
Exception in thread "main" java.lang.IncompatibleClassChangeError: Expecting non-static method B.hello()V
at A.main(A.java:4)
分析
把 A.class 拖入反编译工具中也看不出什么名堂
所以用 javap 看看 A.class 的字节码:
Compiled from "A.java"
public class A {
public A();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class B
3: dup
4: invokespecial #3 // Method B."<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method B.hello:()V
12: return
}
看 main 方法字节码第 9 行:invokevirtual,是用于调用非私有实例方法。 而现在 B.class 的 hello 方法已经被改为了静态方法,应当由 invokestatic 指令调用。
期望的方法修饰与实际不相符,便抛出了 IncompatibleClassChangeError 异常,异常中的 msg 写的也很清楚:Expecting non-static method.
指令表
| 指令 | 说明 |
|---|---|
| invokeinterface | 调用接口方法,在运行时搜索特定对象所实现的该接口方法 (Invoke interface method) |
| invokevirtual | 调用对象的实例方法,根据对象的实际类型进行分派 (Invoke instance method; dispatch based on class) |
| invokestatic | 调用类方法 (Invoke a class (static) method) |
| invokespecial | 调用一些需要特殊处理的实例方法,包括父类方法、私有方法和构造方法。 (Invoke instance method; special handling for superclass, private, and instance initialization method invocations) |
| invokedynamic | JDK 1.7 加入的指令,允许应用级别的代码来确定执行哪一个方法调用,只有在调用要执行的时候,才会进行这种判断,从而达到动态语言的支持。 (Invoke dynamic method) |
更多一点
别的修饰符会造成 IncompatibleClassChangeError 么?
final & synchronized
public class B {
public final synchronized void hello(){
System.out.println("hello");
}
}
hello() 去掉 static 修饰符,增加 final 和 synchronized.
javac 仅编译 B.java,再次运行 A.class 依然正常:
>>> java A
hello
protected -> public
public class SuperB {
protected void superFunction() {
System.out.println("SuperB#superFunction");
}
}
public class B extends SuperB {
public void hello() {
superFunction();
System.out.println("hello");
}
}
让 B 继承 SuperB , hello() 中调用 superFunction().
javac 编译 A.java 然后运行,日志正常打印:
>>> java A
SuperB#superFunction
hello
然后调整 SuperB 的 superFunction() 的可见性由 protected 变为 public.
javac 仅编译 SuperB.java ,然后运行 A.class,日志依然能正常打印:
>>> java A
SuperB#superFunction
hello
附
本文 java 版本:
java version "1.8.0_281"
Java(TM) SE Runtime Environment (build 1.8.0_281-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.281-b09, mixed mode)