本節(jié)開(kāi)始我們來(lái)分析一下proguard里面我們最熟悉的功能:混淆 ,分析一下proguard是如何把類(lèi)跟方法字段名固定下來(lái),又是如何為沒(méi)有被keep住的類(lèi)跟方法字段創(chuàng)建出新的a.b.c這樣的新名字 鲁纠。
在proguard里Obfuscator接口負(fù)責(zé)了混淆的工作,大致的可以把整個(gè)混淆過(guò)程分為四步驟:
- 名字固定
- 創(chuàng)建混淆名
- 應(yīng)用混淆名
- 壓縮常量池
下面我們一步步的分析一下
固定名字
固定名字顧名思義的鳍寂,就是混淆前混淆后這些名字不會(huì)發(fā)生改變改含,又或者是會(huì)根據(jù)mapping配置文件里的內(nèi)容來(lái)發(fā)生變化。proguard會(huì)在混淆前根據(jù)keep規(guī)則配置迄汛,或者mapping文件映射關(guān)系捍壤,提前把一些類(lèi)名或方法字段名給固定下來(lái)骤视,沒(méi)被固定下來(lái)的就會(huì)以a.b.c這樣的隨機(jī)名字重新命名。
-
keep規(guī)則固定
前面已經(jīng)說(shuō)了鹃觉,常用的名字固定方式有兩種专酗,一種就是最直接的通過(guò)keep規(guī)則來(lái)固定,我們直接定位到Obfuscator的execute
方法盗扇,如下:
NameMarker nameMarker = new NameMarker();
ClassPoolVisitor classPoolvisitor =
ClassSpecificationVisitorFactory.createClassPoolVisitor(configuration.keep,
nameMarker,
nameMarker,
false,
false,
true);
// Mark the seeds.
programClassPool.accept(classPoolvisitor);
libraryClassPool.accept(classPoolvisitor);
首先也是先創(chuàng)建ClassPoolVisitor祷肯,之前的文章也已經(jīng)分析過(guò)ClassPoolVisitor的創(chuàng)建過(guò)程了,這里就不再贅述了疗隶,下面也是方便理解直接給出ClassPoolVisitor的層級(jí)嵌套關(guān)系:
有了之前的經(jīng)驗(yàn)這里我們可以直接看NameMarker就可以了佑笋,其他一層層的ClassPoolVisitor嵌套也只是增加了一些預(yù)處理邏輯而已,最終的代碼實(shí)現(xiàn)都會(huì)在NameMarker里
public void visitProgramClass(ProgramClass programClass)
{
//1. 把類(lèi)名保存下來(lái).
keepClassName(programClass);
//2. 遍歷屬性表,把內(nèi)部類(lèi)名稱(chēng)也保存一下
programClass.attributesAccept(this);
}
public void keepClassName(Clazz clazz)
{
ClassObfuscator.setNewClassName(clazz,
clazz.getName());
}
NameMarker的visitProgramClass方法比較簡(jiǎn)單斑鼻,主要是把符合keep規(guī)則規(guī)則里的類(lèi)名稱(chēng)以及它的內(nèi)部類(lèi)名稱(chēng)(如果有的話)給保存起來(lái)蒋纬。
接著看它的visitProgramField跟visitProgramMethod方法,也是比較簡(jiǎn)單坚弱,也是要把keep住的字段名跟方法先保存起來(lái)蜀备,代碼如下:
public void visitProgramField(ProgramClass programClass, ProgramField programField)
{
keepFieldName(programClass, programField);
}
public void visitProgramMethod(ProgramClass programClass, ProgramMethod programMethod)
{
keepMethodName(programClass, programMethod);
}
private void keepFieldName(Clazz clazz, Field field)
{
MemberObfuscator.setFixedNewMemberName(field,
field.getName(clazz));
}
private void keepMethodName(Clazz clazz, Method method)
{
String name = method.getName(clazz);
if (!ClassUtil.isInitializer(name))
{
MemberObfuscator.setFixedNewMemberName(method, name);
}
}
經(jīng)過(guò)了上面這一步,所有在proguard配置里面被keep的類(lèi)名荒叶、字段名跟方法名都會(huì)先被保存了下來(lái)碾阁。
programClassPool.classesAccept(
new ClassVersionFilter(ClassConstants.CLASS_VERSION_1_7,
new AllConstantVisitor(
new DynamicReturnedClassVisitor(
new AllMemberVisitor(nameMarker)))));
}
接著是Java7之后才添加的新特性invokedynamic,關(guān)于這個(gè)新特性大家可自行的搜索相關(guān)資料停撞,這里不多做介紹瓷蛙。
接著就是屬性相關(guān)的名字固定
AttributeVisitor attributeUsageMarker =
new NonEmptyAttributeFilter(
new AttributeUsageMarker());
AttributeVisitor optionalAttributeUsageMarker =
configuration.keepAttributes == null ? null :
new AttributeNameFilter(configuration.keepAttributes,
attributeUsageMarker);
programClassPool.classesAccept(
new AllAttributeVisitor(true,
new RequiredAttributeFilter(attributeUsageMarker, optionalAttributeUsageMarker)));
這段邏輯對(duì)應(yīng)的是proguard里面的keepattributes
配置,通常也不需要怎么配置戈毒,這里也略過(guò)。
-
mapping文件固定
通過(guò)mapping文件來(lái)固定名字也是比較常用的一種方式横堡,譬如使用熱修復(fù)跟插件化就得使用mapping文件來(lái)保證兩次構(gòu)建的類(lèi)名方法名是一致的埋市。mapping文件固定名稱(chēng)的具體代碼邏輯如下:
if (configuration.applyMapping != null)
{
MappingReader reader = new MappingReader(configuration.applyMapping);
MappingProcessor keeper =
new MultiMappingProcessor(new MappingProcessor[]
{
new MappingKeeper(programClassPool, warningPrinter),
new MappingKeeper(libraryClassPool, null),
});
reader.pump(keeper);
}
MappingReader做的工作就是以行為單位,一條條的解析mapping里面的內(nèi)容命贴,這里我們先貼上一份簡(jiǎn)單的mapping數(shù)據(jù)道宅,然后對(duì)著代碼來(lái)看比較好理解
com.nls.lib.MyClass -> com.nls.lib.MyClass:
4:4:void <init>() -> <init>
13:17:void test1() -> test1
com.nls.lib.TestClass1 -> a:
3:3:void <init>() -> <init>
public void pump(MappingProcessor mappingProcessor) throws IOException
{
while (true)
{
//1. 循環(huán)讀取mapping里面每一行的數(shù)據(jù).
String line = reader.readLine();
if (line == null)
{
break;
}
line = line.trim();
// Is it a non-comment line?
if (!line.startsWith("#"))
{
//2. 如果第一個(gè)字符不是#且最后一個(gè)字符是: 就認(rèn)為這是一條類(lèi)的定義數(shù)據(jù)
if (line.endsWith(":"))
{
//3. 處理mapping文件里面的類(lèi)映射.
className = processClassMapping(line, mappingProcessor, isReferenceClass);
}
else if (className != null)
{
//4. 處理mapping文件里面的方法字段映射
processClassMemberMapping(className, line, mappingProcessor);
}
}
}
}
可以看到MappingReader的pump方法就是一行行的讀取mapping文件,然后解析出類(lèi)跟字段方法胸蛛,解析出類(lèi)跟字段方法后再繼續(xù)解析里面的映射關(guān)系污茵,processClassMapping方法負(fù)責(zé)了解析類(lèi)的映射關(guān)系,代碼如下:
private String processClassMapping(String line,
MappingProcessor mappingProcessor,
boolean isReferenceClass)
{
int arrowIndex = line.indexOf("->");
if (arrowIndex < 0)
{
return null;
}
int colonIndex = line.indexOf(':', arrowIndex + 2);
if (colonIndex < 0)
{
return null;
}
String className = line.substring(0, arrowIndex).trim();
String newClassName = line.substring(arrowIndex + 2, colonIndex).trim();
boolean interested = mappingProcessor.processClassMapping(className, newClassName, isReferenceClass);
return interested ? className : null;
}
代碼也是比較簡(jiǎn)單葬项,就是根據(jù)mapping文件里面的->
跟:
關(guān)鍵字泞当,解析出映射前的類(lèi)名,以及這個(gè)類(lèi)對(duì)應(yīng)的混淆后名字民珍。最后交給了mappingProcessor的processClassMapping方法處理襟士。
public boolean processClassMapping(String className,
String newClassName,
boolean isReference)
{
//省略部分代碼...
String name = ClassUtil.internalClassName(className);
clazz = classPool.getClass(name);
if (clazz != null)
{
String newName = ClassUtil.internalClassName(newClassName);
ClassObfuscator.setNewClassName(clazz, newName);
return true;
}
return false;
}
processClassMapping先是會(huì)根據(jù)解析出來(lái)的類(lèi)名盗飒,從ClassPool里面找出對(duì)應(yīng)的Clazz對(duì)象,接著把解析出來(lái)的映射名(混淆名字)保存起來(lái)陋桂。processClassMemberMapping原理也是一樣逆趣,這里不再多分析了。
通過(guò)上面的解析處理keep規(guī)則以及對(duì)mapping文件映射的處理嗜历,就能把需要固定的類(lèi)名稱(chēng)跟方法字段名給確定下來(lái)了宣渗,接下來(lái)就是要為沒(méi)有被keep住的類(lèi)名跟字段方法名創(chuàng)建一個(gè)新的混淆名字了。
創(chuàng)建混淆名
首先是為類(lèi)創(chuàng)建新的混淆名字梨州,代碼如下
//1. 指定類(lèi)名混淆字典落包,一般不用設(shè)置
DictionaryNameFactory classNameFactory = configuration.classObfuscationDictionary != null ?
new DictionaryNameFactory(configuration.classObfuscationDictionary, null) :
null;
//2. 指定包名混淆字典,一般不用設(shè)置
DictionaryNameFactory packageNameFactory = configuration.packageObfuscationDictionary != null ?
new DictionaryNameFactory(configuration.packageObfuscationDictionary, null) :
null;
//3. 遍歷ClassPool對(duì)類(lèi)進(jìn)行混淆
programClassPool.classesAccept(
new ClassObfuscator(programClassPool,
libraryClassPool,
classNameFactory,
packageNameFactory,
configuration.useMixedCaseClassNames,
configuration.keepPackageNames,
configuration.flattenPackageHierarchy,
configuration.repackageClasses,
configuration.allowAccessModification,
fixedMethodErrorPrinter));
ClassObfuscator接口負(fù)責(zé)類(lèi)混淆名字的創(chuàng)建工作摊唇,根據(jù)前面的經(jīng)驗(yàn)直接定位到visitProgramClass方法咐蝇,代碼如下
public void visitProgramClass(ProgramClass programClass)
{
//1. 如果前面有固定過(guò)名字的話這里返回的就是固定后的名字.
newClassName = newClassName(programClass);
if (newClassName == null)
{
String oldClassName = programClass.getName();
String oldPackagePrefix = ClassUtil.internalPackagePrefix(oldClassName);
String newPackagePrefix = newClassName != null ?
newClassName + ClassConstants.INNER_CLASS_SEPARATOR :
newPackagePrefix(oldPackagePrefix);
//2. 重新生成一個(gè)新名字.
newClassName = newClassName != null && numericClassName ?
generateUniqueNumericClassName(newPackagePrefix) :
generateUniqueClassName(newPackagePrefix);
//3. 設(shè)置這個(gè)類(lèi)名字為新名字.
setNewClassName(programClass, newClassName);
}
}
第二點(diǎn)就是生成混淆名字接口,它的實(shí)現(xiàn)如下
private String generateUniqueClassName(String newPackagePrefix)
{
return generateUniqueClassName(newPackagePrefix, uniqueClassNameFactory);
}
private String generateUniqueClassName(String newPackagePrefix,
NameFactory classNameFactory)
{
//省略部分代碼..
do
{
//1. 生成一個(gè)名稱(chēng).
newClassName = newPackagePrefix +
classNameFactory.nextName();
}
while (classNamesToAvoid.contains(newMixedCaseClassName)); //2. 名字不能用的話需要重新再生成.
return newClassName;
}
NameFactory接口負(fù)責(zé)新的混淆名字巷查,如果沒(méi)有自定字典的話有序,這個(gè)NameFactory接口就是UniqueNameFactory類(lèi)型對(duì)象,nextName的核心算法過(guò)程如下:
private void newName(StringBuilder newName, int index)
{
// If we're allowed to generate mixed-case names, we can use twice as
// many characters.
int baseIndex = index / CHARACTER_COUNT;
int offset = index % CHARACTER_COUNT;
char newChar = charAt(offset);
newName.append(newChar);
if (baseIndex > 0) {
newName(newName, baseIndex - 1);
}
}
代碼并不多岛请,但卻是proguard混淆功能的名字生成核心了旭寿,CHARACTER_COUNT
被定義為26,也是26個(gè)字母的意思崇败,index從0開(kāi)始一直的往上自加盅称,因此從這里我們就能知道,proguard的混淆名字其實(shí)就是從a開(kāi)始 a-z后室,然后兩位數(shù)的名字:aa-ab-az缩膝,ba-bb-bz,za-zb-zz岸霹,接著是三位數(shù)的名字:aaa-aab-aaz-aba 這樣一直下去疾层。最后生成的新名字也會(huì)被存起來(lái)。
這樣混淆后的新類(lèi)名字就創(chuàng)建好了贡避,接著就是為沒(méi)有被keep住的方法或字段名創(chuàng)建出新的混淆名字痛黎,這個(gè)是任務(wù)是由MemberObfuscator 接口負(fù)責(zé),我們直接看它的visitAnyMember方法:
public void visitAnyMember(Clazz clazz, Member member)
{
//這里省略掉部分代碼...
//1. <init>方法不混淆直接過(guò)濾.
String name = member.getName(clazz);
if (ClassUtil.isInitializer(name))
{
return;
}
//2. 是否已經(jīng)固定過(guò)名字了.
String newName = newMemberName(member);
if (newName == null)
{
do
{ //3. 生成一個(gè)新的名字刮吧。
newName = nameFactory.nextName();
}
while (nameMap.containsKey(newName));//3. 名字是否有重復(fù).
//4. 保存名字.
nameMap.put(newName, name);
//5. 把名字保存到ProgramMember對(duì)象里
setNewMemberName(member, newName);
}
}
代碼邏輯跟上面的類(lèi)混淆名字生成過(guò)程是類(lèi)似的湖饱,由UniqueNameFactory負(fù)責(zé)生成一個(gè)新名字,新名字可以用的話就把它先存在ProgramMember對(duì)象里杀捻。
應(yīng)用混淆名
上面這些工作只是把新的類(lèi)名跟新的方法字段名都創(chuàng)建好并且存起來(lái)井厌,但這些新的名字并沒(méi)有更新到Clazz對(duì)象里,下面接著開(kāi)始分析proguard是如何把這些新的混淆后名字更新到Clazz對(duì)象里。
// Actually apply the new names.
programClassPool.classesAccept(new ClassRenamer());
libraryClassPool.classesAccept(new ClassRenamer());
// Update all references to these new names.
programClassPool.classesAccept(new ClassReferenceFixer(false));
libraryClassPool.classesAccept(new ClassReferenceFixer(false));
programClassPool.classesAccept(new MemberReferenceFixer());
代碼如上面所示旗笔,首先是應(yīng)用新的混淆名字到Clazz對(duì)象里彪置,接著還需求修復(fù)一些方法或類(lèi)的引用。我們先分析ClassRenamer
public void visitProgramClass(ProgramClass programClass)
{
//1. 先更新本類(lèi)名稱(chēng)
programClass.thisClassConstantAccept(this);
//2. 再更新字段方法名.
programClass.fieldsAccept(this);
programClass.methodsAccept(this);
}
首先是更新新的混淆類(lèi)名到常量池里蝇恶,接著再更新方法跟字段名拳魁,ClassRenamer的visitClassConstant方法如下:
public void visitClassConstant(Clazz clazz, ClassConstant classConstant)
{
// Update the Class entry if required.
String newName = ClassObfuscator.newClassName(clazz);
if (newName != null)
{
// Refer to a new Utf8 entry.
classConstant.u2nameIndex =
new ConstantPoolEditor((ProgramClass)clazz).addUtf8Constant(newName);
}
}
代碼比較簡(jiǎn)單,最終是通過(guò)ConstantPoolEditor接口向常量池里面插入一條新的數(shù)據(jù)撮弧,并且把類(lèi)名稱(chēng)的索引index指向了剛剛插入的數(shù)據(jù)里潘懊,這樣就能完成了類(lèi)名的替換了。
public int addUtf8Constant(String string)
{
int constantPoolCount = targetClass.u2constantPoolCount;
Constant[] constantPool = targetClass.constantPool;
// Check if the entry already exists.
for (int index = 1; index < constantPoolCount; index++)
{
Constant constant = constantPool[index];
if (constant != null &&
constant.getTag() == ClassConstants.CONSTANT_Utf8)
{
Utf8Constant utf8Constant = (Utf8Constant)constant;
if (utf8Constant.getString().equals(string))
{
return index;
}
}
}
return addConstant(new Utf8Constant(string));
}
在插入新數(shù)據(jù)前先會(huì)遍歷下常量池贿衍,判斷即將要插入的數(shù)據(jù)是否存在授舟,不存在的話就通過(guò)addConstant接口插入。
public int addConstant(Constant constant)
{
int constantPoolCount = targetClass.u2constantPoolCount;
Constant[] constantPool = targetClass.constantPool;
// Make sure there is enough space for another constant pool entry.
if (constantPool.length < constantPoolCount+2)
{
targetClass.constantPool = new Constant[constantPoolCount+2];
System.arraycopy(constantPool, 0,
targetClass.constantPool, 0,
constantPoolCount);
constantPool = targetClass.constantPool;
}
// Create a new Utf8Constant for the given string.
constantPool[targetClass.u2constantPoolCount++] = constant;
return constantPoolCount;
}
addConstant會(huì)向常量池開(kāi)辟新的空間贸辈,然后把新的數(shù)據(jù)塞進(jìn)常量池里释树。
回到ClassRenamer的visitProgramClass方法里,在完成了新類(lèi)名的更新后擎淤,接著就是方法跟字段名的更新了奢啥,原理是一樣的,我們直接定位到它的visitProgramMember方法
public void visitProgramMember(ProgramClass programClass,
ProgramMember programMember)
{
// Has the class member name changed?
String name = programMember.getName(programClass);
String newName = MemberObfuscator.newMemberName(programMember);
if (newName != null &&
!newName.equals(name))
{
programMember.u2nameIndex =
new ConstantPoolEditor(programClass).addUtf8Constant(newName);
}
}
可以看到過(guò)程是一致的嘴拢,也是通過(guò)ConstantPoolEditor向常量池插入一條新的數(shù)據(jù)桩盲,然后修改方法字段索引,把方法字段的名稱(chēng)索引指向了新的混淆名去席吴,這里就不再分析了赌结。
在完成了類(lèi)名跟方法字段名稱(chēng)的更新后,還得把其他依賴(lài)了此類(lèi)的或此類(lèi)方法字段的類(lèi)的引用數(shù)據(jù)糾正回來(lái)孝冒,譬如下面的測(cè)試代碼
class MyClass {
fun test(test: TestClass?) {
test?.test1()
}
}
MyClass類(lèi)依賴(lài)了TestClass類(lèi)跟TestClass的test1方法柬姚,但是現(xiàn)在TestClass類(lèi)已經(jīng)被混淆成新的名字a了,而test1方法名也被混淆成了新的b名字了迈倍,此時(shí)也要更新下MyClass類(lèi)的數(shù)據(jù)結(jié)構(gòu)伤靠,把對(duì)TestClass依賴(lài)引用全部糾正到新的名稱(chēng)來(lái)。
programClassPool.classesAccept(new ClassReferenceFixer(false));
libraryClassPool.classesAccept(new ClassReferenceFixer(false));
programClassPool.classesAccept(new MemberReferenceFixer());
ClassReferenceFixer負(fù)責(zé)類(lèi)依賴(lài)的更新修復(fù)啼染,MemberReferenceFixer負(fù)責(zé)了方法依賴(lài)的更新修復(fù)。先看類(lèi)引用依賴(lài)的修復(fù)更新
public void visitProgramClass(ProgramClass programClass)
{
// Fix the constant pool.
programClass.constantPoolEntriesAccept(this);
// Fix class members.
programClass.fieldsAccept(this);
programClass.methodsAccept(this);
// Fix the attributes.
programClass.attributesAccept(this);
}
public void visitClassConstant(Clazz clazz, ClassConstant classConstant)
{
// Do we know the referenced class?
Clazz referencedClass = classConstant.referencedClass;
if (referencedClass != null)
{
// Has the class name changed?
String className = classConstant.getName(clazz);
String newClassName = newClassName(className, referencedClass);
if (!className.equals(newClassName))
{
// Refer to a new Utf8 entry.
classConstant.u2nameIndex =
new ConstantPoolEditor((ProgramClass)clazz).addUtf8Constant(newClassName);
}
}
}
跟上面的類(lèi)混淆名更新替換是類(lèi)似的焕梅,首先是遍歷常量池迹鹅,拿到常量池里的類(lèi)常量,接著判斷下這個(gè)類(lèi)常量保存的類(lèi)名是否跟引用類(lèi)現(xiàn)在的名字相等贞言,如果不相等的話在常量池里插入一條新名字?jǐn)?shù)據(jù)斜棚,并且把類(lèi)常量里的名字索引指向了新的插入數(shù)據(jù),這樣就完成了引用類(lèi)的混淆名修復(fù)更新工作了。
引用方法名的更新也是類(lèi)似的弟蚀,下面直接給出關(guān)鍵過(guò)程
public void visitProgramMethod(ProgramClass programClass, ProgramMethod programMethod)
{
// Has the descriptor changed?
String descriptor = programMethod.getDescriptor(programClass);
String newDescriptor = newDescriptor(descriptor,
programMethod.referencedClasses);
if (!descriptor.equals(newDescriptor))
{
ConstantPoolEditor constantPoolEditor =
new ConstantPoolEditor(programClass);
// Update the descriptor.
programMethod.u2descriptorIndex =
constantPoolEditor.addUtf8Constant(newDescriptor);
// Update the name, if requested.
if (ensureUniqueMemberNames)
{
String name = programMethod.getName(programClass);
String newName = newUniqueMemberName(name, descriptor);
programMethod.u2nameIndex =
constantPoolEditor.addUtf8Constant(newName);
}
}
// Fix the attributes.
programMethod.attributesAccept(programClass, this);
}
在完成了類(lèi)與類(lèi)之間的引用修復(fù)之后蚤霞,還得修復(fù)本類(lèi)內(nèi)的依賴(lài)修復(fù),這些工作是由MemberReferenceFixer接口負(fù)責(zé)义钉,過(guò)程也是類(lèi)似的昧绣,也是遍歷常量池,判斷名稱(chēng)捶闸,插入新的名稱(chēng)夜畴,修改index索引指向新名稱(chēng),這里就不再繼續(xù)分析了删壮。
壓縮常量池
從前面的分析可以看見(jiàn)贪绘,新的混淆名字是直接在常量池里開(kāi)辟一塊新的數(shù)據(jù)來(lái)存的,所以舊的數(shù)據(jù)就已經(jīng)沒(méi)有用了央碟,所以這個(gè)數(shù)據(jù)是需要被剔除掉的税灌,這樣才能達(dá)到減少class體積的目的。
在Obfuscator的execute
方法最后便是壓縮常量池的工作了亿虽,這個(gè)任務(wù)是由ConstantPoolShrinker來(lái)完成菱涤,代碼如下:
// Remove unused constants.
programClassPool.classesAccept(
new ConstantPoolShrinker());
我們直接看它的visitProgramClass方法實(shí)現(xiàn)
public void visitProgramClass(ProgramClass programClass)
{
// Mark this class's name.
markConstant(programClass, programClass.u2thisClass);
// Mark the superclass class constant.
programClass.superClassConstantAccept(this);
// Mark the interface class constants.
programClass.interfaceConstantsAccept(this);
// Mark the constants referenced by the class members.
programClass.fieldsAccept(this);
programClass.methodsAccept(this);
// Mark the attributes.
programClass.attributesAccept(this);
// Shift the used constant pool entries together, filling out the
// index map.
int newConstantPoolCount =
shrinkConstantPool(programClass.constantPool,
programClass.u2constantPoolCount);
// Remap the references to the constant pool if it has shrunk.
if (newConstantPoolCount < programClass.u2constantPoolCount)
{
programClass.u2constantPoolCount = newConstantPoolCount;
// Remap all constant pool references.
constantPoolRemapper.setConstantIndexMap(constantIndexMap);
constantPoolRemapper.visitProgramClass(programClass);
}
}
代碼也比較多,同樣的這里我們也只挑類(lèi)常量跟方法常量來(lái)分析
- 標(biāo)記類(lèi)常量
// Mark this class's name.
markConstant(programClass, programClass.u2thisClass);
public void visitClassConstant(Clazz clazz, ClassConstant classConstant)
{
markAsUsed(classConstant);
markConstant(clazz, classConstant.u2nameIndex);
}
常量池的壓縮過(guò)程其實(shí)是跟我們前面分析過(guò)的類(lèi)字段方法壓縮過(guò)程是一致的经柴,先是標(biāo)記狸窘,標(biāo)記完了再統(tǒng)一處理,這里先把類(lèi)常量標(biāo)記為UESD
坯认,接著還需要把類(lèi)常引用到的字符常量也標(biāo)記為USED
翻擒,這樣類(lèi)常量的標(biāo)記就完成了。
直接就是遍歷所有方法常量牛哺,把方法常量引用到的字符常量陋气、符合描述常量以及方法的屬性表常量標(biāo)記為USED
programClass.methodsAccept(this);
public void visitProgramMember(ProgramClass programClass, ProgramMember programMember)
{
// Mark the name and descriptor.
markConstant(programClass, programMember.u2nameIndex);
markConstant(programClass, programMember.u2descriptorIndex);
// Mark the attributes.
programMember.attributesAccept(programClass, this);
}
-
壓縮常量池
當(dāng)所有有用的常量都被標(biāo)記完了就可以對(duì)常量池做一輪壓縮了,這個(gè)工作是有shrinkConstantPool方法負(fù)責(zé)引润,代碼如下:
int newConstantPoolCount =
shrinkConstantPool(programClass.constantPool,
programClass.u2constantPoolCount);
private int shrinkConstantPool(Constant[] constantPool, int length)
{
// Create a new index map, if necessary.
if (constantIndexMap.length < length)
{
constantIndexMap = new int[length];
}
int counter = 1;
boolean isUsed = false;
// Shift the used constant pool entries together.
for (int index = 1; index < length; index++)
{
Constant constant = constantPool[index];
// Is the constant being used? Don't update the flag if this is the
// second half of a long entry.
if (constant != null)
{
isUsed = isUsed(constant);
}
if (isUsed)
{
// Remember the new index.
constantIndexMap[index] = counter;
// Shift the constant pool entry.
constantPool[counter++] = constant;
}
else
{
// Remember an invalid index.
constantIndexMap[index] = -1;
}
}
// Clear the remaining constant pool elements.
Arrays.fill(constantPool, counter, length, null);
return counter;
}
可以看到巩趁,標(biāo)記完成后,剩下的壓縮工作也十分的簡(jiǎn)單淳附,就是遍歷常量池议慰,把已標(biāo)記為USED
的常量保留下來(lái),而沒(méi)有被打上USED
標(biāo)簽的則是剔除掉奴曙,最后就是把常量池里多余的空間置為null
-
常量池索引修復(fù)
經(jīng)過(guò)了上面的壓縮步驟后别凹,常量池的結(jié)構(gòu)會(huì)發(fā)生變化,所以對(duì)常量池的索引index也得重新修復(fù)一下洽糟,ConstantPoolRemapper負(fù)責(zé)了常量池索引index的修復(fù)工作炉菲,代碼如下:
// Remap the references to the constant pool if it has shrunk.
if (newConstantPoolCount < programClass.u2constantPoolCount)
{
programClass.u2constantPoolCount = newConstantPoolCount;
// Remap all constant pool references.
constantPoolRemapper.setConstantIndexMap(constantIndexMap);
constantPoolRemapper.visitProgramClass(programClass);
}
public void visitProgramClass(ProgramClass programClass)
{
// Remap the local constant pool references.
programClass.u2thisClass = remapConstantIndex(programClass.u2thisClass);
programClass.u2superClass = remapConstantIndex(programClass.u2superClass);
remapConstantIndexArray(programClass.u2interfaces,
programClass.u2interfacesCount);
// Remap the references of the contant pool entries themselves.
programClass.constantPoolEntriesAccept(this);
// Remap the references in all fields, methods, and attributes.
programClass.fieldsAccept(this);
programClass.methodsAccept(this);
programClass.attributesAccept(this);
}
跟前面的分析一樣堕战,這里我們也只分析下類(lèi)常量的索引跟方法常量的索引修復(fù)過(guò)程。
//先是修復(fù)類(lèi)索引
programClass.u2thisClass = remapConstantIndex(programClass.u2thisClass);
//這段是核心代碼拍霜,索引index的修復(fù)全靠這里的Map來(lái)完成
if (isUsed)
{
// Remember the new index.
constantIndexMap[index] = counter;
// Shift the constant pool entry.
constantPool[counter++] = constant;
}
private int remapConstantIndex(int constantIndex)
{
int remappedConstantIndex = constantIndexMap[constantIndex];
if (remappedConstantIndex < 0)
{
throw new IllegalArgumentException("Can't remap constant index ["+constantIndex+"]");
}
return remappedConstantIndex;
}
索引index的修復(fù)關(guān)鍵是在于前面的shrinkConstantPool
方法在常量池壓縮過(guò)程中保存了一張舊index跟新index的映射關(guān)系嘱丢,通過(guò)這張映射表就能知道,舊的索引index在壓縮后的常量池里所對(duì)應(yīng)的索引index號(hào)了祠饺。
方法索引的修復(fù)也是一樣的過(guò)程越驻,代碼如下:
public void visitProgramMethod(ProgramClass programClass, ProgramMethod programMethod)
{
visitMember(programClass, programMethod);
}
private void visitMember(ProgramClass programClass, ProgramMember programMember)
{
// Remap the local constant pool references.
programMember.u2nameIndex =
remapConstantIndex(programMember.u2nameIndex);
programMember.u2descriptorIndex =
remapConstantIndex(programMember.u2descriptorIndex);
// Remap the constant pool references of the remaining attributes.
programMember.attributesAccept(programClass, this);
}
最后class內(nèi)部的其他數(shù)據(jù)的索引index問(wèn)題也是通過(guò)這樣的方式來(lái)一一修復(fù)的,這里就不再繼續(xù)分析了吠裆。
總結(jié)
本篇文章從源碼的角度出發(fā)伐谈,分析了proguard工具里面我們最熟悉的混淆功能,混淆的過(guò)程是怎么進(jìn)行的试疙,混淆名字是如何生成的诵棵,又是如何被應(yīng)用到class內(nèi)部去的。其實(shí)混淆的過(guò)程十分的復(fù)雜的祝旷,涉及到的細(xì)節(jié)也是十分的多履澳,本篇繞過(guò)了很多細(xì)節(jié)的地方,扼要的概括了混淆過(guò)程的主線來(lái)分析怀跛,關(guān)于更多的細(xì)節(jié)功能大家可以自行翻閱源碼距贷。