上一节我们分析了proguard是如何把项目里面代码的依赖关系给检索出来,有了依赖关系链之后就可以知道哪些代码是有用的,哪些是无用的,proguard会根据配置文件里的keep规则,配合上前面检索出来的代码依赖关系,就可以把部分无用的代码给裁剪掉了。
proguard的代码压缩过程主要包括了两个步骤:标记 跟 压缩,这些事情都是由 Shrinker接口来完成。
标记
Shrinker 首选会根据keep配置规则以及前面检索出来的代码依赖关系,把有用代码跟无用代码都标记出来。标记的过程又可以分为两个步骤,第一个是根据keep配置规则,生成与之相对应的classPool visitor,第二步是classPool visitor会遍历ClassPool,找到需要keep的类,标记为USED
,并且会把此类的依赖也标记为USED
- 创建classPool visitor
/**
* Performs shrinking of the given program class pool.
*/
public ClassPool execute(ClassPool programClassPool,
ClassPool libraryClassPool) throws IOException {
//1 创建classPool visitor
// Create a visitor for marking the seeds.
UsageMarker usageMarker = configuration.whyAreYouKeeping == null ?
new UsageMarker() :
new ShortestUsageMarker();
ClassPoolVisitor classPoolvisitor =
ClassSpecificationVisitorFactory.createClassPoolVisitor(configuration.keep,
classUsageMarker,
usageMarker,
true,
false,
false);
//2 开始标记类
// Mark the seeds.
programClassPool.accept(classPoolvisitor);
libraryClassPool.accept(classPoolvisitor);
}
首先是要创建classPool visitor,prougard为每一条keep规则都创建一个与其相对应的classPool visitor,
public static ClassPoolVisitor createClassPoolVisitor(List keepClassSpecifications,
ClassVisitor classVisitor,
MemberVisitor memberVisitor,
boolean shrinking,
boolean optimizing,
boolean obfuscating)
{
MultiClassPoolVisitor multiClassPoolVisitor = new MultiClassPoolVisitor();
if (keepClassSpecifications != null)
{
for (int index = 0; index < keepClassSpecifications.size(); index++)
{
KeepClassSpecification keepClassSpecification =
(KeepClassSpecification)keepClassSpecifications.get(index);
//keepClassSpecification.printInfo();
if ((shrinking && !keepClassSpecification.allowShrinking) ||
(optimizing && !keepClassSpecification.allowOptimization) ||
(obfuscating && !keepClassSpecification.allowObfuscation))
{
multiClassPoolVisitor.addClassPoolVisitor(
createClassPoolVisitor(keepClassSpecification,
classVisitor,
memberVisitor));
}
}
}
return multiClassPoolVisitor;
}
createClassPoolVisitor内部会根据我们的keep配置,如keep class
keepclassmembers
keepclasseswithmembernames
等等不同的配置生成不同的classPool visitor,不过是怎么样的配置,最终生成的classPool visitor要么就是NamedClassVisitor
要么就是AllClassVisitor
。由于proguard支持的keep规则比较多,这里我们不进行一一分析了,我们只分析最常见的keep class规则。
假如有这样一条keep规则:
keep 'public class com.nls.lib.MyClass { *; }
我们通过这条keep规则告诉proguard要保留MyClass类以及类的所有成员,
public static ClassPoolVisitor createClassPoolVisitor(ClassSpecification classSpecification,
ClassVisitor classVisitor,
MemberVisitor memberVisitor)
{
//1. 创建composedClassVisitor
// Combine both visitors.
ClassVisitor composedClassVisitor =
createCombinedClassVisitor(classSpecification,
classVisitor,
memberVisitor);
//2. 这里的className就是class com.example.lib.MyClass
// By default, start visiting from the named class name, if specified.
String className = classSpecification.className;
//3. 由于指定了public访问类型所以满足了这里的条件
// If specified, only visit classes with the right access flags.
if (classSpecification.requiredSetAccessFlags != 0 ||
classSpecification.requiredUnsetAccessFlags != 0)
{
composedClassVisitor =
new ClassAccessFilter(classSpecification.requiredSetAccessFlags,
classSpecification.requiredUnsetAccessFlags,
composedClassVisitor);
}
//4. 最后创建了NamedClassVisitor.
// If specified, visit a single named class, otherwise visit all classes.
return className != null ?
(ClassPoolVisitor)new NamedClassVisitor(composedClassVisitor, className) :
(ClassPoolVisitor)new AllClassVisitor(composedClassVisitor);
}
createClassPoolVisitor方法比较复杂,为了方便理解,我把这条keep规则不会命中到的条件都剔除掉了,最终可以看见生成的classPool visitor就是NamedClassVisitor了(其实绝大多数的keep规则最终都是生成了NamedClassVisitor)
其中标记了1createCombinedClassVisitor
方法很重要,它内部创建了类的成员访问器,用作为标记哪些类成员以及它们的依赖为USED
private static ClassVisitor createCombinedClassVisitor(ClassSpecification classSpecification,
ClassVisitor classVisitor,
MemberVisitor memberVisitor)
{
//一些无用代码被注释掉了..
// If specified, let the member info visitor visit the class members.
if (memberVisitor != null)
{
ClassVisitor memberClassVisitor =
createClassVisitor(classSpecification, memberVisitor);
// This class visitor may be the only one.
if (classVisitor == null)
{
return memberClassVisitor;
}
multiClassVisitor.addClassVisitor(memberClassVisitor);
}
return multiClassVisitor;
}
private static ClassVisitor createClassVisitor(ClassSpecification classSpecification,
MemberVisitor memberVisitor)
{
MultiClassVisitor multiClassVisitor = new MultiClassVisitor();
addMemberVisitors(classSpecification.fieldSpecifications, true, multiClassVisitor, memberVisitor);
addMemberVisitors(classSpecification.methodSpecifications, false, multiClassVisitor, memberVisitor);
// Mark the class member in this class and in super classes.
return new ClassHierarchyTraveler(true, true, false, false,
multiClassVisitor);
}
可以看到这个composedClassVisitor其实最终就是ClassHierarchyTraveler,其内部包含了两个类成员访问器,它们都是通过addMemberVisitors方法创建的
private static ClassVisitor createClassVisitor(MemberSpecification memberSpecification,
boolean isField,
MemberVisitor memberVisitor)
{
//一些无用代码被注释掉了..
// Depending on what's specified, visit a single named class member,
// or all class members, filtering the matching ones.
return isField ?
fullySpecified ?
(ClassVisitor)new NamedFieldVisitor(name, descriptor, memberVisitor) :
(ClassVisitor)new AllFieldVisitor(memberVisitor) :
fullySpecified ?
(ClassVisitor)new NamedMethodVisitor(name, descriptor, memberVisitor) :
(ClassVisitor)new AllMethodVisitor(memberVisitor);
}
由于我们这里的keep配置是用了*号通配符,所以最终的成员访问器就是AllFieldVisitor
跟AllMethodVisitor
。
这里一层套一层的ClassVisitor理解起来会比较复杂,为了快速记忆跟方便理解,我这里画了个图来整理ClassVisitor的嵌套关系
最外层的是NamedClassVisitor,它的作用是根据名字来检索出对应的Clazz对象,NamedClassVisitor里面有两个平级关系的ClassVisitor,一个是MultiClassVisitor,另外一个是ClassHierarchyTraveler,其中MultiClassVisitor里面的NamedMethodVisitor是负责来检索出类对象的<init>
方法的,而ClassHierarchyTraveler内部的两个All*Visitor则是负责检索所有的类成员变量跟类成员方法
其实不管套了多少层ClassVisitor,每一层的ClassVisitor仅仅是多加了个条件而已,最终标记都是由UsageMarker来完成
- 遍历ClassPool标记类
回来Shrinker的execute方法,
programClassPool.accept(classPoolvisitor);
libraryClassPool.accept(classPoolvisitor);
libraryClassPool.classesAccept(usageMarker);
accept就开始遍历ClassPool,这里传递的classPoolvisitor参数就是上一步创建的NamedClassVisitor,NamedClassVisitor其实只是保存了keep规则里面的类名称而已,代码如下:
public class NamedClassVisitor implements ClassPoolVisitor
{
private final ClassVisitor classVisitor;
private final String name;
public NamedClassVisitor(ClassVisitor classVisitor,
String name)
{
this.classVisitor = classVisitor;
this.name = name;
}
public void visitClassPool(ClassPool classPool)
{
classPool.classAccept(name, classVisitor);
}
}
classAccept方法会根据类名从ClassPool里面找到对应的Clazz对象。
/**
* Applies the given ClassVisitor to the class with the given name,
* if it is present in the class pool.
*/
public void classAccept(String className, ClassVisitor classVisitor)
{
Clazz clazz = getClass(className);
if (clazz != null)
{
clazz.accept(classVisitor);
}
}
Clazz对象找到后就开始标记这个Clazz跟它的依赖。这里的ClassVisitor就是前面创建的ClassAccessFilter,由于ClassAccessFilter只是用来匹配过滤下访问权限,这里我们跳过,只分析比较核心的UsageMarker。
public void visitProgramClass(ProgramClass programClass)
{
if (shouldBeMarkedAsUsed(programClass))
{
// Mark this class.
markAsUsed(programClass);
markProgramClassBody(programClass);
}
}
第一步,如果这个类时没有被标记过的话,直接标记为USED
,接着开始标记类内部数据,代码如下:
protected void markProgramClassBody(ProgramClass programClass)
{
//1. 标记常量池里的本类引用
markConstant(programClass, programClass.u2thisClass);
//2. 标记常量池里的父类引用
if (programClass.u2superClass != 0)
{
markConstant(programClass, programClass.u2superClass);
}
//3. 遍历父类也同样做一次标记
programClass.hierarchyAccept(false, false, true, false,
interfaceUsageMarker);
//4. 标记类的<init>方法
programClass.methodAccept(ClassConstants.METHOD_NAME_CLINIT,
ClassConstants.METHOD_TYPE_CLINIT,
nonEmptyMethodUsageMarker);
//5. 标记类成员
programClass.fieldsAccept(possiblyUsedMemberUsageMarker);
programClass.methodsAccept(possiblyUsedMemberUsageMarker);
//5. 标记属性表
programClass.attributesAccept(this);
}
代码比较简单,通过上面调用本类相关的数据都标记为USED
了,接着是AllFieldVisitor跟AllMemberVisitor,因为都是类同的逻辑,这里我们只分析AllMemberVisitor,
public class AllMemberVisitor implements ClassVisitor
{
//这里省略部分代码....
public void visitProgramClass(ProgramClass programClass)
{
programClass.fieldsAccept(memberVisitor);
programClass.methodsAccept(memberVisitor);
}
}
可以看到AllMemberVisitor本质上就是遍历处理类的所有方法,前面已经提到过,其最终实现都是在UsageMarker类里,我们直接看它的visitProgramMethod实现,过程如下:
public void visitProgramMethod(ProgramClass programClass, ProgramMethod programMethod)
{
if (shouldBeMarkedAsUsed(programMethod))
{
// Is the method's class used?
if (isUsed(programClass))
{
markAsUsed(programMethod);
// Mark the method body.
markProgramMethodBody(programClass, programMethod);
// Mark the method hierarchy.
markMethodHierarchy(programClass, programMethod);
}
//这里省略部分代码...
}
}
protected void markProgramMethodBody(ProgramClass programClass, ProgramMethod programMethod)
{
// Mark the name and descriptor.
markConstant(programClass, programMethod.u2nameIndex);
markConstant(programClass, programMethod.u2descriptorIndex);
// Mark the attributes.
programMethod.attributesAccept(programClass, this);
// Mark the classes referenced in the descriptor string.
programMethod.referencedClassesAccept(this);
}
也是简单的标记一下方法的相关数据为USED
,这里有一点值得注意的就是programMethod.referencedClassesAccept(this);
这句代码的调用,referencedClasses指向了这个方法的依赖对象,但不包含方法局部变量依赖,怎么理解这句话呢,举个例子,譬如有以下测试代码
fun test(test: TestClass?) {
test?.test1()
val test1 = TestClass1()
test1.test()
}
这里的referencedClasses仅包含了TestClass类,而不包含TestClass1类,referencedClasses的检索我们在上一节中以及分析过了,这里就不再赘述了。
referencedClassesAccept方法的实现如下,这里的ClassVisitor就是UsageMarker,这意味着依赖类也会走相同一遍逻辑,也会被标记为USED
public void referencedClassesAccept(ClassVisitor classVisitor)
{
if (referencedClasses != null)
{
for (int index = 0; index < referencedClasses.length; index++)
{
if (referencedClasses[index] != null)
{
referencedClasses[index].accept(classVisitor);
}
}
}
}
到此我们已经知道了proguard是如何把类、父类、字段、方法以及字段方法的依赖给标记起来了,除此以外一些方法局部变量注解等也会引入类依赖,这些也得被标记出来不能把删除,还有实现接口等也是一样。
回到Shrinker的execute
方法接着往下看
//1. 标记接口
programClassPool.classesAccept(new InterfaceUsageMarker(usageMarker));
//2. 标记内部类 注解 方法局部变量带进来的依赖
programClassPool.classesAccept(
new UsedClassFilter(usageMarker,
new AllAttributeVisitor(true,
new MultiAttributeVisitor(new AttributeVisitor[]
{
new InnerUsageMarker(usageMarker),
new AnnotationUsageMarker(usageMarker),
new LocalVariableTypeUsageMarker(usageMarker)
}))));
InterfaceUsageMarker负责了类的实现接口标记,LocalVariableTypeUsageMarker便是方法局部变量引用标记的实现,这些依赖类对象都是通过读取对应的属性表数据来获取的,譬如还是上面的测试代码,它的class字节码如下
它的LocalVariableTable(方法的局部变量描述表)里就会有依赖类信息了。
压缩
前面标记完成后,接下来就可以开始做压缩工作了,压缩就是把前面标记了USED
的东西留下来,其余都抹掉,从而达到减少包体积的效果。
回到Shrinker的execute
方法接着往下看,压缩的代码如下:
//1. 创建新的ClassPool
ClassPool newProgramClassPool = new ClassPool();
//2. 创建ClassVisitor 压缩ClassPool
programClassPool.classesAccept(
new UsedClassFilter(usageMarker,
new MultiClassVisitor(
new ClassVisitor[] {
new ClassShrinker(usageMarker),
new ClassPoolFiller(newProgramClassPool)
})));
//3. 清空旧的ClassPool
programClassPool.clear();
第一步是重新创建了新的ClassPool,用来接受压缩后的Clazz对象数据,接着是创建一系列的ClassVisitor负责压缩的工作,
-
UsedClassFilter
负责类级别的过滤,把没有被标记为USED
的类给过滤掉 -
ClassShrinker
是更细粒度的过滤器,负责把Clazz内部没有被标记为USED
的数据过滤掉 -
ClassPoolFiller
它的任务比较简单,负责接收前面两个过滤器筛选下来的Clazz对象,最终保存在新的ClassPool里
先看下UsedClassFilter,代码如下
public class UsedClassFilter
implements ClassVisitor
{
//这里省略部分代码...
public void visitProgramClass(ProgramClass programClass)
{
if (usageMarker.isUsed(programClass))
{
classVisitor.visitProgramClass(programClass);
}
}
}
代码也是非常的简单,遍历ClassPool,前面没有被标记为USED
的Clazz统统过滤掉。
接着是ClassShrinker,它会对Clazz结构进行一些压缩过滤,譬如我们keep了某个方法,标记的阶段会把此方法给标记为USED
,其余没被keep的方法由于没有被标记为USED
会在这里被过滤掉。代码如下:
public void visitProgramClass(ProgramClass programClass)
{
//1. 引用接口压缩.
if (programClass.u2interfacesCount > 0)
{
new InterfaceDeleter(shrinkFlags(programClass.constantPool,
programClass.u2interfaces,
programClass.u2interfacesCount))
.visitProgramClass(programClass);
}
//2 .常量池压缩
int newConstantPoolCount =
shrinkConstantPool(programClass.constantPool,
programClass.u2constantPoolCount);
//3. 字段集合压缩
programClass.u2fieldsCount =
shrinkArray(programClass.fields,
programClass.u2fieldsCount);
//4. 方法集合压缩
programClass.u2methodsCount =
shrinkArray(programClass.methods,
programClass.u2methodsCount);
//5. 属性表压缩
programClass.u2attributesCount =
shrinkArray(programClass.attributes,
programClass.u2attributesCount);
//6. 遍历所有字段或方法的属性表进行压缩
programClass.fieldsAccept(this);
programClass.methodsAccept(this);
programClass.attributesAccept(this);
if (newConstantPoolCount < programClass.u2constantPoolCount)
{
programClass.u2constantPoolCount = newConstantPoolCount;
//7. 重新建立常量池里面的ID索引
constantPoolRemapper.setConstantIndexMap(constantIndexMap);
constantPoolRemapper.visitProgramClass(programClass);
}
//8. 清除无用类索引
ClassShrinker.MySignatureCleaner signatureCleaner = new ClassShrinker.MySignatureCleaner();
programClass.fieldsAccept(new AllAttributeVisitor(signatureCleaner));
programClass.methodsAccept(new AllAttributeVisitor(signatureCleaner));
programClass.attributesAccept(signatureCleaner);
// Compact the extra field pointing to the subclasses of this class.
programClass.subClasses =
shrinkToNewArray(programClass.subClasses);
}
可以看见ClassShrinker做的事情非常的多,也十分的复杂,它会对class字节码结构里面的每一块数据进行压缩,压缩完后还得修复索引index。
这里我们只分析一下方法的压缩过程,我们把测试用的demo修改为这样
class MyClass {
fun test(test: TestClass?) {
test?.test1()
}
fun test1() {
val test = TestClass1()
test.test()
}
}
把proguard keep规则修改为这样:
keep public class com.nls.lib.MyClass {
public void test1();
}
我们只keep test1
方法,通过之前的标记过程分析,test
方法以及它所依赖的TestClass
类都不会被标记为USED
,最终都会被剔除掉。
在ClassShrinker开始工作之前,我们能看见此时的MyClass类方法数是3(有一个隐藏的<init>方法),前面已经提过了,虽然test
方法依赖了TestClass
类,但是由于我们并没有把test
方法给keep住,最终导致TestClass
类也是无用的,会被剔除掉。
我们单步进入shrinkConstantPool方法,方法如下:
private int shrinkConstantPool(Constant[] constantPool, int length)
{
//1. 遍历常量池
for (int index = 1; index < length; index++)
{
Constant constant = constantPool[index];
if (constant != null)
{
isUsed = usageMarker.isUsed(constant);
}
//2. 判断常量池内容是否被打了USED标签
if (isUsed)
{
// Remember the new index.
constantIndexMap[index] = counter;
//3. 打过USED标签的可以保留下来
constantPool[counter++] = constant;
}
else
{
//4. 没打过USED标签的剔除
constantIndexMap[index] = -1;
}
}
//5. 把常量池里多余的部分清空为null
Arrays.fill(constantPool, counter, length, null);
}
实现也是比较简单,有打过USED
标签的留,没打标签的就不要,譬如我们的测试代码里TestClass
类肯定是没被打标签的,需要被剔除的
不负众望的,跑到了下面的分支去了,这样ClassShrinker就会把常量池里的
TestClass
类引用数据给抹掉了。
压缩完常量池接着就是压缩类成员了,包括了类字段集合跟类方法集合,代码也是比较简单,下面直接给出
private int shrinkArray(VisitorAccepter[] array, int length)
{
int counter = 0;
// Shift the used objects together.
for (int index = 0; index < length; index++)
{
VisitorAccepter visitorAccepter = array[index];
if (usageMarker.isUsed(visitorAccepter))
{
array[counter++] = visitorAccepter;
}
}
// Clear any remaining array elements.
if (counter < length)
{
Arrays.fill(array, counter, length, null);
}
return counter;
}
同样的也是遍历集合,判断集合里面的对象是否被打了USED
标签,打过标签的会被保留下来,没打标签的就直接跳过,最后也是调用Arrays.fill把集合里无用的空间赋值为空。
我们单步调试也能清楚的看见,调用前方法数是3,执行完后方法数就变成了2了,集合里有一个对象被置空了。
最后proguard会以同样的方式对class字节码的各个结构进行压缩,因为压缩后集合里面的数据索引index会发生变化,在压缩完的最后阶段还得做一次索引的重定向修复任务。
总结
本节主要是从源码的角度出发,简单的分析了proguard的压缩功能实现过程,主要分为两个步骤,其一是标记,另外一个就是压缩,压缩的过程较为简单,主要就是根据前面的标记结果,打了USED
标签的就保留,没打标签的就会被剔除。压缩完接下来就是代码优化,再下一节中我会继续给大家分析一下代码优化的过程。