前言:使用java编写的源代码编译后生成了对于的class文件,市面上很多软件都可以对class文件进行反编译,况且Android开发的应用程序是用Java代码写的,为了很好的保护Java源代码,我们需要对编译好后的class文件进行混淆。另外随着apk的版本迭代,功能需求越多,apk发布的包的体积也会越来越大,y因此人们更倾向于安装并保留较小和安装占用空间更小的应用,所以谷歌提供了R8 编译器,您可以通过压缩、混淆和优化,更全面的缩小应用体积。Android构建中,在AGP3.4.0之前也是使用的ProGuard 进行代码优化混淆,但是在3.4.0之后,谷歌将这一工作赋予给了性能更佳的R8编译器。虽然摒弃了ProGuard,但是R8编译器还是兼容ProGuard的配置规则
ProGuard混淆
ProGuard是一个混淆代码的开源项目,它的主要作用是混淆代码,但也包括压缩(Shrink)、优化(Optimize)、混淆(Obfuscate)、预检(Preveirfy),来自官网权威的解释:Proguard是一个Java类文件压缩器、优化器、混淆器、预校验器。压缩环节会检测以及移除没有用到的类、字段、方法以及属性。优化环节会分析以及优化方法的字节码。混淆环节会用无意义的短变量去重命名类、变量、方法。这些步骤让代码更精简,更高效,也更难被逆向。因为R8取代了ProGuard的压缩、优化及预检,保留了ProGuard的混淆配置,所以本文只讲下ProGuard的混淆的一些注意事项。
混淆
定义:简而言之就是使用a,b,c,d这样简短而无意义的名称,对类、字段和方法进行重命名,从而提高反编译后的阅读成本。
使用:主项目的 build.gradle
设置 minifyEnabled true
,proguard-rules.pro
加入混淆规则;
android {
buildTypes {
release {
...
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
...
}
proguard-android-optimize.txt就是默认的基本的混淆文件,包含一些基本的混淆规则。具体在SDK目录下,给图有真相。sdk安装路径因人而异look下我的
混淆的一些常用规则:
- 一颗星:表示保留当前包下的类名,如果有子包,子包中的类名也会被混淆
-keep class com.csj.test.*
也就是com.csj.test.ui包下的所有类(MyView)都会被混淆,但MainActivity类不会被混淆
- 两颗星:表示保留当前包下的类名,如果有子包,子包中的类名也会被保留。
-keep class com.csj.test.**
- 上面的方式虽然保留了类名,但是内容还是会被混淆,使用下面方式保留内容:
-keep class com.csj.test.* {*;}
这样的话com.csj.test包下的所有类名及内容都不会被混淆,但实际开发并不提倡这么使用,因为这样就失去了混淆的意义,所以我们可以针对特定的内容进行保留不被混淆。
在此基础上,我们也可以使用Java的基本规则来保护特定类不被混淆,比如我们可以用extends,implements等这些Java规则
例如:
-keep public class * extends android.app.Activity
以上代码的意思就是所有继承android.app.Activity这个类的子类都不会被混淆。同理
-keep public class * implements java.io.Serializable
保留Serializable序列化的所实现的类不被混淆
以上是针对整个类不被混淆,但如果还是觉得混淆的范围太大,就是一个类中你不希望保持全部内容不被混淆,而只是希望保护类下的特定内容,就可以使用
<init>; //匹配所有构造器
<fields>; //匹配所有域
<methods>; //匹配所有方法方法
在或前面加上private 、public等来进一步指定不被混淆的内容,如
-keep class com.csj.test.MainActivity{
public <methods>;
}
- 当然你还可以加入参数,比如以下表示用String作为入参的构造函数不会被混淆:
-keep class com.csj.test.MainActivity{
public <init>(String);
}
也可以直接指定具体哪个方法不被混淆
-keep class com.csj.test.ui.MyView{
public void test();
}
还有一种就是不需要保持类名,只需要把该类下的特定方法保持不被混淆就好,那你就不能用keep方法了,keep方法会保持类名,而需要用keepclassmembers ,如此类名就不会被保持,为了便于对这些规则进行理解,官网给出了以下表格:
# -keep关键字
# keep:包留类和类中的成员,防止他们被混淆
# keepnames:保留类和类中的成员防止被混淆,但成员如果没有被引用将被删除
# keepclassmembers :只保留类中的成员,防止被混淆和移除。
# keepclassmembernames:只保留类中的成员,但如果成员没有被引用将被删除。
# keepclasseswithmembers:如果当前类中包含指定的方法,则保留类和类成员,否则将被混淆。
# keepclasseswithmembernames:如果当前类中包含指定的方法,则保留类和类成员,如果类成员没有被引用,则会被移除。
到这基本上开发常用的混淆就差不多够用了,接下来就是区分实际开发中,需要注意哪些内容不应该被混淆的。
首先是混淆的一些基本规则,任何APP都要使用,可以作为模板使用。如下:
# 代码混淆压缩比,在0和7之间,默认为5,一般不需要改
-optimizationpasses 5
# 混淆时不使用大小写混合,混淆后的类名为小写
-dontusemixedcaseclassnames
# 指定不去忽略非公共的库的类
-dontskipnonpubliclibraryclasses
# 指定不去忽略非公共的库的类的成员
-dontskipnonpubliclibraryclassmembers
# 不做预校验,preverify是proguard的4个步骤之一
# Android不需要preverify,去掉这一步可加快混淆速度
-dontpreverify
# 有了verbose这句话,混淆后就会生成映射文件
# 包含有类名->混淆后类名的映射关系
# 然后使用printmapping指定映射文件的名称
-verbose
-printmapping proguardMapping.txt
# 指定混淆时采用的算法,后面的参数是一个过滤器
# 这个过滤器是谷歌推荐的算法,一般不改变
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
# 保护代码中的Annotation不被混淆,这在JSON实体映射时非常重要,比如fastJson
-keepattributes *Annotation*
# 避免混淆泛型,这在JSON实体映射时非常重要,比如fastJson
-keepattributes Signature
//抛出异常时保留代码行号,在异常分析中可以方便定位
-keepattributes SourceFile,LineNumberTable
-dontskipnonpubliclibraryclasses用于告诉ProGuard,不要跳过对非公开类的处理。默认情况下是跳过的,因为程序中不会引用它们,有些情况下人们编写的代码与类库中的类在同一个包下,并且对包中内容加以引用,此时需要加入此条声明。
-dontusemixedcaseclassnames,这个是给Microsoft Windows用户的,因为ProGuard假定使用的操作系统是能区分两个只是大小写不同的文件名,但是Microsoft Windows不是这样的操作系统,所以必须为ProGuard指定-dontusemixedcaseclassnames选项
再来说下哪些是需要保留不被混淆的
1、保留所有的本地native方法不被混淆
-keepclasseswithmembernames class * {
native <methods>;
}
因为R8(ProGuard)并未对反射以及JNI等情况进行检测,如果配置文件中未处理,则这部分代码就会被丢弃,会出现NoClassFindException的异常,
2、反射用到的类不混淆
原因同上
3、保留了继承自Activity、Application这些类的子类
-keep public class * extends android.app.Activity
-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
-keep public class * extends android.view.View
-keep public class com.android.vending.licensing.ILicensingService
因为这些子类,都有可能被外部调用,第一行就保证了所有Activity的子类不要被混淆。
4、使用enum类型时需要注意避免以下两个方法混淆,因为enum类的特殊性,以下两个方法会被反射调用
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
5、保留Parcelable、Serializable序列化的类不被混淆,包括自定义的一些和服务器交互的bean类
# 保留Parcelable序列化的类不被混淆
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
# 保留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();
}
使用GSON、fastjson等框架解析服务端数据时,所写的JSON对象类不混淆,否则无法将JSON解析成对应的对象;这一点也是开发中经常忽略的问题
6、使用第三方开源库或者引用其他第三方的SDK包时,如果有特别要求,也需要在混淆文件中加入对应的混淆规则;这个一般三方官网上会有文档说明、例如高德地图SDK
7、有用到WebView的JS调用也需要保证写的接口方法不混淆,
# 保留JS方法不被混淆
-keepclassmembers class com.example.xxx.MainActivity$JSInterface1 {
<methods>;
}
其中JSInterface是MainActivity的子类
8、对WebView的处理
# 对WebView的处理
-keepclassmembers class * extends android.webkit.webViewClient {
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
public boolean *(android.webkit.WebView, java.lang.String)
}
-keepclassmembers class * extends android.webkit.webViewClient {
public void *(android.webkit.webView, java.lang.String)
}
9、内嵌类不被混淆
# 保留内嵌类不被混淆
-keep class com.example.xxx.MainActivity$* { *; }
这个$符号就是用来分割内嵌类与其母体的标志。
也可以在具体点,比如保持ScriptFragment内部类JavaScriptInterface中的所有public内容不被混淆。
-keepclassmembers class cc.csj.test.ScriptFragment$JavaScriptInterface {
public *;
}
10、保留自定义控件(继承自View)不被混淆
# 保留自定义控件(继承自View)不被混淆
-keep public class * extends android.view.View {
*** get*();
void set*(***);
public <init>(android.content.Context);
public <init>(android.content.Context, android.util.AttributeSet);
public <init>(android.content.Context, android.util.AttributeSet, int);
}
ok,到这里,基本proGuard的混淆规则就大致结束了。不用死记规则,但得知道有这么个东西,打包release包时,能定位出混淆所带来的问题就行了
什么是R8?
R8 是一个将我们的 java 字节码转换为优化的 dex 码的工具。它遍历整个应用程序,然后对其进行优化,例如删除未使用的类、方法等。它在编译时运行。它可以帮助我们减少构建的大小并使我们的应用程序更加安全。R8 使用 Proguard 规则来修改其默认行为。
R8 收缩是如何工作的?
在优化代码的同时,R8 减少了我们应用程序的代码,进而减小了 APK 的大小。
为了减小 APK 大小,我们采用了三种不同的技术:
-
收缩或摇树:收缩是从我们的 Android 项目中删除无法访问的代码的过程。R8 执行一些静态分析以摆脱无法访问的代码并删除未实例化的对象。
-
优化:这用于优化代码的大小。它涉及删除死代码、删除未使用的参数、选择性内联、类合并等。
-
标识符重命名:在这个过程中,我们混淆了类名和其他变量名。例如,如果类的名称是“ MainActivity ”,那么它将被混淆为“ a ”或其他名称,但大小更小。
/**
* @Author shengjie.chen
* @Date 2023-06-23 19:19
*/
public class Chen {
private void unused() {
System.out.println("没吊用的代码");
}
private static void greeting() {
System.out.println("hello,端午安康!");
}
public static void main(String[] args) {
greeting();
}
}
程序的入口是 static void main 方法,我们使用以下 keep 规则 指定该方法:
-keep class com.csj.test.Chen{ public static void main(java.lang.String[]); }
R8 缩减算法的运作方式如下:
- 首先,它从程序常见的入口点跟踪所有可访问的代码。这些入口点由 R8 keep 规则定义。例如,在此 Java 代码示例中,R8 会在 main 方法处开始运行。
- 在该示例中,R8 从 main 方法跟踪到 greeting 方法。greeting 方法是在运行时被调用的,因此跟踪在此处停止。
- 跟踪完成后,R8 使用摇树优化来删除未使用的代码。在此示例中,摇树删除了未使用的方法(unused),因为 R8 的跟踪过程检测到从任何已知的入口都无法到达该方法。
- 接下来,R8 将标识重命名为较短的名称,这些名称在 DEX 文件中占用较少的空间。在示例中,R8 可能会将 greeting 方法重命名为短名称 a:
package com.csj.test;
/**
* @Author shengjie.chen
* @Date 2023-06-23 19:19
*/
public class Chen {
private static void a() {
System.out.println("hello,端午安康!");
}
public static void main(String[] args) {
a();
}
}
- 最后,应用代码优化。缩减代码大小的内联是其一。在此示例中,将方法 a 的主体直接迁移到 main 中,代码会显得更简洁:
-
public class Chen { public static void main(String[] args) { System.out.println("hello,端午安康!"); } }
简而言之就是比如你项目中依赖了很多库,但是只使用了库里面少部分代码,为了移除这部分代码,R8会根据配置文件确定应用代码的所有入口点:包括应用启动的第一个Activity或者服务等,R8会根据入口,检测应用代码,并构建出一张图表,列出应用运行过程中可能访问的方法,成员变量和类等,并对图中没有关联到的代码,视为可移除代码。盗个图
图中入口位置:MainActivity,整个调用链路中,使用到了foo,bar函数以及AwesomeApi类中的faz函数,所以这部分代码会被构建到依赖图中,而OkayApi类以及其baz函数都未访问到,则这部分代码就可以被优化。图上所表示的内内容和我上面那个例子是差不多的意思,但人家的直观点。嘎嘎。。。
那么R8狠在哪里呢?好比你的项目引入个三方依赖,但只用了其中一小部分代码,而R8会在你打包的时候,把三方依赖里未关联的代码都会移除掉再打包你的apk中。
在说下R8的代码优化。
为了进一步缩减应用,R8 会在更深的层次上检查代码,以移除更多不使用的代码,或者在可能的情况下重写代码,以使其更简洁。下面是此类优化的几个示例:
1、如果您的代码从未采用过给定 if/else 语句的 else {}
分支,R8 可能会移除 else {}
分支的代码
package com.csj.test
import android.util.Log
/**
* @Author shengjie.chen
* @Date 2023-06-23 19:34
*/
class Demo {
fun test() {
if (true) {
Log.e("TAG", "test: 端午安康")
} else {
Log.e("TAG", "test: 端午放假,但下大雨了")
}
}
}
2、如果您的代码只在一个位置调用某个方法,R8 可能会移除该方法并将其内嵌在这一个调用点
这个上面举过例子了,不举了!
3、如果 R8 确定某个类只有一个唯一子类且该类本身未实例化(例如,一个仅由一个具体实现类使用的抽象基类),它就可以将这两个类组合在一起并从应用中移除一个类。
class Father{}
class Son extends Father{}
这种情况,Son就被干掉。
用 ProGuard 还是 R8?
如果没有历史包袱,直接R8,毕竟兼容绝大部分的ProGuard规则,更快的编译速度,对Kotlin更友好。
还是简单描述下两者吧:
ProGuard
→ 压缩、优化和混淆Java字节码文件的免费工具,开源仓库地址:proguardR8
→ ProGuard的替代工具,支持现有ProGuard规则,更快更强,AGP 3.4.0或更高版本,默认使用R8混淆编译器。
如果不想用R8,想用回ProGuard的话(可以但没必要),可以在 gradle.properties
文件中添加下述配置禁用R8:
android.enableR8=false
android.enableR8.libraries=false
编译APK时可能会报错:
在 proguard-rules.pro
文件中加上 -ignorewarnings
即可解决。
另外,使用ProGuard或R8构建项目会在 build\outputs\mapping\release
输出下述文件:
- mapping.txt → 原始与混淆过的类、方法、字段名称间的转换;
- seeds.txt → 未进行混淆的类与成员;
- usage.txt → APK中移除的代码;
- resources.txt → 资源优化记录文件,哪些资源引用了其他资源,哪些资源在使用,哪些资源被移除;
自动生成不用管。
总结
R8保留了Proguard 混淆规则且有效地内联容器类并删除未使用的类、字段和方法.它减小了应用程序的大小。R8 提供比 Proguard 更好的输出,并且比 Proguard 更快,从而减少了整体构建时间。
参考文章:
补齐Android技能树 - 从害怕到玩转Android代码混淆 - 掘金