Android 不想和你说话,抛了个 java.lang.VerifyError

4,037 阅读4分钟
原文链接: yrom.net

一个奇怪的崩溃

E/AndroidRuntime(22035): FATAL EXCEPTION: main
E/AndroidRuntime(22035): java.lang.VerifyError: com/sample/FileUtils
E/AndroidRuntime(22035): at com.sample.App.onCreate(App.java:16)
E/AndroidRuntime(22035): at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:999)
E/AndroidRuntime(22035): at android.app.ActivityThread.handleBindApplication(ActivityThread.java:4220)
E/AndroidRuntime(22035): at android.app.ActivityThread.access$1300(ActivityThread.java:137)
E/AndroidRuntime(22035): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1262)
E/AndroidRuntime(22035): at android.os.Handler.dispatchMessage(Handler.java:99)
E/AndroidRuntime(22035): at android.os.Looper.loop(Looper.java:137)
E/AndroidRuntime(22035): at android.app.ActivityThread.main(ActivityThread.java:4819)
E/AndroidRuntime(22035): at java.lang.reflect.Method.invokeNative(Native Method)
E/AndroidRuntime(22035): at java.lang.reflect.Method.invoke(Method.java:511)
E/AndroidRuntime(22035): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:793)
E/AndroidRuntime(22035): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:560)
E/AndroidRuntime(22035): at dalvik.system.NativeStart.main(Native Method)

为什么说奇怪?一般地,java.lang.VerifyError 是说 JVM 在加载一个类时,会去校验类的正确性,只有类文件不合法才会报这个Error。
比如,一个类试图extends一个标记为final的类,或者试图override final方法(发生在外部依赖类改变声明且应用没有完整重新编译的情况下)。
Android 中会发生这种情况的,一般是需要兼容API的时候,比如用到了高版本SDK中有的类,低版本没有,或者使用高版本API中有低版本没有的方法。 然而这个FileUtils类在com.sample.App中使用时候并没有用到与Android 版本相关的兼容性方法。

百思不得其解

Debug,有时候看堆栈是不够的,还需要查看Logcat中一些有用的上下文

W/dalvikvm(22035): VFY: unable to resolve static method 13457: Landroid/system/Os;.stat (Ljava/lang/String;)Landroid/system/StructStat;
W/dalvikvm(22035): VFY: unable to resolve exception class 1594 (Landroid/system/ErrnoException;)
W/dalvikvm(22035): VFY: unable to find exception handler at addr 0xe
W/dalvikvm(22035): VFY: rejected Lcom/sample/FileUtils;.getUid (Ljava/lang/String;)I
W/dalvikvm(22035): VFY: rejecting opcode 0x0d at 0x000e
W/dalvikvm(22035): VFY: rejected Lcom/sample/FileUtils;.getUid (Ljava/lang/String;)I
W/dalvikvm(22035): Verifier rejected class Lcom/sample/FileUtils;

Log也似乎与平常使用高版本SDK类时的兼容性警告类似:

W/dalvikvm(22524): VFY: unable to resolve virtual method 684: Landroid/content/res/Resources;.getColor (ILandroid/content/res/Resources$Theme;)I
W/dalvikvm(22524): VFY: unable to resolve virtual method 686: Landroid/content/res/Resources;.getColorStateList (ILandroid/content/res/Resources$Theme;)Landroid/content/res/ColorStateList;
W/dalvikvm(22524): VFY: unable to resolve virtual method 693: Landroid/content/res/Resources;.getDrawable (ILandroid/content/res/Resources$Theme;)Landroid/graphics/drawable/Drawable;

回到崩溃开始的警告,”unable to resolve static method” 这条日志应该不会是导致VerifyError的元凶。(注:出现这个警告意味着你如果运行时用到了这个方法,运行时将会报错,如InstantiationError、NoSuchMethodError之类) 那么应该是关键的一句:”unable to find exception handler at addr 0xe“,导致后面的”rejected Lcom/sample/FileUtils;.getUid (Ljava/lang/String;)I” 并最终导致”Verifier rejected class Lcom/sample/FileUtils;” 仔细查看 FileUtils 这个类里的方法getUid()是否有try-catch代码块:

(21)
public static int getUid(String path) {
  if (Build.VERSION.SDK_INT >= 21) {
    try {
      return Os.stat(path).st_uid;
    } catch (android.system.ErrnoException e) {
      return -1;
  return -1;

确实有尝试catch一个低版本不存在的Exception,但问题在于这个方法并没有使用到!!
而且看起来也十分的正常,一般兼容老版本SDK不都是这样的写法吗?为何单单这里会导致FileUtils类“不合法”? 为了证明是这个在低版本不存在的Exception导致的,对该方法里的try-catch做了简单的处理:

try {
  return Os.stat(path).st_uid;
  } catch (Exception e) {
  return -1;

不出所料,警告只剩下了VFY: unable to resolve static method 13457: Landroid/system/Os;.stat (Ljava/lang/String;)Landroid/system/StructStat;而且没有导致VerifyError what ?????? 想必看到现在的你也是一脸问号……

追根溯源

javap工具查看 FileUtils 修改前后的字节码有何不同之处:

$ javap -v FileUtils.class

未修改之前:

public static int getUid(java.lang.String);
  descriptor: (Ljava/lang/String;)I
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
    stack=2, locals=2, args_size=1
       0: getstatic     #2                  // Field android/os/Build$VERSION.SDK_INT:I
       3: bipush        21
       5: if_icmplt     19
       8: aload_0
       9: invokestatic  #4                  // Method android/system/Os.stat:(Ljava/lang/String;)Landroid/system/StructStat;
      12: getfield      #5                  // Field android/system/StructStat.st_uid:I
      15: ireturn
      16: astore_1
      17: iconst_m1
      18: ireturn
      19: iconst_m1
      20: ireturn
    Exception table:
       from    to  target type
           8    15    16   Class android/system/ErrnoException
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
         17       2     1     e   Landroid/system/ErrnoException;
          0      21     0  path   Ljava/lang/String;
    LineNumberTable:
      line 19: 0
      line 21: 8
      line 22: 16
      line 23: 17
      line 26: 19
    StackMapTable: number_of_entries = 2
      frame_type = 80 /* same_locals_1_stack_item */
        stack = [ class android/system/ErrnoException ]
      frame_type = 2 /* same */
  RuntimeInvisibleAnnotations:
    0: #26(#27=I#28)

修改后,对照只有Exception tableLocalVariableTableStackMapTable有区别:

public static int getUid(java.lang.String);
  ...
    Exception table:
       from    to  target type
           8    15    16   Class java/lang/Exception
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
         17       2     1     e   Ljava/lang/Exception;
          0      21     0  path   Ljava/lang/String;
    ...
    StackMapTable: number_of_entries = 2
      frame_type = 80 /* same_locals_1_stack_item */
        stack = [ class java/lang/Exception ]
      frame_type = 2 /* same */

可以猜想问题产生的原因应该是:被Catch的异常类的加载和普通类应该是不一样的try-catch中的异常类的声明出现在了Exception tableLocalVariableTable,会不会这个原因导致android/system/ErrnoException被提前加载,最终verify不通过?

探察dalvik源码

因为是在低版本手机上触发的问题,运行的仍然是 dalvik VM,很容易的(google)在对应版本(4.1.1)源码中找到类DexVerify.cpp,和 CodeVerify.cpp 前面”Verifier rejected class Lcom/sample/FileUtils;” 就是 DexVerify的报错日志 (感兴趣的可以从 dvmVerifyClass() 开始阅读类检查的全过程。) DexVerify 中的 verifyMethod() 最终会调用 CodeVerify 的 dvmVerifyCodeFlow() 来确保类中的单个方法执行流是合法的。 其中要注意的是,异常处理(Exception Hanler)也是在这个时候被校验的,它的opcode是OP_MOVE_EXCEPTION(0x0d,就是前面日志”rejecting opcode 0x0d”提到的)。 检验方法getCaughtExceptionType() 在找不到catch代码块中指定的异常类(如例子中的ErrnoException)时即会报错:”VFY: unable to resolve exception class 1594 (Landroid/system/ErrnoException;)”,尝试各种可能性之后仍然不知道该如何处理这个异常,接着会认为代码有问题日志报错:”VFY: unable to find exception handler at addr 0xe” 和 “VFY: rejected Lcom/sample/FileUtils;.getUid (Ljava/lang/String;)I” 最终走向方法校验失败的分支”rejecting opcode 0x0d at 0x000e”,于是乎dvmVerifyCodeFlow()方法return false标识着verifyMethod()失败,拒绝加载类:”Verifier rejected class Lcom/sample/FileUtils;” 而简单修改后,就不会导致getCaughtExceptionType()方法执行时出现找不到异常类的情况。

延伸思考

如果try-catch做如下修改还会一言不合抛出VerifyError 吗?

try {
  return Os.stat(path).st_uid;
  } catch (Exception e) {
  if (e instanceOf android.system.ErrnoException)
    return -1;

结论

为了兼容性考虑,在尝试try-catch高版本SDK中的异常时,千万小心! 可以参考support-v4中的处理,如ContextCompat,调用需要兼容性处理的方法时,由不同版本的实现类来处理,如ContextCompatJellybeanContextCompatApi23,即使ContextCompatJellybean中有catch高版本异常类,但运行时不会出现类找不到的情况(不会报运行时异常),更不会导致直接引用类ContextCompatverify不通过而直接报VerifyError