Android进阶之ProGuard代码混淆

简介

Proguard工具通过移除无用的代码以及使用语义隐晦的名称来重命名类、字段和方法,从而达到压缩、优化和混淆代码的目的。最终您将获得一个较小的 .apk 文件,此文件更难于进行反向工程。由于 ProGuard 会使应用更难于进行反向工程,因此当应用使用对安全性要求极高的功能时(例如,当您向应用授予许可时),您必须使用此工具。

ProGuard 已集成到 Android 构建系统,所以您无需手动调用此工具。只有当您在发布模式下构建应用时,ProGuard 才会运行,因此当您在调试模式下构建应用时,就无需处理混淆后的代码。是否运行 ProGuard 完全由您决定,但我们强烈建议您运行该工具。

本文介绍如何启用和配置 ProGuard,以及如何使用 retrace 工具解码混淆后的堆栈跟踪信息。

除了基本的Proguard混淆外,还有一些其他的混淆方式和工具。例如:混淆资源

美团资源混淆

微信资源混淆

开启Proguard

  1. Ant、Eclipse构建

    <project_root>/project.properties 文件中设置 proguard.config 属性。该路径可以是绝对路径,也可以是项目根目录的相对路径。
    
    proguard.config=proguard.cfg
    
  2. AndroidStudio-Gradle构建

    android {
        buildTypes {
            release {
                minifyEnabled true
                proguardFile getDefaultProguardFile('proguard-android.txt')
            }
        }
    
        productFlavors {
            flavor1 {
            }
            flavor2 {
                proguardFile 'some-other-rules.txt'
            }
        }
    }
    

提示:

getDefaultProguardFile()可以返回这两个文件的绝对路径。
proguardFile 可以配置多个混淆文件

配置Proguard

在某些情况下,proguard.cfg或proguard-android.txt 文件中的默认配置足以满足您的需求。不过,在很多情况下,ProGuard 很难做出正确分析,因此可能会移除它认为无用而实际上您的应用却需要的代码。部分示例如下:

  • 一个只在 AndroidManifest.xml 文件中引用的类
  • 一个通过 JNI 调用的方法
  • 动态引用的字段和方法

默认的 proguard.cfg或proguard-android.txt 文件旨在涵盖一般的使用情形,但您可能会遇到异常情况,例如 ClassNotFoundException(此异常情况会在 ProGuard 删除您的应用调用的整个类时发生)。

您可以通过在 proguard.cfg或proguard-android.txt 文件中添加一个 -keep 行,来修复因 ProGuard 在删除代码而造成的错误。例如:

-keep public class <MyClass>

在使用 -keep 选项时,您既有许多选择也有不少需要注意的方面,因此我们强烈建议您阅读 ProGuard 手册,详细了解如何自定义您的配置文件。该手册中的“Keep 选项概述”和“示例”部分尤其有用;问题排查部分则概述了在 ProGuard 删除代码后您可能会遇到的其他常见问题。

下面给出一个常用的默认配置命令:

-include {fileame}  从给定的文件中读取配置参数
-libraryjars libs/xxxx.jar 指定库jar包
-keep public class * extends android.app.Activity 保留类不被删除
-keep class className$InnerName{ 保留内部类的属性和方法
    public <fields>;
    public <methods>;
}
-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();
}
-dontshrink 不压缩输入的类文件
-dontoptimize 不优化输入的类文件
-keepattributes *Annotation* 保留Annotation
-dontwarn xxx.xxx.** 不检查引用

Proguard产生的文件

当混淆后的代码输出堆栈跟踪信息时,方法名称会被混淆,即便仍能进行调试,难度也会很大。幸运的是,ProGuard 在每次运行时都会输出以下文件:

  1. dump.txt

    描述 .apk 文件中所有类文件的内部结构

  2. mapping.txt

    列出原始与混淆后的类、方法和字段名称之间的对应关系。

    Windows 上的 retrace.bat 脚本以及 Linux 或 Mac OS X 上的 retrace.sh 脚本可以将混淆后的堆栈跟踪信息转换成可读文件,此文件位于 /tools/proguard/ 目录中。执行 retrace 工具的语法如下:

    retrace.bat|retrace.sh [-verbose] mapping.txt [<stacktrace_file>]
    

    例如:

    retrace.bat -verbose mapping.txt obfuscated_trace.txt
    

    建议发布时应保留mapping.txt文件。

  3. seeds.txt

    列出未混淆的类和成员

  4. usage.txt

    列出从 .apk 删除的代码

@Keep注解来防止混淆

写到这,你是不是发现了一个问题:非常的麻烦、一点都不灵活,而且通过-keep的方式防止混淆那种有共同特征的类、属性或方式非常有用,但是没有共同特征的呢?

这里介绍一种比较新颖、轻快的方法,通过@Keep注解来灵活的防止混淆,用起来非常的灵活、快捷、方便,怎样用呢?像普通的注解一样,如下:

//防止混淆类
@Keep
public class Person {}

//防止混淆变量
@Keep
public String name;

//防止混淆方法
@Keep
public int getAge(){}

但是当你加上上面的注解后,发现@Keep并没有起作用,该混淆的还是混淆了,这是为什么呢?

原因目前Gradle还不支持@Keep混淆,Google只是定义好了一个这种注解,并没有实现它,也就是说@Keep目前只是一个空壳。这里我们来手动开启它,让它支持防止混淆,在你的proguard.cfg或proguard-android.txt配置文件里面加入以下代码:

#手动启用support keep注解
-dontskipnonpubliclibraryclassmembers
-printconfiguration
-keep,allowobfuscation @interface android.support.annotation.Keep
-keep @android.support.annotation.Keep class *
-keepclassmembers class * {
    @android.support.annotation.Keep *;
}

Proguard相关语法

后面的文件名,类名,或者包名等可以使用占位符代替
“?”表示一个字符 可以匹配多个字符,但是如果是一个类,不会匹配其前面的包名
“*”可以匹配多个字符,会匹配前面的包名。

  • 输入输出选项
    • -include filename
      从给定的文件中读取配置参数
    • -injars class_path
      输入(即使用的) jar文件路径
    • -outjars class_path
      输出 jar 路径
    • -libraryjars class_path
      指定的jar将不被混淆
    • -skipnonpubliclibraryclasses
      跳过(不混淆) jars中的 非public classes
    • -dontskipnonpubliclibraryclasses
      不跳过(混淆) jars中的 非public classes 默认选项
    • -dontskipnonpubliclibraryclassmembers
      不跳过 jars中的非public classes的members
    • -keepdirectories [directory_filter]
      指定目录 keep 在 out jars中
      • 保持不变的选项(混淆不进行处理的内容)
    • -keep {Modifier} {class_specification}
      保护指定的类文件和类的成员
    • -keepclassmembers {modifier} {class_specification}
      保护指定类的成员,如果此类受到保护他们会保护的更好
    • -keepclasseswithmembers {class_specification}
      保护指定的类和类的成员,但条件是所有指定的类和类成员是要存在。
    • -keepnames {class_specification}
      保护指定的类和类的成员的名称(如果他们不会压缩步骤中删除)
    • -keepclassmembernames {class_specification}
      保护指定的类的成员的名称(如果他们不会压缩步骤中删除)
    • -keepclasseswithmembernames {class_specification}
      保护指定的类和类的成员的名称,如果所有指定的类成员出席(在压缩步骤之后)
    • -printseeds {filename}
      列出类和类的成员-keep选项的清单,标准输出到给定的文件
      • 压缩选项
    • -dontshrink
      不启用 shrink。shrink操作默认启用,主要的作用是将一些无效代码给移除,即没有被显示调用的代码。
    • -printusage [filename]
      打印被移除的代码,在标准输出
    • -whyareyoukeeping class_specification
      打印 在shrink过程中 为什么有些代码被 keep
      • 优化选项
    • -dontoptimize
      该选项表示 不启用。optimization,默认启用
      当不使用该选项时,下面的才有效
    • -optimizations optimization_filter
      根据optimization_filter指定要优化的文件
    • -optimizationpasses n
      优化数量 n
    • -assumenosideeffects class_specification
      优化时允许访问并修改类和类的成员的 访问修饰符,可能作用域会变大。
    • -mergeinterfacesaggressively
      合并接口,即使它们的实现类未实现合并后接口的所有方法。
      • 混淆选项
    • -dontobfuscate
      不混淆
    • -printmapping [filename]
      打印 映射旧名到新名
    • -applymapping filename
      打印相关
    • -obfuscationdictionary filename
      指定外部模糊字典
    • -classobfuscationdictionary filename
      指定class模糊字典
    • -packageobfuscationdictionary filename
      指定package模糊字典
    • -overloadaggressively
      过度加载,多个属性和方法使用相同的名字,只是参数和返回类型不同 可能各种异常
    • -useuniqueclassmembernames
      类和类成员都使用唯一的名字
    • -dontusemixedcaseclassnames
      不使用大小写混合类名
    • -keeppackagenames [package_filter]
      保持packagename 不混淆
    • -flattenpackagehierarchy [package_name]
      指定重新打包,所有包重命名,这个选项会进一步模糊包名 好东西
      将包里的类混淆成n个再重新打包到一个个的package中,注:混淆是有用,但是我用的时候安装会崩溃,不知道为什么?
    • -repackageclasses [package_name]
      将包里的类混淆成n个再重新打包到一个统一的package中 会覆盖flattenpackagehierarchy选项
    • -keepattributes [attribute_filter]
      混淆时可能被移除下面这些东西,如果想保留,需要用该选项。“Annotation、Exceptions, Signature, Deprecated, SourceFile, SourceDir, LineNumberTable”
      • 预校验选项
    • -dontpreverify
      不预校验,默认选项
      • 通用选项
    • -verbose
      打印日志
    • -dontnote [class_filter]
      不打印某些错误
    • -dontwarn [class_filter]
      不打印警告信息
    • -ignorewarnings
      忽略警告,继续执行
    • -printconfiguration [filename]
      打印配置文件
    • -dump [filename]
      指定打印类结构

demo示例

##--- For:android默认 ---
-optimizationpasses 5  # 指定代码的压缩级别
-allowaccessmodification #优化时允许访问并修改有修饰符的类和类的成员
-dontusemixedcaseclassnames  # 是否使用大小写混合
-dontskipnonpubliclibraryclasses  # 是否混淆第三方jar
-dontpreverify  # 混淆时是否做预校验
-verbose    # 混淆时是否记录日志
-ignorewarnings  # 忽略警告,避免打包时某些警告出现
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*  # 混淆时所采用的算法

-keepattributes *Annotation*
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService
-keepclasseswithmembernames class * { # 保持 native 方法不被混淆
    native <methods>;
}

-keepclassmembers public class * extends android.view.View {
   void set*(***);
   *** get*();
}

-keepclassmembers class * extends android.app.Activity {
   public void *(android.view.View);
}

-keepclassmembers enum * {  # 保持枚举 enum 类不被混淆
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

-keep class * implements android.os.Parcelable { # 保持 Parcelable 不被混淆
  public static final android.os.Parcelable$Creator *;
}

-keepclassmembers class **.R$* { #不混淆R文件
    public static <fields>;
}

-dontwarn android.support.**
##--- End android默认 ---

##--- For:不能被混淆的 ---
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Fragment
-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

##--- For:保持自定义控件类不被混淆 ---
-keepclasseswithmembers class * {
    public <init>(android.content.Context, android.util.AttributeSet);
}
-keepclasseswithmembers class * {
    public <init>(android.content.Context, android.util.AttributeSet, int);
}
##--- For:android-support-v4 ---
-dontwarn android.support.v4.**
-keep class android.support.v4.** { *; }
-keep interface android.support.v4.app.** { *; }
-keep class * extends android.support.v4.** { *; }
-keep public class * extends android.support.v4.**
-keep public class * extends android.support.v4.widget
-keep class * extends android.support.v4.app.** {*;}
-keep class * extends android.support.v4.view.** {*;}

##--- For:Serializable ---
-keep class * implements java.io.Serializable {*;}
-keepnames class * implements java.io.Serializable
-keepclassmembers class * implements java.io.Serializable {*;}

##--- For:Gson ---
-keepattributes *Annotation*
-keep class sun.misc.Unsafe { *; }
-keep class com.idea.fifaalarmclock.entity.***
-keep class com.google.gson.stream.** { *; }

##--- For:Remove 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(...);
}

##--- For:attributes(未启用) ---
#-keepattributes SourceFile,LineNumberTable # 保持反编译工具能看到代码的行数,以及release包安装后出现异常信息可以知道在哪行代码出现异常,建议不启用
-keepattributes *Annotation* #使用注解
-keepattributes Signature #过滤泛型  出现类型转换错误时,启用这个
#-keepattributes *Exceptions*,EnclosingMethod  #没试过,未知效果