美文网首页Android
proguard源码分析五 Obfuscator

proguard源码分析五 Obfuscator

作者: 获取失败 | 来源:发表于2022-01-06 13:28 被阅读0次

    本节开始我们来分析一下proguard里面我们最熟悉的功能:混淆 ,分析一下proguard是如何把类跟方法字段名固定下来,又是如何为没有被keep住的类跟方法字段创建出新的a.b.c这样的新名字 。

    在proguard里Obfuscator接口负责了混淆的工作,大致的可以把整个混淆过程分为四步骤:

    • 名字固定
    • 创建混淆名
    • 应用混淆名
    • 压缩常量池

    下面我们一步步的分析一下

    固定名字

    固定名字顾名思义的,就是混淆前混淆后这些名字不会发生改变,又或者是会根据mapping配置文件里的内容来发生变化。proguard会在混淆前根据keep规则配置,或者mapping文件映射关系,提前把一些类名或方法字段名给固定下来,没被固定下来的就会以a.b.c这样的随机名字重新命名。

    • keep规则固定
      前面已经说了,常用的名字固定方式有两种,一种就是最直接的通过keep规则来固定,我们直接定位到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);
    

    首先也是先创建ClassPoolVisitor,之前的文章也已经分析过ClassPoolVisitor的创建过程了,这里就不再赘述了,下面也是方便理解直接给出ClassPoolVisitor的层级嵌套关系:


    有了之前的经验这里我们可以直接看NameMarker就可以了,其他一层层的ClassPoolVisitor嵌套也只是增加了一些预处理逻辑而已,最终的代码实现都会在NameMarker里

    public void visitProgramClass(ProgramClass programClass)
    {
        //1. 把类名保存下来.
        keepClassName(programClass);
        //2. 遍历属性表,把内部类名称也保存一下
        programClass.attributesAccept(this);
    }
    public void keepClassName(Clazz clazz)
    {
        ClassObfuscator.setNewClassName(clazz,
                clazz.getName());
    }
    

    NameMarker的visitProgramClass方法比较简单,主要是把符合keep规则规则里的类名称以及它的内部类名称(如果有的话)给保存起来。

    接着看它的visitProgramField跟visitProgramMethod方法,也是比较简单,也是要把keep住的字段名跟方法先保存起来,代码如下:

    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);
        }
    }
    

    经过了上面这一步,所有在proguard配置里面被keep的类名、字段名跟方法名都会先被保存了下来。

    programClassPool.classesAccept(
        new ClassVersionFilter(ClassConstants.CLASS_VERSION_1_7,
        new AllConstantVisitor(
                new DynamicReturnedClassVisitor(
                        new AllMemberVisitor(nameMarker)))));
    }
    

    接着是Java7之后才添加的新特性invokedynamic,关于这个新特性大家可自行的搜索相关资料,这里不多做介绍。

    接着就是属性相关的名字固定

    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)));
    

    这段逻辑对应的是proguard里面的keepattributes 配置,通常也不需要怎么配置,这里也略过。

    • mapping文件固定
      通过mapping文件来固定名字也是比较常用的一种方式,譬如使用热修复跟插件化就得使用mapping文件来保证两次构建的类名方法名是一致的。mapping文件固定名称的具体代码逻辑如下:
    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里面的内容,这里我们先贴上一份简单的mapping数据,然后对着代码来看比较好理解

    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. 循环读取mapping里面每一行的数据.
            String line = reader.readLine();
    
            if (line == null)
            {
                break;
            }
    
            line = line.trim();
    
            // Is it a non-comment line?
            if (!line.startsWith("#"))
            {
                //2. 如果第一个字符不是#且最后一个字符是: 就认为这是一条类的定义数据
                if (line.endsWith(":"))
                {
                    //3. 处理mapping文件里面的类映射.
                    className = processClassMapping(line, mappingProcessor, isReferenceClass);
                }
                else if (className != null)
                {
                    //4. 处理mapping文件里面的方法字段映射
                    processClassMemberMapping(className, line, mappingProcessor);
                }
            }
        }
    }
    

    可以看到MappingReader的pump方法就是一行行的读取mapping文件,然后解析出类跟字段方法,解析出类跟字段方法后再继续解析里面的映射关系,processClassMapping方法负责了解析类的映射关系,代码如下:

    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;
    }
    

    代码也是比较简单,就是根据mapping文件里面的->:关键字,解析出映射前的类名,以及这个类对应的混淆后名字。最后交给了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先是会根据解析出来的类名,从ClassPool里面找出对应的Clazz对象,接着把解析出来的映射名(混淆名字)保存起来。processClassMemberMapping原理也是一样,这里不再多分析了。

    通过上面的解析处理keep规则以及对mapping文件映射的处理,就能把需要固定的类名称跟方法字段名给确定下来了,接下来就是要为没有被keep住的类名跟字段方法名创建一个新的混淆名字了。

    创建混淆名

    首先是为类创建新的混淆名字,代码如下

    //1. 指定类名混淆字典,一般不用设置    
    DictionaryNameFactory classNameFactory = configuration.classObfuscationDictionary != null ?
            new DictionaryNameFactory(configuration.classObfuscationDictionary, null) :
            null;
    //2. 指定包名混淆字典,一般不用设置   
    DictionaryNameFactory packageNameFactory = configuration.packageObfuscationDictionary != null ?
            new DictionaryNameFactory(configuration.packageObfuscationDictionary, null) :
            null;
    
    //3. 遍历ClassPool对类进行混淆
    programClassPool.classesAccept(
        new ClassObfuscator(programClassPool,
                            libraryClassPool,
                            classNameFactory,
                            packageNameFactory,
                            configuration.useMixedCaseClassNames,
                            configuration.keepPackageNames,
                            configuration.flattenPackageHierarchy,
                            configuration.repackageClasses,
                            configuration.allowAccessModification,
                            fixedMethodErrorPrinter));
    

    ClassObfuscator接口负责类混淆名字的创建工作,根据前面的经验直接定位到visitProgramClass方法,代码如下

    public void visitProgramClass(ProgramClass programClass)
    {
        //1. 如果前面有固定过名字的话这里返回的就是固定后的名字.
        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. 重新生成一个新名字.
            newClassName = newClassName != null && numericClassName ?
                    generateUniqueNumericClassName(newPackagePrefix) :
                    generateUniqueClassName(newPackagePrefix);
    
            //3. 设置这个类名字为新名字.
            setNewClassName(programClass, newClassName);
        }
    }
    

    第二点就是生成混淆名字接口,它的实现如下

    private String generateUniqueClassName(String newPackagePrefix)
    {
        return generateUniqueClassName(newPackagePrefix, uniqueClassNameFactory);
    }
    
    private String generateUniqueClassName(String      newPackagePrefix,
                                           NameFactory classNameFactory)
    {
        //省略部分代码..
        do
        {
            //1. 生成一个名称.
            newClassName = newPackagePrefix +
                    classNameFactory.nextName();
        }
        while (classNamesToAvoid.contains(newMixedCaseClassName)); //2. 名字不能用的话需要重新再生成.
        return newClassName;
    }
    

    NameFactory接口负责新的混淆名字,如果没有自定字典的话,这个NameFactory接口就是UniqueNameFactory类型对象,nextName的核心算法过程如下:

    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个字母的意思,index从0开始一直的往上自加,因此从这里我们就能知道,proguard的混淆名字其实就是从a开始 a-z,然后两位数的名字:aa-ab-az,ba-bb-bz,za-zb-zz,接着是三位数的名字:aaa-aab-aaz-aba 这样一直下去。最后生成的新名字也会被存起来。

    这样混淆后的新类名字就创建好了,接着就是为没有被keep住的方法或字段名创建出新的混淆名字,这个是任务是由MemberObfuscator 接口负责,我们直接看它的visitAnyMember方法:

    public void visitAnyMember(Clazz clazz, Member member)
    {
        //这里省略掉部分代码...
        //1. <init>方法不混淆直接过滤.
        String name = member.getName(clazz);
        if (ClassUtil.isInitializer(name))
        {
            return;
        }
        //2. 是否已经固定过名字了.
        String newName = newMemberName(member);
        if (newName == null)
        {
            do
            {   //3. 生成一个新的名字。
                newName = nameFactory.nextName();
            }
            while (nameMap.containsKey(newName));//3. 名字是否有重复.
    
            //4. 保存名字.
            nameMap.put(newName, name);
    
            //5. 把名字保存到ProgramMember对象里
            setNewMemberName(member, newName);
        }
    }
    

    代码逻辑跟上面的类混淆名字生成过程是类似的,由UniqueNameFactory负责生成一个新名字,新名字可以用的话就把它先存在ProgramMember对象里。

    应用混淆名

    上面这些工作只是把新的类名跟新的方法字段名都创建好并且存起来,但这些新的名字并没有更新到Clazz对象里,下面接着开始分析proguard是如何把这些新的混淆后名字更新到Clazz对象里。

    // 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());
    

    代码如上面所示,首先是应用新的混淆名字到Clazz对象里,接着还需求修复一些方法或类的引用。我们先分析ClassRenamer

    public void visitProgramClass(ProgramClass programClass)
    {
        //1. 先更新本类名称
        programClass.thisClassConstantAccept(this);
    
        //2. 再更新字段方法名.
        programClass.fieldsAccept(this);
        programClass.methodsAccept(this);
    }
    

    首先是更新新的混淆类名到常量池里,接着再更新方法跟字段名,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);
        }
    }
    

    代码比较简单,最终是通过ConstantPoolEditor接口向常量池里面插入一条新的数据,并且把类名称的索引index指向了刚刚插入的数据里,这样就能完成了类名的替换了。

    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));
    }
    

    在插入新数据前先会遍历下常量池,判断即将要插入的数据是否存在,不存在的话就通过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会向常量池开辟新的空间,然后把新的数据塞进常量池里。

    回到ClassRenamer的visitProgramClass方法里,在完成了新类名的更新后,接着就是方法跟字段名的更新了,原理是一样的,我们直接定位到它的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);
        }
    }
    

    可以看到过程是一致的,也是通过ConstantPoolEditor向常量池插入一条新的数据,然后修改方法字段索引,把方法字段的名称索引指向了新的混淆名去,这里就不再分析了。

    在完成了类名跟方法字段名称的更新后,还得把其他依赖了此类的或此类方法字段的类的引用数据纠正回来,譬如下面的测试代码

    class MyClass {
        fun test(test: TestClass?) {
            test?.test1()
        }
    }
    

    MyClass类依赖了TestClass类跟TestClass的test1方法,但是现在TestClass类已经被混淆成新的名字a了,而test1方法名也被混淆成了新的b名字了,此时也要更新下MyClass类的数据结构,把对TestClass依赖引用全部纠正到新的名称来。

    programClassPool.classesAccept(new ClassReferenceFixer(false));
    libraryClassPool.classesAccept(new ClassReferenceFixer(false));
    programClassPool.classesAccept(new MemberReferenceFixer());
    

    ClassReferenceFixer负责类依赖的更新修复,MemberReferenceFixer负责了方法依赖的更新修复。先看类引用依赖的修复更新

    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);
            }
        }
    }
    

    跟上面的类混淆名更新替换是类似的,首先是遍历常量池,拿到常量池里的类常量,接着判断下这个类常量保存的类名是否跟引用类现在的名字相等,如果不相等的话在常量池里插入一条新名字数据,并且把类常量里的名字索引指向了新的插入数据,这样就完成了引用类的混淆名修复更新工作了。

    引用方法名的更新也是类似的,下面直接给出关键过程

    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);
    }
    

    在完成了类与类之间的引用修复之后,还得修复本类内的依赖修复,这些工作是由MemberReferenceFixer接口负责,过程也是类似的,也是遍历常量池,判断名称,插入新的名称,修改index索引指向新名称,这里就不再继续分析了。

    压缩常量池

    从前面的分析可以看见,新的混淆名字是直接在常量池里开辟一块新的数据来存的,所以旧的数据就已经没有用了,所以这个数据是需要被剔除掉的,这样才能达到减少class体积的目的。

    Obfuscatorexecute方法最后便是压缩常量池的工作了,这个任务是由ConstantPoolShrinker来完成,代码如下:

    // Remove unused constants.
    programClassPool.classesAccept(
            new ConstantPoolShrinker());
    

    我们直接看它的visitProgramClass方法实现

    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);
        }
    }
    

    代码也比较多,同样的这里我们也只挑类常量跟方法常量来分析

    • 标记类常量
    // Mark this class's name.
    markConstant(programClass, programClass.u2thisClass);
    
    public void visitClassConstant(Clazz clazz, ClassConstant classConstant)
    {
        markAsUsed(classConstant);
    
        markConstant(clazz, classConstant.u2nameIndex);
    }
    

    常量池的压缩过程其实是跟我们前面分析过的类字段方法压缩过程是一致的,先是标记,标记完了再统一处理,这里先把类常量标记为UESD,接着还需要把类常引用到的字符常量也标记为USED,这样类常量的标记就完成了。
    直接就是遍历所有方法常量,把方法常量引用到的字符常量、符合描述常量以及方法的属性表常量标记为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);
    }
    
    • 压缩常量池
      当所有有用的常量都被标记完了就可以对常量池做一轮压缩了,这个工作是有shrinkConstantPool方法负责,代码如下:
    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;
    }
    

    可以看到,标记完成后,剩下的压缩工作也十分的简单,就是遍历常量池,把已标记为USED的常量保留下来,而没有被打上USED标签的则是剔除掉,最后就是把常量池里多余的空间置为null

    • 常量池索引修复
      经过了上面的压缩步骤后,常量池的结构会发生变化,所以对常量池的索引index也得重新修复一下,ConstantPoolRemapper负责了常量池索引index的修复工作,代码如下:
    // 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);
    }
    

    跟前面的分析一样,这里我们也只分析下类常量的索引跟方法常量的索引修复过程。

    //先是修复类索引
    programClass.u2thisClass  = remapConstantIndex(programClass.u2thisClass);
    
    //这段是核心代码,索引index的修复全靠这里的Map来完成
    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的修复关键是在于前面的shrinkConstantPool方法在常量池压缩过程中保存了一张旧index跟新index的映射关系,通过这张映射表就能知道,旧的索引index在压缩后的常量池里所对应的索引index号了。

    方法索引的修复也是一样的过程,代码如下:

    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内部的其他数据的索引index问题也是通过这样的方式来一一修复的,这里就不再继续分析了。

    总结

    本篇文章从源码的角度出发,分析了proguard工具里面我们最熟悉的混淆功能,混淆的过程是怎么进行的,混淆名字是如何生成的,又是如何被应用到class内部去的。其实混淆的过程十分的复杂的,涉及到的细节也是十分的多,本篇绕过了很多细节的地方,扼要的概括了混淆过程的主线来分析,关于更多的细节功能大家可以自行翻阅源码。

    相关文章

      网友评论

        本文标题:proguard源码分析五 Obfuscator

        本文链接:https://www.haomeiwen.com/subject/ngvxcrtx.html