Android开发之混淆高级教程01

2017-09-03 16:53 阅读 834 次 评论 0 条
版权声明:本文著作权归TeachCourse所有,未经许可禁止转载,谢谢支持!
转载请注明出处:http://teachcourse.cn/2477.html

摘要:

上一篇文章介绍了混淆的基础知识,其中包括保留指定的包名、类名、方法名以及字段名,然后总结了通配符和keep关键字的用法,这篇文章主要将要广义的混淆,其中包括:压缩、优化、混淆几个阶段,读完文章,你将会明白什么是压缩,ProGuard支持哪几种优化的方式,以及-keep-keepnames-keepclassmembers-keepclassmembernames-keepclasseswithmembers-keepclasseswithmembernames之间的区别。

一、什么是压缩?

我们知道,Java在编译的时候将代码(.java)转换成字节码(.class),相比源代码来说,字节码更加的简洁,但因为只是将源代码编译成字节码,生成的字节码中包含一些没有用到的代码,尤其当你的项目包含一些第三方的类库时,没有用到的代码会更多。压缩阶段将分析生成的字节码,然后在保证项目能够正常运行的同时将一些没有引用的类、字段、方法移除,这就是所谓的压缩阶段。

为了验证是否真的移除未引用的类、字段、方法,创建一个module,包含AdvancedCourseActivitySubject两个类,其中Subject没有被引用,对混淆后的项目进行签名打包,然后反编译生成的apk文件,检查是否还存在Subject字节码文件,如下图:

混淆压缩阶段规则

反编译后的项目结构,如下图:

混淆压缩阶段规则

由上图,发现Subject字节码文件已被移除,接下来修改Subject类中,代码如下:

/**
 * Created by TeachCourse.cn on 2017/9/3 00:51.
 */
public class Subject implements Serializable {
    private String courseName;//课程名称
    private String creditHour;//课程学分

    public Subject(String courseName, String creditHour) {
        this.courseName = courseName;
        this.creditHour = creditHour;
    }

    public String getCourseName() {
        return courseName;
    }

    public void setCourseName(String courseName) {
        this.courseName = courseName;
    }

    public String getCreditHour() {
        return creditHour;
    }

    public void setCreditHour(String creditHour) {
        this.creditHour = creditHour;
    }
}

然后在AdvancedCourseActivity引用Subject提供的构造方法和get方法,是否没有引用的set方法会被移除呢?引用的代码,如下:

...
    private void initData() {
        Log.d(TAG, "initData: courseName="+subject.getCourseName()+"; creditHour="+subject.getCreditHour());
    }

反编译后的项目结构,如下图:

混淆压缩阶段规则

由上图分析,混淆代码后,保留了被引用的字段:courseName、creditHour,保留被引用的方法:getCourseName()、getCreditHour(),以及构造方法,我们将在以后的混淆中巧妙地使用压缩规则,有点类似于Android StudioApp Link Assistant功能。

那么,开启混淆功能,默认执行压缩过程(即执行字节码分析),然后移除无用的类、字段、方法,如果我们不想执行代码的压缩,我们那该怎么做呢?

这时候,就需要学习压缩阶段的一些规则了(除此外,还有优化规则、混淆规则),压缩规则的可选项,包括如下几种:

  • -dontshrink
    开启混淆,默认压缩所有的类,通过上述关键字可以关闭压缩过程,在proguard-rules.pro文件中添加如下规则:
-dontshrink

反编译后的项目结构,如下图:

混淆压缩阶段规则

  • -printusage [filename]
    将压缩阶段未引用的所有类的信息打印到指定的文件中,如果你没有在proguard-rules.pro指定该选项,默认输出路径是:
app-obfuscated\build\outputs\mapping\release\usage.txt

在上面的例子中,Subject未引用set方法,在usage.txt文件中,可以找到如下信息:

列出未引用代码

作用:可以用于检查签名打包的apk,在压缩阶段移除了哪些代码,是否符合自己的要求。

  • -whyareyoukeeping class_specification
    打印出在压缩阶段,被保留类和内部成员的详细说明,class_specification指定我们需要了解的类名,打印出来的详细说明在Gradle Console查看,比如,我们想要了解为什么保留Subject类信息,在proguard-rules.pro文件中添加如下规则:
-whyareyoukeeping class **.Subject

运行当前Demo,gradle console打印出如下信息:

打印保留类详细信息

前面,我们说到usage.txt文件记录没有引用的类信息,反过来也可以反推出被引用的类信息,Subject类为引用setCourseName()setCreditHour(),除此外,get方法和构造方法被引用,从上图看出,在AdvancedCourseActivityvoid onCreate()被调用。

到此,我们完成了压缩阶段的学习。

二、Proguard支持哪几种优化的方式?

在压缩过程,除了移除未引用的类、字段、方法外,ProGuard同时也执行字节码的优化处理,支持的优化方式,包括以下几种:

  • 评估常量表达式
  • 移除不需要的字段访问和方法调用
  • 移除不需要的修饰符和测试实例
  • 移除未引用的代码块
  • 合并相同的代码块
  • 减少变量的分配
  • 移除只写字段和未引用的方法参数
  • 内联常量字段、方法参数和返回值
  • 内联简短的或仅回调一次的方法
  • 简化尾递归调用
  • 合并可以合并的类和接口
  • 根据使用情况,修改方法为privatestaticfinal
  • 用单一的实现替换接口
  • 执行不止200次的窥孔优化
  • 可选地删除打印的日志代码

实际的优化效果还依赖项目的代码和代码执行的VM,最终,字节码文件变得更小。

2.1 Optimization Options

Android Studio开启混淆,默认执行优化的过程,所有类里面的方法在字节码阶段进行优化处理,同时,为我们提供修改优化过程的规则,这些优化规则包括以下几种:

  • -dontoptimize
    proguard-rule.pro文件添加上述规则,关闭优化过程,需要使用该选项,在proguard-rules.pro添加如下规则:
-dontoptimize
  • -optimizations optimization_filter
    有选择的优化符合optimization_filter过滤器()规则的字节码,需要使用该选项,在proguard-rules.pro添加如下规则:
-optimizations class/marking/final
  • -optimizationpasses n
    指定执行优化级别的整型参数,默认执行一级优化,多级优化进一步改善字节码的质量。

  • -assumenosideeffects class_specification
    为了防止优化过程对类的方法造成影响,使用上述规则摒弃掉这种影响。这是因为,在优化阶段,ProGuard移除方法的依据:分析程序代码自动检查没有被引用的方法,但不分析library类库的代码,因此,在proguard-rules.pro添加上述规则,或许很有用。

  • -allowaccessmodification
    指定类和内部成员的访问修饰符可以在优化处理期间修改,可以改善优化过程的结果。

  • -mergeinterfacesaggressively
    指定可以被合并的接口

2.2 过滤器可选项

其中,optimization_filter过滤器的可选项,可以是下面列表中一项或者多项:

  1. class/marking/final
    在可能时,将类标记成final
  2. class/merging/vertical
    在可能时,垂直地合并各类
  3. class/merging/horizontal
    在可能时,水平地合并各类
  4. field/removal/writeonly
    移除只写字段
  5. field/marking/private
    在可能时,将字段标记成private
  6. field/propagation/value
    跨方法,传递字段值
  7. method/marking/private
    在可能时,将方法标记成private
  8. method/marking/static
    在可能时,将方法标记成static
  9. method/marking/final
    在可能时,将方法标记成final
  10. method/removal/parameter
    移除未引用的方法参数
  11. method/propagation/parameter
    将调用方法的方法参数的值传递给被调用方法
  12. method/propagation/returnvalue
    将方法的返回值传递给调用的方法
  13. method/inlining/short
    内联简短的方法
  14. method/inlining/unique
    内联只调用一次的方法
  15. method/inlining/tailrecursion
    在可能时,简化尾递归调用
  16. code/merging
    当修改分支版本时,合并相同的代码块
  17. code/simplification/variable
    优化变量加载和存储。
  18. code/simplification/arithmetic
    优化算术指令
  19. code/simplification/cast
    优化抛出操作
  20. code/simplification/field
    优化字段加载和存储
  21. code/simplification/branch
    优化分支指令
  22. code/simplification/string
    优化常量字符串
  23. code/simplification/advanced
    基于控制流分析和数据流分析,简化代码
  24. code/removal/advanced
    基于控制流分析和数据流分析,移除代码
  25. code/removal/simple
    基于简单的控制流分析,移除代码
  26. code/removal/variable
    从本地变量帧,移除未引用的变量
  27. code/removal/exception
    移除空try区块异常
  28. code/allocation/variable
    在本地变量帧中,优化变量分配

除了可以是上述可选项中的一项或多项外,还可以使用通配符?*,其中:

,匹配优化名称的单个字符

*,匹配优化名称的多个字符

参考的例子如下:

# 指定多个优化选项
-optimizations "code/simplification/variable,code/simplification/arithmetic"
# 指定多个优化选项(添加'!'和'*')
-optimizations "!method/propagation/*"
# 指定多个优化选项(添加'!'和'*')
-optimizations "!code/simplification/advanced,code/simplification/*"

三、保留混淆类名、方法名几种关键字的区别

在上一篇文章《Android开发之混淆基础教程》我们总结了用于保留类名、方法名的关键字就有6个,如下表格所示:

Command Command Description
-keep -keepnames 保留类和里面的成员
-keepclassmembers -keepclassmembernames 仅保留类里面的成员
-keepclasseswithmembers -keepclasseswithmembernames 保留类和里面的成员,如果类声明有成员的情况下

除了,-keepclassmembers-keepclassmembernames仅用于保留指定的方法而不保留类名外;其他4个关键字即可保留类名,也可以保留方法名,这属于其中一个比较大的区别。

然而,无names的关键字和有names的关键字的关系如下图:

Command Description Command
-keep,allowshrinking **.Subject 等价于 -keepnames **.Subject
-keepclassmembers,allowshrinking class **.Subject 等价于 -keepclassmembernames class **.Subject
-keepclasseswithmembers,allowshrinking class **.Subject 等价于 -keepclasseswithmembernames class **.Subject

开启代码混淆功能后,无names的关键字通过添加修饰符allowshrinking(ps:修饰符要加逗号),等价于有names的关键字。换句话说,前者没有执行压缩过程,后者压缩后可以移除未引用的类、方法和字段,有效减少代码量和内存消耗,因此,可以的话建议使用或者。

到此,完成了几种保留类名、方法名关键字的学习。

四、总结

本篇文章主要介绍了混淆过程的压缩代码的目的,优化代码的几种方式,然后了解各自阶段的规则配置,最后总结无names和有names保留关键字的区别,加深了对混淆过程、混淆规则的理解。在下一篇文章《Android开发之混淆高级教程(二)》,将会在包含多个第三方类库、注解、序列化、枚举等项目中使用混淆功能,那样的规则又该如何写呢?

关注公众号 扫一扫二维码,加我QQ

如果文章对你有帮助,欢迎点击上方按钮关注作者

来源:TeachCourse每周一次,深入学习Android教程,关注(QQ1589359239或公众号TeachCourse)
转载请注明出处:http://teachcourse.cn/2477.html

资源分享

教程Demo源码下载
快速更换完整项目所有引用package属性值(包名) 快速更换完整项目所有引用packag
浅谈SimpleCursorAdapter 浅谈SimpleCursorAdapter
Android Debug Bridge Android Debug Bridge
MediaRecorder实现微信、QQ、人人、易信等语音录制功能工具:MediaUtilAPI MediaRecorder实现微信、QQ、

发表评论

呲牙 憨笑 坏笑 偷笑 色 微笑 抓狂 睡觉 酷 流汗 鼓掌 大哭 可怜 疑问 晕 惊讶 得意 尴尬 发怒 奋斗 衰 骷髅 啤酒 吃饭 礼物 强 弱 握手 OK NO 勾引 拳头 差劲 爱你

表情