proguard源码分析三 依赖关系检索

上一节我们从源码的角度出发分析了proguard是怎么把class字节码解析读取出来,并且通过LibraryClassPool跟ProgramClassPool两个池子把项目里的所有类都管理起来,这节我们来分析下proguard是如何检索类的依赖关系,只有把类依赖关系都找出来了,下面才能做压缩跟裁剪工作。

回到proguard类的execute方法里,readInput读取完类信息后,接着initialize就开始着手一些初始化的事情,这里比较重要的事情就是索引类依赖关系,只有把类的依赖关系索引出来了,才能知道哪些类是有用的,哪些类是无用可以被删除掉的。

package com.nls.demo
import com.nls.demo.ParentClass
import com.nls.demo.Interface1
import com.nls.demo.Test

class Demo : ParentClass() , Interface1 {
   @override fun test() {
        Test().print()
    }
}

假如有这样一段伪代码,很显然的Demo依赖的类有ParentClass Test 接口有Interface1 那么proguard是怎么把它们的依赖关系给分析出来呢,下面我们来分析一下。

父类与接口类依赖检索

public void execute() throws IOException
{
    //省略部分代码...
    readInput();
    if (configuration.printSeeds != null ||
        configuration.shrink    ||
        configuration.optimize  ||
        configuration.obfuscate ||
        configuration.preverify)
    {
        initialize();
    }
    if (configuration.shrink)
    {
        shrink();
    }
    //省略部分代码...
}

initialize方法比较简单,内部new了Initializer对象,然后执行它的execute方法,我们直接看Initializer的内部实现就好。

private void initialize() throws IOException
{
    if (configuration.verbose)
    {
        System.out.println("Initializing...");
    }
    new Initializer(configuration).execute(programClassPool, libraryClassPool);
}

execute方法比较长,我们抽取部分核心关键地方来分析

public void execute(ClassPool programClassPool,
                    ClassPool libraryClassPool) throws IOException
{
    //省略部分代码...
    // Initialize the superclass hierarchies for program classes.
    programClassPool.classesAccept(
        new ClassSuperHierarchyInitializer(programClassPool,
                                           libraryClassPool,
                                           classReferenceWarningPrinter,
                                           null));

    // Initialize the superclass hierarchy of all library classes, without
    // warnings.
    libraryClassPool.classesAccept(
        new ClassSuperHierarchyInitializer(programClassPool,
                                           libraryClassPool,
                                           null,
                                           dependencyWarningPrinter));

    programClassPool.classesAccept(
        new ClassReferenceInitializer(programClassPool,
                                      libraryClassPool,
                                      classReferenceWarningPrinter,
                                      programMemberReferenceWarningPrinter,
                                      libraryMemberReferenceWarningPrinter,
                                      null));

    //省略部分代码...
}

ClassPool的classesAccept方法会遍历里面的所有Clazz去调用它的accept方法,

 public void classesAccept(ClassVisitor classVisitor)
{
    Iterator iterator = classes.values().iterator();
    while (iterator.hasNext())
    {
        Clazz clazz = (Clazz)iterator.next();
        clazz.accept(classVisitor);
    }
}

programClassPool里面的是ProgramClass,libraryClassPool里面对应的就是LibraryClass

//ProgramClass
public void accept(ClassVisitor classVisitor)
{
    classVisitor.visitProgramClass(this);
}
//LibraryClass
public void accept(ClassVisitor classVisitor)
{
    classVisitor.visitLibraryClass(this);
}

对于ProgramClass会回调ClassVisitor的visitProgramClass方法,而LibraryClass就会回调它的visitLibraryClass方法,这里我们只分析ProgramClass。ClassSuperHierarchyInitializer实现了ClassVisitor接口,它的visitProgramClass方法内部如下

public void visitProgramClass(ProgramClass programClass)
{
    // Link to the super class.
    programClass.superClassConstantAccept(this);

    // Link to the interfaces.
    programClass.interfaceConstantsAccept(this);
}

跟在常量池后面的就是访问标记,访问标记后面的就是类索引、父类索引、接口索引,这些索引会指向常量池里面对应的常量对象,这里的常量池对象是ClassConstant对象。

public void superClassConstantAccept(ConstantVisitor constantVisitor)
{
    if (u2superClass != 0)
    {
        constantPool[u2superClass].accept(this, constantVisitor);
    }
}

public void interfaceConstantsAccept(ConstantVisitor constantVisitor)
{
    for (int index = 0; index < u2interfacesCount; index++)
    {
        constantPool[u2interfaces[index]].accept(this, constantVisitor);
    }
}
//ClassConstant
public void accept(Clazz clazz, ConstantVisitor constantVisitor)
{
    constantVisitor.visitClassConstant(clazz, this);
}

u2superClass指向的是常量池里面的ClassConstant对象,accept方法会回调ConstantVisitor接口的visitClassConstant方法,ClassSuperHierarchyInitializer实现了ConstantVisitor接口

// Implementations for ConstantVisitor.

public void visitClassConstant(Clazz clazz, ClassConstant classConstant)
{
    classConstant.referencedClass =
        findClass(clazz.getName(), classConstant.getName(clazz));
}

在class字节码里,常量池的类常量对象里面并没有referencedClass字段的,这个是proguard为了检索依赖链路而加上去的,findClass方法的实现如下

private Clazz findClass(String referencingClassName, String name)
{
    // First look for the class in the program class pool.
    Clazz clazz = programClassPool.getClass(name);
    // Otherwise look for the class in the library class pool.
    if (clazz == null)
    {
        clazz = libraryClassPool.getClass(name);

        //这里省去部分代码....
    }
    return clazz;
}

先通过ClassConstant的getName方法获取ClassConstant对象指向的类名称,当然getName方法本质上也是在常量池里取字符常量得到的,findClass方法会根据类名称从programClassPool或libraryClassPool里找到对应的类对象,最后把获取到的类对象赋值给ClassConstant的referencedClass字段,这样就把两个类的依赖关系给建立起来了,superClassConstantAccept(this) 调用完毕后接着会调用interfaceConstantsAccept(this) 负责来检索interface接口的依赖关系,由于过程是类似的这里不再分析,这样父类跟接口类的依赖就会被检索出来了。

引用依赖类检索

这里有个点需要注意的,并不是在头文件里import了一个类就代表着把类的依赖关系带了进来,依赖类必现是在一个类里面调用了另外一个类,有没有import并不重要,譬如同包名下的就并不需要import了。

在完成了父类与接口类的依赖检索后,接着就是代码中所依赖到的类的检索了,这个事情由ClassReferenceInitializer来完成,代码如下

programClassPool.classesAccept(
    new ClassReferenceInitializer(programClassPool,
                                    libraryClassPool,
                                    classReferenceWarningPrinter,
                                    programMemberReferenceWarningPrinter,
                                    libraryMemberReferenceWarningPrinter,
                                    null));

ClassReferenceInitializer实现了ClassVisitor接口,可以访问ClassPool里面的所有类,它的visitProgramClass (这里我们只分析程序类,不分析library的类依赖)实现如下

public void visitProgramClass(ProgramClass programClass)
{
    // Initialize the constant pool entries.
    programClass.constantPoolEntriesAccept(this);
    // Initialize all fields and methods.
    programClass.fieldsAccept(this);
    programClass.methodsAccept(this);
    // Initialize the attributes.
    programClass.attributesAccept(this);
}
  • 常量池分析
    首先是遍历常量池,对常量池里每一项进行依赖分析,constantPoolEntriesAccept的实现如下:
public void constantPoolEntriesAccept(ConstantVisitor constantVisitor)
{
    for (int index = 1; index < u2constantPoolCount; index++)
    {
        if (constantPool[index] != null)
        {
            constantPool[index].accept(this, constantVisitor);
        }
    }
}

ClassReferenceInitializer同时也实现了ConstantVisitor接口,它的定义如下

public interface ConstantVisitor
{
    public void visitIntegerConstant(           Clazz clazz, IntegerConstant            integerConstant);
    public void visitLongConstant(              Clazz clazz, LongConstant               longConstant);
    public void visitFloatConstant(             Clazz clazz, FloatConstant              floatConstant);
    public void visitDoubleConstant(            Clazz clazz, DoubleConstant             doubleConstant);
    public void visitStringConstant(            Clazz clazz, StringConstant             stringConstant);
    public void visitUtf8Constant(              Clazz clazz, Utf8Constant               utf8Constant);
    public void visitInvokeDynamicConstant(     Clazz clazz, InvokeDynamicConstant      invokeDynamicConstant);
    public void visitMethodHandleConstant(      Clazz clazz, MethodHandleConstant       methodHandleConstant);
    public void visitFieldrefConstant(          Clazz clazz, FieldrefConstant           fieldrefConstant);
    public void visitInterfaceMethodrefConstant(Clazz clazz, InterfaceMethodrefConstant interfaceMethodrefConstant);
    public void visitMethodrefConstant(         Clazz clazz, MethodrefConstant          methodrefConstant);
    public void visitClassConstant(             Clazz clazz, ClassConstant              classConstant);
    public void visitMethodTypeConstant(        Clazz clazz, MethodTypeConstant         methodTypeConstant);
    public void visitNameAndTypeConstant(       Clazz clazz, NameAndTypeConstant        nameAndTypeConstant);
}

本质上就是常量池里每一项都有对应的接口去解析,这里我们重点只看FieldrefConstant MethodrefConstant跟ClassConstant几种类型,在常量池里对应的是CONSTANT_Fieldref_info CONSTANT_Methodref_info以及CONSTANT_Class_info类型

CONSTANT_Class_info
在proguard里类引用常量对应的是ClassConstant结构,这个结构比较简单就只有一个index索引指向了所对应的类的名称


结构比较简单,所以解析也是比较简单,代码如下:

public void visitClassConstant(Clazz clazz, ClassConstant classConstant)
{
    // Fill out the referenced class.
    classConstant.referencedClass =
        findClass(clazz, ClassUtil.internalClassNameFromClassType(classConstant.getName(clazz)));

    // Fill out the Class class.
    classConstant.javaLangClassClass =
        findClass(clazz, ClassConstants.NAME_JAVA_LANG_CLASS);
}

大概就是先拿到这个类常量所对应的类名称,然后在ClassPool里面找到这个类,最后赋值给referencedClass字段,这样就能把两个类的依赖关系给建立起来了,如果想要获取当前类依赖了哪些类对象,只要遍历常量池里面的所有类常量对象拿到它的referencedClass字段便可知道了(AGP 3.x版本 源码里就是这样做的)。

CONSTANT_Methodref_info
在proguard里字段或方法的引用常量都是RefConstant的子类,跟类常量一样他们都会有一个index索引字段,指向了引入此方法或字段的类名称,此外还有另外一个index索引字段,指向了此方法或者字段的名称,如下

ClassReferenceInitializer的visitAnyRefConstant方法负责解析这些引用对象,实现如下

public void visitAnyRefConstant(Clazz clazz, RefConstant refConstant)
{
    //部分代码已被注释只留关键代码....
    String className = refConstant.getClassName(clazz);

    Clazz referencedClass = findClass(clazz, className);

    if (referencedClass != null)
    {
        String name = refConstant.getName(clazz);
        String type = refConstant.getType(clazz);

        boolean isFieldRef = refConstant.getTag() == ClassConstants.CONSTANT_Fieldref;

        // See if we can find the referenced class member somewhere in the
        // hierarchy.
        refConstant.referencedMember = memberFinder.findMember(clazz,
                                                                referencedClass,
                                                                name,
                                                                type,
                                                                isFieldRef);
        refConstant.referencedClass  = memberFinder.correspondingClass();
        
    }
}

首先是通过getClassName获取到了提供此方法或字段的类名称,接着通过findClass方法找到了所对应的类对象结构,最后memberFinder会遍历这个类的所有方法或字段,找出对应的方法或者字段,实现如下:

public Member findMember(Clazz   referencingClass,
                            Clazz   clazz,
                            String  name,
                            String  descriptor,
                            boolean isField)
{
    //这个地方比较有意思,在遍历clazz的所有方法跟字段时,如找到了会回调visitAnyMember.
    //进行保存,同时会抛一个异常出来告诉主调用方查找结束
    try
    {
        this.clazz  = null;
        this.member = null;
        clazz.hierarchyAccept(true, true, true, false, isField ?
            (ClassVisitor)new NamedFieldVisitor(name, descriptor,
                            new MemberClassAccessFilter(referencingClass, this)) :
            (ClassVisitor)new NamedMethodVisitor(name, descriptor,
                            new MemberClassAccessFilter(referencingClass, this)));
    }
    catch (MemberFoundException ex)
    {
        // We've found the member we were looking for.
    }

    return member;
}

public void visitAnyMember(Clazz clazz, Member member)
{
    this.clazz  = clazz;
    this.member = member;

    throw MEMBER_FOUND;
}

这里有一个比较有意思的逻辑就是,在遍历clazz的所有方法或字段时,若找到了需要通过抛异常的方式来通知结束查找。由于字段的查找跟方法的查找雷同,这里我们只分析方法的查找。

hierarchyAccept方法能根据传参去访问本类、父类、接口、子类等等能力,NamedMethodVisitor实现了ClassVisitor接口,用做clazz类访问,代码如下:

public NamedMethodVisitor(String        name,
                            String        descriptor,
                            MemberVisitor memberVisitor)
{
    this.name          = name;
    this.descriptor    = descriptor;
    this.memberVisitor = memberVisitor;
}
// Implementations for ClassVisitor.
public void visitProgramClass(ProgramClass programClass)
{
    programClass.methodAccept(name, descriptor, memberVisitor);
}

NamedMethodVisitor类比较简单,构造的时候会传入方法名,描述符等参数,在visitProgramClass里会调用ProgramClass提供的methodAccept方法把对应的方法找出来,这里的memberVisitor对应的就是外面构造的MemberClassAccessFilter对象,负责做一些简单的访问权限校验操作,代码如下:

public void visitProgramMethod(ProgramClass programClass, ProgramMethod programMethod)
{
    if (accepted(programClass, programMethod.getAccessFlags()))
    {
        memberVisitor.visitProgramMethod(programClass, programMethod);
    }
}

private boolean accepted(Clazz clazz, int memberAccessFlags)
{
    int accessLevel = AccessUtil.accessLevel(memberAccessFlags);

    return
        (accessLevel >= AccessUtil.PUBLIC                                                              ) ||
        (accessLevel >= AccessUtil.PRIVATE         && referencingClass.equals(clazz)                   ) ||
        (accessLevel >= AccessUtil.PACKAGE_VISIBLE && (ClassUtil.internalPackageName(referencingClass.getName()).equals(
                                                        ClassUtil.internalPackageName(clazz.getName())))) ||
        (accessLevel >= AccessUtil.PROTECTED       && (referencingClass.extends_(clazz)                  ||
                                                        referencingClass.extendsOrImplements(clazz))            );
}

最后把查找到的方法回调出去给MemberFinder的visitProgramMethod方法,MemberFinder做一些简单的保存工作接着会抛出一个异常通知查找结束,代码如下:

public void visitAnyMember(Clazz clazz, Member member)
{
    this.clazz  = clazz;
    this.member = member;
    throw MEMBER_FOUND;
}

最后把查找出来的方法或者字段赋值给referencedMember,而所对应的类对象就赋值给referencedClass,这样就能把本类依赖的哪些方法字段以及这些方法或字段的提供者类对象都给检索出来了。

  • 类方法字段分析
    回到ClassReferenceInitializer类的visitProgramClass里,在分析完常量池后,接着就是分析本类的字段跟方法
public void visitProgramClass(ProgramClass programClass)
{
    // Initialize the constant pool entries.
    programClass.constantPoolEntriesAccept(this);

    // Initialize all fields and methods.
    programClass.fieldsAccept(this);
    programClass.methodsAccept(this);
}

由于字段跟方法的解析类似,这里只分析方法的解析过程。这里解析本类方法的主要原因是,方法参数里可能会引入一些类对象,如有一下测试代码

class MyClass {

    fun test(test: TestClass?) {
        //test?.test1()
    }
}

test方法会把TestClass类依赖引入进来。
ClassReferenceInitializer类实现了MemberVisitor接口,方法的解析在visitProgramMethod接口里,实现如下:

public void visitProgramMethod(ProgramClass programClass, ProgramMethod programMethod)
{
    programMethod.referencedClasses =
        findReferencedClasses(programClass,
                                programMethod.getDescriptor(programClass));
}

首先是通过findReferencedClasses把引入的方法类对象解析出来,最后赋值给referencedClasses,这样就建立了方法对一组类的依赖关系了,findReferencedClasses的具体实现如下:

private Clazz[] findReferencedClasses(Clazz  referencingClass,
                                        String descriptor)
{
    DescriptorClassEnumeration enumeration =
        new DescriptorClassEnumeration(descriptor);

    int classCount = enumeration.classCount();
    if (classCount > 0)
    {
        Clazz[] referencedClasses = new Clazz[classCount];

        boolean foundReferencedClasses = false;

        for (int index = 0; index < classCount; index++)
        {
            String fluff = enumeration.nextFluff();
            String name  = enumeration.nextClassName();

            Clazz referencedClass = findClass(referencingClass, name);

            if (referencedClass != null)
            {
                referencedClasses[index] = referencedClass;
                foundReferencedClasses = true;
            }
        }

        if (foundReferencedClasses)
        {
            return referencedClasses;
        }
    }
    return null;
}

核心解析是在DescriptorClassEnumeration类里,本质上就是对string的一个解析操作,譬如上面的测试代码里的test方法,它的描述符是(Lcom/example/lib/TestClass;)V,DescriptorClassEnumeration类通过解析字符串最终会把com/example/lib/TestClass类给解析出来,最后findClass已经是很熟悉的方法了,它根据传入的类名称,从ClassPool里面把类对象给找出来,效果如下:


  • 属性分析
    解析完了类方法跟字段后跟着的就是属性集合的分析了,属性表有很多类,这里就不再进行一一分析了。

总结

本节主要是从源码的角度出发,分析了下proguard是怎么把代码里面的class类依赖关系给检索出来的,有了依赖关系才知道哪些类或者代码是有用的,哪些是没有被任何类使用到的可以直接删除的,有了这些关键的信息后便可以进行下一步的压缩优化。

你可能感兴趣的:(proguard源码分析三 依赖关系检索)