1. android使用proguard
我们在使用Android Studio创建一个Android工程的时候,Android Studio已经在build.gradle中自动配置了proguard。
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
- minifyEnabled: 是否开启混淆,默认为false,需要混淆时要设为true。
- proguardFiles: 它有两个参数,proguard-android.txt代表默认的android 混淆配置文件,路径为 $SDK/tools/proguard/proguard-android.txt ,该文件已经包含了基本的混淆声明。proguard-rules.pro是我们项目里的自定义的混淆配置文件,路径一般为: $project/$module/proguard-rules.pro,在这个文件里我们可以声明一些我们自定义的混淆规则。
2. android proguard 默认配置文件
我们先来讲讲android 默认的proguard配置文件:
- proguard-android.txt,不带优化选项的配置文件。
- proguard-android-optimize.txt,带优化选项的配置文件。
- proguard-project.txt,全部都是注释,没有有效内容,忽略它即可。
2.1 proguard-android.txt
# This is a configuration file for ProGuard.
# http://proguard.sourceforge.net/index.html#manual/usage.html
#
# This file is no longer maintained and is not used by new (2.2+) versions of the
# Android plugin for Gradle. Instead, the Android plugin for Gradle generates the
# default rules at build time and stores them in the build directory.
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-verbose
# Optimization is turned off by default. Dex does not like code run
# through the ProGuard optimize and preverify steps (and performs some
# of these optimizations on its own).
-dontoptimize
-dontpreverify
# Note that if you want to enable optimization, you cannot just
# include optimization flags in your own project configuration file;
# instead you will need to point to the
# "proguard-android-optimize.txt" file instead of this one from your
# project.properties file.
-keepattributes *Annotation*
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService
# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
-keepclasseswithmembernames class * {
native <methods>;
}
# keep setters in Views so that animations can still work.
# see http://proguard.sourceforge.net/manual/examples.html#beans
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}
# We want to keep methods in Activity that could be used in the XML attribute onClick
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator CREATOR;
}
-keepclassmembers class **.R$* {
public static <fields>;
}
# The support library contains references to newer platform versions.
# Don't warn about those in case this app is linking against an older
# platform version. We know about them, and they are safe.
-dontwarn android.support.**
# Understand the @Keep support annotation.
-keep class android.support.annotation.Keep
-keep @android.support.annotation.Keep class * {*;}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <methods>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <fields>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <init>(...);
}
2.2 proguard-android-optimize.txt
# This is a configuration file for ProGuard.
# http://proguard.sourceforge.net/index.html#manual/usage.html
#
# This file is no longer maintained and is not used by new (2.2+) versions of the
# Android plugin for Gradle. Instead, the Android plugin for Gradle generates the
# default rules at build time and stores them in the build directory.
# Optimizations: If you don't want to optimize, use the
# proguard-android.txt configuration file instead of this one, which
# turns off the optimization flags. Adding optimization introduces
# certain risks, since for example not all optimizations performed by
# ProGuard works on all versions of Dalvik. The following flags turn
# off various optimizations known to have issues, but the list may not
# be complete or up to date. (The "arithmetic" optimization can be
# used if you are only targeting Android 2.0 or later.) Make sure you
# test thoroughly if you go this route.
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
-optimizationpasses 5
-allowaccessmodification
-dontpreverify
# The remainder of this file is identical to the non-optimized version
# of the Proguard configuration file (except that the other file has
# flags to turn off optimization).
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-verbose
-keepattributes *Annotation*
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService
# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
-keepclasseswithmembernames class * {
native <methods>;
}
# keep setters in Views so that animations can still work.
# see http://proguard.sourceforge.net/manual/examples.html#beans
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}
# We want to keep methods in Activity that could be used in the XML attribute onClick
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator CREATOR;
}
-keepclassmembers class **.R$* {
public static <fields>;
}
# The support library contains references to newer platform versions.
# Don't warn about those in case this app is linking against an older
# platform version. We know about them, and they are safe.
-dontwarn android.support.**
# Understand the @Keep support annotation.
-keep class android.support.annotation.Keep
-keep @android.support.annotation.Keep class * {*;}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <methods>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <fields>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <init>(...);
}
2.3 proguard-android-optimize.txt和proguard-android.txt对比
proguard-android.txt和proguard-android-optimize.txt的大部分内容是相同的,仅在优化选项上不同。下面我们就来对比一下,在各个选项上方加注释标明选项的作用。
不同的部分:
proguard-android.txt:
# 不启用优化
-dontoptimize
proguard-android-optimize.txt:
# 优化选项
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
# 优化遍数
-optimizationpasses 5
# 允许修改类和类成员的访问修饰符
-allowaccessmodification
相同部分:
# 不预检,预检主要是针对JavaME的,Java 6 以上都不需要预检。
-dontpreverify
# 指定在混淆时不生成大小写混合的类名,混合后类名为小写,主要是应对大小写不敏感的操作系统, 例如Windows
-dontusemixedcaseclassnames
# 指定不跳过非公共库的类文件
-dontskipnonpubliclibraryclasses
# 指定在处理期间写出更多信息。如果程序因异常终止,设置此选项将打印出整个堆栈,而不仅仅是异常消息。
-verbose
# 保留指定的属性
-keepattributes *Annotation*
# 保留指定的类
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService
# 保留包含native方法的类及其类成员
-keepclasseswithmembernames class * {
native <methods>;
}
# 保留View类的子类的getter和setter方法
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}
# 保留Activity类的子类的任意名称、参数为View的方法,一般用于保留在XML属性中使用的方法,例如,onClick方法。
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
# 保留枚举的values和valueOf方法
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
#保留Parcelable 子类的成员CREATOR
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator CREATOR;
}
#保留R类及其内部类的公共且静态的成员
-keepclassmembers class **.R$* {
public static <fields>;
}
# 不提示support包的警告,因为support包是可以兼容的,google已经处理好了。
-dontwarn android.support.**
# 保留指定类Keep,为了方便使用Keep注解。
-keep class android.support.annotation.Keep
# 保留使用了Keep进行注解的类及其所有字段和方法
-keep @android.support.annotation.Keep class * {*;}
# 保留使用了Keep进行注解的类及其中使用了Keep进行注解的方法
-keepclasseswithmembers class * {
@android.support.annotation.Keep <methods>;
}
# 保留使用了Keep进行注解的类及其中使用了Keep进行注解的字段
-keepclasseswithmembers class * {
@android.support.annotation.Keep <fields>;
}
# 保留使用了Keep进行注解的类及其中使用了Keep进行注解的构造方法
-keepclasseswithmembers class * {
@android.support.annotation.Keep <init>(...);
}
3. Android 中比较通用的proguard配置原则
3.1 继承默认proguard配置
继承是指我们使用proguard配置文件时尽量在原有的默认配置文件的基础上去添加自定义配置选项,因为原有的默认配置已经帮我们预置了很多常用的选项。例如,保留native方法、保留枚举类,保留R类等等。需要优化的项目,复制proguard-android-optimize.txt的内容,然后在此基础上去添加自定义的配置选项。不需要优化的项目,复制proguard-android.txt的内容,在此基础上去添加自定义的配置选项。
3.2 保留反射使用到的类
在《ProGuard基础》的入口点一节中提到,要保留所有反射动态创建或调用的类或类成员。保留方式有很多种,例如,可以指定保留包或者指定保留类。
-keep class com.example.** { *; }
-keep public class com.example.Example
3.3 保留可能被外部调用的类
为了对外的接口在proguard处理后,外部还能正常调用,对外接口是一定需要保留的。常见的如四大组件,自定义的Application等等。
-keep public class * extends android.app.Activity
-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.Appliction
-keep public class * extends android.app.backup.BackupAgentHelper
3.4 保留实体类及可序列化类
实体类和序列化类之所以会放到一起,是因为实际项目很多情况下实体类本身也是可序列化类。实体类在一般需要保留Getter和Setter、isxxx方法。可序列化类在android中Parcelable用得多一些,Serializable用得少一些。默认的配置文件中已经有保留Parcelable的选项了,这里使用通配符扩展一下。为了兼容一些第三方的java库,最好把保留Serializable的选项也加上。
# 保留实体类
-keep public class com.example.entity.** {
public void set*(***);
public *** get*();
public *** is*();
}
# 保留Parcelable序列化类
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
# 保留Serializable序列化类
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
3.5 保留继承自View的自定义控件
同样的,默认的配置文件中也已经有保留View子类的选项了,这里扩展一下。
-keep public class * extends android.view.View{
*** get*();
void set*(***);
public <init>(android.content.Context);
public <init>(android.content.Context, android.util.AttributeSet);
public <init>(android.content.Context, android.util.AttributeSet, int);
}
3.6 保留WebView和JavaScript交互类
如果我们项目中使用带有JavaScript的WebView,则需要保留JavaScript交互类,同时我们可能还需要保留WebViewClient的子类。
# fqcn.of.javascript.interface.for.webview 是the fully qualified class name to the JavaScript interface
# class的缩写,即表示JavaScript交互类的完整包名.类名。
# 有些文章可能将保留JavaScript交互类单独列为一节,其实是没有必要的,因为通配符 * 可以匹配任意字段和方法。
-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, java.lang.String);
}
3.7 保留第三方库
proguard使用手册上说:使用proguard处理库时,至少应保留Exceptions,InnerClasses和Signature属性。一般还应该保留SourceFile和LineNumberTable属性,以产生有用的堆栈信息,即出了异常能保留有效的文件名和行号。
但实际项目中第三方库最好全部保留。如果是开源的第三方库,根本没有混淆的必要;如果是第三方的SDK,SDK厂商给你之前肯定也混淆过了。大部分情况下,我们可以直接使用dontwarn和keep保留即可,但是SDK厂商一般也会在SDK的API文档中提供自己的混淆策略,具体以SDK厂商提供的混淆策略为准。例如:
# 支付宝
-libraryjars libs/alipaysdk.jar
-dontwarn com.alipay.android.app.**
-keep public class com.alipay.** { *; }
# rxjava
-dontwarn sun.misc.**
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
long producerIndex;
long consumerIndex;
}
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
rx.internal.util.atomic.LinkedQueueNode producerNode;
}
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueConsumerNodeRef {
rx.internal.util.atomic.LinkedQueueNode consumerNode;
}
3.8 保留源文件名和源代码行号
为了更好的排查问题,我们可能希望proguard处理后的代码运行时产生的日志中能保留有源代码的文件名和行号。在选择保留源文件名和源代码行号时,一般有两种:
- -keepattributes SourceFile,LineNumberTable,抛出异常时在堆栈信息中保留源文件名称及源代码行号
- -renamesourcefileattribute SourceFile,抛出异常时在堆栈信息中不保留源文件名,但保留源代码行号
一般情况下,这两个选项至少要二选一。两者的区别,会在接下来的第4节中介绍。
3.9 删除无效代码
assumenosideeffects是一个优化选项,它可以用来删除没有任何影响的方法,比如没有返回值的且不影响执行结果的方法、有返回值但未使用其返回值且不影响执行结果的方法。例如,我们可以用来优化无用的Log:
-assumenosideeffects class android.util.Log {
public static *** d(...);
public static *** e(...);
public static *** i(...);
public static *** v(...);
public static *** w(...);
}
注意:
assumenosideeffects是一个很有争议的选项,assumenosideeffects全局生效,被很多人拿来删除无效日志。但实际上它很容易破坏原有代码,它无法判断这个日志的打印对于项目是否真的无用。有可能某条日志对于我们分析问题是非常关键的,但它只能一刀切的删除,因此建议尽量少用assumenosideeffects。最好的做法是保持一个良好的编码习惯,冗余代码是在编码的时候就把它删掉。
3.10 开发和生产环境配置一致
我们的项目如果确定了要使用proguard处理,最好在项目一开始就进行处理。并且建议debug版本和release版本使用同样的proguard配置,保持开发和生产环境一致。这样由proguard引起的问题,我们可以在项目的开发阶段就能够及时发现和处理,不会因为开发和生产环境的不同,而导致出了问题找不到原因。如果确实有需要,也可在自己调试时禁用proguard来验证相关问题。
4. Proguard 混淆后的堆栈信息还原
4.1 retrace
retrace是proguard的辅助工具,可以用来还原堆栈。当混淆后的程序出现异常时,生成的堆栈通常是没有什么有效信息的。 源代码中的类名和方法名已由无意义的短字符串替换,源文件名和行号完全丢失。 调试问题时非常不方便。因此,我们就需要用到retrace。retrace可以读取经过混淆的堆栈,并将其还原为没有混淆的样子。 还原基于proguard在混淆过程中产生的映射文件。 映射文件将原始类名和类成员名链接到它们的混淆名。以下是retrace的流程:
使用retrace,在命令行输入:
java -jar retrace.jar [options...] mapping_file [stacktrace_file]
参数及选项说明:
参数:
- mapping_file,映射文件。
- stacktrace_file,包含堆栈的文件, 如果未指定,则从标准输入读取堆栈。
选项:
- -verbose,打印出更多有用的堆栈信息,这些信息不仅包括方法名称,还包括方法返回类型和参数。
- -regex Regular_expression,用于解析堆栈中的行的正则表达式。默认值适用于大多数JVM生成的堆栈信息
4.2 proguardgui 的使用
$SDK/tools/proguard/bin/proguardgui.bat是一个GUI工具,它集成了proguard.bat和retrace.bat,在可视化界面中提供了处理过程的各个步骤的配置项,比在命令行使用更加方便。我们打开proguardgui,点击Retrace选项卡,它的界面如图:
- Mapping file:它是一个映射文件,记录源代码到混淆后的代码之间的映射关系,一般位于:$project\app\build\outputs\mapping\[debug|release]\mapping.txt。注意。只有启用proguard才会生成,即build.gradle中的minifyEnabled为true才会生成。
- Obfuscated stack trace:混淆后的代码运行出现的异常的堆栈信息,可以直接从错误或异常日志中拷贝进来,也可以点击右下角的Load stack trace按钮选择文件。
- De-obfuscated stack trace:还原后的堆栈信息,可以对应源代码的具体文件及行号。
- Load stack trace:选择记录了堆栈信息的日志文件。
- Retrace:指定好mapping文件和日志之后,点击Retrace就可以进行还原。
注意:
启用proguard处理代码后,每发一个版本,我们都需要保留好mapping文件,以便将来进行日志的堆栈还原,准确定位出错位置。
为了介绍retrace的使用,我们先制造一个异常:
MainActivity.java:
package com.example.qiuxintai.myapp;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Test.divide();
}
}
Test.java
package com.example.qiuxintai.myapp;
public class Test {
public static int divide() {
return 1 / 0;
}
}
错误日志堆栈:
2020-09-01 19:08:17.929 32700-32700/com.example.qiuxintai.myapp E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.qiuxintai.myapp, PID: 32700
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.qiuxintai.myapp/com.example.qiuxintai.myapp.MainActivity}: java.lang.ArithmeticException: divide by zero
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2944)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3079)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1836)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6702)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:911)
Caused by: java.lang.ArithmeticException: divide by zero
at com.example.qiuxintai.myapp.a.a(Unknown Source:1)
at com.example.qiuxintai.myapp.MainActivity.onCreate(Unknown Source:9)
at android.app.Activity.performCreate(Activity.java:8604)
at android.app.Activity.performCreate(Activity.java:8595)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1271)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2924)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3079)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1836)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6702)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:911)
由于日志太长了,下面只截取跟我们的代码直接相关的部分。
retrace后:
Caused by: java.lang.ArithmeticException: divide by zero
at com.example.qiuxintai.myapp.Test.divide(Unknown Source:1)
at com.example.qiuxintai.myapp.MainActivity.onCreate(Unknown Source:9)
直接retrace后,括号部分的文件名还是Unknown Source,依然没有准确的源代码行号。这就涉及到要保留行号了。为了保留行号,我们可以在proguard-rules.pro中使用两个选项:
- -renamesourcefileattribute SourceFile
- -keepattributes SourceFile,LineNumberTable
接下来我们看看两者的区别。
4.3 -renamesourcefileattribute SourceFile,混淆文件名但保留行号
混淆后的日志包含源代码准确的行号,但文件名和方法名是未知的:
Caused by: java.lang.ArithmeticException: divide by zero
at com.example.qiuxintai.myapp.a.a(Unknown Source:5)
at com.example.qiuxintai.myapp.MainActivity.onCreate(Unknown Source:12)
retrace后,括号内的文件名还是UnKnown Source,但是前面已经有类名、方法名,再加上源代码准确的行号,我们已经可以准确的定位问题了:
Caused by: java.lang.ArithmeticException: divide by zero
at com.example.qiuxintai.myapp.Test.divide(Unknown Source:5)
at com.example.qiuxintai.myapp.MainActivity.onCreate(Unknown Source:12)
4.4 -keepattributes SourceFile,LineNumberTable,保留文件名和行号
混淆后的日志就包含源代码的文件名和行号,如果不介意方法名被混淆了,甚至都不需要retrace了:
Caused by: java.lang.ArithmeticException: divide by zero
at com.example.qiuxintai.myapp.a.a(Test.java:5)
at com.example.qiuxintai.myapp.MainActivity.onCreate(MainActivity.java:12)
retrace后,源代码的文件名、方法名、行号都被还原出来了:
Caused by: java.lang.ArithmeticException: divide by zero
at com.example.qiuxintai.myapp.Test.divide(Test.java:5)
at com.example.qiuxintai.myapp.MainActivity.onCreate(MainActivity.java:12)
通过上面的对比,我们可以很清楚的看出区别了。一般我们建议选择-renamesourcefileattribute SourceFile,因为它仅有行号但不会暴露源文件名,保密性更强,更难被反编译破解。但是实际上所有人的时间都是宝贵的,你写的代码可能并不值得别人花时间去反编译破解。因此,使用哪一种并不是真的那么重要,除非你真的需要。
5. 参考
本文参考了以下两篇文章:
ProGuard详解
Android混淆代码错误堆栈还原
感谢两位原作者的辛勤付出。
欢迎交流、点赞、转载,码字不易,转载请注明出处。