Android混淆(ProGuard)从0到1

本文为原创,转载请注明出处:
http://www.jianshu.com/p/1b76e4c10495

说在前面的

作为一个Android开发者,或多或少都知道点关于混淆的概念,在使用的时候大家有可能也只是网上找找相关的,然后改改复制粘贴到自己的项目中使用。
公司有一个产品,工程也比较大,已经有四年多的历程了,之前一直在eclipse中开发,一直疲于应付需求bug之类的,零零散散的时间又不想去弄混淆,只是一直感觉混淆好像是个很高大上的东西。
后来转到了Android Studio上开发,我知道在Android Studio上启动混淆是一件比较容易的事情,但是由于工程比较巨大了,一直不敢轻易做尝试,自己深知工程也比较危险,可以被别人反编译,就好比扒光了衣服给人家看一样,很没有安全感,所以今天下定决心好好研究一下混淆方面的东西。网上有很多资料,但是大多同样一句话,确有不同的解释,并且每个人的配置都不一样,因此我觉得有必要对混淆做进一步的认识,并不应该只是停留在‘改改能用就完事大吉’的程度,毕竟混淆是对工程非常有用的,花点时间掌握,一劳永逸。

混淆简介

Android代码混淆,又称Android混淆,是一种Android APP保护技术,用于保护APP不被破解和逆向分析。
从Android2.3开始,Google在SDK中加入了一款叫ProGuard的代码混淆工具,ProGuard会删除这些调试信息,并用无意义的字符序列来替换类名、方法名等,使得使用反编译出来的代码难以阅读,提升逆向难度。
ProGuard它可以混淆Android项目里面的java代码,
注意:仅仅是java代码。它是无法混淆Native代码,资源文件drawable、xml等,资源压缩通过 Android Plugin for Gradle 提供,该插件会移除封装应用中未使用的资源,包括代码库中未使用的资源,两者协同使用,使得在移除未使用的代码后,任何不再被引用的资源也能安全地移除。

ProGuard的三大作用

  • 压缩(Shrinking):移除未被使用的类、属性、方法等,并且会在优化动作执行之后再次执行(因为优化后可能会再次暴露一些未被使用的类和成员。

  • 优化(Optimization): 优化字节码,并删除未使用的结构。

  • 混淆(Obfuscation):将类名、属性名、方法名混淆为难以读懂的字母,比如a,b,c等,增大反编译难度。

开启ProGuard

在Android Studio中开启混淆是一件非常容易的事情。
创建module的时候,编译器会在module下自动创建proguard-rules.pro文件,这个也是我们写混淆规则的地方。
在module的build.grade文件默认生成了下面的代码:

android {
    ...
    buildTypes {
        release {
            //只需将false改为true即可启用混淆
            minifyEnabled false
          
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    ...
    }
}

getDefaultProguardFile(‘proguard-android.txt’) 是获取默认的 ProGuard 设置。proguard-android.txt描述了系统提供的基础混淆配置信息,文件所在路径为:\sdk\tools\proguard\proguard-android.txt。
后面会展示文件的相关内容,其中里面关闭了优化功能,所以如果要想做进一步的代码压缩,同一路径下 proguard-android-optimize.txt 文件。它包括相同的 ProGuard 规则,但还包括其他在字节码一级(方法内和方法间)执行分析的优化,以进一步减小 APK 大小和帮助提高其运行速度。

建议在debug的时候先测试混淆规则是否正确,所以需要在debug模式启用混淆,只需要增加一个debug结点的定义即可:

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

混淆规则

  • 保留(Keep options)

-keep [,modifier,…] class_specification 保留类和类的成员(字段和方法),常用规则。

  1. 保留类名
#一颗星表示只保持该包下的类名,而子包下的类名还是会被混淆
-keep class com.ms.bean.*
#两颗星表示把本包和所含子包下的类名都保持
-keep class com.ms.bean.** 

使用上面两个规则都只是保持类名不变,类中的方法和成员都会被混淆

  1. 保留类名及类的成员
#保留 com.ms.bean包下的类及类的成员
-keep class com.ms.bean.*{*;}
#保留具体的某个类及类的成员
-keep class com.ms.bean.Person{*;}

如果不希望保留类的所有成员,可以使用:

<init>;     //匹配所有构造器
<fields>;   //匹配所有域
<methods>;  //匹配所有方法方法
# 如保留构造方法
-keep class com.ms.bean.Person{
<init>;
}
# 还可以在<fields>或<methods>前面加上private 、public、native等来进一步指定不被混淆的内容。
#如保留public构造方法
-keep class com.ms.bean.Person{
public <init>;
}
  1. 保留指定类的所有子类(implement/extends)
# 保留Activity的所有子类
-keep public class * extends android.app.Activity

其它保留规则,详见官网

  • 压缩(Optimization options)

#关闭压缩
-dontshrink 
-printusage {filename}
-whyareyoukeeping {class_specification}

更多详情见:地址

  • 优化(Optimization options)

#关闭优化
-dontoptimize  
#迭代优化,n表示proguard对代码进行迭代优化的次数,Android一般为5
-optimizationpasses n 

更多详见:地址

  • 混淆(Obfuscation options)

#关闭混淆
-dontobfuscate 

默认开启,保持默认即可。
更多详见:地址

  • 预校验选项(Preverification options)

-dontpreverify #禁用预校验
-microedition

更多详见:地址

  • 常规选项(General options)

更多详见:地址

混淆的注意事项

ProGuard 难以对许多情况进行正确分析,可能会移除应用真正需要的代码,所以有些代码是需要保留的

  1. 在AndroidManifest中配置的类,比如四大组件
  2. JNI调用的方法
# 保持native方法不被混淆    
-keepclasseswithmembernames class * { 
    native <methods>;
}
  1. 反射用到的类
  2. WebView中JavaScript调用的方法,规则同第2条
-keepclassmembers class fqcn.of.javascript.interface.for.webview {
   public *;
}
  1. Layout文件引用到的自定义View
  2. 与服务端交互时,Json对象
  3. Parcelable的子类和Creator静态成员变量不混淆,否则会产生Android.os.BadParcelableException异常;
# 保持Parcelable不被混淆
-keep class * implements Android.os.Parcelable { 
    public static final Android.os.Parcelable$Creator *;
}
  1. 使用enum类型时需要注意避免以下两个方法混淆,因为enum类的特殊性,以下两个方法会被反射调用,见第3条规则
-keepclassmembers enum * {  
    public static **[] values();  
    public static ** valueOf(java.lang.String);  
}
  • 一些引入的第三方库
    这里推荐两个开源项目,里面收集了一些第三方库的混淆规则
    android-proguard-snippets
    android-proguard-cn
    但是说实话,用处应该不大,仅作参考,毕竟第三方库都应该有混淆说明,并且这些开源库的更新并不及时

以上是在混淆的时候应该避免混淆的,其实想通了是很好理解的,混淆之后对应的类会变成没有具体含义的a,b,c等,哪么在调用的地方就可能无法分清楚具体调用的是哪一个
所以混淆导致调用者无法分清楚具体调用的是哪一个时都应该避免混淆

小技巧:如果在代码中有需要保留的代码,可以对想要保留的部分添加@Keep 注解。在类上添加 @Keep 可原样保留整个类;在方法或字段上添加它可完整保留方法/字段(及其名称)以及类名称。

由于英语水平有限,规则部分只能慢慢的去研究,后续补全规则介绍,并给出在Android中混淆的常规配置。

扩展了解

  • ProGuard的输出文件说明

混淆后,会在 <module-name>/build/outputs/mapping/release(or debug)/目录下输出下面的文件

dump.txt 说明 APK 中所有类文件的内部结构。
mapping.txt 提供原始与混淆过的类、方法和字段名称之间的转换。
seeds.txt 列出未进行混淆的类和成员。
usage.txt 列出从 APK 移除的代码。

当我们需要处理crash log的时候,就可以通过mapping.txt的映射关系找到对应的类,方法,字段等。

mapping.txt的映射关系找到对应的类,方法,字段方法如下:
sdk\tools\proguard\bin 目录下有个retrace工具可以将混淆后的报错堆栈解码成正常的类名window下为retrace.bat,linux和mac为retrace.sh,

使用方法如下:

  1. 将crash log保存为yourfilename.txt
  2. 拿到版本发布时生成的mapping.txt
  3. 执行命令retrace.bat -verbose mapping.txt yourfilename.txt

所以我们每次打包版本都需要保存最新的mapping.txt文件。如果要使用到第三方的crash统计平台,比如bugly,还需要我们上传APP版本对应的mapping.txt.每次都要保存最新的mapping文件,那不就很麻烦?放心,gradle会帮到你,只需要在bulid.gradle加入下面的一句。每次我们编译的时候,都会自动帮你保存mapping文件到本地的。

android {
applicationVariants.all { variant ->
        variant.outputs.each { output ->
            if (variant.getBuildType().isMinifyEnabled()) {
                variant.assemble.doLast{
                        copy {
                            from variant.mappingFile
                            into "${projectDir}/mappings"
                            rename { String fileName ->
                                "mapping-${variant.name}.txt"
                            }
                        }
                }
            }
        }
        ......
    }
}
  • Android项目混淆配置参考

在此之前,开启ProGuard一节提到proguard-android.txt为系统提供的基础混淆配置,文件所在路径为:\sdk\tools\proguard\proguard-android.txt,我门可以找到这个文件先了解一下系统提供了哪些基础配置

通过上述了解,我门对混淆有了一定认识和理解,那么我门可以整理出一个关于Android工程混淆的配置规则(完整版,包括基础配置):

#(Basic 包名不混合大小写
-dontusemixedcaseclassnames
#(Basic)不忽略非公共的库类
-dontskipnonpubliclibraryclasses
#(Basic)输出混淆日志
-verbose

# Optimization is turned off by default. Dex does not like code run
# through the ProGuard optimize and preverify steps (and performs some
# of these optimizations on its own).
#(Basic)不进行优化
-dontoptimize
#(Basic)不进行预检验
-dontpreverify
# Note that if you want to enable optimization, you cannot just
# include optimization flags in your own project configuration file;
# instead you will need to point to the
# "proguard-android-optimize.txt" file instead of this one from your
# project.properties file.

#混淆注意事项第一条,保留四大组件及Android的其它组件
-keep public class * extends android.app.Activity
#(Basic)
-keepclassmembers class * extends android.app.Activity {
   public void *(android.view.View);
}
-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
#(Basic)
-keep public class com.google.vending.licensing.ILicensingService
#(Basic)
-keep public class com.android.vending.licensing.ILicensingService
#(Basic)混淆注意事项第二条,保持 native 方法不被混淆  
-keepclasseswithmembernames class * {
    native <methods>;
}
# 混淆注意事项第四条,保持WebView中JavaScript调用的方法  
-keepclassmembers class * {
    @android.webkit.JavascriptInterface <methods>;
}
#混淆注意事项第五条 自定义View (Basic)
-keepclassmembers public class * extends android.view.View {
   void set*(***);
   *** get*();
}
# (Basic)混淆注意事项第七条,保持 Parcelable 不被混淆  
-keepclassmembers class * implements android.os.Parcelable {
  public static final android.os.Parcelable$Creator CREATOR;
}
#(Basic) 混淆注意事项第八条,保持枚举 enum 类不被混淆  
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}
#(Basic)
-keepclassmembers class **.R$* {
    public static <fields>;
}
#(Basic)保留注解 
-keepattributes *Annotation*
# (Basic)排除警告
-dontwarn android.support.**
# Understand the @Keep support annotation.
# (Basic)不混淆指定的类及其类成员
-keep class android.support.annotation.Keep
# (Basic)不混淆使用注解的类及其类成员
-keep @android.support.annotation.Keep class * {*;}
# (Basic)不混淆所有类及其类成员中的使用注解的方法
-keepclasseswithmembers class * {
    @android.support.annotation.Keep <methods>;
}
# (Basic)不混淆所有类及其类成员中的使用注解的字段
-keepclasseswithmembers class * {
    @android.support.annotation.Keep <fields>;
}
# 不混淆所有类及其类成员中的使用注解的初始化方法
-keepclasseswithmembers class * {
    @android.support.annotation.Keep <init>(...);
}
#保留源文件以及行号 方便查看具体的崩溃信息
-keepattributes SourceFile,LineNumberTable

其中(Basic)代表系统提供的文件(proguard-android.txt)中的基础配置包含的内容,
之前我在网上查别人的配置,就有
#(Basic) -keep public class com.android.vending.licensing.ILicensingService这么一条语句,当然从语法上讲我知道是保留com.android.vending.licensing.ILicensingService这个类,但心里纳闷这是什么鬼,为什么别人知道呢,至少从现在知道系统提供的基本混淆中包含了这句话,也知道出处了。
如果想进一步优化,配置信息请看(proguard-android-optimize.txt)
也可以看我目前在使用的混淆配置信息:https://github.com/SupLuo/MS/blob/master/msproguard/proguard-rules_basic.pro

参考

https://segmentfault.com/a/1190000004461614
https://blog.gmem.cc/proguard-study-note
https://developer.android.google.cn/studio/build/shrink-code.html?hl=zh-cn#shrink-code

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