【Android进阶】Android中使用ProGuard
对于一个有经验的程序员,ProGuard大家一定不陌生,有人说他是混淆,我必须纠正,混淆只是ProGuard的一个过程(我不会称ProGuard为混淆,ProGuard就是ProGuard,谁再说ProGuard才是真正的混淆视听,下面有解释).
ProGuard其实不是Android特有的工具,他其实更早适用于java项目中,优化java代码,保证java程序的安全性。由于Android程序大部分也是是Java代码,所以ProGuard成为Android工程师必修的一门课程。
简单认识ProGuard
在平时开发中,我们的项目是ProGuard开关默认是是关的,我们通过如下代码打开ProGuard:
1 | android { |
先解释下我们可以通过minifyEnabled
打开开关,通过proguardFiles
指定我们的ProGuard Rule
的多个文件(后面会介绍)。我们也可以使用proguardFile
指定一个一个文件,其中proguard-android.txt
实际上是我们的{Android Sdk path}/tools/proguard/
下proguard-android.txt
。这样就可以简单使用我们的ProGuard,当然了对应的ProGuard
也是我们SDK下proguard-android.txt
什么是ProGuard
那个究竟什么是ProGuard呢,我们可以看下{Android Sdk path}/tools/proguard/
下的ProGuard的doc文档:
ProGuard is a free Java class file shrinker, optimizer, obfuscator, and preverifier. It detects and removes unused classes, fields, methods, and attributes. It optimizes bytecode and removes unused instructions. It renames the remaining classes, fields, and methods using short meaningless names. Finally, it preverifies the processed code for Java 6 or for Java Micro Edition.
大概意思就是: ProGuard是一个免费的可以将java class文件进行压缩,优化,混淆,预校验的工具。它能删除无用的类,变量、方法以及属性。它能优化字节码,移除无用的指令,同时它将(压缩后)保留的类,变量和方法使用无意义的段字符来重命名,而后还能在对应的java版本上进行预校验这些处理后的code。
## ProGuard四兄弟
如上图,ProGuard实际上四大天王(魔家四将),他们分别是压缩(shrinking),优化(optimization),混淆(obfuscation)和预校验(preverification),一个class和四大天王碰过面才可以到最后的南天门(。
压缩(shrinking):检测并移除代码中无用的类、字段、方法和特性(Attribute)
优化(optimization):对字节码进行优化,移除无用的指令
混淆(obfuscation):使用a,b,c,d这样简短而无意义的名称,对类、字段和方法进行重命名
预检(preverification):在Java平台上对处理后的代码进行预检,确保加载的class文件是可执行的。
对于一个class文件也要经历如下四步:
会首先检查这个类是否有地方用到(没用到直接删除),这个类里面的方法或者变量哪些我们没用到,没用到就直接删除
若类没有被删除,检查class文件的无用指令和字节
将仍然保留的类,混淆class名及内部的变量和方法名
在对应的java的平台上预校验这个class是否完整有效
这个是默认的情况下经历着四个步骤,这个也不是必须的。很像目前中国的大环境,你可以通过人情来避免不必要的麻烦,这四大天王也不例外。
例如我写个SDK,里面封装很多java文件,最后类库为第三方服务,最后我打出一个jar,这个jar我期望用混淆。严格按照混淆的四大天王的规则,就会遇到很多问题:保留一些类。里面的类好对外使用的,不会被SDK内部使用,按照混淆的四大天王的规则,第一步直接就被干掉了,所有就得走点后门
2. 某些class,method或者变量,我期望不要混淆。例如一个支付的class为PaySDK,里面的方法也尽量是aliPay,wxPay,名称有意义,使用混淆后PaySDK成了a,方法名也是无意义的a,b,这对使用sdk的开发者是不友好的。
为了避免这个问题,就需要我们自己来制定一个规则,在去南天门的路上,你只管这个规则来给四大天王看,告诉按照我们的规则走,而这个规则就是ProGuard Rule
我们刚才配置的proguardFiles
,对于我们来说写一个ProGuard Rule
是目前比较不太好写的一件事。
ProGuard Rule
在第一部分我们提到在主项目下的build.gradle配置我们的ProGuard Rule
,其中Google官方提供了一个简单的基础示例:
我们就来大致看下他们是分别的作用:
1 | # dont 一般是代表do not不要的意思,这个就表示混淆的字符不适用大小写混合的(如Ab) |
这个是Google建议的,若你非使用大小混合,没人会拦你。😆。
1 | # 不优化,dex不是运行在JVM上的,当然了也不做预校验 |
这个是针对android的开发而言,不过你要使用优化,尽量使用{Android Sdk path}/tools/proguard/
下的proguard-android-optimize.txt
文件。
1 | # 注解相关的 |
可以看到有个keepattributes
肯定是保留类的属性啦,还有个-keep public class 类名
这个肯定就是为了保留这个类不被压缩或者混淆。
1 | -keepclasseswithmembernames class * { |
这个-keepclasseswithmembernames public class 类名
不难解释吧,字面意思是保留class和类的成员,并且这个类有个native方法。
1 | -keepclassmembers public class * extends android.view.View { |
这个就代表保留类中的getter/setter方法,这个类是继承自android.view.View
1 | -keepclassmembers class **.R$* { |
这个肯定是保留我们资源ID对应的R文件中所有的静态变量。
看了这么多我们会产生疑问:keep、keepclassmembers等这些keep开头的有哪些,区别是什么,怎么配置参数,class,method这些怎么写,*以及分别代表什么,就让我们来一起探索ProGuard Rule
的写法吧。
Keep options
keep目前常见的就6六种:
- -keep [,modifier,…] class_specification
- -keepclassmembers [,modifier,…] class_specification
- -keepclasseswithmembers [,modifier,…] class_specification
- -keepnames class_specification 在压缩(删除无用的类和成员)后保留下来的类和其成员中,阻止类和类的成员(里面的变量和方法都是成员)混淆
- -keepclassmembernames class_specification 在压缩后保留下来的类中,阻止类的成员被混淆
- -keepclasseswithmembernames class_specification
有无name的keep*
可以看到前三个实际上是比后三个少了name的,但是意义却大不同啊。就以keep和keepName为例吧,
keep是阻止类和类的变量(实际上就是类中的方法和变量)被压缩和混淆,而keepnames那么则是在压缩后,防止类和类的成员被混淆。换句话说,keep在压缩第一道大门时,就已经生效,可以避免被压缩掉,即使这个类或者成员没有被用到也不会被移除,而keepnames则是在压缩后,从保留的类中来看哪些类和成员不能被混淆,是在压缩后才生效。
官方文档解释的很到位:
Short for -keep,allowshrinking class_specification
实际上加了allowshrinking
的kepp命令,允许压缩。
关键字 | 概述 |
---|---|
keep | 阻止类和成员被和混淆 |
keepclassmember | 阻止类的成员被和混淆 |
keepclasseswithmembers | 阻止类和类中特定的成员被压缩和混淆 |
keepnames | 在压缩后保留下来的类和其成员中,阻止类和类的成员(里面的变量和方法都是成员)混淆 |
keepclassmembernames | 在压缩后保留下来的类和其成员中,阻止类的成员被混淆 |
keepclassmembernames | 在压缩后保留下来的类和其成员中,阻止类和类中特定的成员被混淆 |
keep VS keepclasseswithmembers
这两个都可以注释保留住class及其成员,但是二者区分最大之处在于:keep只看后面的class 是什么,不关心后面的成员,而keepclasseswithmembers则是更关心后面的条件。干说太硬,来点湿的。
如下规则:
1 | -keepclasseswithmembernames class me.cyning.* { |
当在me.cyning包下,遇到方法体有native方法时,则keep其class,同时keep这个class的native的方法(其他无用方法可能会被删掉或者方法名会被混淆)。
而
1 | -keep class me.cyning.* { |
则会将class不压缩
这个keep*可以保证类或者方法不被压缩和混淆,至于选哪个看你心情喽。
##
Class specifications
在刚才示例中,有个代码片段:
1 | -keep public class com.google.vending.licensing.ILicensingService |
通过这一句keep
了com.google.vending.licensing.ILicensingService
,但是我们若是keep
某一类时而不只是一个class时,我们要怎么办?
这就需要我们将这一类的做个抽象,而将这所有的类都列出来一个个keep。
我们可以来看下class的基本示例:
1 | [public|final|abstract|@ ...] [!]interface|class|enum classname ] [[!] |
先来解释下[]吧,这个都是可选的,可以选择无,也可以选择[]里的一种或者多种。如:
1 | class * { .cyning.Keep |
就表示所有加了me.cyning.Keep
类注解且有native方法的类。
就让我们分别来看Class specifications
- annotationtype实际上就是类注解,如上面
@me.cyning.Keep
- class/interface/enum 前可以添加修饰符如public,final,abstract等修饰符,也可以在这些修饰符前加!表示否定,如!private 非私有的;在class/interface/enum前也可以加!表示否定,都是黑科技啊
- extends/implements 后可以添加我们继承或者实现的类或者接口,但是记住一定要写全名(包名+classname)。
表示里面的变量,他的前面可以加入修饰符 public
、private
、protected
、static
、volatile
、transient
等。可以表示出了构造函数外的方法,它前面也可以加入 public
、private
、protected
、static
|synchronized
、native
、abstract
、strictfp
等修饰符;而则表示构造函数 - 针对一个class/interface/enum下的所有方法我们可以用*来表示如
public *;
则表示所有修饰符为public的方法和变量。
针对上面的问题,我们还遗漏了一个很重的东西就是通配符,如,*。而通配符又分为以下:
packageName/className/methodName
包名或者类名是我们常见需要用到的,我们列出我们常用的通配符。
通配符 | 概述
—- | —
? | 表示任何一个任意但不是包名中的.分隔符。如me.cyning.Test?
可以表示me.cyning.Test1
和me.cyning.TestA
,而不能代表me.cyning.Test11
- | 表示任何多个字符但不能是包名中的.分隔符。如
me.cyning.Test*
可以表示me.cyning.Test1
和me.cyning.Test11
,而不能代表me.cyning.Test.A
- | 表示任何多个字符,可以是包名中的.分隔符,如
me.cyning.**
可以表示me.cyning.Test1
和me.cyning.Test11
,也可以是me.cyning.Test.A
- | 表示任何多个字符,可以是包名中的.分隔符,如
类型描述
包名或者类名是我们常见需要用到的,我们列出我们常用的通配符。和packageName/className/methodName
有类似之处
通配符 | 概述
—- | —
% | 除了void的基本类型
? | 单个字符
- | 不包括.包分隔符的任意类名的一部分
- | 包括.包分隔符的任意类名的一部分
- ** | 任意类型
… | 可表示多个任意且任意类型的类型的参数 如init(…)可代表init(int a, int b) 也可以代表是init(String)
熟悉了ProGuard Rule的规则,那么我们再来看最开始我们提到的[默认的ProGuard Rule](—
title: android_better_progurad
date: 2017-10-30 22:08:30
tags:
过了19大,党有了未来5年的规划,新理念你Get了没?(我觉得开完19大,做地铁都轻松多了,哈哈)。我也给自己裂了个规划,两周一篇博客,越干(gan 一声)越好。今天给自己带来的干货是ProGuard。
对于一个有经验的程序员,ProGuard大家一定不陌生,有人说他是混淆,我必须纠正,混淆只是ProGuard的一个过程(我不会称ProGuard为混淆,ProGuard就是ProGuard,谁再说ProGuard才是真正的混淆视听).
ProGuard其实不是Android特有的工具,他其实更早适用于java项目中,优化java代码,保证java程序的安全性。由于Android程序大部分也是是Java代码,所以ProGuard成为Android工程师必修的一门课程。
简单认识ProGuard
在平时开发中,我们的项目是ProGuard开关默认是是关的,我们通过如下代码打开ProGuard:
1 | android { |
先解释下我们可以通过minifyEnabled
打开开关,通过proguardFiles
指定我们的ProGuard Rule
的多个文件(后面会介绍)。我们也可以使用proguardFile
指定一个一个文件,其中proguard-android.txt
实际上是我们的{Android Sdk path}/tools/proguard/
下proguard-android.txt
。这样就可以简单使用我们的ProGuard,当然了对应的ProGuard
也是我们SDK下proguard-android.txt
什么是ProGuard
那个究竟什么是ProGuard呢,我们可以看下{Android Sdk path}/tools/proguard/
下的ProGuard的doc文档:
ProGuard is a free Java class file shrinker, optimizer, obfuscator, and preverifier. It detects and removes unused classes, fields, methods, and attributes. It optimizes bytecode and removes unused instructions. It renames the remaining classes, fields, and methods using short meaningless names. Finally, it preverifies the processed code for Java 6 or for Java Micro Edition.
大概意思就是: ProGuard是一个免费的可以将java class文件进行压缩,优化,混淆,预校验的工具。它能删除无用的类,变量、方法以及属性。它能优化字节码,移除无用的指令,同时它将(压缩后)保留的类,变量和方法使用无意义的段字符来重命名,而后还能在对应的java版本上进行预校验这些处理后的code。
## ProGuard四兄弟
如上图,ProGuard实际上四大天王(魔家四将),他们分别是压缩(shrinking),优化(optimization),混淆(obfuscation)和预校验(preverification),一个class和四大天王碰过面才可以到最后的南天门(。
压缩(shrinking):检测并移除代码中无用的类、字段、方法和特性(Attribute)
优化(optimization):对字节码进行优化,移除无用的指令
混淆(obfuscation):使用a,b,c,d这样简短而无意义的名称,对类、字段和方法进行重命名
预检(preverification):在Java平台上对处理后的代码进行预检,确保加载的class文件是可执行的。
对于一个class文件也要经历如下四步:
会首先检查这个类是否有地方用到(没用到直接删除),这个类里面的方法或者变量哪些我们没用到,没用到就直接删除
若类没有被删除,检查class文件的无用指令和字节
将仍然保留的类,混淆class名及内部的变量和方法名
在对应的java的平台上预校验这个class是否完整有效
这个是默认的情况下经历着四个步骤,这个也不是必须的。很像目前中国的大环境,你可以通过人情来避免不必要的麻烦,这四大天王也不例外。
例如我写个SDK,里面封装很多java文件,最后类库为第三方服务,最后我打出一个jar,这个jar我期望用混淆。严格按照混淆的四大天王的规则,就会遇到很多问题:保留一些类。里面的类好对外使用的,不会被SDK内部使用,按照混淆的四大天王的规则,第一步直接就被干掉了,所有就得走点后门
2. 某些class,method或者变量,我期望不要混淆。例如一个支付的class为PaySDK,里面的方法也尽量是aliPay,wxPay,名称有意义,使用混淆后PaySDK成了a,方法名也是无意义的a,b,这对使用sdk的开发者是不友好的。
为了避免这个问题,就需要我们自己来制定一个规则,在去南天门的路上,你只管这个规则来给四大天王看,告诉按照我们的规则走,而这个规则就是ProGuard Rule
我们刚才配置的proguardFiles
,对于我们来说写一个ProGuard Rule
是目前比较不太好写的一件事。
ProGuard Rule
在第一部分我们提到在主项目下的build.gradle配置我们的ProGuard Rule
,其中Google官方提供了一个简单的基础示例:
我们就来大致看下他们是分别的作用:
1 | # dont 一般是代表do not不要的意思,这个就表示混淆的字符不适用大小写混合的(如Ab) |
这个是Google建议的,若你非使用大小混合,没人会拦你。😆。
1 | # 不优化,dex不是运行在JVM上的,当然了也不做预校验 |
这个是针对android的开发而言,不过你要使用优化,尽量使用{Android Sdk path}/tools/proguard/
下的proguard-android-optimize.txt
文件。
1 | # 注解相关的 |
可以看到有个keepattributes
肯定是保留类的属性啦,还有个-keep public class 类名
这个肯定就是为了保留这个类不被压缩或者混淆。
1 | -keepclasseswithmembernames class * { |
这个-keepclasseswithmembernames public class 类名
不难解释吧,字面意思是保留class和类的成员,并且这个类有个native方法。
1 | -keepclassmembers public class * extends android.view.View { |
这个就代表保留类中的getter/setter方法,这个类是继承自android.view.View
1 | -keepclassmembers class **.R$* { |
这个肯定是保留我们资源ID对应的R文件中所有的静态变量。
看了这么多我们会产生疑问:keep、keepclassmembers等这些keep开头的有哪些,区别是什么,怎么配置参数,class,method这些怎么写,*以及分别代表什么,就让我们来一起探索ProGuard Rule
的写法吧。
Keep options
keep目前常见的就6六种:
- -keep [,modifier,…] class_specification
- -keepclassmembers [,modifier,…] class_specification
- -keepclasseswithmembers [,modifier,…] class_specification
- -keepnames class_specification 在压缩(删除无用的类和成员)后保留下来的类和其成员中,阻止类和类的成员(里面的变量和方法都是成员)混淆
- -keepclassmembernames class_specification 在压缩后保留下来的类中,阻止类的成员被混淆
- -keepclasseswithmembernames class_specification
有无name的keep*
可以看到前三个实际上是比后三个少了name的,但是意义却大不同啊。就以keep和keepName为例吧,
keep是阻止类和类的变量(实际上就是类中的方法和变量)被压缩和混淆,而keepnames那么则是在压缩后,防止类和类的成员被混淆。换句话说,keep在压缩第一道大门时,就已经生效,可以避免被压缩掉,即使这个类或者成员没有被用到也不会被移除,而keepnames则是在压缩后,从保留的类中来看哪些类和成员不能被混淆,是在压缩后才生效。
官方文档解释的很到位:
Short for -keep,allowshrinking class_specification
实际上加了allowshrinking
的kepp命令,允许压缩。
关键字 | 概述 |
---|---|
keep | 阻止类和成员被和混淆 |
keepclassmember | 阻止类的成员被和混淆 |
keepclasseswithmembers | 阻止类和类中特定的成员被压缩和混淆 |
keepnames | 在压缩后保留下来的类和其成员中,阻止类和类的成员(里面的变量和方法都是成员)混淆 |
keepclassmembernames | 在压缩后保留下来的类和其成员中,阻止类的成员被混淆 |
keepclassmembernames | 在压缩后保留下来的类和其成员中,阻止类和类中特定的成员被混淆 |
keep VS keepclasseswithmembers
这两个都可以注释保留住class及其成员,但是二者区分最大之处在于:keep只看后面的class 是什么,不关心后面的成员,而keepclasseswithmembers则是更关心后面的条件。干说太硬,来点湿的。
如下规则:
1 | -keepclasseswithmembernames class me.cyning.* { |
当在me.cyning包下,遇到方法体有native方法时,则keep其class,同时keep这个class的native的方法(其他无用方法可能会被删掉或者方法名会被混淆)。
而
1 | -keep class me.cyning.* { |
则会将class不压缩
这个keep*可以保证类或者方法不被压缩和混淆,至于选哪个看你心情喽。
##
Class specifications
在刚才示例中,有个代码片段:
1 | -keep public class com.google.vending.licensing.ILicensingService |
通过这一句keep
了com.google.vending.licensing.ILicensingService
,但是我们若是keep
某一类时而不只是一个class时,我们要怎么办?
这就需要我们将这一类的做个抽象,而将这所有的类都列出来一个个keep。
我们可以来看下class的基本示例:
1 | [public|final|abstract|@ ...] [!]interface|class|enum classname ] [[!] |
先来解释下[]吧,这个都是可选的,可以选择无,也可以选择[]里的一种或者多种。如:
1 | class * { .cyning.Keep |
就表示所有加了me.cyning.Keep
类注解且有native方法的类。
就让我们分别来看Class specifications
- annotationtype实际上就是类注解,如上面
@me.cyning.Keep
- class/interface/enum 前可以添加修饰符如public,final,abstract等修饰符,也可以在这些修饰符前加!表示否定,如!private 非私有的;在class/interface/enum前也可以加!表示否定,都是黑科技啊
- extends/implements 后可以添加我们继承或者实现的类或者接口,但是记住一定要写全名(包名+classname)。
表示里面的变量,他的前面可以加入修饰符 public
、private
、protected
、static
、volatile
、transient
等。可以表示出了构造函数外的方法,它前面也可以加入 public
、private
、protected
、static
|synchronized
、native
、abstract
、strictfp
等修饰符;而则表示构造函数 - 针对一个class/interface/enum下的所有方法我们可以用*来表示如
public *;
则表示所有修饰符为public的方法和变量。
针对上面的问题,我们还遗漏了一个很重的东西就是通配符,如,*。而通配符又分为以下:
packageName/className/methodName
包名或者类名是我们常见需要用到的,我们列出我们常用的通配符。
通配符 | 概述
—- | —
? | 表示任何一个任意但不是包名中的.分隔符。如me.cyning.Test?
可以表示me.cyning.Test1
和me.cyning.TestA
,而不能代表me.cyning.Test11
- | 表示任何多个字符但不能是包名中的.分隔符。如
me.cyning.Test*
可以表示me.cyning.Test1
和me.cyning.Test11
,而不能代表me.cyning.Test.A
- | 表示任何多个字符,可以是包名中的.分隔符,如
me.cyning.**
可以表示me.cyning.Test1
和me.cyning.Test11
,也可以是me.cyning.Test.A
- | 表示任何多个字符,可以是包名中的.分隔符,如
类型描述
包名或者类名是我们常见需要用到的,我们列出我们常用的通配符。和packageName/className/methodName
有类似之处
通配符 | 概述
—- | —
% | 除了void的基本类型
? | 单个字符
- | 不包括.包分隔符的任意类名的一部分
- | 包括.包分隔符的任意类名的一部分
- ** | 任意类型
… | 可表示多个任意且任意类型的类型的参数 如init(…)可代表init(int a, int b) 也可以代表是init(String)
熟悉了ProGuard Rule的规则,那么我们再来看最开始我们提到的默认的ProGuard Rule。
—— 建议读者有时间结合上面介绍的Class specifications和Keep Options,来重新认识和验证下。——–
自定义ProGuard Rule)
我们要自定义的ProGuard Rule,其实也是三步走:基本Proguard Rule,业务中常见代码的ProGuard Rule,第三方SDK的ProGuard Rule,一般情况主要按照三步走,尽量做到ProGuard涉及到我们应ProGuard之处。
基本Proguard Rule
可以直接参考我们的默认的ProGuard Rule,这个一般不会涉及到业务,也可以在其尾部继续适当加入,如下:
1 | # 避免混淆泛型,这在JSON实体映射时非常重要,比如fastJson |
这个大家是不是基本上是我们最最常见的,不是一定如此,这个是个人观点,仅供参考使用。
业务常见
这个需要结合自己的业务来做,或者来看,例如我的:
1 | # |
网上有雷锋帮我们准备了一份:msdx/android-proguard-cn,拿走不谢。
彩蛋
之前看赵四大哥的一篇文章,Android安全防护之旅—带你把Apk混淆成中文语言代码
,它从源码上做了处理,不过这个不是我期望的,后来居然发现,ProGuard居然真的可以设置mappding的字典:
-obfuscationdictionary naruto.txt
naruto已经上传到gitgist啦.
打开有惊喜哦。
- 本文链接:http://ownwell.github.io/2017/10/30/android-better-progurad/
- 版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 CN 许可协议。转载请注明出处!