前段時間由于項目原因般卑,需要對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)存里面并且是如何管理起來的蓖墅。