ProGuard是最受欢迎的Java字节码优化器。 它使您的Java和Android应用程序缩小了90%,速度提高了20%。 ProGuard还通过模糊类,字段和方法的名称来提供对逆向工程的最小保护。本篇将从Android apk混淆和jar混淆两个方面进行展开。
我们通常说的proguard包括四个功能,shrinker(压缩), optimizer(优化),obfuscator(混淆),preverifier(预校验)。这里附上官网的一张流程图:
无论是给apk添加混淆也好还是给jar文件或者aar文件添加混淆也好,底层其实都是依赖proguard.jar这个jar包来实现的,这个jar包安卓开发者可以在Android SDK根目录\tools\proguard\lib下找到,其他开发者可以在Proguard官网找到下载方式,以及官方说明文档。
如果有Android SDK的同学可以在{ANDROID_SDK_ROOT}/tools/proguard/lib/目录下找到proguard.jar这个jar包。或者,也可以在{ANDROID_SDK_ROOT}/tools/proguard/bin目录下直接使用脚本执行命令。
使用proguard.jar添加混淆大致分为三种方式,
java -jar proguard.jar options ...
配置文件格式:
在AndroidStudio中借助SDK中自带的Proguard工具,只需要修改build.gradle中的一行配置即可为apk添加混淆。
可以看到minifyEnabled默认为false,我们只需将该设置改为true即可在打release包时候自动加载"proguard-android.txt"和"proguard-rules.pro"文件中的混淆规则为apk添加混淆处理。“proguard-rules.pro”文件大家都很熟悉,就是创建项目时候as默认为我们生成的。"proguard-android.txt"其实是Android Studio为我们配置的一些有关Android的默认混淆配置,位于Android SDK根目录\tools\proguard\proguard-android.txt。
proguard-android.txt这里面是一些比较常规的不能被混淆的代码规则。我们可以找到该文件,看看里面都有什么:
里面一些很少用到的配置我都已经标注出来。可以看到里面添加了一些含有native方法的类、自定义View中的get、set方法、以及R类和带Keep注解的类的保护。
至此,打出的release包应该都是混淆过的,只不过除了默认的混淆规则外,我们没有为自己的apk添加自己的混淆规则,接下来我们看看如何给自己的apk添加混淆规则。
apk混淆规则的编写我大致认为分三个大的方面,Proguard的一些基础配置、Android项目的一些通用配置、根据自己需求定制的一些配置。
1.Proguard的一些基础配置:
-optimizationpasses 5 //代码混淆的压缩比例,值在0-7之间,Android一般推荐用5
-dontusemixedcaseclassnames //不使用大小写类名混淆(混淆后类名都为小写)
-dontskipnonpubliclibraryclasses //指定不去忽略非公共的库的类
-dontskipnonpubliclibraryclassmembers //指定不去忽略非公共的库的类的成员
-dontpreverify //不做预校验的操作
-verbose //混淆时显示详细日志
-dontoptimize //关闭优化
-printmapping proguardMapping.txt //生成原类名和混淆后的类名的映射文件
-dontwarn //忽略警告
-keepparameternames //保证方法参数不被混淆
-keepattributes *Annotation*,InnerClasses //不混淆Annotation和内部类
-keepattributes Signature //不混淆泛型
-keepattributes SourceFile,LineNumberTable //抛出异常时保留代码行号
当然这只是一些最基础的配置,大家可以根据自己的需要看后面的注释更改配置。
2.通用配置:
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class * extends android.view.View
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
#移除log类中的d、v调用处
-assumenosideeffects class android.util.Log {
public static *** d(...);
public static *** v(...);
}
-keep public class com.android.vending.licensing.ILicensingService
-keep class android.support.** {*;}
#避免混淆所有native的方法
-keepclasseswithmembernames class * {
native ;
}
#保持Activity子类里面的参数类型为View的方法不被混淆,如被XML里面应用的onClick方法
-keepclassmembers class * extends android.app.Activity{
public void *(android.view.View);
}
#保持枚举类型values()、以及valueOf(java.lang.String)成员不被混淆
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
//保持View的子类里面的set、get方法不被混淆(*代替任意字符)
-keep public class * extends android.view.View{
*** get*();
void set*(***);
public (android.content.Context);
public (android.content.Context, android.util.AttributeSet);
public (android.content.Context, android.util.AttributeSet, int);
}
-keepclasseswithmembers class * {
public (android.content.Context, android.util.AttributeSet);
public (android.content.Context, android.util.AttributeSet, int);
}
#保持实现Parcelable接口的类里面的Creator成员不被混淆
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
-keepclassmembers class * implements java.io.Serializable {
*;
}
//#保持R类不被混淆
-keep class **.R$* {
*;
}
-keepclassmembers class * {
void *(**On*Event);
}
#不警告support包中不使用的引用
-dontwarn android.support.**
#保持使用了Keep注解的方法以及类不被混淆
-keep class android.support.annotation.Keep
-keep @android.support.annotation.Keep class * {*;}
-keepclasseswithmembers class * {
@android.support.annotation.Keep ;
}
#保持使用了Keep注解的成员域以及类不被混淆
-keepclasseswithmembers class * {
@android.support.annotation.Keep ;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep (...);
}
#---------------------------------webview------------------------------------
-keepclassmembers class fqcn.of.javascript.interface.for.Webview {
public *;
}
-keepclassmembers class * extends android.webkit.WebViewClient {
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
public boolean *(android.webkit.WebView, java.lang.String);
}
-keepclassmembers class * extends android.webkit.WebViewClient {
public void *(android.webkit.WebView, jav.lang.String);
}
里面有一些和上面提到的Android Studio自带的混淆规则重复了,主要是考虑到有些人不使用as自带的proguard工具混淆(后面会讲其他方式)。
3.定制混淆规则
定制混淆规则这边又可以划分为更细致的四步:实体类、第三方库、与JS交互的类(如果有)、反射相关的类(如果有)
这里着重讲一下第三方库的情况,如果是使用gradle依赖方式导入的库,可以在库的GitHub主页或者库的官网找到相应的混淆规则加入即可,如果是通过jar包的形式导入的,就需要使用-libraryjars指令完成,这里以android-support-v4.jar为例:
-libraryjars libs/android-support-v4.jar
-dontwarn android.support.v4.**
-keep class android.support.v4.** { *; }
-keep interface android.support.v4.app.** { *; }
#如果gradle报错的话,可以考虑注释掉-libraryjars libs/android-support-v4.jar这句。
其他三步其实都是通过-keep class 要保护的类所在的包.** { *; } 方式给相应类添加保护,所以编码时要养成良好的习惯,相同类型的class文件放置在相同的包下。下面会讲一下为jar包添加混淆,以及使用Proguardgui工具。
proguardgui是proguard.jar的图形化工具,对paoguard.jar和混淆命令进行了封装,通过可视化的界面帮助我们方便的给apk或jar包添加混淆。
对应于proguard.jar的三种加混淆方式,proguardgui也对应有三种使用方法:
有Android SDK的同学可以通过AndroidSDK安装目录\tools\proguard\bin\proguardgui.bat(Windows)、AndroidSDK安装目录\tools\proguard\bin\proguardgui.sh(mac)方式打开该软件。没有的可以通过proguardgui下载地址进行下载。
上面提到,proguard.jar也好,AndroidStudio也好添加混淆都需要编写混淆规则(即配置文件),下面我们主要来学习一下proguard的语法相关知识:
为了结合proguardgui的使用方法,我们这边把规则分成和proguardgui执行步骤相同的六步来说:
1.加载配置文件:
2.input/output options(输入输出配置):
3.shrinking options(压缩配置):
4.keep配置(这一步在proguardgui中的表现跟第三步在一张图上):
例子:
-keep public class mypackage.MyMain {
public static void main(java.lang.String[]);
}
例子:
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
!static !transient ;
!private ;
!private ;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
例子:
-keepclasseswithmembers public class * {
public static void main(java.lang.String[]);
}
keep命令修饰符:
例子:
-keepclasseswithmembernames,includedescriptorclasses class * {
native ;
}
-keepclasseswithmembernames是保护符合条件的含有native方法的类。附加的includedescriptorclasses是保证参数和返回类型的类同样不被混淆。这样就可以做到这些类的方法签名与调试时完全一致。
5.optimization options(优化配置):
-dontoptimize 声明不优化输入文件。默认情况下,优化选项是开启的,并且所有的优化都是在字节码层进行的。
-allowaccessmodification 这项配置是设置是否允许改变作用域的。使用这项配置之后可以提高优化的效果。但是,如果你的代码是一个库的话,最好不要配置这个选项,因为它可能会导致一些private变量被改成public。
-mergeinterfacesaggressively 指定一些接口可能被合并,即使一些子类没有同时实现两个接口的方法。这种情况在java源码中是不允许存在的,但是在java字节码中是允许存在的。它的作用是通过合并接口减少类的数量,从而达到减少输出文件体积的效果。仅在optimize阶段有效。
-optimizations optimization_filter 更加细粒度地声明优化开启或者关闭。只在optimize这一阶段有效。这个选项的使用难度较高。
-optimizationpasses n 指定执行几次优化,默认情况下,只执行一次优化。执行多次优化可以提高优化的效果,但是,如果执行过一次优化之后没有效果,就会停止优化,剩下的设置次数不再执行。这个选项只在optimizate阶段有效
-assumenosideeffects class_specification 指定一些方法被删除也没有影响(尽管这些方法可能有返回值),在optimize阶段,如果确定这些方法的返回值没有使用,那么就会删除这些方法的调用。proguard会自动的分析你的代码,但不会分析处理类库中的代码。例如,可以指定System.currentTimeMillis(),这样在optimize阶段就会删除所有的它的调用。还可以用它来删除打印Log的调用。这条配置选项只在optimizate阶段有用。
例如:
# 删除代码中Log相关的代码
-assumenosideeffects class android.util.Log {
public static int v(...);
public static int i(...);
public static int w(...);
public static int d(...);
public static int e(...);
}
6.obfuscation options(混淆配置):
查找了字典文件的格式:一行一个单词,空行忽略,重复忽略
# 这里巧妙地使用java中的关键字作字典,混淆之后的代码更加不利于阅读
#
# This obfuscation dictionary contains reserved Java keywords. They can't
# be used in Java source files, but they can be used in compiled class files.
# Note that this hardly improves the obfuscation. Decent decompilers can
# automatically replace reserved keywords, and the effect can fairly simply be
# undone by obfuscating again with simpler names.
# Usage:
# java -jar proguard.jar ..... -obfuscationdictionary keywords.txt
#
do
if
for
int
new
try
byte
case
char
else
goto
long
this
void
break
catch
class
const
final
float
short
super
throw
while
double
import
native
public
return
static
switch
throws
boolean
default
extends
finally
package
private
abstract
continue
strictfp
volatile
interface
protected
transient
implements
instanceof
synchronized
7.preverification options(预校验配置):
8.一般配置(这一步在proguardgui中体现和第七步一张图):
经过以上步骤,proguard的配置文件就已经完成了。下面对于配置文件中的类和方法的匹配规则做一下介绍(即通配符使用)。
* | 匹配任意长度字符,但不含包名分隔符(.)。比如说我们的完整类名是com.example.test.MyActivity,使用com.*,或者com.exmaple.*都是无法匹配的,因为*无法匹配包名中的分隔符,正确的匹配方式是com.exmaple.*.*,或者com.exmaple.test.*,这些都是可以的。但如果你不写任何其它内容,只有一个*,那就表示匹配所有的东西。此外,mypackage.*Test*可以匹配到mypackage.Test和mypackage.YourTestApplication但是不能匹配mypackage.mysubpackage.MyTest。一种常用的写法mypackage.*就是匹配mypackage下的所有子文件。 |
** | 匹配任意长度字符,并且包含包名分隔符(.)。比如proguard-android.txt中使用的-dontwarn android.support.**就可以匹配android.support包下的所有内容,包括任意长度的子包。 |
*** | 匹配任意参数类型。比如void set*(***)就能匹配任意传入的参数类型,*** get*()就能匹配任意返回值的类型。 |
... | 匹配任意长度的任意类型参数。比如void test(…)就能匹配任意void test(String a)或者是void test(int a, String b)这些方法。 |
@ | 符号匹配那些注解标志的类或类成员,它的通配符形式与classname的形式一样。 |
? | 匹配类名中的一个字符,不包括文件分隔符。例如,mypackage.Test?可以匹配mypackage.Test1,mypackage.Test2,但不能匹配mypackage.Test12。 |
% | 匹配java中的初始类型(int, boolean, long, float,double等) |
变量名和方法名可以使用的通配符:
注意
参考资料:
https://www.guardsquare.com/en/products/proguard/manual/usage
https://blog.csdn.net/guolin_blog/article/details/50451259