ProGuard混淆及R8优化

前言:使用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 trueproguard-rules.pro 加入混淆规则;

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

proguard-android-optimize.txt就是默认的基本的混淆文件,包含一些基本的混淆规则。具体在SDK目录下,给图有真相。sdk安装路径因人而异look下我的

ProGuard混淆及R8优化_第1张图片

混淆的一些常用规则:

  • 一颗星:表示保留当前包下的类名,如果有子包,子包中的类名也会被混淆
-keep class com.csj.test.*

ProGuard混淆及R8优化_第2张图片

 也就是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序列化的所实现的类不被混淆

以上是针对整个类不被混淆,但如果还是觉得混淆的范围太大,就是一个类中你不希望保持全部内容不被混淆,而只是希望保护类下的特定内容,就可以使用

;     //匹配所有构造器
;   //匹配所有域
;  //匹配所有方法方法

在或前面加上private 、public等来进一步指定不被混淆的内容,如

-keep class com.csj.test.MainActivity{
    public ;
}
  • 当然你还可以加入参数,比如以下表示用String作为入参的构造函数不会被混淆:
-keep class com.csj.test.MainActivity{
    public (String);
}

也可以直接指定具体哪个方法不被混淆

-keep class com.csj.test.ui.MyView{
    public void test();
}

还有一种就是不需要保持类名,只需要把该类下的特定方法保持不被混淆就好,那你就不能用keep方法了,keep方法会保持类名,而需要用keepclassmembers ,如此类名就不会被保持,为了便于对这些规则进行理解,官网给出了以下表格:

ProGuard混淆及R8优化_第3张图片

# -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 ;
}

因为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

ProGuard混淆及R8优化_第4张图片

 7、有用到WebView的JS调用也需要保证写的接口方法不混淆,

# 保留JS方法不被混淆
-keepclassmembers class com.example.xxx.MainActivity$JSInterface1 {
    ;
}

其中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 (android.content.Context);
    public (android.content.Context, android.util.AttributeSet);
    public (android.content.Context, android.util.AttributeSet, int);
}

ok,到这里,基本proGuard的混淆规则就大致结束了。不用死记规则,但得知道有这么个东西,打包release包时,能定位出混淆所带来的问题就行了

什么是R8?

R8 是一个将我们的 java 字节码转换为优化的 dex 码的工具。它遍历整个应用程序,然后对其进行优化,例如删除未使用的类、方法等。它在编译时运行。它可以帮助我们减少构建的大小并使我们的应用程序更加安全。R8 使用 Proguard 规则来修改其默认行为。

R8 收缩是如何工作的?

在优化代码的同时,R8 减少了我们应用程序的代码,进而减小了 APK 的大小。

为了减小 APK 大小,我们采用了三种不同的技术:

  1. 收缩或摇树:收缩是从我们的 Android 项目中删除无法访问的代码的过程。R8 执行一些静态分析以摆脱无法访问的代码并删除未实例化的对象。

  2. 优化:这用于优化代码的大小。它涉及删除死代码、删除未使用的参数、选择性内联、类合并等。

  3. 标识符重命名:在这个过程中,我们混淆了类名和其他变量名。例如,如果类的名称是“ MainActivity ”,那么它将被混淆为“ a ”或其他名称,但大小更小。

ProGuard混淆及R8优化_第5张图片

/**
 * @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会根据入口,检测应用代码,并构建出一张图表,列出应用运行过程中可能访问的方法,成员变量和类等,并对图中没有关联到的代码,视为可移除代码。盗个图

ProGuard混淆及R8优化_第6张图片

图中入口位置: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字节码文件的免费工具,开源仓库地址:proguard
  • R8 → ProGuard的替代工具,支持现有ProGuard规则,更快更强AGP 3.4.0或更高版本,默认使用R8混淆编译器。

如果不想用R8,想用回ProGuard的话(可以但没必要),可以在 gradle.properties 文件中添加下述配置禁用R8:

android.enableR8=false
android.enableR8.libraries=false

编译APK时可能会报错:

ProGuard混淆及R8优化_第7张图片

 

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代码混淆 - 掘金

【Android性能优化】:ProGuard,混淆,R8优化 - 掘金

Android 中的 R8 与 Proguard的区别 - 简书

你可能感兴趣的:(开发语言,android)