Proguard那些事儿

提到Proguard,做Android的小伙伴想必是耳熟能详的,它虽然不是由Google开发维护的,Google却将其内置在了Android开发的SDK包中,在编译过程中起到了压缩、优化、混淆Android代码的作用,可以说是Android开发必不可少的一个工具。

Proguard做了什么

如下图所示,在Android应用源码的编译过程中,Proguard将Java bytecode转化为了Optimized Java bytecode,也就是说,Proguard起到了优化Java字节码的作用 。
在这里插入图片描述
Proguard优化Java字节码的过程可以分为这四个步骤:shrink(压缩)optimize(优化)obfuscate(混淆)preverify(预校验),如下图所示。
在这里插入图片描述

  • Shrink(压缩)
    • 根据设置的EntryPoint(入口点),遍历每个Java字节码文件,确定哪些类及类成员会被程序使用到,不会被使用的则直接丢弃。
    • EntryPoint(入口点)是Proguard中非常关键的一个概念,它定义了Proguard整个优化流程的入口,通常是由-keep系列的配置来指定的,Shrink、Optimize、obfuscate操作都与EntryPoint紧密关联。
    • Proguard只会丢弃不被使用的Java字节码,开发者通常还会搭配Android Gradle插件提供的shrinkResources功能来对不被使用的资源文件进行丢弃。
  • Optimize(优化)
    • Optimize时深入到Java字节码命令的层次进行优化。
    • class维度的优化,必要时增加final标记、做枚举类拆箱(转为整数常量)、做类合并。
    • field维度的优化,必要时增加private标记、移除write-only字段、方法中直接传递字段值。
    • method维度的优化,必要时增加privatestaticfinal标记、去除synchronized标记、移除没用到的参数、直接传递参数的值而不是引用、直接返回结果值而不是引用、将方法(较短的方法、或调用次数少的方法)内联到调用方中、尾部递归简化。
    • code维度的优化,必要时合并不同分支下相同的代码块,使用窥孔优化对变量、属性的存储加载、算术指令、类型转换、分支指令、常量字符、对象实例化进行优化。基于控制流和数据流分析,移除无效的代码、空捕捉的异常、内存占用等等。
  • Obfuscator(混淆)
    • Obfuscator时会对非EntryPoint的类、类成员做重命名,默认情况下它们会被命名为简短无意义的单个或多个的英文字母。
  • Preverify(预校验)
    • 对于Android开发场景,该步骤可以直接跳过。

Proguard与R8

R8的出现

2019年Google在发布的Android Gradle插件3.3.0版本中,提到了可以替代Proguard进行代码压缩和混淆的新工具R8,开发者可以通过在grale.properties文件中添加以下配置启用R8。

android.enableR8 = true

如下图所示,在使用R8时,Android代码的编译步骤四步变为了三步,R8将Proguard和D8做的事情合并为了一步,因此很大程度上可以认为R8相当于Proguard + D8。
在这里插入图片描述
下图是R8与Proguard在处理时间、产出包大小上的对比图,可以看出使用R8后,从Java字节码到Dex字节码的构建时间有了较大提升,在产出包的体积方面则和使用Proguard相差不大。
在这里插入图片描述

在后来发布Android Gradle插件3.4.0版本中,R8变为默认启用状态,也就是说,开发者只要升级Android Gradle插件到3.4.0,在打包时使用的优化器就不再是Proguard而是R8了,当然,google也给开发者保留了继续使用Proguard的方法,在grale.properties文件中,根据自身需要,选择添加以下配置中的一条则可以继续使用Proguard。

# Disables R8 for Android Library modules only.
android.enableR8.libraries = false
# Disables R8 for all modules.
android.enableR8 = false

Proguard VS R8

如果仅看上面,R8似乎各方面都比Proguard要好,可能有人会想,既然如此那还学proguard作甚,直接R8就完事儿了呗!但事实真的是这样吗?这里简单的对比一下Proguard与R8各自的优缺点。

  • 就应用的构建速度来看,R8更快。

    • R8的构建时间之所以比Proguard更短,最重要的原因是因为它合并了优化、Dex编译两个步骤,免去了中间重新对所有文件遍历读写的过程。如果只是比较编译AAR包(不含D8步骤)的时间,R8将丧失它的优势。
  • 就产出包的压缩率来看,R8略优。

  • 就支持的优化项来看,Proguard支持的更多。

    • 想详细了解的小伙伴,可以看这篇文章:https://www.guardsquare.com/en/blog/comparison-proguard-vs-r8-october-2019-edition。
  • 就稳定性来看,Proguard更可靠。

    • 毕竟Proguard已经走过了15年,在数百万的应用和开发者中形成了一个良性的反馈。而R8自开放以来也就1年多,并且还在进行大量的开发和功能扩展工作。
  • 在使用文档方面,Google目前只在AndroidStudio的官方使用文档中提及到了R8的一些理念和使用方法,至于具体的配置项,R8完全基于Proguard的配置规则,文档也是完全依赖Proguard官方的配置文档。

  • 就受众来说,R8的受众只是Android开发者,而Proguard在设计时的受众是所有Java、Kotlin开发者,只不过它因为内置在Android SDK包中,被众多Android开发者所熟知。

由于R8的配置项几乎完全套用了Proguar中定义的规范,因此Proguard与R8之间的切换是非常顺滑的,作为一名Android开发者,不论你使用Proguard还是R8来做字节码优化,Proguard都是你必须学习和掌握的。

Proguard小知识

要想Proguard用的好,熟读配置少不了,关于配置项的知识建议大家直接去官方文档学习,这里只介绍一些比较重要的或者使用时的注意项。

keep配置项

keep可以说是Proguard配置文件中最常见的一个配置项了,使用keep配置时一定要选择合适的keep级别。

  • -keep
    • 保护类和类成员,不被移除、不被重命名
  • -keepnames
    • 保护类和类成员,不被重命名
  • -keepclassmembers
    • 保护类成员,不被移除、不被重命名
  • -keepclassmembernames
    • 保护类成员,不被重命名
  • -keepclasseswithmembers
    • 保护含有指定成员的类和类成员,不被移除、不被重命名
  • -keepclasseswithmembernames
    • 保护含有指定成员的类和类成员,不被重命名

配置文件合并

说到Proguard配置文件,首先想到的肯定是<module-dir>/proguard-rules.pro这个文件,但其实这只是Android使用的所有Proguard配置的冰山一角罢了。为了Android开发者能更方便地使用Proguard,Android IDE会通过插件自动生成一些通用的Proguard配置放在其他配置文件中,最终在运行Proguard时,将所有这些配置文件中的配置项进行合并。

那除了<module-dir>/proguard-rules.pro,实际上还有哪些Proguard配置文件呢,这里介绍一下:

  • 由Android Gradle插件引入的proguard-android.txtproguard-android-optimize.txt

    • 在新建一个module时,它的build.gradle文件中总会包含这样一段配置:
    proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    

    意思是将使用两个proguard规则文件,第一个配置文件是getDefaultProguardFile('proguard-android-optimize.txt'),如果进入getDefaultProguardFile方法查看,会发现它指向了Android SDK目录下的tools/proguard/proguard-android-optimize.txt文件,第二个配置文件其实就是上面提到的<module-dir>/proguard-rules.pro

        public File getDefaultProguardFile(String name) {
            File sdkDir = sdkHandler.getAndCheckSdkFolder();
            return new File(sdkDir,
                    SdkConstants.FD_TOOLS + File.separatorChar
                            + SdkConstants.FD_PROGUARD + File.separatorChar
                            + name);
        }
    
    • proguard-android.txtproguard-android-optimize.txt两个文件中都包含了对大多数 Android 项目有用的规则,proguard-android-optimize.txt只是在proguard-android.txt的基础上打开了Optimize功能并配置了一些Optimize规则,因此如果要使用Optimize功能请使用proguard-android-optimize.txt
  • 由依赖的aar包引入的<library-dir>/proguard.txt、由依赖的jar包引入的<library-dir>/META-INF/proguard/

    • 依赖库中包含的proguard配置文件的内容,实际上是开发Library时,在consumerProguardFiles指定的文件中定义的,用来帮助开发者在使用该库时自动处理该库的proguard配置。
    android {
    
        defaultConfig {
            minSdkVersion 14
            targetSdkVersion 29
            versionCode 1
            versionName "1.0"
    				//在consumer-rules.pro文件中设置,依赖该库时会自动应用的proguard配置
            consumerProguardFiles 'consumer-rules.pro'
        }
        
    }
    
  • AAPT2工具生成的 <module-dir>/build/intermediates/proguard-rules/debug/aapt_rules.txt

    • aapt2主要负责在构建时解析资源文件,在解析的同时也会生成Proguard配置文件。例如解析AndroidManifest.xml文件时,将其中注册的四大组件全部生成keep配置,解析res/layout下的布局文件时,将其中用到的view组件生成keep配置等等。
  • 开发者通过<module-dir>/build.gradle中的proguardFile设置增加的自定义配置文件,如下所示,增加了一个aaa.txt配置文件。

buildTypes {
    release {
        minifyEnabled true
        //增加一个aaa.txt作为proguard配置文件
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro', 'aaa.txt'
    }
}

技巧与建议

  • 为了达到最好的优化效果,建议只配置最小的keep维度,不要过度keep。

  • aidl中的所有方法,都要记得做好keep。

  • 所有运行时动态加载相关的代码,如反射,都要记得做好keep。

    • proguard的官网提到了proguard能够自动识别和处理一些反射相关的代码场景,不需要人为配置,但试用下来感觉并不全面,建议大家还是手动配置最好。
  • 在线上应用出错时,往往需要把线上收集的混淆过的错误栈信息,对照着mapping文件翻译为可读的栈信息,这个过程其实可以借助Proguard提供的retrace命令帮我们自动完成的。

    1. 假设应用抛出了以下异常信息
    java.io.IOException: Can't read [dummy.jar] (No such file or directory)
        at proguard.y.a(MyApplication:188)
        at proguard.y.a(MyApplication:158)
        at proguard.y.a(MyApplication:136)
        at proguard.y.a(MyApplication:66)
        at proguard.ProGuard.c(MyApplication:218)
        at proguard.ProGuard.a(MyApplication:82)
        at proguard.ProGuard.main(MyApplication:538)
    Caused by: java.io.IOException: No such file or directory
        at proguard.d.q.a(MyApplication:50)
        at proguard.y.a(MyApplication:184)
        ... 6 more
    
    1. 可以将异常堆栈信息保存到stacktrace.txt文件中,运行以下命令(retrace命令在proguard包的bin目录下)
    retrace mapping.txt stacktrace.txt
    
    1. 运行成功,可以看到输出了可读的堆栈信息
    java.io.IOException: Can't read [dummy.jar] (No such file or directory)    at proguard.InputReader.readInput(InputReader.java:188)    at proguard.InputReader.readInput(InputReader.java:158)    at proguard.InputReader.readInput(InputReader.java:136)    at proguard.InputReader.execute(InputReader.java:66)    at proguard.ProGuard.readInput(ProGuard.java:218)    at proguard.ProGuard.execute(ProGuard.java:82)    at proguard.ProGuard.main(ProGuard.java:538)Caused by: java.io.IOException: No such file or directory    at proguard.io.DirectoryPump.pumpDataEntries(DirectoryPump.java:50)    at proguard.InputReader.readInput(InputReader.java:184)    ... 6 more
    
  • Proguard支持设置混淆时使用的字符字典,并不是只能混淆为’a’, ‘b’, 'c’这种。

# 设置用于混淆field,method名称的字典-obfuscationdictionary Tibet.txt# 设置用于混淆类名称的字典-classobfuscationdictionary Tibet.txt# 设置用于混淆包名的字典-packageobfuscationdictionary Tibet.txt

例如,这里定义一个Tibet.txt字典文件,其中定义了一些藏语字符。
在这里插入图片描述

使用该字典的混淆效果如下所示
在这里插入图片描述

  • 多Module工程中使用Proguard时的建议和注意事项

    • 主Module中Proguard的开关会直接影响到子Module。只要主Module中的proguard开关是开的,那么即使子Module中的proguard开关是关闭的,在执行主Module的打包命令时,仍然会对子Module做Proguard操作。
    • 建议各个Module自行维护自己的Proguard配置文件,避免所有Model全都配置在同一个文件中。
  • porguard-rules.pro文件 和 consumer-rules.pro文件

    • proguard-rules.pro文件一般指的是新建module时proguardFiles指定的默认文件。(当然开发者可以换成别的文件名)
      在这里插入图片描述

    • consumer-rules.pro文件一般指的是一个在Library类型的Module中才会出现的配置文件,它是新建Library类型的Module时consumerProguardFiles指定的默认文件。(当然开发者也可以换成别的文件名)
      在这里插入图片描述

    • porguard-rules.pro文件 和 consumer-rules.pro的区别在于

      • 在打包当前Module时,Proguard会用到该Module下的proguard-rules.pro文件中的配置项对其进行优化,打包完成后该文件不会出现在产出的包中。
      • consumer-rules.pro文件在Library Module打包成AAR或Jar时,仍然会在包中保留该文件。
      • 在打包其他用到该Library Module(只能是Library类型)或AAR/Jar包的Module时,Proguard在处理该Library Module时,会用到该Library Module或AAR/Jar包提供的consumer-rules.pro文件中的配置项进行优化。
  • 在开发Library时,尽量不要在consumer-rules.pro中设置如dontoptimize这样的配置项,因为Android在打包时会合并所有的配置文件,如果在Library中设置了关闭optimize,那么所有使用该Library的Module都将关闭optimize。如果Library一定要关闭optimize,请在Library下的proguard-rules.pro文件中设置,并将Library打包成AAR/Jar包后,再给其他Module使用。

Proguard总结

  • Proguard在Android Application/Library的编译过程中,起到了Shrink(压缩)、Optimize(优化)、Obfuscate(混淆) Java字节码的作用。
  • 虽然Google开发了R8来替代Proguard,但R8基本完全套用了Proguard的配置,因此Proguard仍然是Android开发者必须掌握的。
  • Android IDE为了方便Android开发者使用Proguard做了很多自动化管理,开发者通常只需要关注与自身项目相关的Proguard配置即可。
  • Proguard只会处理项目中的Java字节码部分,对于资源文件的优化,可以通过Android Gradle插件提供的shrinkResources配置移除没有用到的资源,通过AndResGuard Gradle插件对资源文件进行压缩和混淆。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值