proguard源碼分析一 參數(shù)解析

前段時間由于項目原因般卑,需要對proguard做一些定制化工作酿秸,因此克隆了一份proguard源碼下來對它進行了些研究跟改造。從本篇開始嵌言,我將會通過一個系列的文章嗅回,從源碼出發(fā),跟大家一起分析一下proguard的原理摧茴,本篇中研究的proguard源碼版本是5.3.4

proguard的整個執(zhí)行流程可以大致的分為以下幾個階段


  • 解析參數(shù)
    proguard的入口函數(shù)在ProGuard.java文件里绵载,在入口函數(shù)main函數(shù)里面,首先是new了一個ConfigurationParser對象負責解析input args苛白,解析出來的內(nèi)容會通過一個類型為Configuration的對象來保存娃豹,代碼如下:
/**
 * The main method for ProGuard.
 */
public static void main(String[] args)
{
    //此處省略部分代碼...
    // Create the default options.
    Configuration configuration = new Configuration();
    try
    {
        // Parse the options specified in the command line arguments.
        ConfigurationParser parser = new ConfigurationParser(args,
                                                                System.getProperties());
        try
        {
            parser.parse(configuration);
        }
        finally
        {
            parser.close();
        }
        // Execute ProGuard with these options.
        new ProGuard(configuration).execute();
    }
    //此處省略部分代碼...
    System.exit(0);
}

ConfigurationParser會在內(nèi)部又new了一個ArgumentWordReader對象來負責解析輸入進來的參數(shù)

/**
 * Creates a new ConfigurationParser for the given String arguments,
 * with the given base directory and the given Properties.
 */
public ConfigurationParser(String[]   args,
                            File       baseDir,
                            Properties properties) throws IOException
{
    this(new ArgumentWordReader(args, baseDir), properties);
}

/**
 * Creates a new ConfigurationParser for the given word reader and the
 * given Properties.
 */
public ConfigurationParser(WordReader reader,
                            Properties properties) throws IOException
{
    this.reader     = reader;
    this.properties = properties;
    readNextWord();
}

readNextWord的時候本質(zhì)上是會調(diào)用ArgumentWordReader的nextWord接口來開始解析參數(shù)名來,nextWord的實現(xiàn)也比較簡單购裙,就是一些字符串的判斷與裁剪懂版,下面貼出一段邏輯出來分析

/**
 * Reads a word from this WordReader, or from one of its active included
 * WordReader objects.
 *
 * @param isFileName         return a complete line (or argument), if the word
 *                           isn't an option (it doesn't start with '-').
 * @param expectSingleFile   if true, the remaining line is expected to be a
 *                           single file name (excluding path separator),
 *                           otherwise multiple files might be specified
 *                           using the path separator.
 * @return the read word.
 */
public String nextWord(boolean isFileName,
                        boolean expectSingleFile) throws IOException
{
    //此處省略部分代碼...
    currentWord = null;
    // Make sure we have a non-blank line.
    while (currentLine == null || currentIndex == currentLineLength)
    {
        //讀取下一行輸入?yún)?shù)...
        currentLine = nextLine();
        if (currentLine == null)
        {
            return null;
        }

        currentLineLength = currentLine.length();

        //跳過空格符...
        // Skip any leading whitespace.
        currentIndex = 0;
        while (currentIndex < currentLineLength &&
                Character.isWhitespace(currentLine.charAt(currentIndex)))
        {
            currentIndex++;
        }

        // Remember any leading comments.
        if (currentIndex < currentLineLength &&
            isComment(currentLine.charAt(currentIndex)))
        {
            // Remember the comments.
            String comment = currentLine.substring(currentIndex + 1);
            currentComments = currentComments == null ?
                comment :
                currentComments + '\n' + comment;

            // Skip the comments.
            currentIndex = currentLineLength;
        }
    }

    //找到了輸入?yún)?shù)的startIndex
    // Find the word starting at the current index.
    int startIndex = currentIndex;
    int endIndex;

    char startChar = currentLine.charAt(startIndex);
    //此處省略部分代碼...
    else
    {
        // The next word is a simple character string.
        // Find the end of the line, the first delimiter, or the first
        // white space.
        while (currentIndex < currentLineLength)
        {
            char currentCharacter = currentLine.charAt(currentIndex);
            if (isNonStartDelimiter(currentCharacter)    ||
                Character.isWhitespace(currentCharacter) ||
                isComment(currentCharacter)) {
                break;
            }

            currentIndex++;
        }

        endIndex = currentIndex;
    }

    // Remember and return the parsed word.
    currentWord = currentLine.substring(startIndex, endIndex);
    return currentWord;
}

這里舉個簡單的例子,譬如執(zhí)行java –jar proguard.jar -injars test.jar躏率,nextWord這里就能把-injars這個參數(shù)keyword給解析出來了躯畴,名字解析出來了,接著就需要解析它的參數(shù)薇芝,回到ConfigurationParser的parse方法里蓬抄,我們能看到,keyword給解析出來了夯到,接著會根據(jù)不用的keyword會有一套不同的parse代碼嚷缭,最后會通過一個while循環(huán),把所有input的參數(shù)都給解析出來耍贾,代碼如下:

/**
 * Parses and returns the configuration.
 * @param configuration the configuration that is updated as a side-effect.
 * @throws ParseException if the any of the configuration settings contains
 *                        a syntax error.
 * @throws IOException if an IO error occurs while reading a configuration.
 */
public void parse(Configuration configuration)
throws ParseException, IOException
{
    while (nextWord != null)
    {
        lastComments = reader.lastComments();

        // First include directives.
        if      (ConfigurationConstants.AT_DIRECTIVE                                     .startsWith(nextWord) ||
                    ConfigurationConstants.INCLUDE_DIRECTIVE                                .startsWith(nextWord)) configuration.lastModified                          = parseIncludeArgument(configuration.lastModified);
        else if (ConfigurationConstants.BASE_DIRECTORY_DIRECTIVE                         .startsWith(nextWord)) parseBaseDirectoryArgument();

        // Then configuration options with or without arguments.
        else if (ConfigurationConstants.INJARS_OPTION                                    .startsWith(nextWord)) configuration.programJars                           = parseClassPathArgument(configuration.programJars, false);
        else if (ConfigurationConstants.OUTJARS_OPTION                                   .startsWith(nextWord)) configuration.programJars                           = parseClassPathArgument(configuration.programJars, true);
        //篇幅原因 下面省略掉一波類似代碼....
        else
        {
            throw new ParseException("Unknown option " + reader.locationDescription());
        }
    }
}
  • 保存解析參數(shù)
    前面我們提到了proguard解析出來的所有input參數(shù)會被保存到類型為Configuration的對象里面阅爽,這個對象會貫穿整個proguard過程,包括了proguard實例化ClassPool 讀取ProgramClass LibraryClass shrink的時候需要保留哪些類方法逼争,obfuscate的時候取mapping file來做混淆等等优床,都需要先從Configuration對象里獲得參數(shù)。
/*
 * ProGuard -- shrinking, optimization, obfuscation, and preverification
 *             of Java bytecode.
 *
 * Copyright (c) 2002-2016 Eric Lafortune @ GuardSquare
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */
package proguard;

import java.io.File;
import java.util.List;

/**
 * The ProGuard configuration.
 *
 * @see ProGuard
 *
 * @author Eric Lafortune
 */
public class Configuration
{
    public static final File STD_OUT = new File("");

    ///////////////////////////////////////////////////////////////////////////
    // Keep options.
    ///////////////////////////////////////////////////////////////////////////

    /**
     * A list of {@link KeepClassSpecification} instances, whose class names and
     * class member names are to be kept from shrinking, optimization, and/or
     * obfuscation.
     */
    public List      keep;


    ///////////////////////////////////////////////////////////////////////////
    // Shrinking options.
    ///////////////////////////////////////////////////////////////////////////

    /**
     * Specifies whether the code should be shrunk.
     */
    public boolean   shrink                           = true;

    /**
     * Specifies whether the code should be optimized.
     */
    public boolean   optimize                         = true;

    public boolean   optimizeNoSideEffects           = false;

    /**
     * A list of <code>String</code>s specifying the optimizations to be
     * performed. A <code>null</code> list means all optimizations. The
     * optimization names may contain "*" or "?" wildcards, and they may
     * be preceded by the "!" negator.
     */
    public List      optimizations;

    /**
     * A list of {@link ClassSpecification} instances, whose methods are
     * assumed to have no side effects.
     */
    public List      assumeNoSideEffects;

    /**
     * Specifies whether the access of class members can be modified.
     */
    public boolean   allowAccessModification          = false;

    ///////////////////////////////////////////////////////////////////////////
    // Obfuscation options.
    ///////////////////////////////////////////////////////////////////////////

    /**
     * Specifies whether the code should be obfuscated.
     */
    public boolean   obfuscate                        = true;

    /**
     * An optional output file for listing the obfuscation mapping.
     * An empty file name means the standard output.
     */
    public File      printMapping;

    /**
     * An optional input file for reading an obfuscation mapping.
     */
    public File      applyMapping;

    /**
     * An optional name of a file containing obfuscated class member names.
     */
    public File      obfuscationDictionary;

    /**
     * A list of <code>String</code>s specifying package names to be kept.
     * A <code>null</code> list means no names. An empty list means all
     * names. The package names may contain "**", "*", or "?" wildcards, and
     * they may be preceded by the "!" negator.
     */
    public List      keepPackageNames;


    /**
     * Specifies whether to print verbose messages.
     */
    public boolean   verbose                          = false;

    /**
     * A list of <code>String</code>s specifying a filter for the classes for
     * which not to print notes, if there are noteworthy potential problems.
     * A <code>null</code> list means all classes. The class names may contain
     * "**", "*", or "?" wildcards, and they may be preceded by the "!" negator.
     */
    public List      note                             = null;

    /**
     * A list of <code>String</code>s specifying a filter for the classes for
     * which not to print warnings, if there are any problems.
     * A <code>null</code> list means all classes. The class names may contain
     * "**", "*", or "?" wildcards, and they may be preceded by the "!" negator.
     */
    public List      warn                             = null;

    /**
     * Specifies whether to ignore any warnings.
     */
    public boolean   ignoreWarnings                   = false;
}

Configuration里面的字段比較多誓焦,這里我只保留了部分比較常見的參數(shù)胆敞,這些參數(shù)基本就是我們平時會在配置文件里面會配置到的。這里我們只分析一下比較重要的keep字段杂伟,我們在配置文件里面寫的keep規(guī)則最終就是會被保存到這個字段里頭去的移层。

回到ConfigurationParser對象的parse方法里,當ArgumentWordReader解析出來的keyword是 -keep -keepclassmembers -keepclasseswithmembers -keepnames -keepclassmembernames -keepclasseswithmembernames等等這些時赫粥,proguard便會解析后面的keep參數(shù)观话,把我們想要保留的類規(guī)則給讀取出來(溫馨提示,如果想知道proguard到底還支持哪些功能越平,直接來parse方法里找keyword就知道了)

public void parse(Configuration configuration)
throws ParseException, IOException
{
    while (nextWord != null)
    {
        lastComments = reader.lastComments();
        else if (ConfigurationConstants.IF_OPTION                                        .startsWith(nextWord)) configuration.keep                                  = parseIfCondition(configuration.keep);
        else if (ConfigurationConstants.KEEP_OPTION                                      .startsWith(nextWord)) configuration.keep                                  = parseKeepClassSpecificationArguments(configuration.keep, true,  false, false, null);
        else if (ConfigurationConstants.KEEP_CLASS_MEMBERS_OPTION                        .startsWith(nextWord)) configuration.keep                                  = parseKeepClassSpecificationArguments(configuration.keep, false, false, false, null);
        else if (ConfigurationConstants.KEEP_CLASSES_WITH_MEMBERS_OPTION                 .startsWith(nextWord)) configuration.keep                                  = parseKeepClassSpecificationArguments(configuration.keep, false, true,  false, null);
        else if (ConfigurationConstants.KEEP_NAMES_OPTION                                .startsWith(nextWord)) configuration.keep                                  = parseKeepClassSpecificationArguments(configuration.keep, true,  false, true,  null);
        else if (ConfigurationConstants.KEEP_CLASS_MEMBER_NAMES_OPTION                   .startsWith(nextWord)) configuration.keep                                  = parseKeepClassSpecificationArguments(configuration.keep, false, false, true,  null);
        else if (ConfigurationConstants.KEEP_CLASSES_WITH_MEMBER_NAMES_OPTION            .startsWith(nextWord)) configuration.keep                                  = parseKeepClassSpecificationArguments(configuration.keep, false, true,  true,  null);
        else if (ConfigurationConstants.PRINT_SEEDS_OPTION                               .startsWith(nextWord)) configuration.printSeeds                            = parseOptionalFile();
    }
}

可以看到不管你怎么寫keep規(guī)則的频蛔,最終的讀取其實都是通過parseKeepClassSpecificationArguments方法來讀取的灵迫,parseKeepClassSpecificationArguments的功能比較簡單,內(nèi)部只是new了個ArrayList晦溪,至于真正的解析都交給了重載方法去實現(xiàn)了瀑粥,

/**
 * Parses and returns a class specification to keep classes and class
 * members.
 * @throws ParseException if the class specification contains a syntax error.
 * @throws IOException    if an IO error occurs while reading the class
 *                        specification.
 */
private KeepClassSpecification parseKeepClassSpecificationArguments(boolean            markClasses,
                                                                    boolean            markConditionally,
                                                                    boolean            allowShrinking,
                                                                    ClassSpecification condition)
throws ParseException, IOException
{
    boolean markDescriptorClasses = false;
    boolean markCodeAttributes    = false;
    //boolean allowShrinking        = false;
    boolean allowOptimization     = false;
    boolean allowObfuscation      = false;

    // Read the keep modifiers.
    while (true)
    {
        readNextWord("keyword '" + ConfigurationConstants.CLASS_KEYWORD +
                        "', '"      + JavaConstants.ACC_INTERFACE +
                        "', or '"   + JavaConstants.ACC_ENUM + "'",
                        false, false, true);

        if (!ConfigurationConstants.ARGUMENT_SEPARATOR_KEYWORD.equals(nextWord))
        {
            // Not a comma. Stop parsing the keep modifiers.
            break;
        }

        readNextWord("keyword '" + ConfigurationConstants.ALLOW_SHRINKING_SUBOPTION +
                        "', '"      + ConfigurationConstants.ALLOW_OPTIMIZATION_SUBOPTION +
                        "', or '"   + ConfigurationConstants.ALLOW_OBFUSCATION_SUBOPTION + "'");

        if      (ConfigurationConstants.INCLUDE_DESCRIPTOR_CLASSES_SUBOPTION.startsWith(nextWord))
        {
            markDescriptorClasses = true;
        }
        else if (ConfigurationConstants.INCLUDE_CODE_SUBOPTION              .startsWith(nextWord))
        {
            markCodeAttributes    = true;
        }
        else if (ConfigurationConstants.ALLOW_SHRINKING_SUBOPTION           .startsWith(nextWord))
        {
            allowShrinking        = true;
        }
        else if (ConfigurationConstants.ALLOW_OPTIMIZATION_SUBOPTION        .startsWith(nextWord))
        {
            allowOptimization     = true;
        }
        else if (ConfigurationConstants.ALLOW_OBFUSCATION_SUBOPTION         .startsWith(nextWord))
        {
            allowObfuscation      = true;
        }
        else
        {
            throw new ParseException("Expecting keyword '" + ConfigurationConstants.INCLUDE_DESCRIPTOR_CLASSES_SUBOPTION +
                                        "', '"                + ConfigurationConstants.INCLUDE_CODE_SUBOPTION +
                                        "', '"                + ConfigurationConstants.ALLOW_SHRINKING_SUBOPTION +
                                        "', '"                + ConfigurationConstants.ALLOW_OPTIMIZATION_SUBOPTION +
                                        "', or '"             + ConfigurationConstants.ALLOW_OBFUSCATION_SUBOPTION +
                                        "' before " + reader.locationDescription());
        }
    }

    // Read the class configuration.
    ClassSpecification classSpecification =
        parseClassSpecificationArguments(false);

    // Create and return the keep configuration.
    return new KeepClassSpecification(markClasses,
                                        markConditionally,
                                        markDescriptorClasses,
                                        markCodeAttributes,
                                        allowShrinking,
                                        allowOptimization,
                                        allowObfuscation,
                                        condition,
                                        classSpecification);
}

markClasses markConditionally參數(shù)會在shrink階段被使用到,用來標識類是否需要被保留三圆,這里我們能看到直接用-keep的時候 markClasses會傳true狞换,意味著類會被保留下來,而用-keepclassmembers的時候markClasses是傳了false舟肉,表示類還是有可能會shrink階段被剔除掉的修噪,通過閱讀proguard的源碼,我們能更加深入的了解到了-keep規(guī)則的一些用法了路媚。

parseKeepClassSpecificationArguments方法的前面一部分也非常的好理解黄琼,也是通過讀取keyword,通過字符的判斷的方式來獲得allowShrinking等一些傳參了磷籍,舉個例子适荣,譬如有以下keep規(guī)則
-keep, allowObfuscation class com.test.test
這里就能把allowObfuscation參數(shù)讀取出來了,test類雖然被keep住院领,但也能被混淆弛矛。

接著的parseClassSpecificationArguments會解析出類更加詳細的keep規(guī)則,譬如類名比然、父類丈氓、類的哪些字段需要被保留、類的哪些方法需要被保留等等强法,最后會創(chuàng)建出KeepClassSpecification對象并且保存所有解析出來的參數(shù)万俗,KeepClassSpecification最終會被保存到Configuration對象的keep成員里。

  • 總結(jié)
    本節(jié)主要介紹了proguard的幾個工作階段饮怯,以及分析了proguard的參數(shù)解析階段的整個過程闰歪,下一節(jié)我們將會繼續(xù)分析proguard里面的ClassPool ProgramClass等等的初始化,介紹下proguard是怎么把class文件解析到內(nèi)存里面并且是如何管理起來的蓖墅。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末库倘,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子论矾,更是在濱河造成了極大的恐慌教翩,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件贪壳,死亡現(xiàn)場離奇詭異饱亿,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進店門彪笼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來钻注,“玉大人,你說我怎么就攤上這事杰扫《涌埽” “怎么了?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵章姓,是天一觀的道長。 經(jīng)常有香客問我识埋,道長凡伊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任窒舟,我火速辦了婚禮系忙,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘惠豺。我一直安慰自己银还,他們只是感情好,可當我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布洁墙。 她就那樣靜靜地躺著蛹疯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪热监。 梳的紋絲不亂的頭發(fā)上捺弦,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天,我揣著相機與錄音孝扛,去河邊找鬼列吼。 笑死,一個胖子當著我的面吹牛苦始,可吹牛的內(nèi)容都是我干的寞钥。 我是一名探鬼主播,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼陌选,長吁一口氣:“原來是場噩夢啊……” “哼理郑!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起柠贤,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤香浩,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后臼勉,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體邻吭,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年宴霸,在試婚紗的時候發(fā)現(xiàn)自己被綠了囱晴。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片膏蚓。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖畸写,靈堂內(nèi)的尸體忽然破棺而出驮瞧,到底是詐尸還是另有隱情,我是刑警寧澤枯芬,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布论笔,位于F島的核電站,受9級特大地震影響千所,放射性物質(zhì)發(fā)生泄漏狂魔。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一淫痰、第九天 我趴在偏房一處隱蔽的房頂上張望最楷。 院中可真熱鬧,春花似錦待错、人聲如沸籽孙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽犯建。三九已至,卻和暖如春烛占,著一層夾襖步出監(jiān)牢的瞬間胎挎,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工忆家, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留犹菇,地道東北人。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓芽卿,卻偏偏與公主長得像揭芍,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子卸例,可洞房花燭夜當晚...
    茶點故事閱讀 45,691評論 2 361

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