1.What and why?
-
What?
代码混淆(Obfuscated code)亦称花指令,是将计算机程序的代码,转换成一种功能上等价,但是难于阅读和理解的形式的行为。
-
Why?
混淆的目的是为了加大反编译的成本,但是并不能彻底防止反编译.
2.How?
ProGuard由shrink、optimize、obfuscate和preverify四个步骤组成,每个步骤都是可选的,需要哪些步骤都可以在脚本中配置。参见ProGuard官方介绍。
ProGuard_build_process.pngEntry Points(入口点):
为了确定哪些代码应该被保留,哪些代码应该被移除或混淆,需要确定一个或多个Entry Point。Entry Point经常是带有main methods,applets,midlets的classes,它们在混淆过程中会被保留。
What does each step do?
-
shrink: Proguard从上述EntryPoints开始遍历搜索哪些类和类成员被使用。其他没有被使用的类和类成员会移除。
-
optimize: 优化代码,非EntryPoints类会加上private/static/final, 没有用到的参数会被删除,一些方法可能会变成内联代码。
-
obfuscate: 使用短又没有语义的名字重命名非EntryPoints的类名,变量名,方法名。EntryPoints的名字保持不变。
-
preverify: 预校验代码是否符合Java1.6或者更高的规范(唯一一个与入口类不相关的步骤)
3.Usage
要执行proguard,可以直接执行命令:
java -jar proguard.jar options ...
如果有Android SDK的同学可以在{ANDROID_SDK_ROOT}/tools/proguard/lib/目录下找到proguard.jar这个jar包。或者,也可以在{ANDROID_SDK_ROOT}/tools/proguard/bin目录下直接使用脚本执行命令。
我们也可以把proguard的参数写到一个配置文件中,比如说proguard.cfg。那我们的命令可以这样写:
java -jar proguard.jar @proguard.cfg
这个文件也就是我们在Android Studio中经常配置的混淆文件了。我们在编译正式包的时候打包脚本自动帮我们执行了这条命令。通过这个脚本可以避免重复输入参数。
当然,我们也可以配置文件与命令行参数混用,例如:
java -jar proguard.jar @proguard.cfg -verbose
AndroidStudio中开启混淆
参考Android官方文档
如果需要开启混淆,在build.gradle文件中相应的BuildType下将minifyEnabled 设置为true,开启混淆会降低构建速度,因此避免在debug版本中开启混淆。
以下是以release版本为例,开启混淆的gradle脚本片段:
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
...
}
其中proguardFiles属性用于定义 ProGuard 规则,与上文中直接使用proguard.jar进行混淆时指定的文件选项是一个意思。
- getDefaultProguardFile(‘proguard-android.txt’) 方法可从 Android SDK tools/proguard/ 文件夹获取默认的 ProGuard 设置。要想做进一步的代码压缩,请尝试使用位于同一位置的 proguard-android-optimize.txt 文件。它包括相同的 ProGuard 规则,但还包括其他在字节码一级(方法内和方法间)执行分析的优化,以进一步减小 APK 大小和帮助提高其运行速度。
- proguard-rules.pro 文件用于添加自定义 ProGuard 规则。默认情况下,该文件位于模块根目录(build.gradle 文件旁),内容为空。
构建输出
构建时Proguard都会输出下列文件:
- dump.txt 说明APK中所有类文件的内部结构
- mapping.txt 提供原始与混淆过的类、方法和字段名称之间的转换
- seeds.txt 列出未进行混淆的类和成员
- usage.txt 列出从APK移除的代码
这些文件保存在 <module-name>/build/outputs/mapping/release/目录下。
每新发布一个版本,都会产生新的 mapping.txt文件,所以要保存好相应的 mapping.txt文件,方便解码混淆过的stack trace。
解码混淆过的stack trace
使用位于 <sdk-root>/tools/proguard/目录下的retrace脚本,将混效果的stack trace 和mapping.txt作为输入,可以使输出已解码的stack trace.
例如:
retrace.bat -verbose mapping.txt obfuscated_trace.txt
proguard-android.txt 解读
不使用大小写混写类名,默认情况下混淆的类名可以包含大小写字符的混合,以防止在大小写不敏感的系统,比如windows上出现问题。
-dontusemixedcaseclassnames
不忽略公共类库
-dontskipnonpubliclibraryclasses
关闭optimize和preverify选项,因为Android的dex并不像Java虚拟机需要optimize(优化)和previrify(预检)两个步骤。
-dontoptimize
-dontpreverify
指定哪个属性不要混淆,可一次指定多个属性
-keepattributes [attribute_filter]
通常Exceptions, Signature, Deprecated, SourceFile, SourceDir, LineNumberTable, LocalVariableTable, LocalVariableTypeTable, Synthetic, EnclosingMethod, RuntimeVisibleAnnotations, RuntimeInvisibleAnnotations, RuntimeVisibleParameterAnnotations, RuntimeInvisibleParameterAnnotations, and AnnotationDefault属性需要被保留,根据项目具体使用情况保留。
这里需要特别注意的一点是,gradle默认的keepattributes属性不全,只保留了Annotation,Signature,InnerClasses,EnclosingMethod,为了混淆之后定位csh代码方便,我们需要在proguard_rules.pro中手动添加抛出异常时保留代码行号,并且重命名抛出异常时的文件名称,这样能方便定位问题:
抛出异常时保留代码行号
-keepattributes SourceFile,LineNumberTable
重命名抛出异常时的文件名称
-renamesourcefileattribute SourceFile
Keep配置
***-keep [,modifier, ...] class_specification ***
指定类和类成员(变量和方法)不被混淆
指定类名不被改变
-keep public class com.google.vending.licensing.ILicensingService
指定使用了Keep注解的类和类成员都不被改变
-keep @android.support.annotation.Keep class * {*;}
-keepclassmembers
指定类成员不被混淆,类名会被混淆
eg.keep setters in views 使得animations仍然能够工作
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}
***-keepclasseswithmembers ***
指定类和类成员都不被混淆
eg.包含native方法的类名和native方法都不能被混淆,如果native方法未被调用,则被移除。由于native方法与对应so库中的方法名称对应,方法名被混淆会导致调用出现问题,所以native方法不能被混淆。
-keepclasseswithmembernames class * {
native <methods>;
}
-keepnames
是 -keep,allowshrinking class_pecification 的简写。指定一些类名受到保护,前提是他们在shrink这一阶段没有被去掉。也就是说没有被入口节点直接或间接引用的类还是会被删除。
-keepclassmembernames
与-keepclassmember相似。保护指定的类成员,前提是这些成员在shrink阶段没有被删除。
-keepclasseswithmembernames
与-keepclasseswithmembers类似。保护指定的类,如果它们没有在shrink阶段被删除。
注意
If you specify a class, without class members, ProGuard only preserves the class and its parameterless constructor as entry points. It may still remove, optimize, or obfuscate its other class members.
以上六种keep配置类型,以names结尾的配置不保证被Keep的类或者成员不被删除,只有在obfuscation 这一阶段有效,如果不确定使用哪种,只需要使用不带names结尾的Keep配置即可,因为不带names的keep在shrink阶段有效,可以保证被Keep的类或者属性不被删除。
通用Options:
-verbose 打印混淆详细信息
-dontnote:指定不去输出打印该类产生的错误或遗漏
-dontnote com.android.vending.licensing.ILicensingService
-dontnote android.support.**
-dontwarn:指定不去warn unresolved references和其他重要的problem
-dontwarn android.support.**
自定义混淆文件
Keep配置后面要如何写类的信息?
[@annotationtype] [[!]public|final|abstract|@ ...] [!]interface|class|enum classname
[extends|implements [@annotationtype] classname]
[{
[@annotationtype] [[!]public|private|protected|static|volatile|transient ...] <fields> |
(fieldtype fieldname);
[@annotationtype] [[!]public|private|protected|static|synchronized|native|abstract|strictfp ...] <methods> |
<init>(argumenttype,...) |
classname(argumenttype,...) |
(returntype methodname(argumenttype,...));
[@annotationtype] [[!]public|private|protected|static ... ] *;
...
}]
Filters
? matches any single character in a name.(匹配一个字符)
* matches any part of a name not containing the directory separator.(匹配一个名字,除了目录分隔符外的任意部分)
** matches any part of a name, possibly containing any number of directory separators.(匹配任意名,可能包含任意路径分隔符)
! exclude
<field> 匹配类中的所有字段
<method> 匹配类中所有的方法
<init> 匹配类中所有的构造函数
-keep class com.lily.test.** 本包和所包含子包下的类名都保持
-keep class com.lily.test.* 保持该包下的类名
-keep class com.lily.test.** {*;} 保持包和子包的类名和里面的内容均不被混淆
-keepclassmembers class **.R$* {
public static <fields>;
}
assumenosideeffects选项
指定一些方法被删除也没有影响(尽管这些方法可能有返回值),在optimize阶段,如果确定这些方法的返回值没有使用,那么就会删除这些方法的调用。proguard会自动的分析你的代码,但不会分析处理类库中的代码。例如,可以指定System.currentTimeMillis(),这样在optimize阶段就会删除所有的它的调用。还可以用它来删除打印Log的调用。这条配置选项只在optimizate阶段有用。
注意:Only use this option if you know what you’re doing!
eg:
# 删除代码中Log相关的代码
-assumenosideeffects class android.util.Log {
public static boolean isLoggable(java.lang.String, int);
public static int v(...);
public static int i(...);
public static int w(...);
public static int d(...);
public static int e(...);
}
下面是自定义混淆文件的一个范例,四大组件,native方法,反射用到的类,一些引入的第三方库等,都不能进行混淆:
# 代码混淆压缩比,在0~7之间
-optimizationpasses 5# 混合时不使用大小写混合,混合后的类名为小写
-dontusemixedcaseclassnames
# 指定不去忽略非公共库的类
-dontskipnonpubliclibraryclasses
# 不做预校验,preverify是proguard的四个步骤之一,Android不需要preverify,去掉这一步能够加快混淆速度。
-dontpreverify
-verbose
#google推荐算法
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
# 避免混淆Annotation、内部类、泛型、匿名类
-keepattributes *Annotation*,InnerClasses,Signature,EnclosingMethod
# 重命名抛出异常时的文件名称
-renamesourcefileattribute SourceFile
# 抛出异常时保留代码行号
-keepattributes SourceFile,LineNumberTable
# 处理support包
-dontnote android.support.**
-dontwarn android.support.**
# 保留四大组件,自定义的Application等这些类不被混淆
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Appliction
-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.preference.Preference
-keep public class com.android.vending.licensing.ILicensingService
# 保留本地native方法不被混淆
-keepclasseswithmembernames class * {
native <methods>;
}
# 保留枚举类不被混淆
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# 保留Parcelable序列化类不被混淆
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
#第三方jar包不被混淆
-keep class com.github.test.** {*;}
#保留自定义的Test类和类成员不被混淆
-keep class com.lily.Test {*;}
#保留自定义的xlog文件夹下面的类、类成员和方法不被混淆
-keep class com.test.xlog.** {
<fields>;
<methods>;
}
#assume no side effects:删除android.util.Log输出的日志
-assumenosideeffects class android.util.Log {
public static *** v(...);
public static *** d(...);
public static *** i(...);
public static *** w(...);
public static *** e(...);
}
#保留Keep注解的类名和方法
-keep,allowobfuscation @interface android.support.annotation.Keep
-keep @android.support.annotation.Keep class *
-keepclassmembers class * {
@android.support.annotation.Keep *;
}
下面的Proguard的思路可以参考:5分钟搞定android混淆
主要将自定义Proguard分成几个区域:
#--------------------------------定制化区域------------------------------
#---------------------------------1.实体类--------------------------------
#-------------------------------------------------------------------------
#---------------------------------2.第三方包-------------------------
#-------------------------------------------------------------------------
#---------------------------------3.与js互相调用的类----------------
#-------------------------------------------------------------------------
#---------------------------------4.反射相关的类和方法-----------------
#----------------------------5.基本不用动区域(可参考上文进行区分)-------------
4.资源文件的混淆
上面讲述了如何进行代码混淆,再来讲讲如何对资源文件进行混淆。对资源文件进行混淆操作本质上是通过修改resources.arsc(参见文末链接详见resources.arsc作用及文件格式)。现针对两种资源混淆方案进行简要说明。第一种是微信的资源混淆方案,第二种是美团的资源混淆方案,两篇文章中都对原理进行了详细的阐述。
5.混淆时常见的问题解决
TroubleShooting
Error:Uncaught translation error: com.android.dex.util.ExceptionWithContext: name already added: string{"a"}
参考:
Proguard官方文档中的一些关于Android混淆的例子
网友评论