安卓混淆文件使用
简单介绍Android SDK 自带了混淆工具 Proguard。它位于 SDK 根目录 \tools\proguard 下面。如果开启了混淆,Proguard 默认情况下会对所有代码,包括第三方包都进行混淆(可能需要编写混淆规则来保持不能被混淆的部分)。作为 Android 开发者,如果你不想开源你的应用,那么在应用发布前,就需要对代码进行混淆处理,从而让我们代码即使被反编译,也难以阅读。混淆的
简单介绍
Android SDK 自带了混淆工具 Proguard。它位于 SDK 根目录 \tools\proguard 下面。如果开启了混淆,Proguard 默认情况下会对所有代码,包括第三方包都进行混淆(可能需要编写混淆规则来保持不能被混淆的部分)。
作为 Android 开发者,如果你不想开源你的应用,那么在应用发布前,就需要对代码进行混淆处理,从而让我们代码即使被反编译,也难以阅读。
混淆的作用
压缩(Shrinking)
默认开启,用以减小应用体积,移除未被使用的类和成员,并且会在优化动作执行之后再次执行(因为优化后可能会再次暴露一些未被使用的类和成员)。
-dontshrink 关闭压缩
优化(Optimization)
默认开启,在字节码级别执行优化,让应用运行的更快。
-dontoptimize 关闭优化
-optimizationpasses n 表示 proguard 对代码进行迭代优化的次数,Android 一般为5。
混淆(Obfuscation)
默认开启,增大反编译难度,类和类成员会被随机命名(像 a、b()、c之类的),除非用 keep 保护。
-dontobfuscate:关闭混淆。
开启混淆
在 app 的 gradle 文件中修改,在 android - buildTypes - release(debug)下:
android {
...
buildTypes {
release {
...
//可优化字节码
zipAlignEnabled true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
...
}
说明
‘proguard-android.txt’ 是AndroidStudio默认自动导入的规则,这个文件位于Android SDK根目录\tools\proguard\proguard-android.txt。这里面是一些比较常规的不能被混淆的代码规则。
'proguard-rules.pro’是针对我们自己的项目需要特别定义混淆规则,它位于项目根目录下面,里面的内容需要我们自己编写。
zipAlignEnabled true 这个在打包时需要设置为true,能优化我们的java字节码,提高运行效率。zipAlign 可以让安装包中的资源按4字节对齐,这样可以减少应用在运行时的内存消耗。像Google Play 还强制要求开发者上传的应用必须是经过 zipAlign 的,
简单混淆规则
从上面说明知道,我们要编写的混淆规则就是写在 proguard-rules.pro 文件中,在 AS 左边列表中很好找到,每个 module 都有自己的 proguard-rules.pro 文件,我们对应着改就可以。
下面说说简单的混淆规则,好多文章都讲的不是清楚:
保持类
-keep class cn.hadcn.test.*
-keep class cn.hadcn.test.**
一颗星表示只是保持该包下的类名,而子包下的类名还是会被混淆;两颗星表示把本包和所含子包下的类名都保持;
保持类及其中方法及变量
用以上方法保持类后,你会发现类名虽然未混淆,但里面的具体方法和变量命名还是变了,这时如果既想保持类名,又想保持里面的内容不被混淆,我们就需要以下方法了:
-keep class com.silence.test.* {*;}
-keep class com.silence.test.** {*;}
保持特定类
在此基础上,我们也可以使用 Java 的基本规则来保护特定类不被混淆,比如我们可以用 extend,implement 等这些 Java 规则。如下例子就避免所有继承 Activity 的类被混淆
-keep public class * extends android.app.Activity
保持类及其中内部类
如果我们要保留一个类中的内部类不被混淆则需要用$符号,如下例子表示保持 TestFragment 内部类 MyClass 中的所有 public 内容不被混淆。
-keep class com.silence.TestFragment$MyClass{public*;}
保持类及其中特定内容
再者,如果一个类中你不希望保持全部内容不被混淆,而只是希望保护类下的特定内容(包含类名),就可以使用
<init>;//匹配所有构造器
<fields>;//匹配所有域
<methods>;//匹配所有方法方法
你还可以在或前面加上 private 、public、native 等来进一步指定不被混淆的内容,如
-keep class cn.hadcn.test.One{public<methods>;}
表示 One 类下的所有public方法都不会被混淆,当然你还可以加入参数,比如以下表示用JSONObject 作为入参的构造函数不会被混淆
-keep class cn.hadcn.test.One{public<init>(org.json.JSONObject);}
复杂混淆规则
作用范围
应该注意到上面我们都保持类(使用 keep),实际还可以仅保持类成员,但是简单使用的话,上面应该足够了,其他的下面列举一下:
作用范围 | 保持所指定类、成员 | 所指定类、成员在压缩阶段没有被删除,才能被保持 |
---|---|---|
类和类成员 | -keep | -keepnames |
仅类成员 | -keepclassmembers | -keepclassmembernames |
类和类成员(前提是成员都存在) | -keepclasseswithmembers | -keepclasseswithmembernames |
默认包含的规则文件说明
#混淆时不生成大小写混合的类名
-dontusemixedcaseclassnames
#不忽略非公共的类库
-dontskipnonpubliclibraryclasses
#混淆过程中打印详细信息
-verbose
#关闭优化
-dontoptimize
#不预校验
-dontpreverify
# Annotation注释不能混淆
-keepattributes *Annotation*
#对于NDK开发 本地的native方法不能被混淆
-keepclasseswithmembernames class * {
native <methods>;
}
#保持View的子类里面的set、get方法不被混淆(*代替任意字符)
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}
#保持Activity子类里面的参数类型为View的方法不被混淆,如被XML里面应用的onClick方法
# We want to keep methods in Activity that could be used in the XML attribute onClick
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
#保持枚举类型values()、以及valueOf(java.lang.String)成员不被混淆
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
#保持实现Parcelable接口的类里面的Creator成员不被混淆
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator CREATOR;
}
#保持R类静态成员不被混淆
-keepclassmembers class **.R$* {
public static <fields>;
}
#不警告support包中不使用的引用
-dontwarn android.support.**
-keep class android.support.annotation.Keep
-keep @android.support.annotation.Keep class * {*;}
#保持使用了Keep注解的方法以及类不被混淆
-keepclasseswithmembers class * {
@android.support.annotation.Keep <methods>;
}
#保持使用了Keep注解的成员域以及类不被混淆
-keepclasseswithmembers class * {
@android.support.annotation.Keep <fields>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <init>(...);
}
上面默认的规则中指示了些需要保持不能别混淆的代码,包括:
-
继承至 Android 组件(Activity, Service…)的类。
-
自定义控件,继承至 View 的类(被xml文件引用到的,名字已经固定了的)
enum 枚举 -
实现了 android.os.Parcelable 接口的
-
Android R文件
-
数据库驱动…
-
Android support 包等
-
Android 的注释不能混淆
-keepattributes *Annotation*
-
对于 NDK 开发 本地的 native 方法不能被混淆
-keepclasseswithmembernames class * { native <methods>; }
其他不能混淆内容
-
自定义控件不进行混淆
-
枚举类不被混淆
-
反射类不进行混淆
-
实体类不被混淆
-
JS调用的Java方法
-
四大组件不进行混淆
-
JNI 中调用类不进行混淆
-
Layout布局使用的
View
构造函数、android:onClick
等 -
Parcelable
的子类和Creator
静态成员变量不混淆 -
第三方开源库或者引用其他第三方的SDK包不进行混淆
混淆模板
对于特定的项目还有很多不能被混淆的,需要我们自己写规则来指示,将在下面来说明:
#压缩级别0-7,Android一般为5(对代码迭代优化的次数)
-optimizationpasses 5
#不使用大小写混合类名
-dontusemixedcaseclassnames
#混淆时记录日志
-verbose
#不警告org.greenrobot.greendao.database包及其子包里面未应用的应用
-dontwarn org.greenrobot.greendao.database.**
-dontwarn rx.**
-dontwarn org.codehaus.jackson.**
......
#保持jackson包以及其子包的类和类成员不被混淆
-keep class org.codehaus.jackson.** {*;}
#--------重要说明-------
#-keep class 类名 {*;}
#-keepclassmembers class 类名{*;}
#一个*表示保持了该包下的类名不被混淆;
# -keep class org.codehaus.jackson.*
#二个**表示保持该包以及它包含的所有子包下的类名不被混淆
# -keep class org.codehaus.jackson.**
#------------------------
#保持类名、类里面的方法和变量不被混淆
-keep class org.codehaus.jackson.** {*;}
#不混淆类ClassTwoOne的类名以及类里面的public成员和方法
#public 可以换成其他java属性如private、public static 、final等
#还可以使<init>表示构造方法、<methods>表示方法、<fields>表示成员,
#这些前面也可以加public等java属性限定
-keep class com.dev.demo.two.ClassTwoOne {
public *;
}
#不混淆类名,以及里面的构造函数
-keep class com.dev.demo.ClassOne {
public <init>();
}
#不混淆类名,以及参数为int 的构造函数
-keep class com.dev.demo.two.ClassTwoTwo {
public <init>(int);
}
#不混淆类的public修饰的方法,和private修饰的变量
-keepclassmembers class com.dev.demo.two.ClassTwoThree {
public <methods>;
private <fields>;
}
#不混淆内部类,需要用$修饰
#不混淆内部类ClassTwoTwoInner以及里面的全部成员
-keep class com.dev.demo.two.ClassTwoTwo$ClassTwoTwoInner{*;}
......
更多混淆模板说明,可以参考下面这篇博客:
https://www.jianshu.com/p/90feb5c50cce
实用例子
关闭 Log日志
一般我们开发的时候会自己写一个 Log 封装类,去解决正式版日志的开关,但是其实使用混淆文件也能起到正式版不输出日志的用处,还能直接作用到代码上,不用怕忘记删 Log 了。
-assumenosideeffects class android.util.Log {
public static boolean isLoggable(java.lang.String, int);
public static int v(...);
public static int i(...);
public static int w(...);
public static int d(...);
public static int e(...);
}
反推崩溃日志
混淆后根据 Crash 崩溃日志反推代码
成功打包后会在目录 app\build\outputs\mapping\release 下生成几个文件:
dump.txt 混淆后类的内部结构说明;
mapping.txt 混淆前与混淆后名称对应关系;
seeds.txt 经过了一系列keep语句的保持,没有被混淆的类,成员的名称列表文件。
usage.txt 经过压缩后被删除的没有使用的代码,方法…等的名称的列表文件
retrace工具:
混淆反推工具为 etrace.sh( Mac 平台)或者 retrace.bat(Windows平台),该工具在sdk根目录\tools\proguard\bin\retrace.sh ( Windows 平台类似);
命令格式:
./retrace.sh [mapping.txt目录] [崩溃日历目录]
retrace.bat [mapping.txt目录] [崩溃日历目录])
使用说明
-
第一步:保存 Crash 日志如下:截取日志保存为 txt 文件如:bug.txt。
03-21 03:09:32.389: E/AndroidRuntime(3582): FATAL EXCEPTION: main 03-21 03:09:32.389: E/AndroidRuntime(3582): Process: com.dev.demo, PID: 3582 03-21 03:09:32.389: E/AndroidRuntime(3582): java.lang.NullPointerException: Attempt to invoke virtual method 'boolean java.lang.String.equals(java.lang.Object)' on a null object reference 03-21 03:09:32.389: E/AndroidRuntime(3582): at com.dev.demo.two.b.b(Unknown Source) 03-21 03:09:32.389: E/AndroidRuntime(3582): at com.dev.demo.a.a.printTest(Unknown Source) 03-21 03:09:32.389: E/AndroidRuntime(3582): at com.dev.demo.MainActivity.i(Unknown Source) 03-21 03:09:32.389: E/AndroidRuntime(3582): at com.dev.demo.MainActivity.a(Unknown Source) 03-21 03:09:32.389: E/AndroidRuntime(3582): at com.dev.demo.MainActivity$1.onClick(Unknown Source)
-
第二步:删掉Crash日志中 **03-21 03:09:32.389: E/AndroidRuntime(3582)😗*部分,否则无法反推还原。删除后如下:
FATAL EXCEPTION: main Process: com.dev.demo, PID: 3582 java.lang.NullPointerException: Attempt to invoke virtual method 'boolean java.lang.String.equals(java.lang.Object)' on a null object reference at com.dev.demo.two.b.b(Unknown Source) at com.dev.demo.a.a.printTest(Unknown Source) at com.dev.demo.MainActivity.i(Unknown Source) at com.dev.demo.MainActivity.a(Unknown Source) at com.dev.demo.MainActivity$1.onClick(Unknown Source)
-
第三步:打开命令窗口输入命令:如 😗*./retrace.sh mapping.txt bug.txt ** 如下:
按确认后得到结果如下:
FATAL EXCEPTION: main Process: com.dev.demo, PID: 3582 java.lang.NullPointerException: Attempt to invoke virtual method 'boolean java.lang.String.equals(java.lang.Object)' on a null object reference at com.dev.demo.two.ClassTwoThree.void test()(Unknown Source) at com.dev.demo.one.ClassOneOne.void printTest()(Unknown Source) at com.dev.demo.MainActivity.void printWifi()(Unknown Source) at com.dev.demo.MainActivity.void access$000(com.dev.demo.MainActivity)(Unknown Source) at com.dev.demo.MainActivity$1.void onClick(android.view.View)(Unknown Source) ```
可视化工具
除了有 retrace.sh 工具外还有可视化工具,和 retrace.sh 同目录下 proguardgui.sh,这是一块 Progurad 的可视化工具(Windows平台类似):
有时为了Crash日志更容易定位可以在规则里面添加:
-keepattributes SourceFile, LineNumberTable
这样Crash日志里面就能保留类名称和行号了。
结语
以上就是我对混淆文件的一些总结,收集了一些资料整合了一下,具体参考文章如下:
希望本文对读者有所帮助!
更多推荐
所有评论(0)