• Proguard源码分析(一) 读取


    Proguard是Android中经常用的混淆工具,当然你也可以采用其他的混淆工具。但我这边谈到的只是Proguard。

    大多数人了解Proguard大都通过文档,但是我这次决定从源码入手,分析Proguard。我个人觉得Proguard的源码写的还是非常的出 彩的,当然你可能跟我有不一样的品味,我也不做深究。我这边只想说明一点,那就是,如果你想从这几篇文章里面试图不通过源码就弄懂文章的主体意思,我觉得 你还是绕路吧。下载的网址我就不找了,相信跟我有相同爱好的开源爱好者都不会因为这个而放弃。文章中可能有些地方不当或者语句不通顺的地方敬请见谅。有错 误直接指出,当然如果你要从其他点来分析,补充说明的话我也非常支持,可以在留言板中标注,我们讨论后我会将它补充到内容中去。顺便提一下,如果你有意向 转到其他的博客的话,请标明出处。本人的QQ号码是:

    1025250620 目前做的是Android方面的开发,如果你觉得你自己也是一个源码的爱好者,并且喜欢阅读Android相关的系统代码,可以加我的q与在下交流。

    直接切入主题,第一部分先要提到的是Proguard的入口和读取,

    Proguard的入口在Proguard.main 中或者更正确的应该在Proguard.execute()中,所有代码的执行都在这里面.这个函数的代码很清晰,分成几个主要步骤也就是接下来文章的主题。

    本章先说下配置和读取。

    Proguard中通过ConfigurationParser 将配置文件转成Configuration 类的参数值,对应的参数表在

    ConfigurationConstants记录

    比如说我们一进入就看到

    configuration.printConfiguration参数,这个参数对应的是printconfiguration

    这个参数的目的是打印Configuration的参数值

     if (configuration.printConfiguration != null)
    {
                printConfiguration();
    }

    Proguard的配置更像一个开关,也就是直接通过有参数无参数来控制混淆结果

    解析配置文件在ConfigurationParser.parse(Configuration) 中,

    Configuration将是我们以后经常到打交道的类。这块我们会不断的回放,

    我们继续往下走,这就到了

    readInput();

    读取工作通过InputReader 类来完成

    这里出现了configuration.programJars参数

    这个参数通过指定非常重要的参数-injars,-outjars来指定,这个参数的数据结构是采用classpath的结构,可以是一个jar,也可以是一个目录。通过断点,可以知道程序真正意义上读取jar文件是在这个方法中.

     readInput("Reading program ",
                      configuration.programJars,
                      filter);

    configuration.programJars 是一个ClassPath,本质上是一个迭代器(也可以看作List) 将每一个输入源,记录为

    ClassPathEntry 数据结构,比如你的配置文件为:

    -injars test.jar
    -outjars out.jar

    那么你的configuration.programJars就是一个长度为2的ClassPath,里面有个叫做test.jar ,out.jar的ClassPathEntry.ClassPathEntry通过借口isOutput来区分两种ClassPath。在读入 Class文件的时候,Proguard会为每一个ClassPathEntry生成一个DataEntryReader 的数据读取器,

    通过工厂:

    DataEntryReaderFactory.createDataEntryReader(messagePrefix,
                                                                 classPathEntry,
                                                                 dataEntryReader);

    来实例化。

    然后通过DirectoryPump 的pumpDataEntries来读取Class对象。

    DirectoryPump的核心方法是:

    private void readFiles(File file, DataEntryReader dataEntryReader)
        throws IOException
        {
            // Pass the file data entry to the reader.
            dataEntryReader.read(new FileDataEntry(directory, file));

            if (file.isDirectory())
            {
                // Recurse into the subdirectory.
                File[] files = file.listFiles();

                for (int index = 0; index < files.length; index++)
                {
                    readFiles(files[index], dataEntryReader);
                }
            }
        }

    可以看出,当你采用classPath如果是Directory的时候,将采用递归的方式来读取文件,这会儿我们输入的是jar文件所以我们直接跟入 dataEntryReader.read(new FileDataEntry(directory, file));

    现在的主要核心是dataEntryReader

    dataEntryReader初始的时候给的类是ClassReader ,Proguard里面采用的装饰器模式,用来包装读入数据。

    ClassReader通过isLibrary变量来区分不同的读入数据类型

    值得一提的是,这里面除了使用装饰器模式意外还是用了访问者模式。这里的被访问者是Clazz,也就是在Proguard里面的字节码结构。

    访问者是不同的Reader,Proguard里大量采用了这种模式,访问者加装饰器,所以代码读起来颇有难度,但是代码结构非常的好。这里举个例子看一眼吧:

    ClassReader 在read 一个数据DataEntry的时候将要给一个Clazz下定义

    if (isLibrary)
    {
                    clazz = new LibraryClass();
                    clazz.accept(new LibraryClassReader(dataInputStream, skipNonPublicLibraryClasses, skipNonPublicLibraryClassMembers));
    } else {
                    clazz = new ProgramClass();
                    clazz.accept(new ProgramClassReader(dataInputStream));
     }

    可以看到如果是库文件Clazz定义为LibraryClass,如果是程序文件Clazz定义为ProgramClass。

    对于LibraryClass设置LibraryClassReader为它的访问者,当LibraryClassReader访问它的时候,将按库文件的方式来读取,记录在被访问者中。我们来捋一下这个过程:

    输入参数injars 以后被转成Configration的参数,在读入参数对应的文件的时候将生成不同的Reader,这些Reader将以访问者的方式来给被访问者的 Clazz文件填充数据。ClassReader的代码主要涉及Clazz的文件结构以后有机会我们可以专门分析~

    那么Proguard又在什么地方来选定适合的Reader装饰器呢?

    答案就是在Factory里面,从代码结构来看的话采用,Proguard采用的是静态工厂的方式来实现工厂功能。这样做的好处是简单快捷。

    我们跟进去看看在DataEntryReaderFactory.createDataEntryReader

    boolean isJar = classPathEntry.isJar();
    boolean isWar = classPathEntry.isWar();
    boolean isEar = classPathEntry.isEar();
    boolean isZip = classPathEntry.isZip();

    public boolean classPathEntry.isJar()
     {
            return hasExtension(".jar");
    }

    好吧,我觉得接下去的代码大家猜都能猜到怎么写了。

    我们深入一点看一下JarReader怎样的一个数据读取包装

    ZipInputStream zipInputStream = new ZipInputStream(dataEntry.getInputStream());

            try
            {
                // Get all entries from the input jar.
                while (true)
                {
                    // Can we get another entry?
                    ZipEntry zipEntry = zipInputStream.getNextEntry();
                    if (zipEntry == null)
                    {
                        break;
                    }

                    // Delegate the actual reading to the data entry reader.
                    dataEntryReader.read(new ZipDataEntry(dataEntry,
                                                          zipEntry,
                                                          zipInputStream));
                }
            }

    从这段代码我们可以看到实际上对于Jar文件的话是通过ZipInputStream来解压的,也可以这么理解:jar实际上就是zip.ZipDataEntry可以理解为就是一个.class被被包装reader读取。

    那么好,我现在已经给你返回了具体的class类,那有怎么办呢~?我又怎么往对应的池子里面放呢?

    还记得前面的访问者么?我们回溯到之前看一下dataEntryReader最底层的被包装对象的构造器:

    ClassFilter filter =  new ClassFilter(
                                    new ClassReader(
                                        false,
                                        configuration.skipNonPublicLibraryClasses, //false
                                        configuration.skipNonPublicLibraryClassMembers, //true
                                        warningPrinter,
                                        new ClassPresenceFilter(
                                                programClassPool,
                                                duplicateClassPrinter,
                                                new ClassPoolFiller(programClassPool)))
                                    );

    也就是在InputReader的execute方法中。

    我们可以看到实际上是ClassFilter 包装了ClassReader并且引入了ClassPresenceFilter访问者。ClassPresenceFilter 又包装了ClassPoolFiller

    ClassPoolFiller的访问操作很简单,只需要往池子里面加入参数就行这里的池子就是programClassPool

    public void visitAnyClass(Clazz clazz)
        {
            classPool.addClass(clazz);
        }

    ClassPresenceFilter的包装目的我估计是为了过滤重复类或者是对重复类进行打印提示,我们来看下它的主要实现:

    private ClassVisitor classFileVisitor(Clazz clazz)
        {
            return classPool.getClass(clazz.getName()) != null ?
                presentClassVisitor :
                missingClassVisitor;
        }

    不论是访问那种类型的class集合都会调用这个方法:我们可以很容易的看出,它的目的很简单,如果池子中不存在,则会调用missingClassVisitor,这个访问者就是ClassPoolFiller它的作用就是往池子里面加,而如果存在的话,

    返回presentClassVisitor代表已经注入的访问者。这里的实现类是DuplicateClassPrinter,Duplicate的意思是

    重复,也就是通过字面意思可以很容易的看出这个是用来打印重复类的访问者。

    我们看到代码的实际结果也如我们所期待的那样

    public void visitProgramClass(ProgramClass programClass)
        {
            notePrinter.print(programClass.getName(),
                              "Note: duplicate definition of program class [" +
                              ClassUtil.externalClassName(programClass.getName()) + "]");
        }


        public void visitLibraryClass(LibraryClass libraryClass)
        {
            notePrinter.print(libraryClass.getName(),
                              "Note: duplicate definition of library class [" +
                              ClassUtil.externalClassName(libraryClass.getName()) + "]");
        }

    回到最初的位置,JarReader读入文件返回给ClassReader class数据,class数据被ClassReader接受到以后并不直接被解析,而是通过访问者访问,这个访问者可以是 ProgramClassReader和LibClassReader也可以是他们的包装类。ClassReader在这里的角色更像是个代理,在 ClassReader.read()方法里面区分不同的class类型

    用不同的Reader来访问它

     if (isLibrary)
                {
                    clazz = new LibraryClass();
                    clazz.accept(new LibraryClassReader(dataInputStream, skipNonPublicLibraryClasses, skipNonPublicLibraryClassMembers));
                } else {
                    clazz = new ProgramClass();
                    clazz.accept(new ProgramClassReader(dataInputStream));
                }

    更像一个控制器。

    好的读入程序class文件的代码就暂时结束,接下来自然是读取lib的字节码

     readInput("Reading library ",
                          configuration.libraryJars,
                          new ClassFilter(
                          new ClassReader(true,
                                          configuration.skipNonPublicLibraryClasses,
                                          configuration.skipNonPublicLibraryClassMembers,
                                          warningPrinter,
                          new ClassPresenceFilter(programClassPool, duplicateClassPrinter,
                          new ClassPresenceFilter(libraryClassPool, duplicateClassPrinter,
                          new ClassPoolFiller(libraryClassPool))))));

    libraryJars由libraryjars 参数来指定,在Proguard里面常常要用到rt.jar但是rt.jar里面有很多多余的class。前面我们提到过Classpath可以指定文件 目录期间用递归的方式来解析。如果你是优化高手,可以解压以后删除多余的class以增加lib的载入速度。

    readInput的参数可能不那么好解,没关系,我们一步步的来拆解它。就像罗升阳说的那样,read the fuck source
    !

    依旧到最底层,是一个new ClassPoolFiller(libraryClassPool) 这个类很明显是为了加入池子而设计的,然后在外面包装了个去重的操作类ClassPresenceFilter,但是我们惊讶的发现又在外面包装了 ClassPresenceFilter这个类,其实目的很明显是为了去掉programClassPool和libraryClassPool中的可能 重复类避免最终生成两个字节码。这里要强调一点,不论是那种类,一般情况下是被Lib或者Program的解析类处理完成以后才被其他的访问者访问,代码 在ClassReader中,其他的访问这被作为classVisitor的参数来访问。

    好了,到这里差不多读入类操作完成了,我们来看下我们的结果,结果就是读入了class文件放在了

    programClassPool, libraryClassPool这两个池子中.

  • 相关阅读:
    最长公共上升子序列
    最长公共子序列
    3847: Mowing the Lawn (单调队列)
    A/B(扩展欧几里得)
    One Person Game(扩展欧几里得)
    Substring with Concatenation of All Words, 返回字符串中包含字符串数组所有字符串元素连接而成的字串的位置
    Divide two numbers,两数相除求商,不能用乘法,除法,取模运算
    Merge k Sorted Lists, k路归并
    二路归并排序,利用递归,时间复杂度o(nlgn)
    StrStr,判断一个字符串是不是另一个字符串的字串,并返回子串的位置
  • 原文地址:https://www.cnblogs.com/feizimo/p/3523822.html
Copyright © 2020-2023  润新知