前言
作为一个Android开发,大家或多或少都有一些关于混淆的了解(毕竟披个纱布也比裸奔要好的多吧)。混淆的概念虽然容易理解,但相信大多数开发可能还是在网上搜索通用配置后通过C-V大法接入到自己的项目中,这也使得混淆配置比较混乱,缺乏针对性。
来吧,让我们看看怎么才能穿好这件衣服!!
混淆的必要性
Java是一种广泛使用的计算机编程语言,拥有跨平台、面向对象、泛型编程的特性。但不同于一般的编译语言或解释型语言,它首先将源代码编译成字节码,再依赖各种不同平台上的虚拟机来解释执行字节码,从而具有“一次编写,到处运行”的跨平台特性。而这些字节码带有许多的语义信息,很容易被反编译成Java源代码。
WTFK!!
那么作为使用Java作为编程语言(别较劲,我知道Kotlin)的Android应用怎么办,不是动不动就被反编译还原啦!!没错,如果你不做些保护措施,那你的APP、SDK在别人眼里就是那么的赤果果。
混淆就是你现在需要的一顶保护伞。
混淆就是将原本正常的项目文件,对其类、方法、字段,重新命名a,b,c…之类的字母,使得处理后的代码在实现相同功能的同时,降低阅读性,从而增加反编译的难度。但是,但是,但是,重要的事情说三遍,混淆只能增加反编译的难度并不能保证觉得的安全,比较你也懂得,总有人的技术比你。。。。。。
ProGuard
官网:https://stuff.mit.edu/afs/sipb/project/android/sdk/android-sdk-linux/tools/proguard/docs/index.html#manual/introduction.html
在官网上是这样说的:
ProGuard is a Java class file shrinker, optimizer, obfuscator, and preverifier. The shrinking step detects and removes unused classes, fields, methods, and attributes. The optimization step analyzes and optimizes the bytecode of the methods. The obfuscation step renames the remaining classes, fields, and methods using short meaningless names. These first steps make the code base smaller, more efficient, and harder to reverse-engineer. The final preverification step adds preverification information to the classes, which is required for Java Micro Edition or which improves the start-up time for Java 6.
简单来说就是:
ProGuard 是一款针对于Java class的压缩、优化、混淆以及预校验工具;压缩环节检测并删除未使用的类、字段、方法和属性;优化环节分析和优化方法的字节码;混淆环节使用无意义的短名称重命名剩余的类、字段和方法。这些操作使代码库更小、更高效并且更难进行逆向工程。最后的预验证环节将预验证信息添加到类中,这是 Java Micro Edition 或者 Java 6及高版本所必需的,当然这一环节在Android开发者并不需要。
如流程图所示,ProGuard主要包含四个环节:压缩、优化、混淆、预检验;
- 压缩(Shrink):检测并删除未使用的类、字段、方法和属性;
- 优化(Optimize):优化字节码并删除未使用的指令;
- 混淆(Obfuscate):使用无意义的短名称重命名剩余的类、字段和方法;
- 预预检验(Preverify):对 class 文件进行预检验,确保虚拟机加载的 class 文件是安全并且可以执行的;
Android使用ProGuard
Android混淆说明:https://developer.android.com/studio/build/shrink-code#keep-code
Android使用ProGuard手册:https://www.guardsquare.com/manual/home
在Android Gradle 插件 3.4.0 或更高版本构建项目时,已不再使用 ProGuard 执行编译时代码优化,而是与 R8 编译器协同工作,完成编译任务。若想不想启用R8则可在gradle.properties中添加如下配置:
android.enableR8=false
android.enableR8.libraries=false
在正式使用ProGuard之前先让我们用几张图来直观的感受一下混淆的效果:
混淆前
混淆后
对比结果:
- 包体积缩减,因为作为示例的SDK本身内容优先,资源文件极少,所以这里看来效果不明显,各位看官可以在自己的项目中看下实际的缩减效果;
- 包名、类名、方法名替换为无意义的字母,增加理解难度;
既然看过了对比效果,那接下来就让我们撸起袖子开干吧。
开启ProGuard
新建module的时候系统会默认在build.gradle中添加以下配置,以开启混淆:
android {...buildTypes {release {//true - 开启混淆;false - 关闭混淆minifyEnabled falseproguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'}}
}
minifyEnabled:混淆开关;
proguardFiles:混淆配置文件列表;
getDefaultProguardFile:获取系统提供的默认混淆配置文件,文件路径为:\sdk\tools\proguard\proguard-android.txt;系统还提供了“proguard-android-optimize.txt”默认配置,如果在SDK中开启混淆这里建议使用“proguard-android.txt”;
proguard-rules.pro:编译器自动创建的配置文件,无内容,用于编写我们自定义的混淆规则;
小技巧:当针对SDK开启混淆可通过下面的设置将混淆配置直接打进SDK中,避免使用者对于SDK混淆配置的依赖:
android {defaultConfig{//将混淆配置打包进aarconsumerProguardFiles 'proguard-rules.pro'}
}
混淆规则
混淆开启后,编译器将根据配置文件进行处理,而需要我们自定义制定的规则则是需要明确告诉编译器哪些是我们需要保留的(编译器:老哥啊,你不告诉我哪些要留着,我怎么制定哪些是你需要的,我当然一股脑全给干了啊,毕竟我可是一个尽职尽责的编译器!!!);
下面让我们来看看一些关键的规则配置:
keep关键字
-keep [,modifier,…] class_specification
指定要保留的类和类成员(字段和方法),类不会被移除,但类成员若不声明保留且未被使用的话将会被移除(SDK开发尤其需要注意这一点);
例:
正确配置:
错误配置:
-keepclassmembers [,modifier,…] class_specification
指定要保留的类成员,其他类成员将会被混淆或移除,类名将会混淆,若类未被引用也将被移除;
例:
声明保留static属性及public构造方法;
可以看到,非static的name属性被混淆,test方法未引用被移除;
-keepclasseswithmembers [,modifier,…] class_specification
当指定的类成员都存在时,保留类和类成员,否则将被混淆或移除;
例:
声明保留具有指定构造方法及keepclasseswithmembers()方法的类
满足条件的类Keepclasseswithmembers1被保留,Keepclasseswithmembers2因缺少对应的构造方法不满足要求被移除
-keepnames、-keepclassmembernames、-keepclasseswithmembernames
这是三个规则对应 -keep(-keepclassmembers、-keepclasseswithmembers) allowshrinking class_specification 的缩写,即在原有混淆基础上若类或类成员未被使用则在压缩阶段允许移除;
keep规则总结对比
关键字 | 目标对象 | 说明 |
---|---|---|
keep | 类、类成员 | 保留类和类中的成员,防止它们被混淆或移除; |
keepclassmembers | 类成员 | 保留类成员,防止它们被混淆或移除 |
keepclasseswithmembers | 类、类成员 | 指定的类成员都存在时,保留类和类成员,防止它们被混淆或移除 |
keepnames | 类、类成员 | 保留类和类中的成员,防止它们被混淆,但当类或成员未被引用时会被移除; |
keepclassmembernames | 类成员 | 保留类成员,防止它们被混淆,但当类成员未被引用时会被移除; |
keepclasseswithmembernames | 类、类成员 | 指定的类成员都存在时,保留类和类成员,防止它们被混淆,但当类未被引用时会被移除; |
从表格中我们可以知道每个规则都有自己针对的目标,我们必须根据实际场景来选择对应的关键字进行组合使用;尤其是必须选取合适的入口类进行keep,否则可能导致其他规则配置因为类或类成员未引用而被移除导致混淆配置无效; |
通配符
通配符 | 意义 |
---|---|
<init> | 匹配任何构造函数 |
<fields> | 匹配任何字段 |
<methods> | 匹配任何方法 |
? | 匹配任何单个字符 |
% | 匹配任何基本类型(“ boolean”、“ int”等)或“ void”类型 |
-
| 匹配任意长度字符,但是不包括类分隔符“.”
-
| 匹配任意长度字符,包括类分隔符“.”
*** | 匹配任何类型(原始或非原始、数组或非数组)
… | 匹配任意数量的任意类型的参数
其他常见规则
-printmapping [filename]
指定生成混淆映射mapping文件;
-keepattributes[属性过滤器]
指定要保留的任何可选属性。属性可以指定一个或多个,如:
#保留Annotation不混淆
-keepattributes *Annotation*,InnerClasses
#避免混淆泛型
-keepattributes Signature
#抛出异常时保留代码行号
-keepattributes SourceFile,LineNumberTable
-dontwarn [class_filter]
指定不警告未解决的引用和其他问题;
引入三方library时,强烈建议配置相关包名,如:
#比如关闭Twitter sdk的警告,我们可以这样做
-dontwarn com.twitter.sdk.**
-allowaccessmodification
允许修改扩大访问修饰符,如default扩大为public,但是并不推荐配置该规则(尤其是SDK),因为这可能导致其他实现场景中的方法冲突;
还记得开头我们说的getDefaultProguardFile获取默认配置文件吗,正式因为这条规则的存在我并不推荐使用“proguard-android-optimize.txt”;
-dontshrink
关闭压缩,默认开启;
默认情况下,因为类或类成员未引用时将会被移除,但是程序猿小哥说:我不想管那么多,我只知道这些类和成员不能混淆,其他你(编译器)看着办;
这时候我们就只能加上这个配置了,保留所有类和成员只做Obfuscate(混淆);
-repackageclasses [package_name]
指定重新打包所有重命名的类文件,将它们移动到给定的包中;
-keepparameternames
保留参数名称和方法类型;
混淆产物
每次打包完成后将在 app/build/outputs/mapping/{flavor}/ 目录下生成一些混淆相关的文件;
文件名 | 作用 |
---|---|
configuration.txt | 所有混淆配置的汇总 |
mapping.txt | 原始与混淆过的类、方法、字段名称间的转换 |
resources.txt | 资源优化记录文件,哪些资源引用了其他资源,哪些资源在使用,哪些资源被移除 |
seeds.txt | 未进行混淆的类与成员 |
usage.txt | APK中移除的代码 |
哪些不应该混淆
- 自定义控件不混淆;
- 枚举类不混淆;
- 第三方库中的类不混淆,需要根据三方的混淆规则进行配置,若无法确定则整个包名都进行保留;
- 运用了反射的类不混淆;
- 使用了 Gson 之类的工具要使 JavaBean 类即实体类不被混淆;
- 有用到 WebView 的 JS 调用也需要保证写的接口方法不混淆;
- 继承了Serializable接口的类不混淆;
- Parcelable 的子类和 Creator 静态成员变量不混淆,否则会产生 Android.os.BadParcelableException 异常;
- 使用的四大组件,自定义的Application* 实体类
- JNI中调用的类
- Layout布局使用的View构造函数(自定义控件)、android:onClick等。
常见问题
包冲突
因为混淆会使用无意义的a、b、c等字母对类、类成员进行替换,这就导致在多包集成的情况存在包名冲突的可能;
这里就需要我们在打包的时候添加 -repackageclasses 规则,将所有文件迁移至自定义的包名下,避免冲突;
运行中数据为null
这个没啥好说的,仔细检查下混淆配置是否完全吧,肯定是混淆了不该混淆的类;
Unsupported version number [55.0] (maximum 54.0, Java 10)
ProGuard版本不匹配;
在根目录的build.gradle中指定版本
buildscript{dependencies{classpath 'net.sf.proguard:proguard-gradle:6.1.1'}
}
通用配置
最后再提供一份通用的配置,这里的配置可能和系统默认混淆配置文件中存在重复(勿喷);
#---------------------------------基本指令区----------------------------------
# 设置混淆的压缩比率 0 ~ 7
-optimizationpasses 5
#混合时不使用大小写混合,混合后的类名为小写,windows下必须使用该选项
-dontusemixedcaseclassnames
#指定不去忽略非公共库的类和成员
-dontskipnonpubliclibraryclassmembers
#生成map文件
-printmapping proguardMapping.txt
##不做预校验,preverify是proguard的四个步骤之一,Android不需要preverify,去掉这一步能够加快混淆速度。
#-dontpreverify
# 混淆采用的算法.
-optimizations !code/simplification/cast,!field/*,!class/merging/*
#避免混淆注解类
-dontwarn android.annotation
-keepattributes *Annotation*
#保留内部类
-keepattributes InnerClasses
#避免混淆泛型
-keepattributes Signature
#抛出异常时保留代码行号
-keepattributes SourceFile,LineNumberTable
# 将.class信息中的类名重新定义为"Proguard"字符串
-renamesourcefileattribute Proguard
#----------------------------------------------------------------------------#保留枚举类不被混淆
-keepclassmembers enum * {public static **[] values();public static ** valueOf(java.lang.String);
}#保留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();
}
-keep public class * implements java.io.Serializable {*;}#----------------------Android通用-----------------
# 避免混淆Android基本组件,下面是兼容性比较高的规则
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Fragment
-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# 保留support下的所有类及其内部类
-keep class android.support.** {*;}
-keep interface android.support.** {*;}
-keep public class * extends android.support.v4.**
-keep public class * extends android.support.v7.**
-keep public class * extends android.support.annotation.**
-dontwarn android.support.**# 保留androidx下的所有类及其内部类
-keep class androidx.** {*;}
-keep public class * extends androidx.**
-keep interface androidx.** {*;}
-keep class com.google.android.material.** {*;}
-dontwarn androidx.**
-dontwarn com.google.android.material.**
-dontnote com.google.android.material.**# 保持Activity中与View相关方法不被混淆
-keepclassmembers class * extends android.app.Activity{public void *(android.view.View);
}# 避免混淆所有native的方法,涉及到C、C++
-keepclasseswithmembernames class * {native <methods>;
}# 避免混淆自定义控件类的get/set方法和构造函数
-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);
}# 避免混淆枚举类
-keepclassmembers enum * {public static **[] values();public static ** valueOf(java.lang.String);
}# 避免混淆序列化类
# 不混淆Parcelable和它的实现子类,还有Creator成员变量
-keep class * implements android.os.Parcelable {public static final android.os.Parcelable$Creator *;
}# 不混淆Serializable和它的实现子类、其成员变量
-keep public class * implements java.io.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();
}# 资源ID不被混淆
-keep class **.R$* {*;}# 回调函数事件不能混淆
-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, java.lang.String);}# 使用GSON、fastjson等框架时,所写的JSON对象类不混淆,否则无法将JSON解析成对应的对象
-keepclassmembers class * {public <init>(org.json.JSONObject);
}#kotlin 相关
-dontwarn kotlin.**
-keep class kotlin.** { *; }
-keep interface kotlin.** { *; }
-keepclassmembers class kotlin.Metadata {public <methods>;
}
-keepclasseswithmembers @kotlin.Metadata class * { *; }
-keepclassmembers class **.WhenMappings {<fields>;
}
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
}-keep class kotlinx.** { *; }
-keep interface kotlinx.** { *; }
-dontwarn kotlinx.**
-dontnote kotlinx.serialization.SerializationKt-keep class org.jetbrains.** { *; }
-keep interface org.jetbrains.** { *; }
-dontwarn org.jetbrains.**#需要log分析问题
#(可选)避免Log打印输出
-assumenosideeffects class android.util.Log {public static boolean isLoggable(java.lang.String,int);public static *** d(...);public static *** e(...);public static *** i(...);public static *** v(...);public static *** println(...);public static *** w(...);public static *** wtf(...);
}