Android混淆Proguard使用



一、引言

关于Android应用的混淆,现在网上有很多的资料,也有很多的相关案例和模板;作为一个开发者,或多或少都知道点关于混淆的概念,通常作法是从网上找找相关的模板,然后修改下放到自己的项目中使用。否则项目就比较危险,被别人反编译就现出原形,好比扒光了衣服给人家看一样,没有安全感。所以,个人觉得有必要对Proguard混淆做个总结,项目中还没用上的赶紧用上。

二、Proguard混淆介绍

Proguard是一个Java类文件压缩器、优化器、混淆器、预校验器(ProGuard is a Java class file shrinker, optimizer, obfuscator, and preverifier)。压缩环节会检测以及移除没有用到的类、字段、方法以及属性。优化环节会分析以及优化方法的字节码。混淆环节会用无意义的短变量去重命名类、变量、方法。这些步骤让代码更精简,更高效,也更难被逆向(破解)。

  • 压缩(Shrink):检测并移除代码中无用的类、字段、方法和特性(Attribute)
  • 优化(Optimize):对字节码进行优化,移除无用的指令
  • 混淆(Obfuscate):使用a,b,c,d这样简短而无意义的名称,对类、字段和方法进行重命名
  • 预检(Preveirfy):在Java平台上对处理后的代码进行预检,确保加载的class文件是可执行的

简单的说:混淆就是移除没有用到的代码,然后对代码里面的类、变量、方法重命名为可读性很差的简短名字。

三、Proguard混淆应用

3.1 开始混淆

将项目的build.gradle 中的minifyEnabled 置为true。使用Release 打包APK,注意这里不能直接run,默认编译运行的APP为debug版本,是不会进行混淆的。

buildTypes {
    release {
        minifyEnabled true // 是否混淆

        # 这里的proguard-android.txt以及proguard-rules.pro就是入口点的配置文件
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        buildConfigField "boolean", "LOG_DEBUG", "false"//关闭调试log
    }
}

proguard-android.txt文件是google官方提供的一个android app项目的默认通用配置文件,适用于所有android app项目。而proguard-rules.pro默认为空,可以在这里放置一些和特定app相关的配置。

3.2 编译出错处理

proguard混淆的整个编译过程,经常发现混淆失败报错了或者运行异常,但明明代码是对的,用debug编译运行都没问题,那可能是混淆的哪个步骤出错了(通常是把不能混淆的内容也加入混淆),所以要根据编译报错信息或者运行报错信息来分析。

#-------------------------------------------基本不用动区域--------------------------------------------
#---------------------------------基本指令区----------------------------------
-optimizationpasses 5       # 指定代码的压缩级别(0-7)
-dontusemixedcaseclassnames     # 是否使用大小写混合
-dontskipnonpubliclibraryclasses        # 指定不去忽略非公共的库类
-dontskipnonpubliclibraryclassmembers       # 指定不去忽略包可见的库类的成员
-dontpreverify      # 混淆时是否做预校验
-verbose        # 混淆时是否记录日志
-printmapping proguardMapping.txt

# 指定混淆是采用的算法,后面的参数是一个过滤器
# 这个过滤器是谷歌推荐的算法,一般不做更改
-optimizations !code/simplification/cast,!field/*,!class/merging/*      # 混淆时所采用的算法

-keepattributes *Annotation*,InnerClasses
-keepattributes Signature
-keepattributes SourceFile,LineNumberTable
#----------------------------------------------------------------------------
-ignorewarnings     # 是否忽略检测,(是)
#---------------------------------默认保留区---------------------------------
-keep public class * extends android.app.Activity
-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
-keep public class * extends android.view.View
-keep public class com.android.vending.licensing.ILicensingService
-keep class android.support.** {*;}
#-ignorewarnings -keep class * { public private *; }

#如果有引用v4包可以添加下面这行
-keep class android.support.v4.** { *; }
-keep public class * extends android.support.v4.**
-keep public class * extends android.app.Fragment

# 保留本地native方法不被混淆
-keepclasseswithmembernames class * {
    native <methods>;
}

# 保留在Activity中的方法参数是view的方法,
# 这样以来我们在layout中写的onClick就不会被影响
-keepclassmembers class * extends android.app.Activity{
    public void *(android.view.View);
}

# 保留枚举类不被混淆
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

# 保留我们自定义控件(继承自View)不被混淆
-keep public class * extends android.view.View{
    *** get*();
    void set*(***);
    public <init>(android.content.Context);
    public <init>(android.content.Context, android.util.AttributeSet);
    public <init>(android.content.Context, android.util.AttributeSet, int);
}

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

# 保留Parcelable序列化类不被混淆
-keep class * implements android.os.Parcelable {
  public static final android.os.Parcelable$Creator *;
}

# 保留Serializable序列化的类不被混淆
-keepclassmembers class * implements java.io.Serializable {
    static final long serialVersionUID;
    private static final java.io.ObjectStreamField[] serialPersistentFields;
    private void writeObject(java.io.ObjectOutputStream);
    private void readObject(java.io.ObjectInputStream);
    java.lang.Object writeReplace();
    java.lang.Object readResolve();
}

#表示不混淆R文件中的所有静态字段
-keep class **.R$* {
    public static <fields>;
}
# 对于带有回调函数的onXXEvent、**On*Listener的,不能被混淆
-keepclassmembers class * {
    void *(**On*Event);
    void *(**On*Listener);
}
#----------------------------------------------------------------------------

#---------------------------------webview------------------------------------
-keepclassmembers class fqcn.of.javascript.interface.for.Webview {
   public *;
}
-keepclassmembers class * extends android.webkit.WebViewClient {
    public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
    public boolean *(android.webkit.WebView, java.lang.String);
}
-keepclassmembers class * extends android.webkit.WebViewClient {
    public void *(android.webkit.WebView, jav.lang.String);
}

可以看到,这里主要保留的是一些不能被混淆的类、方法或者变量名,编写配置文件的核心思想就是尽可能地去做混淆,除非有不得不混淆的理由。以下列举一些常用的不能被混淆的情况:

  • 自定义控件
  • 实体类(JavaBean)
  • 枚举
  • 本地方法:因为本地方法是根据方法名去调用的,若混淆后会导致找不到此方法名
  • 反射相关的方法和类:反射原理就是通过方法名和类名去实例化相应的对象,调用相关的方法,当然也不能混淆
  • setXX和getXX方法:这里指的是通过配置文件直接生成相应的set和get方法的相关库,所以javabean类很多情况下不能做混淆
  • 第三方jar包:这个需要具体情况具体分析,很多知名库都会提供默认的混淆配置,大多数情况可以不用做混淆,毕竟不属于项目的核心代码

3.3 混淆规则

保留(Keep options)

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

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

保留类名及类的成员

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

保留指定类的所有子类(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 

3.4 混淆实例对比

配置了proguard,用proguard打包生成了APK,什么知道混淆的代码已经混淆。可用实际成果来检验,通过反编译来验证,本例采用jadx反编译工具来做:
(没有混淆的工程反编译情况)

(加入混淆的工程反编译情况)

从上图中可知,没有加混淆,反编译后基本上可以清晰地看源码,但加入混淆之后,大都是简单字母abc等方法,这说明使用proguard混淆代码已经生效了。

四、总结

混淆是为了保护APP项目的代码,使APP更难被破解;其次是优化APP,去除无用代码和资源,减小APP的大小。你还让你的工程裸奔吗?


参考资料:

  • https://developer.android.google.cn/studio/build/shrink-code?hl=zh-cn#shrink-code