代码混淆工具——Proguard

代码混淆的好处

  1. 类、方法、变量名变成短且无意义的名字,提高反编译后代码的阅读成本;
  2. 删除无用的类、方法与属性,缩减了 APK 包的大小;
  3. 对字节码进行优化,移除无用指令,应用运行更快。

混淆后 Crash 日志定位问题

一种低效的解决方法:自行对照混淆后生成的 mapping.txt 文件;但日常开发不建议用此法,推荐另一种方法:去混淆,Google Play 和 Bugly 都支持上传混淆文件。

没有上传到 Google Play/Bugly 也没关系,直接用 android-sdk/tools/proguard/bin/proguardgui 也可以去混淆。

⚠️注意:第 3 步中填入的错误堆栈每行都要以 “at” 开头:如:

at com.a.l.a.<init>(A:3)
at com.a.l.a.<init>(A:1)
at com.a.l.a$b.<clinit>(A:1)
at com.a.l.a$b.a(A:1)

ProGuard 和 R8 的混淆都可以去,老版本的脚本可能不支持 R8,更新下 sdk 即可。

混淆规则详解

1、混淆设置参数

-optimizationpasses 5                       # 代码混淆的压缩比例,值介于0-7,默认5
-verbose                                    # 混淆时记录日志
-dontoptimize                               # 不优化输入的类文件
-dontshrink                                 # 关闭压缩
-dontpreverify                              # 关闭预校验(作用于Java平台,Android不需要,去掉可加快混淆)
-dontoptimize                               # 关闭代码优化
-dontobfuscate                              # 关闭混淆
-ignorewarnings                             # 忽略警告
-dontwarn com.squareup.okhttp.**            # 指定类不输出警告信息
-dontusemixedcaseclassnames                 # 混淆后类型都为小写
-dontskipnonpubliclibraryclasses            # 不跳过非公共的库的类
-printmapping mapping.txt                   # 生成原类名与混淆后类名的映射文件mapping.txt
-useuniqueclassmembernames                  # 把混淆类中的方法名也混淆
-allowaccessmodification                    # 优化时允许访问并修改有修饰符的类及类的成员
-renamesourcefileattribute SourceFile       # 将源码中有意义的类名转换成SourceFile,用于混淆具体崩溃代码
-keepattributes SourceFile,LineNumberTable  # 保留行号
-keepattributes *Annotation*,InnerClasses,Signature,EnclosingMethod # 避免混淆注解、内部类、泛型、匿名类
-optimizations !code/simplification/cast,!field/ ,!class/merging/   # 指定混淆时采用的算法

2、保持不被混淆的设置

语法组成:

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

保持命令:

-keep                           # 防止类和类成员被移除或被混淆;
-keeppackagenames               # 防止包名被混淆,只对包名生效,不会影响包内类的混淆规则;
-keepnames                      # 防止类和类成员被混淆;
-keepclassmembers               # 防止类成员被移除或被混淆;
-keepclassmembernames           # 防止类成员被混淆;
-keepclasseswithmembers         # 防止拥有该成员的类和类成员被移除或被混淆;
-keepclasseswithmembernames     # 防止拥有该成员的类和类成员被混淆;

类:

  • 具体的类;
  • 访问修饰符 → public、private、protected;
  • 通配符(*) → 匹配任意长度字符,但不包含包名分隔符 (.) ;
  • 通配符(**) → 匹配任意长度字符,且包含包名分隔符 (.);
  • extends → 匹配实现了某个父类的子类;
  • implements → 匹配实现了某接口的类;
  • $ → 内部类;

成员:

  • 匹配所有构造器 → <init>
  • 匹配所有域 → “`;
  • 匹配所有方法 → <methods>
  • 访问修饰符 → publicprivateprotected
  • 除了 * 和 ** 通配符外,还支持 *** 通配符,匹配任意参数类型;
  • ... → 匹配任意长度的任意类型参数,如 void test(...) 可以匹配不同参数个数的 test 方法;

常用自定义混淆规则范例:

# 不混淆包名,多个包名用逗号分隔
-keeppackagenames com.test.demo,com.test.bug

## 不混淆以 com.test.demo 为前缀的包名,无论包层级有多少
-keeppackagenames com.test.demo.**

# 不混淆某个类的类名,及类中的内容
-keep class cn.coderpig.myapp.example.Test { *; }

# 不混淆指定包名下的类名,不包括子包下的类名
-keep class cn.coderpig.myapp*

# 不混淆指定包名下的类名,及类里的内容
-keep class cn.coderpig.myapp* {*;}

# 不混淆指定包名下的类名,包括子包下的类名
-keep class cn.coderpig.myapp**

# 不混淆某个类的子类
-keep public class * extends cn.coderpig.myapp.base.BaseFragment

# 不混淆实现了某个接口的类
-keep class * implements cn.coderpig.myapp.dao.DaoImp

# 不混淆类名中包含了"entity"的类,及类中内容
-keep class **.*entity*.** {*;}

# 不混淆内部类中的所有public内容
-keep class cn.coderpig.myapp.widget.CustomView$OnClickInterface {
  public *;
}

# 不混淆指定类的所有方法
-keep cn.coderpig.myapp.example.Test {
  public <methods>;
}

# 不混淆指定类的所有字段
-keep cn.coderpig.myapp.example.Test {
  public <fields>;
}

# 不混淆指定类的所有构造方法
-keep cn.coderpig.myapp.example.Test {
  public <init>;
}

# 不混淆指定参数作为形参的方法
-keep cn.coderpig.myapp.example.Test {
  public <methods>(java.lang.String);
}

# 不混淆类的特定方法
-keep cn.coderpig.myapp.example.Test {
  public test(java.lang.String);
}

# 不混淆native方法
-keepclasseswithmembernames class * {
  native <methods>;
}

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

#不混淆资源类
-keepclassmembers class **.R$* {
  public static <fields>;
}

# 不混淆自定义控件
-keep public class * entends android.view.View {
  *** get*();
  void set*(***);
  public <init>;
}

# 不混淆实现了Serializable接口的类成员,此处只是演示,也可以直接 *;
-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();
}

# 不混淆实现了parcelable接口的类成员
-keep class * implements android.os.Parcelable {
  public static final android.os.Parcelable$Creator *;
}

# 注意事项:
# 
# ① jni方法不可混淆,方法名需与native方法保持一致;
# ② 反射用到的类不混淆,否则反射可能出问题;
# ③ 四大组件、Application子类、Framework层下的类、自定义的View默认不会被混淆,无需另外配置;
# ④ WebView的JS调用接口方法不可混淆;
# ⑤ 注解相关的类不混淆;
# ⑥ GSON、Fastjson等解析的Bean数据类不可混淆;
# ⑦ 枚举enum类中的values和valuesof这两个方法不可混淆(反射调用);
# ⑧ 继承Parceable和Serializable等可序列化的类不可混淆;
# ⑨ 第三方库或SDK,请参考第三方提供的混淆规则,没提供的话,建议第三方包全部不混淆;

3、混淆规则的叠加

不知道你有没有想过:上面日常使用的创建的代码示例,proguard-rules.pro 没有配置混淆规则,却混淆了?

其实是因为混淆规则是叠加的,而混淆规则的来源不止主模块里的 proguard-rules.pro,还有这些:

1. /proguard-rules.pro

不止主模块有 proguard-rules.pro,子模块也可以有,因为规则是叠加的,故某个模块的配置都可能影响其它模块。

2. proguard-android-optimize.txt

AGP 编译时生成,其中包含了对大多数 Android 项目都有用的规则,并且启用 @Keep* 注解。

AGP 提供的规则文件还有 proguard-defaults.txt 或 proguard-android.txt,可通过 getDefaultProguardFile 进行设置,不过建议还是使用这个文件 (多了些优化配置)。

3. /build/intermediates/proguard-rules/debug/aapt_rules.txt

自动生成,AAPT2 会根据对应用清单中的类、布局及其他应用资源的引用,生成保留规则,如不混淆每个 Activity。

4. AAR 库 → /proguard.txt

5. Jar 库 → /META-INF/proguard/

如果想查看所有规则叠加后的混淆规则,可在主目录的 proguard-rules.pro 添加下述配置:

# 输出所有规则叠加后的混淆规则
-printconfiguration ./build/outputs/mapping/full-config.txt

资源压缩

资源压缩其实分为两步:资源合并资源移除,前者无论是否配置 shrinkResources true,AGP 构建 APK 时都会执行,当存在两个或更多名称相同的资源,才会进行资源合并,AGP 会从重复项中选择优先级更高的文件,并只将此资源传递给 AAPT2,以供在 APK 中分发。

级联优先顺序:依赖项 < 主资源< 渠道 < 构建类型

比如:重复资源存在于主资源及渠道中,Gradle 会选择渠道中的资源;但如果重复资源在同一层次出现,如 src/main/res/ 和 src/main/res2 中有重复资源,Gradle 就会报资源合并错误。

对应打包 Task 中的 processDebugResources,将 aapt 编译后的 flat 产物和合并后的清单文件进行链接处理生成 ._ap 文件(包含资源文件、清单文件、资源关系映射表文件 resources.arsc)及 R.java 文件(保存了资源类型、资源名称及地址的映射关系)。

说完资源合并,接着说下资源移除,开启资源压缩后,所有未被使用的资源默认会被移除,如果你想定义那些资源需要保留,可以在 res/raw/ 路径下创建一个 xml 文件,如 keep.xml,配置示例如下(此文件不会打包到 APK 中,支持通配符 *,此类文件可有多份):

<?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"
  <!-- 开启严苟模式,可选值strict,safe,前者严格按照keep和discard指定的资源保留 -->
  <!-- 后者保守删除未引用资源,如代码中使用Resources.getIdentifier()引用的资源会保留 -->
  tools:shrinkMode="strict"/>

另外,还可以在 build.gradle 中添加 resConfigs 来移除不需要的备用资源文件,如只保留中文:

android {
  ...
  defaultConfig {
    resConfigs "zh-rCN"// 不用支持国际化只需打包中文资源
  }
}

参考

https://blog.csdn.net/plokmju88/article/details/119397358

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注