背景
最近业务上反馈在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;
}
问题排查方向
有了上面的分析和问题的特征,我初步的排查方案就出来了
- debug和release两个构建类型的依赖差异(以前gson采过坑)
- kotlin版本(stackoverflow上很多人提出)
- transform中使用的Javassist或者ASM
- Proguard规则
- 类的加载方式
依赖差异
这块我通过自定义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这里发生的崩溃,希望大佬们路过多多指教。