在排查项目中的代码垃圾时,处理无引用类是最简单直接的,因为没有其他代码引用到它,直接删除也不会影响到项目。但靠人肉去检索项目中所有的类是否有引用又显得是重复低效的,所以在这里提供一个方案,做成gradle插件供大家参考。
原理
Gradle编译过程
App在编译时会经历多个步骤,但关键的会有:1、将所有Module的代码编译成.class文件,并存放在build目录里;2、将所有.class文件(包括项目工程、外部依赖、SDK)合并并制成.dex文件。其中,Android gradle为了让开发者可以对class做动态操作,提供了接口让开发者在dex之前自定义TransForm对class文件进行修改。当然,查找无引用类并不需要修改class,只是需要在这个时机上获取到所有Module编译后生成的.class文件。
代码语言:txt复制class UnusedClassPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
project.extensions.create(UnusedExtension.NAME, UnusedExtension)
def android = project.extensions.getByType(AppExtension)
def checker = new UnusedClassChecker(project)
project.afterEvaluate {
// 收集所有Module的build目录
List<String> paths = new ArrayList<>()
project.rootProject.subprojects { Project subProject ->
paths.add(subProject.buildDir.absolutePath)
}
checker.setPaths(paths)
}
// 注册transform
android.registerTransform(checker)
}
}
解析class文件
有了所有Module编译后的.class文件后,得到allClasses集合,同时开始对每一个.class文件进行分析。分析.class文件时,可以使用一个非常好用的分析class文件的工具库javassist。引用后,只要将所有Module的编译目录加入到classpath后,通过类名即可以得到解析.class文件抽象后的CtClass对象,如下:
代码语言:txt复制ClassPool classPool = ClassPool.getDefault()
// 加入classpath
mProjectsBuildPath.each { buildPath ->
classPool.appendClassPath(buildPath)
}
// 根据类型获取CtClass对象
// CtClass ctClass = classPool.get(className)
分析依赖
有了.class文件的CtClass对象后,就可以获取到该CtClass所依赖的所有class(class文件会记载),并将所有依赖的class信息记录在集合dependentClasses中。主要从class文件中的常量池、父类、实现接口、Field、Method中获取依赖类。
代码语言:txt复制CtClass ctClass = classPool.get(className)
ClassFile classFile = ctClass.getClassFile()
for (String dependent : classFile.getConstPool().getClassNames()) {
if (dependent.startsWith('[') && dependent.endsWith(';')) {
dependent = dependent.replaceAll("\[", "")
dependent = dependent.substring(1, dependent.length()-1)
}
// 排除自身
if (!dependent.equals(replaceClassName)) {
putIntoDependent(dependent, "importClass")
}
}
// 找出同包名依赖
// 找出父类
String superClass = classFile.getSuperclass()
if (!"".equals(superClass) && superClass != null) {
superClass = superClass.replace('.', File.separator)
putIntoDependent(superClass, "superClass")
}
// 找出接口
String[] interfaces = classFile.getInterfaces()
if (interfaces != null) {
for (String face : interfaces) {
face = face.replace('.', File.separator)
putIntoDependent(face, "interface")
}
}
// 找出字段
List<FieldInfo> fieldInfoList = classFile.getFields()
if (fieldInfoList != null) {
for (FieldInfo fieldInfo : fieldInfoList) {
try {
String descriptor = fieldInfo.getDescriptor()
descriptor = descriptor.replaceAll("\[", "")
if (descriptor.length() > 3) {
descriptor = descriptor.substring(1, descriptor.length()-1)
descriptor = descriptor.replace('.', File.separator)
putIntoDependent(descriptor, "field")
}
} catch(Throwable t) {
t.printStackTrace()
}
}
}
// 找出方法声明
List<MethodInfo> methodInfoList = classFile.getMethods();
if (methodInfoList != null) {
for (MethodInfo methodInfo : methodInfoList) {
String descriptor = methodInfo.getDescriptor()
String reg = "(L. ?;)"
Pattern pattern = Pattern.compile(reg)
Matcher matcher = pattern.matcher(descriptor)
while (matcher.find()) {
String methodClassMember = matcher.group()
methodClassMember = methodClassMember.substring(1, methodClassMember.length() - 1)
methodClassMember = methodClassMember.replace('.', File.separator)
putIntoDependent(methodClassMember, "method")
}
}
}
// 加入依赖集合
private boolean putIntoDependent(String className, String tag) {
//
if (!className.contains("$") && !dependentClasses.contains(className)) {
dependentClasses.add((className))
}
}
找出无引用类
经过上述步骤后,得到两个集合allClasses所有类、dependentClasses所有有被依赖的类。此时,只需要遍历一下allClasses,若某些类不在dependentClasses上则说明该类有可能是无引用的,所以在得到扫描结果后,需要检查下类是否真的无引用。为什么是可能呢?因为:
- 某些类可能只有在xml里有引用(如AndroidManifest、layout资源等),只通过class分析没有找出xml的引用;
- 只用作基本类型常量使用的类,编译时不会把class给import进去。