简单介绍

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

作为 Android 开发者,如果你不想开源你的应用,那么在应用发布前,就需要对代码进行混淆处理,从而让我们代码即使被反编译,也难以阅读。

混淆的作用

压缩(Shrinking)

默认开启,用以减小应用体积,移除未被使用的类和成员,并且会在优化动作执行之后再次执行(因为优化后可能会再次暴露一些未被使用的类和成员)。

-dontshrink 关闭压缩

优化(Optimization)

默认开启,在字节码级别执行优化,让应用运行的更快。

-dontoptimize 关闭优化

-optimizationpasses n 表示 proguard 对代码进行迭代优化的次数,Android 一般为5。

混淆(Obfuscation)

默认开启,增大反编译难度,类和类成员会被随机命名(像 a、b()、c之类的),除非用 keep 保护。

-dontobfuscate:关闭混淆。

开启混淆

在 app 的 gradle 文件中修改,在 android - buildTypes - release(debug)下:

android {
    ...
    buildTypes {
        
        release {
            ...
            //可优化字节码
            zipAlignEnabled true
            
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    ...
} 
说明

‘proguard-android.txt’ 是AndroidStudio默认自动导入的规则,这个文件位于Android SDK根目录\tools\proguard\proguard-android.txt。这里面是一些比较常规的不能被混淆的代码规则。

'proguard-rules.pro’是针对我们自己的项目需要特别定义混淆规则,它位于项目根目录下面,里面的内容需要我们自己编写。

zipAlignEnabled true 这个在打包时需要设置为true,能优化我们的java字节码,提高运行效率。zipAlign 可以让安装包中的资源按4字节对齐,这样可以减少应用在运行时的内存消耗。像Google Play 还强制要求开发者上传的应用必须是经过 zipAlign 的,

简单混淆规则

从上面说明知道,我们要编写的混淆规则就是写在 proguard-rules.pro 文件中,在 AS 左边列表中很好找到,每个 module 都有自己的 proguard-rules.pro 文件,我们对应着改就可以。

下面说说简单的混淆规则,好多文章都讲的不是清楚:

保持类
-keep class cn.hadcn.test.*
-keep class cn.hadcn.test.**

一颗星表示只是保持该包下的类名,而子包下的类名还是会被混淆;两颗星表示把本包和所含子包下的类名都保持;

保持类及其中方法及变量

用以上方法保持类后,你会发现类名虽然未混淆,但里面的具体方法和变量命名还是变了,这时如果既想保持类名,又想保持里面的内容不被混淆,我们就需要以下方法了:

-keep class com.silence.test.* {*;}
-keep class com.silence.test.** {*;}
保持特定类

在此基础上,我们也可以使用 Java 的基本规则来保护特定类不被混淆,比如我们可以用 extend,implement 等这些 Java 规则。如下例子就避免所有继承 Activity 的类被混淆

-keep public class * extends android.app.Activity
保持类及其中内部类

如果我们要保留一个类中的内部类不被混淆则需要用$符号,如下例子表示保持 TestFragment 内部类 MyClass 中的所有 public 内容不被混淆。

-keep class com.silence.TestFragment$MyClass{public*;}
保持类及其中特定内容

再者,如果一个类中你不希望保持全部内容不被混淆,而只是希望保护类下的特定内容(包含类名),就可以使用

<init>;//匹配所有构造器
<fields>;//匹配所有域
<methods>;//匹配所有方法方法

你还可以在或前面加上 private 、public、native 等来进一步指定不被混淆的内容,如

-keep class cn.hadcn.test.One{public<methods>;}

表示 One 类下的所有public方法都不会被混淆,当然你还可以加入参数,比如以下表示用JSONObject 作为入参的构造函数不会被混淆

-keep class cn.hadcn.test.One{public<init>(org.json.JSONObject);}

复杂混淆规则

作用范围

应该注意到上面我们都保持类(使用 keep),实际还可以仅保持类成员,但是简单使用的话,上面应该足够了,其他的下面列举一下:

作用范围保持所指定类、成员所指定类、成员在压缩阶段没有被删除,才能被保持
类和类成员-keep-keepnames
仅类成员-keepclassmembers-keepclassmembernames
类和类成员(前提是成员都存在)-keepclasseswithmembers-keepclasseswithmembernames
默认包含的规则文件说明
#混淆时不生成大小写混合的类名
-dontusemixedcaseclassnames
#不忽略非公共的类库
-dontskipnonpubliclibraryclasses
#混淆过程中打印详细信息
-verbose

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

# Annotation注释不能混淆
-keepattributes *Annotation*
#对于NDK开发 本地的native方法不能被混淆
-keepclasseswithmembernames class * {
    native <methods>;
}
#保持View的子类里面的setget方法不被混淆(*代替任意字符)
-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文件引用到的,名字已经固定了的)
    enum 枚举

  3. 实现了 android.os.Parcelable 接口的

  4. Android R文件

  5. 数据库驱动…

  6. Android support 包等

  7. Android 的注释不能混淆

    -keepattributes *Annotation*
    
  8. 对于 NDK 开发 本地的 native 方法不能被混淆

    -keepclasseswithmembernames class * { native <methods>; }
    
其他不能混淆内容
  • 自定义控件不进行混淆

  • 枚举类不被混淆

  • 反射类不进行混淆

  • 实体类不被混淆

  • JS调用的Java方法

  • 四大组件不进行混淆

  • JNI 中调用类不进行混淆

  • Layout布局使用的View构造函数、android:onClick

  • Parcelable的子类和 Creator静态成员变量不混淆

  • 第三方开源库或者引用其他第三方的SDK包不进行混淆

混淆模板

对于特定的项目还有很多不能被混淆的,需要我们自己写规则来指示,将在下面来说明:

#压缩级别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属性如privatepublic 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{*;}
......

更多混淆模板说明,可以参考下面这篇博客:

https://www.jianshu.com/p/90feb5c50cce

实用例子

关闭 Log日志

一般我们开发的时候会自己写一个 Log 封装类,去解决正式版日志的开关,但是其实使用混淆文件也能起到正式版不输出日志的用处,还能直接作用到代码上,不用怕忘记删 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(...);
}

反推崩溃日志

混淆后根据 Crash 崩溃日志反推代码

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

在这里插入图片描述

dump.txt 混淆后类的内部结构说明;

mapping.txt 混淆前与混淆后名称对应关系;

seeds.txt 经过了一系列keep语句的保持,没有被混淆的类,成员的名称列表文件。

usage.txt 经过压缩后被删除的没有使用的代码,方法…等的名称的列表文件


retrace工具:

混淆反推工具为 etrace.sh( Mac 平台)或者 retrace.bat(Windows平台),该工具在sdk根目录\tools\proguard\bin\retrace.sh ( Windows 平台类似);

命令格式:

./retrace.sh [mapping.txt目录] [崩溃日历目录]
retrace.bat [mapping.txt目录] [崩溃日历目录])

使用说明

  • 第一步:保存 Crash 日志如下:截取日志保存为 txt 文件如:bug.txt。

    03-21 03:09:32.389: E/AndroidRuntime(3582): FATAL EXCEPTION: main
    03-21 03:09:32.389: E/AndroidRuntime(3582): Process: com.dev.demo, PID: 3582
    03-21 03:09:32.389: E/AndroidRuntime(3582): java.lang.NullPointerException: Attempt to invoke virtual method 'boolean java.lang.String.equals(java.lang.Object)' on a null object reference
    03-21 03:09:32.389: E/AndroidRuntime(3582):     at com.dev.demo.two.b.b(Unknown Source)
    03-21 03:09:32.389: E/AndroidRuntime(3582):     at com.dev.demo.a.a.printTest(Unknown Source)
    03-21 03:09:32.389: E/AndroidRuntime(3582):     at com.dev.demo.MainActivity.i(Unknown Source)
    03-21 03:09:32.389: E/AndroidRuntime(3582):     at com.dev.demo.MainActivity.a(Unknown Source)
    03-21 03:09:32.389: E/AndroidRuntime(3582):     at com.dev.demo.MainActivity$1.onClick(Unknown Source)
    
  • 第二步:删掉Crash日志中 **03-21 03:09:32.389: E/AndroidRuntime(3582)😗*部分,否则无法反推还原。删除后如下:

    FATAL EXCEPTION: main
    Process: com.dev.demo, PID: 3582
    java.lang.NullPointerException: Attempt to invoke virtual method 'boolean java.lang.String.equals(java.lang.Object)' on a null object reference
    at com.dev.demo.two.b.b(Unknown Source)
    at com.dev.demo.a.a.printTest(Unknown Source)
    at com.dev.demo.MainActivity.i(Unknown Source)
    at com.dev.demo.MainActivity.a(Unknown Source)
    at com.dev.demo.MainActivity$1.onClick(Unknown Source)
    
  • 第三步:打开命令窗口输入命令:如 😗*./retrace.sh mapping.txt bug.txt ** 如下:

    在这里插入图片描述

    按确认后得到结果如下:

    FATAL EXCEPTION: main
    Process: com.dev.demo, PID: 3582
    java.lang.NullPointerException: Attempt to invoke virtual method 'boolean java.lang.String.equals(java.lang.Object)' on a null object reference
    at com.dev.demo.two.ClassTwoThree.void test()(Unknown Source)
    at com.dev.demo.one.ClassOneOne.void printTest()(Unknown Source)
    at com.dev.demo.MainActivity.void printWifi()(Unknown Source)
    at com.dev.demo.MainActivity.void access$000(com.dev.demo.MainActivity)(Unknown Source)
    at com.dev.demo.MainActivity$1.void onClick(android.view.View)(Unknown Source) ```
    
可视化工具

除了有 retrace.sh 工具外还有可视化工具,和 retrace.sh 同目录下 proguardgui.sh,这是一块 Progurad 的可视化工具(Windows平台类似):

在这里插入图片描述

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

-keepattributes SourceFile, LineNumberTable

这样Crash日志里面就能保留类名称和行号了。

结语

以上就是我对混淆文件的一些总结,收集了一些资料整合了一下,具体参考文章如下:

希望本文对读者有所帮助!

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐