返回

深挖VerifyError,避开血淋淋的线上崩溃

Android

VerifyError的 血淋淋教训

在软件开发中,VerifyError是一种令人头疼的崩溃类型,它发生在Java虚拟机(JVM)验证字节码时。它的成因多种多样,包括类的层次结构、反射的滥用、以及ClassLoader的配置不当。

崩溃的序幕

前不久,我们在发布了一个新版本后,收到线上反馈,报告了VerifyError崩溃。该崩溃发生在一个通用的方法中,用于获取不同Android版本的Drawable对象。

public static Drawable getDrawable(Context context, int resId) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        return context.getResources().getDrawable(resId, context.getTheme());
    } else {
        return context.getResources().getDrawable(resId);
    }
}

经过一番排查,我们发现崩溃发生在getDrawable方法中,它根据Android版本返回不同的Drawable对象。在Android 5.0及以上版本中,getDrawable返回一个RippleDrawable对象,而对于5.0以下版本,则返回一个StateListDrawable对象。

罪魁祸首:非法反射

在调查过程中,我们发现该崩溃是由反射的非法使用引起的。具体来说,在getDrawable方法中,我们使用了Class.forName来动态加载一个类,用于获取Drawable对象的类型。

Class<?> drawableClass = Class.forName(className);

在Android 5.0以下版本中,Class.forName的行为与Android 5.0及以上版本不同。在5.0以下版本中,Class.forName会触发类的初始化过程,这会导致VerifyError崩溃。这是因为在getDrawable方法中,我们没有显式地初始化drawableClass,JVM会在使用drawableClass之前对其进行验证,从而触发崩溃。

解决之道

为了解决这个问题,我们修改了代码,避免在getDrawable方法中使用反射。我们改为使用Resources.getDrawable方法,它可以安全地获取不同Android版本的Drawable对象。

public static Drawable getDrawable(Context context, int resId) {
    return context.getResources().getDrawable(resId, context.getTheme());
}

通过消除反射的使用,我们解决了VerifyError崩溃,保证了线上代码的稳定性。

教训总结

这次VerifyError崩溃给我们上了一次宝贵的教训。它提醒我们,在使用反射时要格外小心,特别是对于可能导致类初始化的反射操作。此外,我们还了解到ClassLoader在安全性和类验证中的重要作用。

为了避免类似的错误,我们总结了以下最佳实践:

  • 谨慎使用反射,避免在运行时加载和初始化类。
  • 理解ClassLoader的机制,并根据需要进行适当的配置。
  • 定期进行代码审查,以识别潜在的安全问题和VerifyError风险。

通过遵循这些最佳实践,我们可以提高代码质量,降低VerifyError崩溃的风险,从而为用户提供稳定可靠的软件体验。