proguard源碼分析五 Obfuscator

本節(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)固定,我們直接定位到Obfuscatorexecute方法盗扇,如下:
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體積的目的。

Obfuscatorexecute方法最后便是壓縮常量池的工作了亿虽,這個(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é)功能大家可以自行翻閱源碼距贷。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市吻谋,隨后出現(xiàn)的幾起案子忠蝗,更是在濱河造成了極大的恐慌,老刑警劉巖漓拾,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件阁最,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡骇两,警方通過(guò)查閱死者的電腦和手機(jī)速种,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)低千,“玉大人配阵,你說(shuō)我怎么就攤上這事∈狙” “怎么了棋傍?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)难审。 經(jīng)常有香客問(wèn)我舍沙,道長(zhǎng),這世上最難降的妖魔是什么剔宪? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上葱绒,老公的妹妹穿的比我還像新娘感帅。我一直安慰自己,他們只是感情好地淀,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布失球。 她就那樣靜靜地躺著,像睡著了一般帮毁。 火紅的嫁衣襯著肌膚如雪实苞。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,007評(píng)論 1 284
  • 那天烈疚,我揣著相機(jī)與錄音黔牵,去河邊找鬼。 笑死爷肝,一個(gè)胖子當(dāng)著我的面吹牛猾浦,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播灯抛,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼金赦,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了对嚼?” 一聲冷哼從身側(cè)響起夹抗,我...
    開(kāi)封第一講書(shū)人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎纵竖,沒(méi)想到半個(gè)月后漠烧,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡磨确,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年沽甥,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片乏奥。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡摆舟,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出邓了,到底是詐尸還是另有隱情恨诱,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布骗炉,位于F島的核電站照宝,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏句葵。R本人自食惡果不足惜厕鹃,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一兢仰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧剂碴,春花似錦把将、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至催训,卻和暖如春洽议,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背漫拭。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工亚兄, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人嫂侍。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓儿捧,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親挑宠。 傳聞我的和親對(duì)象是個(gè)殘疾皇子菲盾,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容