上一節(jié)我們分析了proguard是如何把項目里面代碼的依賴關(guān)系給檢索出來钻注,有了依賴關(guān)系鏈之后就可以知道哪些代碼是有用的凳鬓,哪些是無用的,proguard會根據(jù)配置文件里的keep規(guī)則媒惕,配合上前面檢索出來的代碼依賴關(guān)系,就可以把部分無用的代碼給裁剪掉了笑诅。
proguard的代碼壓縮過程主要包括了兩個步驟:標(biāo)記 跟 壓縮,這些事情都是由 Shrinker接口來完成疮鲫。
標(biāo)記
Shrinker 首選會根據(jù)keep配置規(guī)則以及前面檢索出來的代碼依賴關(guān)系吆你,把有用代碼跟無用代碼都標(biāo)記出來。標(biāo)記的過程又可以分為兩個步驟俊犯,第一個是根據(jù)keep配置規(guī)則妇多,生成與之相對應(yīng)的classPool visitor,第二步是classPool visitor會遍歷ClassPool燕侠,找到需要keep的類者祖,標(biāo)記為USED
,并且會把此類的依賴也標(biāo)記為USED
- 創(chuàng)建classPool visitor
/**
* Performs shrinking of the given program class pool.
*/
public ClassPool execute(ClassPool programClassPool,
ClassPool libraryClassPool) throws IOException {
//1 創(chuàng)建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 開始標(biāo)記類
// Mark the seeds.
programClassPool.accept(classPoolvisitor);
libraryClassPool.accept(classPoolvisitor);
}
首先是要創(chuàng)建classPool visitor绢彤,prougard為每一條keep規(guī)則都創(chuàng)建一個與其相對應(yīng)的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內(nèi)部會根據(jù)我們的keep配置,如keep class
keepclassmembers
keepclasseswithmembernames
等等不同的配置生成不同的classPool visitor茫舶,不過是怎么樣的配置械巡,最終生成的classPool visitor要么就是NamedClassVisitor
要么就是AllClassVisitor
。由于proguard支持的keep規(guī)則比較多饶氏,這里我們不進行一一分析了讥耗,我們只分析最常見的keep class規(guī)則。
假如有這樣一條keep規(guī)則:
keep 'public class com.nls.lib.MyClass { *; }
我們通過這條keep規(guī)則告訴proguard要保留MyClass類以及類的所有成員嚷往,
public static ClassPoolVisitor createClassPoolVisitor(ClassSpecification classSpecification,
ClassVisitor classVisitor,
MemberVisitor memberVisitor)
{
//1. 創(chuàng)建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. 最后創(chuàng)建了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方法比較復(fù)雜,為了方便理解柠衅,我把這條keep規(guī)則不會命中到的條件都剔除掉了皮仁,最終可以看見生成的classPool visitor就是NamedClassVisitor了(其實絕大多數(shù)的keep規(guī)則最終都是生成了NamedClassVisitor)
其中標(biāo)記了1createCombinedClassVisitor
方法很重要,它內(nèi)部創(chuàng)建了類的成員訪問器菲宴,用作為標(biāo)記哪些類成員以及它們的依賴為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贷祈,其內(nèi)部包含了兩個類成員訪問器,它們都是通過addMemberVisitors方法創(chuàng)建的
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理解起來會比較復(fù)雜,為了快速記憶跟方便理解谣蠢,我這里畫了個圖來整理ClassVisitor的嵌套關(guān)系
最外層的是NamedClassVisitor粟耻,它的作用是根據(jù)名字來檢索出對應(yīng)的Clazz對象查近,NamedClassVisitor里面有兩個平級關(guān)系的ClassVisitor,一個是MultiClassVisitor挤忙,另外一個是ClassHierarchyTraveler霜威,其中MultiClassVisitor里面的NamedMethodVisitor是負責(zé)來檢索出類對象的<init>
方法的,而ClassHierarchyTraveler內(nèi)部的兩個All*Visitor則是負責(zé)檢索所有的類成員變量跟類成員方法
其實不管套了多少層ClassVisitor册烈,每一層的ClassVisitor僅僅是多加了個條件而已戈泼,最終標(biāo)記都是由UsageMarker來完成
- 遍歷ClassPool標(biāo)記類
回來Shrinker的execute方法,
programClassPool.accept(classPoolvisitor);
libraryClassPool.accept(classPoolvisitor);
libraryClassPool.classesAccept(usageMarker);
accept就開始遍歷ClassPool赏僧,這里傳遞的classPoolvisitor參數(shù)就是上一步創(chuàng)建的NamedClassVisitor大猛,NamedClassVisitor其實只是保存了keep規(guī)則里面的類名稱而已,代碼如下:
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方法會根據(jù)類名從ClassPool里面找到對應(yīng)的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對象找到后就開始標(biāo)記這個Clazz跟它的依賴挽绩。這里的ClassVisitor就是前面創(chuàng)建的ClassAccessFilter,由于ClassAccessFilter只是用來匹配過濾下訪問權(quán)限窑滞,這里我們跳過琼牧,只分析比較核心的UsageMarker。
public void visitProgramClass(ProgramClass programClass)
{
if (shouldBeMarkedAsUsed(programClass))
{
// Mark this class.
markAsUsed(programClass);
markProgramClassBody(programClass);
}
}
第一步哀卫,如果這個類時沒有被標(biāo)記過的話巨坊,直接標(biāo)記為USED
,接著開始標(biāo)記類內(nèi)部數(shù)據(jù)此改,代碼如下:
protected void markProgramClassBody(ProgramClass programClass)
{
//1. 標(biāo)記常量池里的本類引用
markConstant(programClass, programClass.u2thisClass);
//2. 標(biāo)記常量池里的父類引用
if (programClass.u2superClass != 0)
{
markConstant(programClass, programClass.u2superClass);
}
//3. 遍歷父類也同樣做一次標(biāo)記
programClass.hierarchyAccept(false, false, true, false,
interfaceUsageMarker);
//4. 標(biāo)記類的<init>方法
programClass.methodAccept(ClassConstants.METHOD_NAME_CLINIT,
ClassConstants.METHOD_TYPE_CLINIT,
nonEmptyMethodUsageMarker);
//5. 標(biāo)記類成員
programClass.fieldsAccept(possiblyUsedMemberUsageMarker);
programClass.methodsAccept(possiblyUsedMemberUsageMarker);
//5. 標(biāo)記屬性表
programClass.attributesAccept(this);
}
代碼比較簡單趾撵,通過上面調(diào)用本類相關(guān)的數(shù)據(jù)都標(biāo)記為USED
了,接著是AllFieldVisitor跟AllMemberVisitor共啃,因為都是類同的邏輯占调,這里我們只分析AllMemberVisitor,
public class AllMemberVisitor implements ClassVisitor
{
//這里省略部分代碼....
public void visitProgramClass(ProgramClass programClass)
{
programClass.fieldsAccept(memberVisitor);
programClass.methodsAccept(memberVisitor);
}
}
可以看到AllMemberVisitor本質(zhì)上就是遍歷處理類的所有方法移剪,前面已經(jīng)提到過究珊,其最終實現(xiàn)都是在UsageMarker類里,我們直接看它的visitProgramMethod實現(xiàn)纵苛,過程如下:
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);
}
也是簡單的標(biāo)記一下方法的相關(guān)數(shù)據(jù)為USED
剿涮,這里有一點值得注意的就是programMethod.referencedClassesAccept(this);
這句代碼的調(diào)用,referencedClasses指向了這個方法的依賴對象攻人,但不包含方法局部變量依賴取试,怎么理解這句話呢,舉個例子怀吻,譬如有以下測試代碼
fun test(test: TestClass?) {
test?.test1()
val test1 = TestClass1()
test1.test()
}
這里的referencedClasses僅包含了TestClass類瞬浓,而不包含TestClass1類,referencedClasses的檢索我們在上一節(jié)中以及分析過了蓬坡,這里就不再贅述了猿棉。
referencedClassesAccept方法的實現(xiàn)如下磅叛,這里的ClassVisitor就是UsageMarker,這意味著依賴類也會走相同一遍邏輯铺根,也會被標(biāo)記為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);
}
}
}
}
到此我們已經(jīng)知道了proguard是如何把類宪躯、父類、字段位迂、方法以及字段方法的依賴給標(biāo)記起來了访雪,除此以外一些方法局部變量注解等也會引入類依賴,這些也得被標(biāo)記出來不能把刪除掂林,還有實現(xiàn)接口等也是一樣臣缀。
回到Shrinker的execute
方法接著往下看
//1. 標(biāo)記接口
programClassPool.classesAccept(new InterfaceUsageMarker(usageMarker));
//2. 標(biāo)記內(nèi)部類 注解 方法局部變量帶進來的依賴
programClassPool.classesAccept(
new UsedClassFilter(usageMarker,
new AllAttributeVisitor(true,
new MultiAttributeVisitor(new AttributeVisitor[]
{
new InnerUsageMarker(usageMarker),
new AnnotationUsageMarker(usageMarker),
new LocalVariableTypeUsageMarker(usageMarker)
}))));
InterfaceUsageMarker負責(zé)了類的實現(xiàn)接口標(biāo)記,LocalVariableTypeUsageMarker便是方法局部變量引用標(biāo)記的實現(xiàn)泻帮,這些依賴類對象都是通過讀取對應(yīng)的屬性表數(shù)據(jù)來獲取的精置,譬如還是上面的測試代碼,它的class字節(jié)碼如下
它的LocalVariableTable(方法的局部變量描述表)里就會有依賴類信息了锣杂。
壓縮
前面標(biāo)記完成后脂倦,接下來就可以開始做壓縮工作了,壓縮就是把前面標(biāo)記了USED
的東西留下來元莫,其余都抹掉赖阻,從而達到減少包體積的效果。
回到Shrinker的execute
方法接著往下看踱蠢,壓縮的代碼如下:
//1. 創(chuàng)建新的ClassPool
ClassPool newProgramClassPool = new ClassPool();
//2. 創(chuàng)建ClassVisitor 壓縮ClassPool
programClassPool.classesAccept(
new UsedClassFilter(usageMarker,
new MultiClassVisitor(
new ClassVisitor[] {
new ClassShrinker(usageMarker),
new ClassPoolFiller(newProgramClassPool)
})));
//3. 清空舊的ClassPool
programClassPool.clear();
第一步是重新創(chuàng)建了新的ClassPool火欧,用來接受壓縮后的Clazz對象數(shù)據(jù),接著是創(chuàng)建一系列的ClassVisitor負責(zé)壓縮的工作茎截,
-
UsedClassFilter
負責(zé)類級別的過濾苇侵,把沒有被標(biāo)記為USED
的類給過濾掉 -
ClassShrinker
是更細粒度的過濾器,負責(zé)把Clazz內(nèi)部沒有被標(biāo)記為USED
的數(shù)據(jù)過濾掉 -
ClassPoolFiller
它的任務(wù)比較簡單企锌,負責(zé)接收前面兩個過濾器篩選下來的Clazz對象榆浓,最終保存在新的ClassPool里
先看下UsedClassFilter,代碼如下
public class UsedClassFilter
implements ClassVisitor
{
//這里省略部分代碼...
public void visitProgramClass(ProgramClass programClass)
{
if (usageMarker.isUsed(programClass))
{
classVisitor.visitProgramClass(programClass);
}
}
}
代碼也是非常的簡單撕攒,遍歷ClassPool陡鹃,前面沒有被標(biāo)記為USED
的Clazz統(tǒng)統(tǒng)過濾掉。
接著是ClassShrinker打却,它會對Clazz結(jié)構(gòu)進行一些壓縮過濾杉适,譬如我們keep了某個方法谎倔,標(biāo)記的階段會把此方法給標(biāo)記為USED
柳击,其余沒被keep的方法由于沒有被標(biāo)記為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做的事情非常的多片习,也十分的復(fù)雜捌肴,它會對class字節(jié)碼結(jié)構(gòu)里面的每一塊數(shù)據(jù)進行壓縮蹬叭,壓縮完后還得修復(fù)索引index。
這里我們只分析一下方法的壓縮過程状知,我們把測試用的demo修改為這樣
class MyClass {
fun test(test: TestClass?) {
test?.test1()
}
fun test1() {
val test = TestClass1()
test.test()
}
}
把proguard keep規(guī)則修改為這樣:
keep public class com.nls.lib.MyClass {
public void test1();
}
我們只keep test1
方法秽五,通過之前的標(biāo)記過程分析,test
方法以及它所依賴的TestClass
類都不會被標(biāo)記為USED
饥悴,最終都會被剔除掉坦喘。
在ClassShrinker開始工作之前,我們能看見此時的MyClass類方法數(shù)是3(有一個隱藏的<init>方法)西设,前面已經(jīng)提過了瓣铣,雖然test
方法依賴了TestClass
類,但是由于我們并沒有把test
方法給keep住贷揽,最終導(dǎo)致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. 判斷常量池內(nèi)容是否被打了USED標(biāo)簽
if (isUsed)
{
// Remember the new index.
constantIndexMap[index] = counter;
//3. 打過USED標(biāo)簽的可以保留下來
constantPool[counter++] = constant;
}
else
{
//4. 沒打過USED標(biāo)簽的剔除
constantIndexMap[index] = -1;
}
}
//5. 把常量池里多余的部分清空為null
Arrays.fill(constantPool, counter, length, null);
}
實現(xiàn)也是比較簡單蓖救,有打過USED
標(biāo)簽的留,沒打標(biāo)簽的就不要印屁,譬如我們的測試代碼里TestClass
類肯定是沒被打標(biāo)簽的循捺,需要被剔除的
不負眾望的,跑到了下面的分支去了库车,這樣ClassShrinker就會把常量池里的
TestClass
類引用數(shù)據(jù)給抹掉了巨柒。
壓縮完常量池接著就是壓縮類成員了,包括了類字段集合跟類方法集合柠衍,代碼也是比較簡單洋满,下面直接給出
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
標(biāo)簽珍坊,打過標(biāo)簽的會被保留下來牺勾,沒打標(biāo)簽的就直接跳過,最后也是調(diào)用Arrays.fill把集合里無用的空間賦值為空阵漏。
我們單步調(diào)試也能清楚的看見驻民,調(diào)用前方法數(shù)是3,執(zhí)行完后方法數(shù)就變成了2了履怯,集合里有一個對象被置空了回还。
最后proguard會以同樣的方式對class字節(jié)碼的各個結(jié)構(gòu)進行壓縮,因為壓縮后集合里面的數(shù)據(jù)索引index會發(fā)生變化叹洲,在壓縮完的最后階段還得做一次索引的重定向修復(fù)任務(wù)柠硕。
總結(jié)
本節(jié)主要是從源碼的角度出發(fā),簡單的分析了proguard的壓縮功能實現(xiàn)過程,主要分為兩個步驟蝗柔,其一是標(biāo)記闻葵,另外一個就是壓縮,壓縮的過程較為簡單癣丧,主要就是根據(jù)前面的標(biāo)記結(jié)果槽畔,打了USED
標(biāo)簽的就保留,沒打標(biāo)簽的就會被剔除胁编。壓縮完接下來就是代碼優(yōu)化厢钧,再下一節(jié)中我會繼續(xù)給大家分析一下代碼優(yōu)化的過程。