0%

Android代码混淆笔记

概述

Android SDK 自带了混淆工具Proguard。它位于SDK根目录\tools\proguard下面。如果开启了混淆,Proguard默认情况下会对所有代码,包括第三方包都进行混淆,可是有些代码或者第三方包是不能混淆的,这就需要我们手动编写混淆规则来保持不能被混淆的部分。

混淆有几个作用(默认开启):

  1. 【优化】它能优化java的字节码,使程序运行更快(-dontoptimize:关闭优化);
  2. 【压缩】最直观的就是减少App大小,在混淆过程中它会找出未被使用过的类和类成员并删除他们(-dontshrink,关闭压缩);
  3. 【混淆】这个功能使我们的java代码中的类、函数、变量名随机变成无意义的代号(-dontobfuscate:关闭混淆)。

在Android Studio新版本中启用了R8混淆:

开启混淆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
android {
compileSdkVersion 29
buildToolsVersion "29.0.1"
defaultConfig {
}
buildTypes {
release {
zipAlignEnabled true // 能优化java字节码,提高运行效率
minifyEnabled true // 开启混淆
// 'proguard-android.txt' 是默认导入的规则,这个文件位于/tools/proguard/下
// 'proguard-rules.pro'是针对我们自己的项目需要特别定义混淆规则
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
....
}

混淆规则

实例

proguard-android.txt文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#混淆时不生成大小写混合的类名
-dontusemixedcaseclassnames
#不忽略非公共的类库
-dontskipnonpubliclibraryclasses
#混淆过程中打印详细信息
-verbose

#关闭优化
-dontoptimize
#不预校验
-dontpreverify

# Annotation注释不能混淆
-keepattributes *Annotation*
#对于NDK开发 本地的native方法不能被混淆
-keepclasseswithmembernames class * {
native <methods>;
}
#保持View的子类里面的set、get方法不被混淆(*代替任意字符)
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}

#保持Activity子类里面的参数类型为View的方法不被混淆,如被XML里面应用的onClick方法
# 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);
}

#保持枚举类型values()、以及valueOf(java.lang.String)成员不被混淆
-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包中不使用的引用
-dontwarn android.support.**
-keep class android.support.annotation.Keep
-keep @android.support.annotation.Keep class * {*;}
#保持使用了Keep注解的方法以及类不被混淆
-keepclasseswithmembers class * {
@android.support.annotation.Keep <methods>;
}
#保持使用了Keep注解的成员域以及类不被混淆
-keepclasseswithmembers class * {
@android.support.annotation.Keep <fields>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <init>(...);
}

上面默认的规则中指示了些需要保持不能别混淆的代码,包括:

  1. 继承至Android组件(Activity, Service…)的类。
  2. 自定义控件,继承至View的类(被xml文件引用到的,名字已经固定了的)
  3. enum 枚举
  4. 实现了 android.os.Parcelable 接口的
  5. Android R文件
  6. 数据库驱动…
  7. Android support 包等
  8. Android 的注释不能混淆
  9. 对于NDK开发 本地的native方法不能被混淆
  10. 对于特定的项目还有很多不能被混淆的,需要我们自己写规则来指示

自己编写proguard-rules.pro

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#压缩级别0-7,Android一般为5(对代码迭代优化的次数)
-optimizationpasses 5

#不使用大小写混合类名
-dontusemixedcaseclassnames

#混淆时记录日志
-verbose

#不警告org.greenrobot.greendao.database包及其子包里面未引用的引用
-dontwarn org.greenrobot.greendao.database.**
-dontwarn rx.**
-dontwarn org.codehaus.jackson.**
......
#保持jackson包以及其子包的类和类成员不被混淆
-keep class org.codehaus.jackson.** {*;}
#--------重要说明-------
#-keep class 类名 {*;}
#-keepclassmembers class 类名{*;}
#一个*表示保持了该包下的类名不被混淆;
# -keep class org.codehaus.jackson.*
#二个**表示保持该包以及它包含的所有子包下的类名不被混淆
# -keep class org.codehaus.jackson.**
#------------------------
#保持类名、类里面的方法和变量不被混淆
-keep class org.codehaus.jackson.** {*;}
#不混淆类ClassTwoOne的类名以及类里面的public成员和方法
#public 可以换成其他java属性如private、public static 、final等
#还可以使<init>表示构造方法、<methods>表示方法、<fields>表示成员,
#这些前面也可以加public等java属性限定
-keep class com.dev.demo.two.ClassTwoOne {
public *;
}
#不混淆类名,以及里面的构造函数
-keep class com.dev.demo.ClassOne {
public <init>();
}
#不混淆类名,以及参数为int 的构造函数
-keep class com.dev.demo.two.ClassTwoTwo {
public <init>(int);
}
#不混淆类的public修饰的方法,和private修饰的变量
-keepclassmembers class com.dev.demo.two.ClassTwoThree {
public <methods>;
private <fields>;
}
#不混淆内部类,需要用$修饰
#不混淆内部类ClassTwoTwoInner以及里面的全部成员
-keep class com.dev.demo.two.ClassTwoTwo$ClassTwoTwoInner{*;}

混淆规则

  • -keepattributes {name}:保护给定的属性不被混淆

  • -dontwarn {name}:不要警告指定库中找不到的引用。混淆在默认情况下会检查每个库的引用是否正确,但是有些第三方库里面会有用不到的类,有些没有正确引用,所以需要对第三方库取消警告,否则会报错,而且有可能混淆时间会很长

  • -keep {Modifier} {class_specification}:保留指定的类、类成员不被混淆

    1
    2
    3
    4
    #例如对一个可执行jar包来说,需要保护main的入口类,对一个类库来说需要保护它的所有public元素
    -keep public class MyMain{
    public static void main(java.lang.String[]);
    }
  • -keepclassmembers {modifier} {class_specification}:保留指定的类成员不被混淆

    1
    2
    3
    4
    5
    6
    7
    #例如指定继承了Serizalizable的类的如下成员不被混淆,成员属性和方法,非私有的属性非私有的方法.
    -keepclassmembers class * implements java.io.Serializable{
    static final long seriaVersionUID;
    !private <fields>;
    !private <methods>;
    private void writeObject(java.io.ObjectOuputStream);
    }
  • -keepclasseswithmembers {class_specification}:保护指定成员的类,根据成员确定一些将要被保护的类

    1
    2
    3
    4
    #保护含有main方法的类以及这个类的main方法
    -keepclasseswithmembers public class * {
    public static void main(java.lang.String[]);
    }
  • -keepnames {Modifier}{class_specification}:这个是-keep,allowshrinking {Modifier}{class_specification}的简写.意思是说允许压缩操作,如果在压缩过程中没有被删除,那么类名和该类中的成员的名字会被保护

  • -keepclassmembernames {Modifier}{class_specification}:如果在压缩过程中没有被删除在保护这个类中的成员

  • -keepclasseswithmembernames {Modifier}{class_specification}:如果在压缩过程中该类没有被删除,同样如果该类中有某个成员字段或者方法,那么保护这个类和这个方法

  • -printseeds {filename}将keep的成员输出到文件或者打印到标准输出

通配符

通配符 描述
<fields> 匹配类中的所有字段
<methods> 匹配类中的所有方法
<init> 匹配类中的所有构造函数
* 匹配任意长度字符,但不含包名分隔符(.)。比如说我们的完整类名是com.example.test.MyActivity,使用com.,或者com.exmaple.都是无法匹配的,因为\无法匹配包名中的分隔符,正确的匹配方式是com.exmaple..*,或者com.exmaple.test.*,这些都是可以的。
** 匹配任意长度字符,并且包含包名分隔符(.)。比如proguard-android.txt中使用的-dontwarn android.support.**就可以匹配android.support包下的所有内容,包括任意长度的子包。
*** 匹配任意参数类型。比如void set*(***),就能匹配任意传入的参数类型,*** get*()就能匹配任意返回值的类型。

删除日志

1
2
3
4
5
6
7
8
9
10
11
# 删除日志 (一定要把 -dontoptimize 配置去掉,否则无法删除日志)
-assumenosideeffects class org.apache.log4j.** {*;}
-assumenosideeffects class de.mindpipe.android.logging.log4j.LogConfigurator {*;}
-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(...);
}

混淆输出

成功打包后会在目录 app/build/outputs/mapping/release 下生成几个文件:

  • dump.txt 混淆后类的内部结构说明
  • mapping.txt 混淆前与混淆后名称对应关系
  • seeds.txt 经过了一系列keep语句的保持,没有被混淆的类,成员的名称列表文件
  • usage.txt 经过压缩后被删除的没有使用的代码,方法…等的名称的列表文件

ReTrace

可以使用retrace工具和mapping文件反推崩溃日志以及还原混淆代码,retrace工具在tools/proguard/bin 下,可以使用retrace命令行工具,也可以使用proguardgui工具。

1
retrace.sh -verbose mapping.txt stacktrace.txt > out.txt

有时为了Crash日志更容易定位可以在规则里面添加:

1
-keepattributes SourceFile, LineNumberTable