代碼混淆器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這兩個池子中

 

 

 

 

 

 

 

發佈了106 篇原創文章 · 獲贊 2 · 訪問量 3666
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章