对ProGuard使用方法的思考

Proguard, 标准名称叫ProGuard, 我这里偷懒就叫proguard好了, 相信不少项目中都有用到, 也有不少人自己写过keep规则. 使用proguard, 一方面是混淆代码, 另一方面是裁剪代码. 实际使用中当然不可能所有的代码都混淆, 总要keep一些, 但如果不对使用加以规范, proguard配置文件破千行是很轻松的事情, 同时也可能会导致大量没用的方法和代码被keep住.

去年年底做了一件事, 在不影响app正常功能的情况下, 简单调整proguard规则, 降低主dex方法数, 前后花了两周, 其实大部分时间都是在等打包和追查一个proguard导致的崩溃, 以较低的成本干掉了47000方法中的1500个, 虽说还有空间, 不过剩下的很少, 因此proguard规则这块没必要继续做了.

在做这事的过程中, 我发现不论是公司的大神, 第三方SDK提供方, 组内成员, 还是网上的文章, 都对proguard的理解存在错误, 因此打算在这里讨论一下, 给出我认为比较正确的使用方法, 也欢迎大家补充.

看到的问题

1.暴力keep

这是朕为你keep的江山

-keep class package.name.** {*;}

这种keep规则简单暴力, 直接keep住一个包下面所有的东西, 让一切无所遁形, 我刚开始不知道怎么用proguard的时候就经常写这种规则, 最常见的使用对象就是各种第三方库, 混淆之后可能跑不起来, 就直接全部keep住.

2.神秘规则

你猜我为什么要keep, 如果你猜到我, 我就让你嘿嘿嘿

由于代码和混淆规则是分开的, 所以很容易出现修改不同步的情况, 随着项目成员的更迭, 最后你会发现, 你根本不知道某些keep规则为什么要写.

3.规则繁多

俺娘说了, 这个要keep, 俺娘还说了, 那个也要keep

比如某些地方使用到了属性动画, 于是就在混淆规则里加上

-keepclasseswithmembers class package.name.view.PropertyView {
        void setSth(int);
        float getSth();
}

另一个地方用到了反射, 相应的也加上

-keepclasseswithmembers class package.name.reflect.ReflectMethod {
        void reflectMethod();
}

没错, 这样的keep规则非常精准, 但是最后的结果是proguard规则里全是各种各样的规则, 配置文件轻轻松松破千行, 看的人头都大了. 万一以后搞什么重构, 移动代码, 更是麻烦的很.

4.不提供混淆规则

你混啥! 再混试试!

这种情况常常出现在第三方提供的jar包上, 提供jar包的人要么不提供混淆规则, 要么说他的jar已经混淆过了, 接入的人keep住整个jar包里的内容就行. 面对这种第三方的SDK, 如果它引入的方法数多, 要么不接, 要么赶紧弄个动态加载dex的方案, 否则迟早被这些SDK害死.

5.添加第三方混淆规则

我插你后面行吗.

某些SDK提供混淆规则, 因此我们需要手动把SDK的混淆规则插入proguard配置, 假如以后SDK更新了, 混淆规则也更新了, 那么两边都需要修改, 只要是要同时改两处, 总有可能出问题.

此外, 有些SDK提供的规则是他们自己的打包规则, 那其实没有卵用, 因为SDK本身就是用那个规则混淆过的, 现在用同样的规则再混淆一次, 第二次混淆等于没有混淆.

针对上面提到的一些问题, 有些要明确概念, 需要你纠正别人的观念, 有些是有比较好的解决方案的.

科学思想

下面先尝试解答一下常见的proguard问题, 纠正一些错误认识, 培养使用proguard的科学观念.

1.proguard只是一个混淆工具?

错, proguard不止能混淆, 还会做代码裁剪, 方法内联等等优化, 一般在Android工程里, 我们用的最多的是裁剪(shrink), 优化(optimize)和混淆(obfuscate), 其中代码裁剪, 特别是对方法的裁剪, 是降低dex方法数的一条捷径. 在日常使用中, 我们把proguard的裁剪, 优化, 混淆等一系列步骤笼统的称为混淆, 下文也会这样使用这个词, 请大家注意.

2.接入第三方SDK需要将它的代码全部keep住?

错, 第三方SDK是有混淆的价值的, 全部keep住是一种非常不负责任的做法. 特别是第三方SDK占用了非常多的方法数时, 混淆是非常好的降低方法数的手段.

3.第三方SDK如果已经混淆了, 可以全部keep住, 无需再次混淆?

错, 第三方SDK无论是否混淆过, 都是有混淆价值的.

第三方SDK说它混淆过了, 说的是打包SDK时, 如构建jar, aar等的时候, 做了混淆, 但是他们的混淆规则其实是分为两部分, 一部分是SDK必须keep的部分, 如使用了反射等技术时, 需要做对应的keep, 第二部分则是对公开API的keep, 这部分如果不keep, SDK接入方根本没法用. 但是SDK接入方并不可能使用到SDK里所有的公开API, 对于没有使用到的API, 完全可以通过proguard裁剪掉.

4.直接使用第三方SDK打包用的混淆规则就可以了吗?

完全不行, 理由同上, 第三方SDK打包时keep了公开API, 这些规则对SDK接入方来说没有意义, 因此第三方SDK构建时的混淆规则和使用时的混淆规则肯定是不一样的.

科学做法

1.提取共性

可部分解决keep规则过多.

proguard的混淆规则本身有一套匹配机制, 对于那些只要写了就必须要keep的代码, 我们可以提取共性统一配置keep规则, 例如, Android里面Activity必须声明在Manifest里面, 这也导致了只要是个Activity就必须keep住, 至少要keep住类名. 他们的共性就是都继承了Activity, 那么下面的规则就能keep住它们.

-keep public class * extends android.app.Activity

至于那种拥有set和get方法的类, 不建议配置这种通用规则, 毕竟不是所有的拥有这类方法的类都需要keep, 那些用到属性动画的单独keep就行, 单独keep的方法后面说.

有比如, 对于JNI方法, 可以直接使用如下规则将它们全部keep住

-keepclasseswithmembers,includedescriptorclasses class * {
    native <methods>;
}

注意includedescriptorclasses很关键, 我在调整proguard规则时, 就发现缺了includedescriptorclasses, 加上它就能保证方法参数的类型不被混淆, 否则的话你还得自己去单独keep参数中的自定义类, 很容易出问题.

提取共性也有个缺点, 假如某种写法恰好命中了你的共性规则, 假如这个方法没人调用又没有删掉, 那么它会长期被keep住, 因此我们决定添加共性的规则之前, 一定要慎重, 例如以下规则.

-keepclasseswithmembers class * { 
    public <init>(android.content.Context, android.util.AttributeSet);
}

这个规则是希望keep住所有会在xml中使用的控件, 但是它忽略了一个事实, 许多开发者根本就不知道View的那几个构造方法有什么用, 我见过许多拥有这个构造方法, 但又根本不在xml中使用的控件类, 因此这种共性规则最好不要添加, 否则日积月累, 代码里会出现一大堆被keep住的无用控件类.

2.使用注解

可解决神秘规则, 缓解keep规则过多的副作用.

对于那些通用规则keep不了的, 我们也不在混淆规则里面单独添加对它的keep规则, 而是利用注解配合proguard来实现将混淆规则和代码写在一起.

为什么要用注解?

用一个比较形象的说法, 注解和代码是高内聚强耦合关系, 它们不仅有关系, 而且直接就写在一起. proguard规则与代码则是低内聚强耦合关系, 尽管它们密不可分, 但它们偏偏有不是写在一块的, 如果改一个方法名, 需要同时改两处地方, 那就很容易出问题.

首先需要写一个注解, 例如在com.shaw下, 我定义了一个叫Keep的注解

@Retention(RetentionPolicy.CLASS)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.FIELD})
public @interface Keep { }

然后在proguard规则中keep住这个注解, 并且允许混淆.

-keep,allowobfuscation @interface com.shaw.Keep

然后添加规则, 告诉proguard把这个注解修饰的东西keep住

-keep @com.shaw.Keep class * {
    *;
}

-keepclasseswithmembers class * {
    @com.shaw.Keep <fields>;
}

-keepclasseswithmembers class * {
    @com.shaw.Keep <init>(...);
}

-keepclasseswithmembers class * {
    @com.shaw.Keep <methods>;
}

然后在代码里用@Keep修饰想keep的类, 方法或字段就行.

这里只是一个范例, 大家可以自行修改.

3.使用aar

解决第三方SDK的混淆配置管理问题.

尽管有些SDK提供方非常靠谱的给了混淆规则, 但是毕竟要添加到自己项目的proguard配置里, 以后如果混淆规则改了, 配置还得同步改, 对于那些用maven仓库管理的SDK, 非常不友好.

也许有人注意到了, Android归档文件, 也就是俗称的aar文件里可以携带一个proguard.txt文件, 这似乎表明aar文件可以自带混淆配置.

事实也是如此, aar在构建时, 可以通过consumerProguardFiles属性指定一个proguard配置, 这个配置会被打入aar, 它和proguardFiles属性指定的proguard配置不同, proguardFiles是用于构建aar的混淆规则, consumerProguardFiles则是aar的接入方在构建时会使用的混淆规则.

需要注意的是, 网上有一些说法说aar的混淆规则只对aar自身有效, 不会影响到其他代码, 这是对官方表述的误解, 直接翻译过来也有可能造成误解, 根据Android Gradle DSL, consumerProguardFiles属性只对Library工程有效, Application工程会忽略这个属性, 意思是只有Library工程里用这个属性, 才会把对应的混淆配置打入aar, 而不是说aar里的混淆配置不会对接入方产生影响.

根据Android Build Workflow, proguard过程是将所有的混淆配置合在一起对所有的class进行处理. 因此aar只是给SDK提供方提供了一个管理混淆配置的渠道, 如果SDK提供方在混淆配置里写如下规则:

-keep class com.** { *; }

那SDK接入方com包下的一切都没法混淆了.

对于第三方SDK, 我的建议是不混淆, 并且提供混淆规则, 混淆规则不要使用通用的, 而是需要keep什么就keep什么.

好在aar文件里的proguard.txt是可以直接改的, SDK提供方如果太不给力, 可以自己去改改.

再说几句

我发现SDK提供方有时候有个毛病, 就是爱混淆, 包括公司内部的一些依赖, 也是混淆过的, 更奇葩的是那些带有混淆配置的aar, 竟然也都混淆过, 这就很莫名其妙, 代码跑在我们的项目里, 还遮遮掩掩的, 怕不是有什么阴谋. 现在aar都能自带混淆配置了, 不混淆SDK, 也方便开发者遇到问题自行调查, 更何况SDK是给别人用的, 怎么就不愿意给别人看呢?

    原文作者:Shawon
    原文地址: https://www.jianshu.com/p/14af4a474d55
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞