Android上关于NoClassDefFoundError那些事

1,907 阅读8分钟

背景

最近业务上反馈在android12上部分手机在反射操作的时候发生崩溃,但是也不是必现,自己找了一台android12的手机也没有复现这个问题,崩溃日志有如下:

Fatal Exception: java.lang.NoClassDefFoundError:xxx.Fragment
Caused by: java.lang.VerifyError:Verifier rejected class xxx.Fragment: 
xxx.Fragment.onCreate(android.os.Bundle) failed to verify: xxx.Fragment.onCreate(android.os.Bundle): 
[0xC] 'this' argument 'Precise Reference: android.os.Bundle' 
not instance of 'Unresolved Reference: android.os.Bundle'

这个堆栈直接在这里报错
java.lang.Class.newInstance Class.java 
xxxClassLoader.loadClass 

表现的特征有两个:
1. 只在android12的机器上发生崩溃,不同的机器崩溃概率还不一样。高概率复现的手机上也不是稳定复现
2. 只在release线上版本会发生崩溃,debug版本在一些高概率复现的手机完全不出现

关于NoClassDefFoundError和ClassNotFoundException

在先聊这个问题怎么解决之前,先谈谈这个错误;NoClassDefFoundError还有个常见的孪生兄弟ClassNotFoundException,他们之前有什么区别呢?

ClassNotFoundException

当应用尝试在类路径中用全类名去加载某个类时,如果找你不到它的定义就会报ClassNotFoundException, 它是一个可检测可抓取的异常,发生在类的装载阶段;通常来说就是我们的BaseDexClassLoader中的loadClass和findClass中抛出的异常,这个时候我们需要加载的Class还没有进行实例化操作。

NoClassDefFoundError

通俗来说就是编译时正常编译,但是运行时找不到这个类,他属于错误,虽然我们也可以检测或者抓取,但是强烈不建议抓取;这个错误通常发生在我们通过new或者反射newInstance实例化对象的时候;比如在Class A中定义了一个Class B,然后我们在类A中定义B b = new B(),程序编译成功之后,将B的字节码文件删除重新运行,则会在运行的时候抛出NoClassDefFoundError错误,日常开发我们肯定不会删除类文件,但是会有其他原因导致类似的错误,比如jar包的重复引入,gradle transform中使用Javassist或者ASM操作不当(或者字节码修改库本身的bug)导致对class文件修改不兼容;混淆规则以及包含你在class文件编译成功后对class文件做其他操作都可能导致这个错误,所以这个错误本质上发生在我们的Class已经加载,但是在类的链接或者初始化的发生问题

那是不是在ClassLoader的loadClass中就一定不会抛出NoClassDefFoundError?
我们先看看安卓源码中ClassLoader的相关代码

359    protected Class<?> loadClass(String name, boolean resolve)
360        throws ClassNotFoundException
361    {
362            // First, check if the class has already been loaded
363            Class<?> c = findLoadedClass(name);
364            if (c == null) {
365                try {
366                    if (parent != null) {
367                        c = parent.loadClass(name, false);
368                    } else {
369                        c = findBootstrapClassOrNull(name);
370                    }
371                } catch (ClassNotFoundException e) {
372                    // ClassNotFoundException thrown if class not found
373                    // from the non-null parent class loader
374                }
375
376                if (c == null) {
377                    // If still not found, then invoke findClass in order
378                    // to find the class.
379                    c = findClass(name);
380                }
381            }
382            return c;
383    }

// ......
732    protected final Class<?> findLoadedClass(String name) {
733        ClassLoader loader;
734        if (this == BootClassLoader.getInstance())
735            loader = null;
736        else
737            loader = this;
738        return VMClassLoader.findLoadedClass(loader, name);
739    }

上面是java层的代码,在看看对应到native层的代码

static jclass VMClassLoader_findLoadedClass(JNIEnv* env, jclass, jobject javaLoader,
                                            jstring javaName) {
  ScopedFastNativeObjectAccess soa(env);
  ObjPtr<mirror::ClassLoader> loader = soa.Decode<mirror::ClassLoader>(javaLoader);
  ScopedUtfChars name(env, javaName);
  if (name.c_str() == nullptr) {
    return nullptr;
  }
  ClassLinker* cl = Runtime::Current()->GetClassLinker();

  // Compute hash once.
  std::string descriptor(DotToDescriptor(name.c_str()));
  const size_t descriptor_hash = ComputeModifiedUtf8Hash(descriptor.c_str());

  ObjPtr<mirror::Class> c = VMClassLoader::LookupClass(cl,
                                                       soa.Self(),
                                                       descriptor.c_str(),
                                                       descriptor_hash,
                                                       loader);
  if (c != nullptr && c->IsResolved()) {
    return soa.AddLocalReference<jclass>(c);
  }
  // If class is erroneous, throw the earlier failure, wrapped in certain cases. See b/28787733.
  if (c != nullptr && c->IsErroneous()) {
    cl->ThrowEarlierClassFailure(c);
    Thread* self = soa.Self();
    ObjPtr<mirror::Class> iae_class =
        self->DecodeJObject(WellKnownClasses::java_lang_IllegalAccessError)->AsClass();
    ObjPtr<mirror::Class> ncdfe_class =
        self->DecodeJObject(WellKnownClasses::java_lang_NoClassDefFoundError)->AsClass();
    ObjPtr<mirror::Class> exception = self->GetException()->GetClass();
   // 从这里就已经可以知道,也有可能会抛出NoClassDefFound,
   // 但是native层在这里做了转换,异常转换成了ClassNotFoundException,所以在通过ClassLoader loadClass这一步java层得到的应该是ClassNotFound
    if (exception == iae_class || exception == ncdfe_class) {
      self->ThrowNewWrappedException("Ljava/lang/ClassNotFoundException;",
                                     c->PrettyDescriptor().c_str());
    }
    return nullptr;
  }

问题排查方向

有了上面的分析和问题的特征,我初步的排查方案就出来了

  1. debug和release两个构建类型的依赖差异(以前gson采过坑)
  2. kotlin版本(stackoverflow上很多人提出)
  3. transform中使用的Javassist或者ASM
  4. Proguard规则
  5. 类的加载方式

依赖差异

这块我通过自定义gradle task就可以得出差异

task showDependencyDifferent {
  String flavor = "你的flavor"
  HashMap<String, String> debugRuntimeDependency = new HashMap<>()
  HashMap<String, String> releaseRuntimeDependency = new HashMap<>()
  project.afterEvaluate {
    project.android.applicationVariants.all {
      variant ->
        if(variant.name == (flavor + "Debug")){
          Configuration configuration = project.configurations."${variant.name}RuntimeClasspath"
          configuration.resolvedConfiguration.lenientConfiguration.allModuleDependencies.each { item ->
            debugRuntimeDependency.put("${item.getModuleGroup()}:${item.getModuleName()}", item.getModuleVersion())
          }
        } else if(variant.name == (flavor + "Release")){
          Configuration configuration = project.configurations."${variant.name}RuntimeClasspath"
          configuration.resolvedConfiguration.lenientConfiguration.allModuleDependencies.each { item ->
            releaseRuntimeDependency.put("${item.getModuleGroup()}:${item.getModuleName()}", item.getModuleVersion())
          }
        }
    }
  }
  doLast {
    if(debugRuntimeDependency.size() >= releaseRuntimeDependency.size()){
      def iterator = releaseRuntimeDependency.entrySet().iterator()
      while (iterator.hasNext()) {
        def entry = iterator.next()
        def releaseKey = entry.key
        def releaseValue = entry.value
        if(debugRuntimeDependency.containsKey(releaseKey)){
          def debugValue = debugRuntimeDependency.get(releaseKey)
          if(debugValue != null){
            if(releaseValue == debugValue){
            } else {
              println("library : ${releaseKey}   DebugVersion : ${debugValue}    ReleaseVersion : ${releaseValue}")
            }
          } else {
            println("library : ${releaseKey}   DebugVersion : Null    ReleaseVersion : ${releaseValue}")
          }
          debugRuntimeDependency.remove(releaseKey)
        } else {
          println("library : ${releaseKey}   DebugVersion : Null    ReleaseVersion : ${releaseValue}")
        }
      }
      def debugIterator = debugRuntimeDependency.entrySet().iterator()
      while (debugIterator.hasNext()){
        def entry = debugIterator.next()
        println("library : ${entry.getKey()}   DebugVersion : ${entry.getValue()}    ReleaseVersion : Null")
      }
      debugRuntimeDependency.clear()
      releaseRuntimeDependency.clear()
    } else if(debugRuntimeDependency.size() < releaseRuntimeDependency.size()){
      def iterator = debugRuntimeDependency.entrySet().iterator()
      while (iterator.hasNext()) {
        def entry = iterator.next()
        def debugKey = entry.key
        def debugValue = entry.value
        if(releaseRuntimeDependency.containsKey(debugKey)){
          def releaseValue = releaseRuntimeDependency.get(debugKey)
          if(releaseValue != null){
            if(releaseValue == debugValue){
            } else {
              println("library : ${debugKey}   DebugVersion : ${debugValue}    ReleaseVersion : ${releaseValue}")
            }
          } else {
            println("library : ${debugKey}   DebugVersion : ${debugValue}    ReleaseVersion : Null")
          }
          releaseRuntimeDependency.remove(debugKey)
        } else {
          println("library : ${releaseKey}   DebugVersion : ${debugValue}    ReleaseVersion : Null")
        }
      }
      def releaseIterator = releaseRuntimeDependency.entrySet().iterator()
      while (releaseIterator.hasNext()){
        def entry = releaseIterator.next()
        println("library : ${entry.getKey()}   DebugVersion : None    ReleaseVersion : ${entry.getValue()}")
      }
      releaseRuntimeDependency.clear()
      debugRuntimeDependency.clear()
    }
  }
}

但是观察了一阵,也没有发现可疑的三方库

kotlin版本&Javassist,ASM

这块的排查比较吃力,虽然kotlin,javassist,ASM都有人报类似的错误,但是我在工程里面改完版本之后基本都打不出来包,这个方向的排查只能滞后。

Proguard规则

这一块的排查有一定的收获,发现了一些问题,也解决了线上堆积在android12上的其中一个bug。

-repackageclasses 'xxx'

注释掉了混淆文件上的配置规则

类的加载

在经历种种探索之后仍然没有结果,只能在回归到崩溃日志上面。

java.lang.Class.newInstance Class.java
...省略部分代码
xxxClassLoader.loadClass

因为我们是通过自定义的ClassLoader去加载的类,在加载完成后在处理一些其他逻辑,然后在进行类的实例化,在实例化的过程发生错误,因为Classloader.loadClass不会对类进行初始化,所以初始化被延迟到newInstance这个操作里面, 那我直接将类的加载,链接,初始化在一步完成呢?

// xxxClassloader.loadClass("xxx)
// 将之前的加载方式修改为下面这种
Class class = Class.forName("xxx", true, xxxClassloader)
... 其他逻辑
Object object = class.newInstance();

my god,问题居然解决了,找到一个对接的qa,在他那台可恶的手机(高概率复现)连续测试了三天,都没问题,这才放下心。 但是还有很多不解一直让人比较困惑。于是我又尝试修改

Class.forName("xxx", false, xxxClassloader);

问题又复现了,看来真是的出现类的初始化阶段。继续跟下源码

// "name" is in "binary name" format, e.g. "dalvik.system.Debug$1".
static jclass Class_classForName(JNIEnv* env, jclass, jstring javaName, jboolean initialize,
                                 jobject javaLoader) {
  ScopedFastNativeObjectAccess soa(env);
  ScopedUtfChars name(env, javaName);
  if (name.c_str() == nullptr) {
    return nullptr;
  }

  // We need to validate and convert the name (from x.y.z to x/y/z).  This
  // is especially handy for array types, since we want to avoid
  // auto-generating bogus array classes.
  if (!IsValidBinaryClassName(name.c_str())) {
    soa.Self()->ThrowNewExceptionF("Ljava/lang/ClassNotFoundException;",
                                   "Invalid name: %s", name.c_str());
    return nullptr;
  }

  std::string descriptor(DotToDescriptor(name.c_str()));
  StackHandleScope<2> hs(soa.Self());
  // 这里先进行类的装载
  Handle<mirror::ClassLoader> class_loader(
      hs.NewHandle(soa.Decode<mirror::ClassLoader>(javaLoader)));
 // 然后进行类的链接
  ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
  Handle<mirror::Class> c(
      hs.NewHandle(class_linker->FindClass(soa.Self(), descriptor.c_str(), class_loader)));

// 上两部错误的话直接抛异常
  if (c == nullptr) {
    ScopedLocalRef<jthrowable> cause(env, env->ExceptionOccurred());
    env->ExceptionClear();
    jthrowable cnfe = reinterpret_cast<jthrowable>(
        env->NewObject(WellKnownClasses::java_lang_ClassNotFoundException,
                       WellKnownClasses::java_lang_ClassNotFoundException_init,
                       javaName,
                       cause.get()));
    if (cnfe != nullptr) {
      // Make sure allocation didn't fail with an OOME.
      env->Throw(cnfe);
    }
    return nullptr;
  }
  
  // 这里进行类的初始化,问题就发生在这里,如果这里不执行,那么在我下一步newInstance的时候就会在
  // android12 的部分手机上出现一定概率的崩溃
  if (initialize) {
    class_linker->EnsureInitialized(soa.Self(), c, true, true);
  }
  return soa.AddLocalReference<jclass>(c.Get());
}

下面在看下newInstance的源码


static jobject Class_newInstance(JNIEnv* env, jobject javaThis) {
  ScopedFastNativeObjectAccess soa(env);
  StackHandleScope<4> hs(soa.Self());
  Handle<mirror::Class> klass = hs.NewHandle(DecodeClass(soa, javaThis));
  if (UNLIKELY(klass->GetPrimitiveType() != 0 || klass->IsInterface() || klass->IsArrayClass() ||
               klass->IsAbstract())) {
    soa.Self()->ThrowNewExceptionF("Ljava/lang/InstantiationException;",
                                   "%s cannot be instantiated",
                                   klass->PrettyClass().c_str());
    return nullptr;
  }
  auto caller = hs.NewHandle<mirror::Class>(nullptr);
  // Verify that we can access the class.
  if (!klass->IsPublic()) {
    caller.Assign(GetCallingClass(soa.Self(), 1));
    if (caller != nullptr && !caller->CanAccess(klass.Get())) {
      soa.Self()->ThrowNewExceptionF(
          "Ljava/lang/IllegalAccessException;", "%s is not accessible from %s",
          klass->PrettyClass().c_str(), caller->PrettyClass().c_str());
      return nullptr;
    }
  }
  ArtMethod* constructor = klass->GetDeclaredConstructor(
      soa.Self(),
      ScopedNullHandle<mirror::ObjectArray<mirror::Class>>(),
      kRuntimePointerSize);
  if (UNLIKELY(constructor == nullptr) || ShouldDenyAccessToMember(constructor, soa.Self())) {
    soa.Self()->ThrowNewExceptionF("Ljava/lang/InstantiationException;",
                                   "%s has no zero argument constructor",
                                   klass->PrettyClass().c_str());
    return nullptr;
  }
  // Invoke the string allocator to return an empty string for the string class.
  if (klass->IsStringClass()) {
    gc::AllocatorType allocator_type = Runtime::Current()->GetHeap()->GetCurrentAllocator();
    ObjPtr<mirror::Object> obj = mirror::String::AllocEmptyString<true>(soa.Self(), allocator_type);
    if (UNLIKELY(soa.Self()->IsExceptionPending())) {
      return nullptr;
    } else {
      return soa.AddLocalReference<jobject>(obj);
    }
  }
  auto receiver = hs.NewHandle(klass->AllocObject(soa.Self()));
  if (UNLIKELY(receiver == nullptr)) {
    soa.Self()->AssertPendingOOMException();
    return nullptr;
  }
  // Verify that we can access the constructor.
  ObjPtr<mirror::Class> declaring_class = constructor->GetDeclaringClass();
  if (!constructor->IsPublic()) {
    if (caller == nullptr) {
      caller.Assign(GetCallingClass(soa.Self(), 1));
    }
    if (UNLIKELY(caller != nullptr && !VerifyAccess(receiver.Get(),
                                                          declaring_class,
                                                          constructor->GetAccessFlags(),
                                                          caller.Get()))) {
      soa.Self()->ThrowNewExceptionF(
          "Ljava/lang/IllegalAccessException;", "%s is not accessible from %s",
          constructor->PrettyMethod().c_str(), caller->PrettyClass().c_str());
      return nullptr;
    }
  }
  // Ensure that we are initialized.
 // 如果在上一步Class.forName()没有进行初始化,就会在这里出发去进行类的初始化,崩溃就发生在
 // GetClassLinker()->EnsureInitialized这一步
  if (UNLIKELY(!declaring_class->IsInitialized())) {
    if (!Runtime::Current()->GetClassLinker()->EnsureInitialized(
        soa.Self(), hs.NewHandle(declaring_class), true, true)) {
      soa.Self()->AssertPendingException();
      return nullptr;
    }
  }
  // Invoke the constructor.
  JValue result;
  uint32_t args[1] = { static_cast<uint32_t>(reinterpret_cast<uintptr_t>(receiver.Get())) };
  constructor->Invoke(soa.Self(), args, sizeof(args), &result, "V");
  if (UNLIKELY(soa.Self()->IsExceptionPending())) {
    return nullptr;
  }
  // Constructors are ()V methods, so we shouldn't touch the result of InvokeMethod.
  return soa.AddLocalReference<jobject>(receiver.Get());
}

结论

  • 通常我们在进行new对象操作时,类加载的流程实际上时一气呵成,装载,链接,初始化;但是当我们使用部分反射操作或者自定义类加载器去操作的时候,就会把这个过程给中断,特别是初始化阶段,这样就可能会造成一定的隐患。
  • 如果用自定义的类加载器加载类的时候,优先推荐使用Class.forName("xxx", true, loader),确保类加载流程的完整性。 底层知识还有很多不太明白,特别是newInstance这里发生的崩溃,希望大佬们路过多多指教。