详解Android中的混淆规则

前言

相信不少开发在发布时被代码混淆弄得一头雾水,大多都是百度一下,看看别人的混淆规则,复制粘贴拿来试一试,直到最后弄成了,也不知道为什么混淆规则要这么写,以及混淆都对自己的代码做了什么?不要问我为什么这么清楚,因为我也是这么过来的?

什么是混淆?

混淆就是对发布出去的程序进行重新组织和处理,使得处理后的代码与处理前代码完成相同的功能,而混淆后的代码很难被反编译,即使反编译成功也很难得出程序的真正语义。被混淆过的程序代码,仍然遵照原来的档案格式和指令集,执行结果也与混淆前一样,只是混淆器将代码中的所有变量、函数、类的名称变为简短的英文字母代号,在缺乏相应的函数名和程序注释的情况下,即使被反编译,也将难以阅读。同时混淆是不可逆的,在混淆的过程中一些不影响正常运行的信息将永久丢失,这些信息的丢失使程序变得更加难以理解。

其实混淆包含了一系列的复杂操作,上面的解释只是通俗意义上的解释,Android中的混淆分为两部分,代码压缩和资源压缩,而代码压缩又包含了压缩、优化、混淆、预校验四部分,其中优化和预校验两个步骤在Android中默认是关闭的。

  • shrink:压缩,移除无效的类、类成员、方法、属性等。
  • optimize:优化,分析和优化方法的二进制代码;根据proguard-android-optimize.txt中的描述,优化可能会造成一些潜在风险,不能保证在所有版本的Dalvik上都正常运行。
  • obfuscate:混淆,把类名、属性名、方法名替换为简短且无意义的名称。
  • preverify:预校验,添加预校验信息。这个预校验是作用在Java平台上的,Android平台上不需要这项功能,去掉之后可以加快混淆速度。

对混淆有了一个初步的概念框架后,在看看Android官方是怎么介绍它的。

压缩代码和资源


要尽可能减小 APK 文件,您应该启用压缩来移除发布构建中未使用的代码和资源。此页面介绍如何执行该操作,以及如何指定要在构建时保留或舍弃的代码和资源。

代码压缩通过 ProGuard 提供,ProGuard 会检测和移除封装应用中未使用的类、字段、方法和属性,包括自带代码库中的未使用项(这使其成为以变通方式解决 64k 引用限制的有用工具)。ProGuard 还可优化字节码,移除未使用的代码指令,以及用短名称混淆其余的类、字段和方法。混淆过的代码可令您的 APK 难以被逆向工程,这在应用使用许可验证等安全敏感性功能时特别有用。

资源压缩通过适用于 Gradle 的 Android 插件提供,该插件会移除封装应用中未使用的资源,包括代码库中未使用的资源。它可与代码压缩发挥协同效应,使得在移除未使用的代码后,任何不再被引用的资源也能安全地移除。

压缩代码


要通过 ProGuard 启用代码压缩,请在 build.gradle 文件内相应的构建类型中添加 minifyEnabled true

请注意,代码压缩会拖慢构建速度,因此您应该尽可能避免在调试构建中使用。不过,重要的是您一定要为用于测试和发布的最终 APK 启用代码压缩,因为如果您不能充分地自定义要保留的代码,可能会引入错误。

例如,下面这段来自 build.gradle 文件的代码用于为发布构建启用代码压缩:

android {
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                    'proguard-rules.pro'
        }
    }
    ...
}

:Android Studio 会在使用 Instant Run 时停用 ProGuard。如果您需要为增量式构建压缩代码,请尝试试用 Gradle 压缩器

除了 minifyEnabled 属性外,还有用于定义 ProGuard 规则的 proguardFiles 属性:

  • getDefaultProguardFile('proguard-android.txt') 方法可从 Android SDK tools/proguard/ 文件夹获取默认的 ProGuard 设置。
  • proguard-rules.pro 文件用于添加自定义 ProGuard 规则。默认情况下,该文件位于模块根目录(build.gradle 文件旁)。

提示:要想做进一步的代码压缩,请尝试使用位于同一位置的 proguard-android-optimize.txt 文件。它包括相同的 ProGuard 规则,但还包括其他在字节码一级(方法内和方法间)执行分析的优化,以进一步减小 APK 大小和帮助提高其运行速度。(混淆中的优化部分,默认关闭的)

每次构建时 ProGuard 都会输出下列文件:

dump.txt

说明 APK 中所有类文件的内部结构。

mapping.txt

提供原始与混淆过的类、方法和字段名称之间的转换。

seeds.txt

列出未进行混淆的类和成员。

usage.txt

列出从 APK 移除的代码。

这些文件保存在 <module-name>/build/outputs/mapping/release/ 中。

自定义要保留的代码

对于某些情况,默认 ProGuard 配置文件 (proguard-android.txt) 足以满足需要,ProGuard 会移除所有(并且只会移除)未使用的代码。不过,ProGuard 难以对许多情况进行正确分析,可能会移除应用真正需要的代码。举例来说,它可能错误移除代码的情况包括:

  • 当应用引用的类只来自 AndroidManifest.xml 文件时
  • 当应用调用的方法来自 Java 原生接口 (JNI) 时
  • 当应用在运行时(例如使用反射或自检)操作代码时

Android默认已经帮我们保持了AndroidManifest.xml里面所有的类,这里的第一条我有些不明白是什么意思

测试应用应该能够发现因不当移除的代码而导致的错误,但您也可以通过查看 <module-name>/build/outputs/mapping/release/ 中保存的 usage.txt 输出文件来检查移除了哪些代码。

要修正错误并强制 ProGuard 保留特定代码,请在 ProGuard 配置文件中添加一行 -keep 代码。例如:

-keep public class MyClass

-keep的相关使用后面另说

或者,您可以向您想保留的代码添加 @Keep 注解。在类上添加 @Keep 可原样保留整个类。在方法或字段上添加它可完整保留方法/字段(及其名称)以及类名称。请注意,只有在使用注解支持库时,才能使用此注解。

通过 Instant Run 启用代码压缩

如果代码压缩在您增量构建应用时非常重要,请尝试适用于 Gradle 的 Android 插件内置的试用代码压缩器。与 ProGuard 不同,此压缩器支持 Instant Run

您也可以使用与 ProGuard 相同的配置文件来配置 Android 插件压缩器。但是,Android 插件压缩器不会对您的代码进行混淆处理或优化,它只会删除未使用的代码。因此,您应该仅将其用于调试构建,并为发布构建启用 ProGuard,以便对发布 APK 的代码进行混淆处理和优化。

要启用 Android 插件压缩器,只需在 "debug" 构建类型中将 useProguard 设置为 false(并保留 minifyEnabled 设置 true):

android {
    buildTypes {
        debug {
            minifyEnabled true
            useProguard false
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                    'proguard-rules.pro'
        }
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                    'proguard-rules.pro'
        }
    }
}

压缩资源


资源压缩只与代码压缩协同工作。代码压缩器移除所有未使用的代码后,资源压缩器便可确定应用仍然使用的资源。这在您添加包含资源的代码库时体现得尤为明显 - 您必须移除未使用的库代码,使库资源变为未引用资源,才能通过资源压缩器将它们移除。

要启用资源压缩,请在 build.gradle 文件中将 shrinkResources 属性设置为 true(在用于代码压缩的 minifyEnabled 旁边)。例如:

android {
    ...
    buildTypes {
        release {
            shrinkResources true
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                    'proguard-rules.pro'
        }
    }
}

shrinkResources只有在 minifyEnabled启用后才有用

:资源压缩器目前不会移除 values/ 文件夹中定义的资源(例如字符串、尺寸、样式和颜色)。这是因为 Android 资源打包工具 (AAPT) 不允许 Gradle 插件为资源指定预定义版本。

自定义要保留的资源

如果您有想要保留或舍弃的特定资源,请在您的项目中创建一个包含 <resources> 标记的 XML 文件,并在 tools:keep 属性中指定每个要保留的资源,在 tools:discard 属性中指定每个要舍弃的资源。这两个属性都接受逗号分隔的资源名称列表。您可以使用星号字符作为通配符。

例如:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"
    tools:discard="@layout/unused2" />

将该文件保存在项目资源中,例如,保存在 res/raw/keep.xml。构建不会将该文件打包到 APK 之中。

启用严格引用检查

正常情况下,资源压缩器可准确判定系统是否使用了资源。不过,如果您的代码调用 Resources.getIdentifier()(或您的任何库进行了这一调用 - AppCompat 库会执行该调用),这就表示您的代码将根据动态生成的字符串查询资源名称。当您执行这一调用时,默认情况下资源压缩器会采取防御性行为,将所有具有匹配名称格式的资源标记为可能已使用,无法移除。

例如,以下代码会使所有带 img_ 前缀的资源标记为已使用。

String name = String.format("img_%1d", angle + 1);
res = getResources().getIdentifier(name, "drawable", getPackageName());

资源压缩器还会浏览代码以及各种 res/raw/ 资源中的所有字符串常量,寻找格式类似于 file:///android_res/drawable//ic_plus_anim_016.png 的资源网址。如果它找到与其类似的字符串,或找到其他看似可用来构建与其类似的网址的字符串,则不会将它们移除。

这些是默认情况下启用的安全压缩模式的示例。但您可以停用这一“有备无患”处理方式,并指定资源压缩器只保留其确定已使用的资源。要执行此操作,请在 keep.xml 文件中将 shrinkMode 设置为 strict,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:shrinkMode="strict" />

如果您已启用严格压缩模式,并且代码也引用了包含动态生成字符串的资源(如上所示),则必须利用 tools:keep 属性手动保留这些资源。

移除未使用的备用资源

Gradle 资源压缩器只会移除未被您的应用代码引用的资源,这意味着它不会移除用于不同设备配置的备用资源。必要时,您可以使用 Android Gradle 插件的 resConfigs 属性来移除您的应用不需要的备用资源文件。

例如,如果您使用的库包含语言资源(例如使用的是 AppCompat 或 Google Play 服务),则 APK 将包括这些库中消息的所有已翻译语言字符串,无论应用的其余部分是否翻译为同一语言。如果您想只保留应用正式支持的语言,则可以利用 resConfig 属性指定这些语言。系统会移除未指定语言的所有资源。

下面这段代码展示了如何将语言资源限定为仅支持英语和法语:

android {
    defaultConfig {
        ...
        resConfigs "en", "fr"
    }
}

Google的API是相当细致了,对混淆的使用、配置和混淆文件都做了详细的介绍。相信大部分开发接触混淆都是代码压缩上,以前我所了解的资源压缩只有那一句配置代码,看了文档才知道资源压缩其实涉及很多东西,可以做非常细致的定制,所以,没事还是多看看官方文档。。还有,上面只是文档中的部分内容,想要看完整介绍的可以点击这个

我们该混淆什么?


其实上面的文档中提到,Android默认的 ProGuard 配置文件 (proguard-android.txt) 足以满足需要,ProGuard 会移除所有(并且只会移除)未使用的代码。那么我们就看看默认的 ProGuard 配置文件里都做了哪些配置

# 混合时不使用大小写混合,混合后的类名为小写
-dontusemixedcaseclassnames
# 不去忽略非公共库的类
-dontskipnonpubliclibraryclasses
-verbose

#关闭字节码优化
-dontoptimize
#关闭预校验
-dontpreverify

# 保留Annotation不混淆
-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
#保持集成自view对象的get、set方法
-keepclassmembers public class * extends android.view.View {
   void set*(***);
   *** get*();
}

#保持Activity中的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);
}

#保持枚举类成员属性
# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

#保持Android的Parcelable序列化对象
-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.**

# Understand the @Keep support annotation.
# 保持keep注解相关类和类成员
-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>(...);
}

哈哈,是不是有很多很眼熟的配置,百度混淆规则的时候,可没少看到吧,其实Android默认规则里就有了,所以根本不需要再添加到自己的混淆规则中。

那么我们要在自己的混淆文件中做哪些配置呢?

  1. 首先,照着上面先把自己配置文件中多余的规则去掉,不需要。
  2. 第三方依赖,需要混淆的话应该都有标明。
  3. 实体类,比如根据json转换相关的类。
  4. 跟反射相关、JNI相关的类和属性、方法以及应用引用的类只来自 AndroidManifest.xml 文件时

我的项目就按上面的配置来的,一点事儿没有,应该能满足大部分项目的混淆需求了,如果还需要加什么,到时跟着日志提示往配置里加就是了。

Keep指令的用法


配置文件里常见的什么-keep、-keepnames、-keepclassmembers...都是什么意思,又有啥区别?

作用范围 防止移除或重命名 防止重命名
类和类成员 -keep -keepnames
类成员 -keepclassmembers -keepclassmembernames
如果存在某成员,保留该成员和类  -keepclasseswithmembers -keepclasseswithmembernames

简单来讲,后缀带names的指令只能防止被重命名,如果该资源没有被用到,还是会被移除,而后缀不带names的指令,不管资源有没有被用到,都会保留。至于带with的两条指令,看着好像和keep、keepnames都是保持类和类成员的,其实它们的区别看butterknife的混淆规则就知道了

-keepclasseswithmembernames class * {
    @butterknife.* <fields>;
}
-keepclasseswithmembernames class * {
    @butterknife.* <methods>;
}

这啥意思呢,这两句声明了凡是使用了butterknife注解的方法和属性和它们所在的类保持不混淆,如果有就不混淆,没有则该咋样咋样,相比keep、keepnames更加灵活一些。

示例

com.***.***.activity.MainActivity -> com.***.***.activity.MainActivity:
    android.widget.LinearLayout mLlContent -> mLlContent
    android.widget.RadioGroup mRadioGroup -> mRadioGroup
    android.widget.RadioButton mRbFile -> mRbFile
    android.support.v4.app.Fragment mCurrentFrag -> d
    android.support.v4.app.FragmentManager mFragmentManager -> e
    android.support.v4.app.FragmentTransaction mTransaction -> f

可以看到用了butterknife注解的属性和类没有混淆,其他属性还是混淆了。其实如果不知道用哪个合适,用keep准没错,简单粗暴。

keep指令修饰符


一句完整的keep指令包括

[保持命令] [类] {
    [成员] 
}

“类”代表类相关的限定条件,它将最终定位到某些符合该限定条件的类。它的内容可以使用:

  • 具体的类
  • 访问修饰符(public、protected、private)
  • 通配符*,匹配任意长度字符,但不含包名分隔符(.)
  • 通配符**,匹配任意长度字符,并且包含包名分隔符(.)
  • extends,即可以指定类的基类
  • implement,匹配实现了某接口的类
  • $,内部类

“成员”代表类成员相关的限定条件,它将最终定位到某些符合该限定条件的类成员。它的内容可以使用:

  • <init> 匹配所有构造器
  • <fields> 匹配所有属性
  • <methods> 匹配所有方法
  • 通配符*,匹配任意长度字符,但不含包名分隔符(.)
  • 通配符**,匹配任意长度字符,并且包含包名分隔符(.)
  • 通配符***,匹配任意参数类型
  • …,匹配任意长度的任意类型参数。比如void test(…)就能匹配任意 void test(String a) 或者是 void test(int a, String b) 这些方法。
  • 访问修饰符(public、protected、private)

常用keep指令示例


不移除和混淆某个类和类成员

-keep class com.test.demo.TestBean{*;}

不移除和混淆某个类的子类的类成员

-keepclassmember class * extends com.test.demo.Test{*;}

不移除和混淆某个包下的所有类及类成员(包括子包)

-keep class com.test.demo.**{*;}

不混淆某个类的公共方法和公共静态属性

-keepclassmembernames class com.test.demo.Test{
    public <methods>;
    public static <fields>;
}

不混淆被@keep注解标注的构造方法

-keepclasseswithmembers class * {
    @android.support.annotation.Keep <init>(...);
}
发布了45 篇原创文章 · 获赞 18 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/Ever69/article/details/88422926