在《Android高性能高稳定性代码覆盖率方案技术实践》一文中,我们实现了高效稳定的代码覆盖率采集方案。不少同学对其背后原理非常感兴趣,甚至发私信来询问底层的理论支撑。为了方便大家了解其中原理,我们写下了这篇详细的解析文章,欢迎大家阅读探讨。
方案中使用了新建ClassLoader,复制目标ClassLoader的classTable字段并进行类加载状态查询的方式,如图:
为什么要采取这样的方式?下面我们来一探究竟。
从ClassLoader入手
我们都知道Android和Java的类加载机制非常相似,都是使用ClassLoader进行。在Java中,要采集类级别的代码覆盖率非常简单,直接反射调用ClassLoader的findLoadedClass方法就行,那为什么在Android中却那么困难呢?这得从Android独有的类加载优化机制说起。
我们先来看看Android类加载涉及的几个主要的ClassLoader,如下图:
其中,
BootClassLoader负责加载系统类
PathClassLoader负责加载应用自定义类
DexClassLoader一般用于动态加载应用安装包以外的类
代码覆盖率应当关注的是应用自定义的类,所以我们重点看看PathClassLoader和DexClassLoader。
我们先来看一下ClassLoader是如何加载一个类的,核心代码都在它的loadClass方法中,如下:
//源码路径:/java/lang/ClassLoader.java
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
可以清楚的看到,ClassLoader加载一个类时主要有以下三个步骤:
1.检查类是否已经加载,如果是,则直接返回加载过的类,否则进入下一步。
2.如果有parent,则尝试从parent加载(双亲委派模式);否则,从BootStrap ClassLoader中查找。如果成功找到类,则直接返回,否则进入下一步。
3.尝试自己加载类。
第一步中用来判断类是否加载的findLoadedClass方法,源码如下:
//源码路径:/java/lang/ClassLoader.java
protected final Class<?> findLoadedClass(String name) {
ClassLoader loader;
if (this == BootClassLoader.getInstance())
loader = null;
else
loader = this;
return VMClassLoader.findLoadedClass(loader, name);
}
按照注释描述,如果当前的ClassLoader被虚拟机记录为指定类的初始加载器,那么就返回这个类的Class对象,否则返回null。换句话说,如果一个类被加载过,那么调用真正加载它的ClassLoader的findLoadedClass方法,就能得到它的Class对象。
既然如此,我们只要找到合适的ClassLoader,并调用它的findLoadedClass方法,是不是就能知道某个类是否已经加载过了?
我们写一段代码来验证一下:
public static boolean isClassLoaded(ClassLoader classLoader, String className) {
Method mtd;
try {
mtd = ClassLoader.class.getDeclaredMethod("findLoadedClass", String.class);
mtd.setAccessible(true);
} catch (NoSuchMethodException e) {
e.printStackTrace();
return false;
}
try {
return mtd.invoke(classLoader, className) != null;
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return false;
}
因为findLoadedClass方法并非public方法,所以我们需要通过反射来对它进行调用。
我们编写了上面的方法,传入指定的ClassLoader和类名,判断类是否已经加载。
由于我们关心的是应用自定义类而非系统类的加载情况,所以我们需要使用负责加载自定义类的ClassLoader。通常而言,对于Java Application,应该使用AppClassLoader;而对于Android Application,应该使用PathClassLoader。
我们也分别在Java Application和Android Application进行了测试:
在Java Application中,上述方法能准确的返回应用自定义类的加载情况,符合预期。
在Android Application中,无论我们传入的类是否已加载,统统都返回了true。
Java Application中的测试结果,能满足我们预期的诉求,证明了我们的思路是正确的。但是为什么同样的代码在Android平台上却会失效?
findLoadedClass探究
通过前面的源码,我们已经知道findLoadedClass最终调用了VMClassLoader.findLoadedClass方法,而这是一个native方法:
//源码路径:/java/lang/VMClassLoader.java
@FastNative
native static Class findLoadedClass(ClassLoader cl, String name);
看来我们得从Native源码中去寻找答案了。
在Native源码中,我们找到了对应的实现:
//源码路径:art/runtime/native/java_lang_VMClassLoader.cc
static jclass VMClassLoader_findLoadedClass(JNIEnv* env, jclass, jobject javaLoader,
jstring javaName) {
//...
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);
}
//...
// Hard-coded performance optimization: We know that all failed libcore calls to findLoadedClass
// are followed by a call to the the classloader to actually
// load the class.
if (loader != nullptr) {
// Try the common case.
StackHandleScope<1> hs(soa.Self());
c = VMClassLoader::FindClassInPathClassLoader(cl,
soa,
soa.Self(),
descriptor.c_str(),
descriptor_hash,
hs.NewHandle(loader));
if (c != nullptr) {
return soa.AddLocalReference<jclass>(c);
}
}
// The class wasn't loaded, yet, and our fast-path did not apply (e.g., we didn't understand the
// classloader chain).
return nullptr;
}
可以看到,findLoadedClass方法可分为两步:
1.通过VMClassLoader::LookupClass方法查找类,如果能找到并且类已经resolved,则直接返回class对象。
2.如果传入的ClassLoader不为空,则通过VMClassLoader::FindClassInPathClassLoader方法继续查找类,找到就返回。
标准的JDK中,其实只会做第一步操作,具体可参考OpenJDK源码中的findLoadedClass0方法与JVM_FindLoadedClass方法的实现。
而第二步的操作,就完全是Android平台自己的优化了。我们先看看这行注释:
// Hard-coded performance optimization: We know that all failed libcore calls to findLoadedClass
// are followed by a call to the the classloader to actually load the class.
翻译过来就是:
硬编码实现性能优化:我们了解到,每次libcore调用findLoadedClass失败后,都会接着调用对应的ClassLoader来加载这个类
也就是说,系统认为,当调用findLoadedClass方法时,说明马上就要使用这个类,如果这个类还未被加载,那紧接着就会通过ClassLoader加载这个类。既然如此,系统就干脆做了这样一个优化:在Native层直接加载这个类。
那么是怎么加载这个类的呢?我们继续来看VMClassLoader::FindClassInPathClassLoader方法的源码:
//源码路径:art/runtime/native/java_lang_VMClassLoader.cc
static ObjPtr<mirror::Class> FindClassInPathClassLoader(ClassLinker* cl,
ScopedObjectAccessAlreadyRunnable& soa,
Thread* self,
const char* descriptor,
size_t hash,
Handle<mirror::ClassLoader> class_loader)
REQUIRES_SHARED(Locks::mutator_lock_) {
ObjPtr<mirror::Class> result;
if (cl->FindClassInBaseDexClassLoader(soa, self, descriptor, hash, class_loader, &result)) {
DCHECK(!self->IsExceptionPending());
return result;
}
if (self->IsExceptionPending()) {
self->ClearException();
}
return nullptr;
}
};
可以看到这里直接调用了ClassLinker的FindClassInBaseDexClassLoader方法,它的源码如下:
//源码路径:art/runtime/class_linker.cc
bool ClassLinker::FindClassInBaseDexClassLoader(ScopedObjectAccessAlreadyRunnable& soa,
Thread* self,
const char* descriptor,
size_t hash,
Handle<mirror::ClassLoader> class_loader,
/*out*/ ObjPtr<mirror::Class>* result) {
//...
if (IsPathOrDexClassLoader(soa, class_loader) || IsInMemoryDexClassLoader(soa, class_loader)) {
//...
RETURN_IF_UNRECOGNIZED_OR_FOUND_OR_EXCEPTION(
FindClassInBaseDexClassLoaderClassPath(soa, descriptor, hash, class_loader, result),
*result,
self);
//...
}
//...
// Unsupported class loader.
*result = nullptr;
return false;
}
如果传入的ClassLoader为PathClassLoader或者DexClassLoader,则会调用FindClassInBaseDexClassLoaderClassPath方法。它的源码如下:
//源码路径:art/runtime/class_linker.cc
bool ClassLinker::FindClassInBaseDexClassLoaderClassPath(
ScopedObjectAccessAlreadyRunnable& soa,
const char* descriptor,
size_t hash,
Handle<mirror::ClassLoader> class_loader,
/*out*/ ObjPtr<mirror::Class>* result) {
//...
*result = DefineClass(soa.Self(), descriptor, hash, class_loader, *dex_file, *class_def);
//...
}
这里最终调用到了ClassLinker::DefineClass方法。仔细研究可以发现,所有的BaseDexClassLoader最终都会调用这个方法来完成DexFile中的类的加载(有兴趣的同学可以去看看libcore/dalvik/src/main/java/dalvik/system/DexFile.java的实现)。
整个调用过程如图:
我们调用PathClassLoader的findLoadedClass方法,传入一个未加载的(App自定义的)类时,系统会直接调用ClassLinker::DefineClass方法完成类的加载,而不是先向Java层返回null,然后Java层再层层向下最终调用ClassLinker::DefineClass来加载这个类。
可见,这个优化最直观的收益就是减少了Java /Native 来回交互的时间以及跨语言和跨方法栈的开销。到这里,我们已经完全弄清上述测试代码总是返回true的原因了。
聚焦ClassTable
整理一下线索,我们要查找App已加载的类,就必须要从PathClassLoader查找,如果直接调用PathClassLoader的findLoadedClass方法,因为系统的优化,会直接触发类的加载,这不是我们期望的。那么有没有办法绕开加载优化,只查找已加载的类呢?
这得从findLoadedClass方法查找已加载类的实现上来寻找答案了。
根据前面的分析,findLoadedClass方法最终会调用VMClassLoader的LookupClass方法,最终调用到了ClassLinker的LookupClass方法,对应的实现如下:
//源码路径:art/runtime/class_linker.cc
ObjPtr<mirror::Class> ClassLinker::LookupClass(Thread* self,
const char* descriptor,
size_t hash,
ObjPtr<mirror::ClassLoader> class_loader) {
//...
ClassTable* const class_table = ClassTableForClassLoader(class_loader);
if (class_table != nullptr) {
ObjPtr<mirror::Class> result = class_table->Lookup(descriptor, hash);
if (result != nullptr) {
return result;
}
}
return nullptr;
}
这里其实是获取了ClassLoader对应的ClassTable对象,然后调用它的Lookup方法查询已加载的类。由此不难看出,ClassTable中存储了已加载类的信息,如果我们能访问到它,判断一个类是否已加载将会易如反掌。我们来看看这里是如何获取ClassTable的,源码如下:
//源码路径:art/runtime/class_linker.cc
ClassTable* ClassLinker::ClassTableForClassLoader(ObjPtr<mirror::ClassLoader> class_loader) {
return class_loader == nullptr ? boot_class_table_.get() : class_loader->GetClassTable();
}
//源码路径:art/runtime/mirror/class_loader.h
ClassTable* GetClassTable() REQUIRES_SHARED(Locks::mutator_lock_) {
return reinterpret_cast<ClassTable*>(
GetField64<kVerifyFlags>(OFFSET_OF_OBJECT_MEMBER(ClassLoader, class_table_)));
}
代码很简单,获取ClassLoader 中的class_table_字段,将其强转为ClassTable对象。因此class_table_是一个指针,对应ClassLoader中的classTable字段:
//源码路径:/java/lang/ClassLoader.java
public abstract class ClassLoader {
//...
/**
* Pointer to the class table, only used from within the runtime.
*/
private transient long classTable;
//...
}
简单总结一下:ClassLoader中的classTable字段,是指向与其关联的ClassTable的指针,ClassLoader的findLoadedClass方法,最终也是调用到它。
我们要查询PathClassLoader加载过哪些类,就需要拿到并访问它的ClassTable对象。因为classTable字段指向的就是ClassTable对象,虽然它是私有字段,但是通过反射拿到它并不难。真正的难点在于,ClassTable这个C++类,并非是Android SDK中的API,我们没办法直接使用它。
虽然可以通过硬编码和某些黑科技来间接使用它,但是复杂性是巨大的挑战,同时会带来很大的稳定性和兼容性风险,得不偿失,因此我们没有这样做。那么有没有其他的方式来访问它呢?
答案是肯定的。
其实ClassLoader中已经提供了访问了ClassTable的方法,即findLoadedClass。我们将PathClassLoader的classTable字段的值,复制给一个新的ClassLoader,就能利用ClassLoader的访问能力直接访问PathClassLoader的ClassTable。
同时,因为新建的ClassLoader即不是PathClassLoader,也不是DexClassLoader,所以调用findLoadedClass查询某个类的时候,即使这个类没有加载,也不会触发加载,会直接返回null。
上述两点的巧妙之处在于,完美绕开了系统对ClassTable访问的限制和不需要的类加载优化,寥寥几行代码就实现了对类加载状态的获取,简洁而不简单。
总结
到这里,原理和方法都已经清晰了:利用新的ClassLoader绕开系统限制和多余的优化,间接访问目标ClassLoader中的类加载状态。有了这个结论,Android高性能高稳定性代码覆盖率方案也就应运而生。大家若有兴趣或还有想要了解的地方,欢迎一起探讨。
推荐阅读
关注「高德技术」,了解更多