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;
翻看 FileUtils 这个类里确实声明了一个与高版本SDK相关的方法:
@TargetApi(21) publicstaticintgetUid(String path){ if(Build.VERSION.SDK_INT >=21) { try{ returnOs.stat(path).st_uid; } catch(android.system.ErrnoException e) { return-1; } } return-1; }
但问题在于这个方法并没有使用到!!
而且看起来也十分的正常,一般兼容老版本SDK不都是这样的写法吗?为何单单这里会导致 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;”
为了证明是这个在低版本不存在的 Exception
导致的,对该方法里的 try-catch
做了简单的处理:
try{ returnOs.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 table
、 LocalVariableTable
和 StackMapTable
有区别:
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的异常类的加载和普通类应该是不一样的 。
因为是在低版本手机上触发的问题,运行的仍然是 dalvik VM,很容易的在对应版本源码中找到类 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()
方法执行时出现找不到异常类的情况。
如果做如下修改还会一言不合抛出 VerifyError
吗?
try{ returnOs.stat(path).st_uid; } catch(Exception e) { if(e instanceOf android.system.ErrnoException) return-1; }