在排查项目中的代码垃圾时,处理无引用类是最简单直接的,因为没有其他代码引用到它,直接删除也不会影响到项目。但靠人肉去检索项目中所有的类是否有引用又显得是重复低效的,所以在这里提供一个方案,做成gradle插件供大家参考。
原理Gradle编译过程App在编译时会经历多个步骤,但关键的会有:1、将所有Module的代码编译成.class文件,并存放在build目录里;2、将所有.class文件(包括项目工程、外部依赖、SDK)合并并制成.dex文件。其中,Android gradle为了让开发者可以对class做动态操作,提供了接口让开发者在dex之前自定义TransForm对class文件进行修改。当然,查找无引用类并不需要修改class,只是需要在这个时机上获取到所有Module编译后生成的.class文件。
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对象,如下:
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中获取依赖类。
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进去。 ---来自腾讯云社区的---Jeffery