Kitkat(API 19)显式调用接口默认方法触发VerifyError问题
问题背景
当实例化一个显式调用所实现接口默认方法的类时,仅在KitKat(API 19)系统中会抛出java.lang.VerifyError异常,Lollipop及以上版本可正常运行。涉及的默认方法是Android 12(API 31)新增的,即使已经通过Build.VERSION.SDK_INT做了版本判断,确保该代码行在旧版本不会执行,KitKat仍会触发异常。
代码示例
public class MyInputConnection implements InputConnection { private void foo() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { SurroundingText surroundingText = InputConnection.super.getSurroundingText(1, 1, 0); //... } } }
MyInputConnection ic = new MyInputConnection(this); // 此处触发错误
错误日志
W/dalvikvm: DexOpt: method is in an interface
I/dalvikvm: Could not find method android.view.inputmethod.InputConnection.getSurroundingText, referenced from method com.ewittman.example.MyInputConnection.foo
W/dalvikvm: VFY: unable to resolve virtual method 44: Landroid/view/inputmethod/InputConnection;.getSurroundingText (III)Landroid/view/inputmethod/SurroundingText;
VFY: rejecting opcode 0x6f at 0x0008
VFY: rejected Lcom/ewittman/example/MyInputConnection;.foo ()V
Verifier rejected class Lcom/ewittman/example/MyInputConnection;
错误信息
java.lang.VerifyError: com/ewittman/example/MyInputConnection
问题原因
KitKat搭载的Dalvik虚拟机在类加载阶段会执行严格的字节码校验,它不会判断代码分支是否会被实际执行,只要字节码中引用了当前系统不存在的方法(这里InputConnection.getSurroundingText()是API 31才新增的默认方法,KitKat的系统接口中根本没有这个方法定义),校验就会直接失败,拒绝加载整个类,最终抛出VerifyError。而Lollipop及以上版本使用的ART虚拟机在这部分校验逻辑上做了优化,允许这类分支保护下的无效方法引用存在。
优雅解决方案
方案1:利用类加载延迟拆分逻辑
把调用默认方法的代码抽成一个单独的辅助类,这个类仅在API 31及以上环境中才会被加载,KitKat下不会触发该类的加载,自然不会触发校验错误。
示例代码:
public class MyInputConnection implements InputConnection { private void foo() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { Api31Helper.getSurroundingText(this); } } private static class Api31Helper { static SurroundingText getSurroundingText(InputConnection connection) { return connection.super.getSurroundingText(1, 1, 0); } } }
方案2:通过反射调用默认方法
使用反射机制动态获取并调用接口的默认方法,这样字节码中不会直接引用不存在的方法,绕开Dalvik的校验。
示例代码:
public class MyInputConnection implements InputConnection { private void foo() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { try { MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle handle = lookup.findSpecial(InputConnection.class, "getSurroundingText", MethodType.methodType(SurroundingText.class, int.class, int.class, int.class), MyInputConnection.class); SurroundingText surroundingText = (SurroundingText) handle.invokeExact(this, 1, 1, 0); // 后续逻辑处理 } catch (Throwable e) { // 异常处理 e.printStackTrace(); } } } }
总结
KitKat的Dalvik校验机制是问题根源,只要字节码中存在无效方法引用就会拒绝加载类,和代码是否会被执行无关。上述方案通过延迟类加载或反射的方式,绕开了Dalvik的严格校验,同时保证了高版本系统中可以正常调用默认方法,比创建dummy实现的方式更优雅。
内容的提问来源于stack exchange,提问作者ewittman

