关于Android混淆

在 Android 日常开发过程中,混淆是我们开发 App 的一项必不可少的技能。只要是我们亲身经历过 App 打包上线的过程,或多或少都需要了解一些代码混淆的基本操作。那么,混淆到底是什么?它的好处有哪些?具体效果如何?别急,下面我们来一一探索它的"独特"魅力。

混淆简介

代码混淆(Obfuscated code)是将程序中的代码以某种规则转换为难以阅读和理解的代码的一种行为。
混淆的好处

混淆的好处也就是目的,分为三点

  • 令APK难以被逆向工程,即很大程度上增加反编译的成本;

  • 减少APK包的大小;因为开启了混淆,伴随着在打包时会移除无用资源;

  • 移除方法数64k引用的限制

对比一下混淆和未混淆的对比图:
未混淆

关于Android混淆_第1张图片
image.png

已混淆

关于Android混淆_第2张图片
image.png

我们可以进行对比,首先包的体积变小了;还有就是经过混淆处理之后,我们的 APK 中包名、类名、成员名等都被替换为随机、无意义的名称,增加了代码阅读和理解的困难程度,提高了反编译的成本。

Android 当中的混淆

在 Android 中,我们平常所说的"混淆"其实有两层意思,一个是 Java 代码的混淆,另外一个是资源的压缩。其实这两者之间并没有什么关联,只不过习惯性地放在一起来使用。那么,说了这么多,Android 平台上到底该如何开启混淆呢?

  • 在项目中的build.Gradle中
 buildTypes {
       release {
           signingConfig signingConfigs.release
           // 如果没有提供混淆规则文件,则设置默认的混淆规则文件(SDK/tools/proguard/proguard-android.txt)
           pseudoLocalesEnabled false
           // 不显示Log
           buildConfigField "boolean", "LOG_DEBUG", "false"
           //Zipalign优化
           zipAlignEnabled false
           // 移除无用的resource文件
           shrinkResources false
           //是否Debug
           debuggable false
           //  minifyEnabled true
           //是否混淆
           minifyEnabled false
           //加载默认混淆配置文件
           proguardFile 'C:/workSpace/project/patient/proguard-rules.pro'
       }

以上就是开启混淆的基本操作了,通过 minifyEnabled 设置为 true 来开启混淆。同时,可以设置 shrinkResources 为 true 来开启资源的压缩。不难看出,我们一般在打 release 包时才启用混淆,因为混淆会增加额外的编译时间,所以不建议在 debug 模式下启用。此外,需要注意的是:只有在启用混淆的前提下开启资源压缩才会有效!以上代码中的 proguard-android.txt 表示 Android 系统为我们提供的默认混淆规则文件,而 proguard-rules.pro则是我们想要自定义的混淆规则,至于如何自定义混淆规则我们将在接下来会讲到。
特别注意的一点

shrinkResource 和 zipAlignEnable 的属性只有设置了minfyEnableb==true中才会起作用,也就是说,只有开启了混淆后才会起到作用,不然单独开启那两个,在编译时会报错;

代码混淆

其实,Java 平台为我们提供了 Proguard 混淆工具来帮助我们快速地对代码进行混淆。根据 Java 官方介绍,Proguard 对应的具体中文定义如下:

  • 它是一个包含代码文件压缩、优化、混淆和校验等功能的工具
  • 它能够检测并删除无用的类、变量、方法和属性
  • 它能够优化字节码并删除未使用的指令
  • 它能够将类、变量和方法的名字重命名为无意义的名称从而达到混淆效果
  • 最后,它还会校验处理后的代码,主要针对 Java 6 及以上版本和 Java ME

资源压缩

Android 中,编译器为我们提供了另外一项强大的功能:资源的压缩。资源压缩能够帮助我们移除项目及依赖仓库中未使用到的资源,有效地降低了apk包的大小。由于资源压缩与代码混淆是协同工作,所以,如果需要开启资源的压缩,切记要先开启代码混淆,否则会出现以下问题:

ERROR: Removing unused resources requires unused code shrinking to be turned on. See 
http://d.android.com/r/tools/shrink-resources.html for more information.
Affected Modules: app

#### proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

自定义保留资源

当我们开启了资源压缩之后,系统会默认替我们移除所有未使用的资源,假如我们需要保留某些特定的资源,可以在我们项目中创建一个被 标记的 XML 文件(如 res/raw/keep.xml),并在 tools:keep 属性中指定每个要保留的资源,在 tools:discard 属性中指定每个要舍弃的资源。这两个属性都接受逗号分隔的资源名称列表。同样,我们可以使用字符 * 作为通配符。如:



启用严格检查模式

正常情况下,资源压缩器可准确判定系统是否使用了资源。不过,如果您的代码(包含库)调用 Resources.getIdentifier(),这就表示您的代码将根据动态生成的字符串查询资源名称。这时,资源压缩器会采取防御性行为,将所有具有匹配名称格式的资源标记为可能已使用,无法移除。例如,以下代码会使所有带 img_ 前缀的资源标记为已使用:

String name = String.format("img_%1d", angle + 1);
res = getResources().getIdentifier(name, "drawable", getPackageName());
这时,我可以开启资源的严格审查模式,只会保留确定已使用的资源。

移除备用资源

Gradle 资源压缩器只会移除未被应用引用的资源,这意味着它不会移除用于不同设备配置的备用资源。必要时,我们可以使用 Android Gradle 插件的 resConfigs 属性来移除您的应用不需要的备用资源文件(常见的有用于国际化支持的 strings.xml,适配用的 layout.xml 等):
android {
defaultConfig {
...
//保留中文和英文国际化支持
resConfigs "en", "zh"
}
}

自定义混淆规则

keep 命令

这里说的 keep 命令指的是一系列以 -keep 开头的命令,它主要用来保留 Java 中不需要进行混淆的元素。以下是常见的 -keep 命令:

  • -keep
    作用:保留指定的类和成员,防止被混淆处理。例如:
# 保留包:com.moos.media.entity 下面的类以及类成员
-keep public class com.moos.media.entity.**
# 保留类:NumberProgressBar
-keep public class com.moos.media.widget.NumberProgressBar {*;}
  • -keepclassmembers
    作用:保留指定的类的成员(变量/方法),它们将不会被混淆。如:
# 保留类的成员:MediaUtils类中的特定成员方法
-keepclassmembers class com.moos.media.MediaUtils {
 public static *** getLocalVideos(android.content.Context);
 public static *** getLocalPictures(android.content.Context);
}
  • -keepclasseswithmembers
    作用:保留指定的类和其成员(变量/方法),前提是它们在压缩阶段没有被删除。与-keep 使用方式类似:
# 保留类:BaseMediaEntity 的子类
-keepclasseswithmembers public class * extends com.moos.media.entity.BaseMediaEntity{*;}
# 保留类:OnProgressBarListener接口的实现类
-keep public class * implements com.moos.media.widget.OnProgressBarListener {*;}
  • @Keep
    除了以上方式,你也可以选择使用 @Keep 注解来保留期望代码,防止它们被混淆处理。比如,我们通过 @Keep 修饰一个类来保留它不被混淆:
@Keep
data class CloudMusicBean(var createDate: String,
 var id: Long,
 var name: String,
 var url: String,
 val imgUrl: String)
  • 同样地,我们也可以让 @Keep 来修饰方法或者字段进而保留它们。

其他命令

dontwarn

-dontwarn 命令一般在我们引入新的 library 时会使用到,常用于处理 library 中无法解决的警告。如:

-dontwarn com.google.android.material.**

-dontnote com.google.android.material.**

-dontwarn androidx.**
其他 Android 默认混淆规则
这些不建议改动,直接copy就行,这就是官方的写法
#混淆时不生成大小写混合的类名
-dontusemixedcaseclassnames
#不跳过非公共的库的类
-dontskipnonpubliclibraryclasses
#混淆过程中记录日志
-verbose
#关闭预校验
-dontpreverify
#关闭优化
-dontoptimize
#保留注解
-keepattributes *Annotation*
#保留所有拥有本地方法的类名及本地方法名
-keepclasseswithmembernames class * {
 native ;
}
#保留自定义View的get和set方法
-keepclassmembers public class * extends android.view.View {
 void set*(***);
 *** get*();
}
#保留Activity中View及其子类入参的方法,如: onClick(android.view.View)
-keepclassmembers class * extends android.app.Activity {
 public void *(android.view.View);
}
#保留枚举
-keepclassmembers enum * {
 **[] $VALUES;
 public *;
}
#保留序列化的类
-keepclassmembers class * implements android.os.Parcelable {
 public static final android.os.Parcelable$Creator CREATOR;
}
#保留R文件的静态成员
-keepclassmembers class **.R$* {
 public static ;
}
-dontwarn android.support.**
-keep class android.support.annotation.Keep
-keep @android.support.annotation.Keep class * {*;}
-keepclasseswithmembers class * {
 @android.support.annotation.Keep ;
}
-keepclasseswithmembers class * {
 @android.support.annotation.Keep ;
}
-keepclasseswithmembers class * {
 @android.support.annotation.Keep (...);
}

混淆"黑名单"

我们在了解了混淆的基本命令之后,很多人应该还是一头雾水:到底哪些内容该混淆?其实,我们在使用代码混淆时,ProGuard 对我们项目中大部分代码进行了混淆操作,为了防止编译时出错,我们应该通过 keep 命令保留一些元素不被混淆。所以,我们只需要知道哪些元素不应该被混淆:

枚举

项目中难免可能会用到枚举类型,然而它不能参与到混淆当中去。原因是:枚举类内部存在 values 方法,混淆后该方法会被重新命名,并抛出 NoSuchMethodException。庆幸的是,Android 系统默认的混淆规则中已经添加了对于枚举类的处理,我们无需再去做额外工作。想了解更多枚举内部细节可以去查看源码,篇幅有限不再细说。

被反射的元素

被反射使用的类、变量、方法、包名等不应该被混淆处理。原因在于:代码混淆过程中,被反射使用的元素会被重命名,然而反射依旧是按照先前的名称去寻找元素,所以会经常发生 NoSuchMethodException 和 NoSuchFiledException 问题。

实体类

实体类即我们常说的"数据类",当然经常伴随着序列化与反序列化操作。很多人也应该都想到了,混淆是将原本有特定含义的"元素"转变为无意义的名称,所以,经过混淆的"洗礼"之后,序列化之后的 value 对应的 key 已然变为没有意义的字段,这肯定是我们不希望的。同时,反序列化的过程创建对象从根本上来说还是借助于反射,混淆之后 key 会被改变,所以也会违背我们预期的效果。

四大组件

Android 中的四大组件同样不应该被混淆。原因在于:

  • 四大组件使用前都需要在 AndroidManifest.xml 文件中进行注册声明,然而混淆处理之后,四大组件的类名就会被篡改,实际使用的类与 manifest 中注册的类并不匹配,故而出错。
  • 其他应用程序访问组件时可能会用到类的包名加类名,如果经过混淆,可能会无法找到对应组件或者产生异常。

JNI 调用的Java 方法

当 JNI 调用的 Java 方法被混淆后,方法名会变成无意义的名称,这就与 C++ 中原本的 Java 方法名不匹配,因而会无法找到所调用的方法。
其他不应该被混淆的

自定义控件不需要被混淆
JavaScript 调用 Java 的方法不应混淆
Java 的 native 方法不应该被混淆
项目中引用的第三方库也不建议混淆

涨姿势的操作

经过上文的介绍,我们知道,APK 在经过代码混淆处理后,包名、类名、成员名被转化为无意义、难以理解的名称,增加反编译的成本。Android ProGuard 为我们提供了默认的"混淆字典",即将元素名称转为英文小写字母的形式。那么,我们可以定义自己的混淆字典吗?卖个关子,我们先来看一张效果图:


关于Android混淆_第3张图片
image.png

这个波操作是不是有点"出类拔萃"了?哈哈,就不卖关子了,其实很简单,只要生成一套自己的 txt 格式的混淆字典,然后在混淆规则 Proguard-rules.pro 中应用一下即可:


关于Android混淆_第4张图片
image.png

你可能感兴趣的:(关于Android混淆)