Android开发混淆的那些事

混淆想必大家都不陌生,android上用的这一套混淆规则和java混淆几乎是一样的。为何需要混淆呢?简单的说,就是将原本正常的项目文件,对其类,方法,字段,重新命名,a,b,c,d,e,f…之类的字母,达到混淆代码的目的,这样反编译出来,结构乱糟糟的,给反编译者制造一些代码阅读的麻烦。

ProGuard简介

ProGuard是2002年由比利时程序员Eric Lafortune发布的一款优秀的开源代码优化、混淆工具,适用于Java和Android应用,目标是让程序更小,运行更快,在Java界处于垄断地位。主要分为四个模块:Shrinker(压缩器)、Optimizer(优化器)、Obfuscator(混淆器)、Retrace(堆栈反混淆)。
ProGuard工作过程

  1. 压缩(shrink)通过引用标记算法,移除未使用的类、方法、字段等
  2. 优化(optimize)优化字节码,简化代码等操作
  3. 混淆(obfuscate)使用简短的,无意义的名称重命名类名,方法名,参数字段等
  4. 预校验(perverify)为class添加预校验消息

Android启用压缩、混淆、优化

在 Android 中,我们平常所说的"混淆"其实有两层意思:

  1. 是 Java 代码的混淆
  2. 是资源的压缩

其实这两者之间并没有什么关联,只不过习惯性地放在一起来使用。那么,说了这么多,Android平台上到底该如何开启混淆呢?

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

以上就是开启混淆的基本操作了,通过 minifyEnabled 设置为 true 来开启混淆。同时,可以设置 shrinkResources 为 true 来开启资源的压缩。

不难看出,我们一般在打 release 包时才启用混淆,因为混淆会增加额外的编译时间,所以不建议在 debug 模式下启用。此外,需要注意的是:只有在启用混淆的前提下开启资源压缩才会有效!

以上代码中的 proguard-android.txt 表示 Android 系统为我们提供的默认混淆规则文件,而 proguard-rules.pro 则是我们想要自定义的混淆规则。

如果你不对proguard-rules.pro文件做定制化,默认是整个工程全开启混淆的,但是由于一些三方代码、反射、自定义view等,一旦这些都混淆了编译器在运行时找不到具体的成员,从而会导致错误,所以我们也要忽略一些类或者成员的混淆。

Android混淆配置关键字

系统混淆配置

#混淆时不使用大小写混合类名
-dontusemixedcaseclassnames 
#不跳过library中的非public的类
-dontskipnonpubliclibraryclasses 
#打印混淆的详细信息
-verbose 
#不进行优化,建议使用此选项
-dontoptimize 
#不进行预校验,Android不需要,可加快混淆速度
-dontpreverify
#忽略警告
-ignorewarnings 
#指定代码的压缩级别
-optimizationpasses 5  

Proguard关键字

Proguard关键字

如果你不确定你需要使用哪一个选项,那么你应该尽量使用 -keep 。它会确保指定的类和成员在 压缩阶段(shrinking step)不会被删除,并且在 混淆阶段(obfuscation step)不会被混淆。

Proguard通配符

Proguard通配符

上边的通配符没有返回类型。仅仅通配符有一个参数列表。

字段和方法的名称可以包含如下通配符:

? 匹配名称中的任意单个字符
* 匹配名称中的任意一部分

类型描述可以包含如下通配符:

% 匹配任意基本类型(‘boolean’,'int'以及其他,但是不包括‘void’).
? 匹配类名中的任意单个字符
* 匹配类名中的任意部分,但是不包含包分隔符 。
** 匹配类名中的任意部分,可以包含任意个数的包分隔符。
*** 匹配任意类型(基本类型或者非基本类型,数组或者非数组)
... 匹配任意个数任意类型的参数。

注意:? , * 以及 ** 通配符永远都不会匹配基本类型。此外,仅仅 *** 通配符会匹配任意长度任意类型的数组。

哪些不应该进行混淆

我们在了解了混淆的基本命令之后,很多人应该还是一头雾水:到底哪些内容该混淆?其实,我们在使用代码混淆时,ProGuard 对我们项目中大部分代码进行了混淆操作,为了防止编译时出错,我们应该通过 keep 命令保留一些元素不被混淆。所以,我们只需要知道哪些元素不应该被混淆:

枚举

项目中难免可能会用到枚举类型,然而它不能参与到混淆当中去。原因是:枚举类内部存在 values 方法,混淆后该方法会被重新命名,并抛出 NoSuchMethodException。庆幸的是,Android 系统默认的混淆规则中已经添加了对于枚举类的处理,我们无需再去做额外工作。

被反射的元素

被反射使用的类、变量、方法、包名等不应该被混淆处理。原因在于:代码混淆过程中,被反射使用的元素会被重命名,然而反射依旧是按照先前的名称去寻找元素,所以会经常发生 NoSuchMethodException 和 NoSuchFiledException 问题。

实体类

实体类即我们常说的"数据类",当然经常伴随着序列化与反序列化操作。很多人也应该都想到了,混淆是将原本有特定含义的"元素"转变为无意义的名称,所以,经过混淆的"洗礼"之后,序列化之后的 value 对应的 key 已然变为没有意义的字段,这肯定是我们不希望的。同时,反序列化的过程创建对象从根本上来说还是借助于反射,混淆之后 key 会被改变,所以也会违背我们预期的效果。

四大组件

Android 中的四大组件同样不应该被混淆。原因在于:
四大组件使用前都需要在 AndroidManifest.xml 文件中进行注册声明,然而混淆处理之后,四大组件的类名就会被篡改,实际使用的类与 manifest 中注册的类并不匹配,故而出错。其他应用程序访问组件时可能会用到类的包名加类名,如果经过混淆,可能会无法找到对应组件或者产生异常。

JNI 调用的Java 方法

当 JNI 调用的 Java 方法被混淆后,方法名会变成无意义的名称,这就与 C++ 中原本的 Java 方法名不匹配,因而会无法找到所调用的方法。
其他不应该被混淆的

自定义控件不需要被混淆

JavaScript 调用 Java 的方法不应混淆

Java 的 native 方法不应该被混淆

项目中引用的第三方库也不建议混淆

混淆举例

以下是一些混淆的举例和说明

//不混淆某个类
-keep public class name.huihui.example.Test { *; }
//不混淆某个类的子类
-keep public class * extends name.huihui.example.Test { *; }
//不混淆所有类名中包含了“model”的类及其成员
-keep public class **.*model*.** {*;}
//不混淆某个接口的实现
-keep class * implements name.huihui.example.TestInterface { *; }
//不混淆某个类的构造方法
-keepclassmembers class name.huihui.example.Test { 
    public <init>(); 
}
//不混淆某个类的特定的方法
-keepclassmembers class name.huihui.example.Test { 
    public void test(java.lang.String); 
}
//不混淆某个类的内部类
-keep class name.huihui.example.Test$* {*;}
//两个常用的混淆命令,注意:
//一颗星表示只是保持该包下的类名,而子包下的类名还是会被混淆;
//两颗星表示把本包和所含子包下的类名都保持;
-keep class com.suchengkeji.android.ui.*
-keep class com.suchengkeji.android.ui.**
//用以上方法保持类后,你会发现类名虽然未混淆,但里面的具体方法和变量命名还是变了,
//如果既想保持类名,又想保持里面的内容不被混淆,我们就需要以下方法了

//不混淆某个包所有的类
-keep class com.suchengkeji.android.bean.** { *; }

//不混淆某个具体类
-keep public class com.android.vending.licensing.ILicensingService

一个通用的混淆模板

这里给大家提供一个通用的混淆模板

###########################################基本指令(基本不动)#######################################

#代码混淆压缩比,在0~7之间,默认为5,一般不做修改
-optimizationpasses 5

#混合时不使用大小写混合,混合后的类名为小写
-dontusemixedcaseclassnames

#指定不去忽略非公共库的类
-dontskipnonpubliclibraryclasses

#指定不去忽略非公共库的类的成员
-dontskipnonpubliclibraryclassmembers

#这句话能够使我们的项目混淆后产生映射文件
#包含有类名->混淆后类名的映射关系
-verbose

#不做预校验,preverify是proguard的四个步骤之一,Android不需要preverify,去掉这一步能够加快混淆速度。
-dontpreverify

#保留Annotation不混淆
-keepattributes *Annotation*,InnerClasses

#保留泛型不混淆
-keepattributes Signature

#抛出异常时保留代码行号
-keepattributes SourceFile,LineNumberTable

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


#########################################java部分###########################################

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

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

#Serializable类不被混淆
-keep public class * implements java.io.Serializable {*;}

#########################################android部分####################################

#保留support下的所有类及其内部类
-keep class android.support.** {*;}
#基类不被混淆
-keep class * extends android.app.Activity
-keep class * extends android.app.Application
-keep class * extends android.app.Service
-keep class * extends android.content.BroadcastReceiver
-keep class * extends android.content.ContentProvider
-keep class * extends android.app.backup.BackupAgentHelper
-keep class * extends android.preference.Preference

#自定义控件类不被混淆
-keepclasseswithmembers class * {
    public <init>(android.content.Context, android.util.AttributeSet);
}
-keepclasseswithmembers class * {
    public <init>(android.content.Context, android.util.AttributeSet, int);
}

#表示不混淆任何一个View中的setXxx()和getXxx()方法,
#因为属性动画需要有相应的setter和getter的方法实现,混淆了就无法工作了。
-keep class * extends android.view.View{
    *** get*();
    void set*(***);
    <init>(...);
}

# 保留R下面的资源
#不混淆资源类下static的
-keepclassmembers class **.R$* {
    public static <fields>;
}

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

#WebView相关不被混淆
-keepclassmembers class * extends android.webkit.WebView {*;}
-keepclassmembers class * extends android.webkit.WebViewClient {*;}
-keepclassmembers class * extends android.webkit.WebChromeClient {*;}
-keepclassmembers class * {
    @android.webkit.JavascriptInterface <methods>;
}

########################################本项目混淆规则##########################################

#非本项目不进行混淆
-keep class !com.ehai.store.** {*;}
-dontwarn **

#########################################其他#####################################################
#
##避免混淆Bugly
#-dontwarn com.tencent.bugly.**
#-keep public class com.tencent.bugly.**{*;}
#
##极光推送
#-dontoptimize
#-dontpreverify
#
#-dontwarn cn.jpush.**
#-keep class cn.jpush.** { *; }
#-keep class * extends cn.jpush.android.helpers.JPushMessageReceiver { *; }
#
#-dontwarn cn.jiguang.**
#-keep class cn.jiguang.** { *; }
#
##微众银行OCR SDK 混淆
##webank-cloud-normal-proguard-rules.pro的规则已经被webank-cloud-ocr-proguard-rules.pro “include”了,不需要再添加
#-include webank-cloud-ocr-proguard-rules.pro

混淆后的堆栈跟踪

代码经过 ProGuard 混淆处理后,想要读取 StackTrace(堆栈追踪)信息就会变得很困难。由于方法名称和类的名称都经过混淆处理,即使程序发生崩溃问题,也很难定位问题所在。幸运的是,ProGuard 为我们提供了补救的措施,在着手进行之前,我们先来看一下 ProGuard 每次构建后生成了哪些内容。

混淆输出结果

混淆构建完成之后,会在<module-name>/build/outputs/mapping/./ 目录下生成以下文件:

  • dump.txt
    说明 APK 内所有类文件的内部结构。
  • mapping.txt
    提供混淆前后的内容对照表,内容主要包含类、方法和类的成员变量。
  • seeds.txt
    罗列出未进行混淆处理的类和成员。
  • usage.txt
    罗列出从 APK 中移除的代码。

如何从堆栈中还原ProGuard混淆后的代码

混淆后的代码一旦发生崩溃,那么所产生的日志也是混淆的,调试起来很麻烦,所以此时有必要将其还原到原生态进行分析。

还原前

Caused by: java.lang.NullPointerException
at net.simplyadvanced.ltediscovery.be.u(Unknown Source)
at net.simplyadvanced.ltediscovery.at.v(Unknown Source)
at net.simplyadvanced.ltediscovery.at.d(Unknown Source)
at net.simplyadvanced.ltediscovery.av.onReceive(Unknown Source)

还原后

Caused by: java.lang.NullPointerException
at net.simplyadvanced.ltediscovery.UtilTelephony.boolean is800MhzNetwork()(Unknown Source)
at net.simplyadvanced.ltediscovery.ServiceDetectLte.void checkAndAlertUserIf800MhzConnected()(Unknown Source)
at net.simplyadvanced.ltediscovery.ServiceDetectLte.void startLocalBroadcastReceiver()(Unknown Source)
at net.simplyadvanced.ltediscovery.ServiceDetectLte$2.void onReceive(android.content.Context,android.content.Intent)(Unknown Source)

那么如何还原呢?这里提供两种方式:GUI工具和命令行

GUI工具还原

  1. 打开/tools/proguard/bin/proguardgui.bat
  2. 选择左边栏的ReTrace选项
  3. 添加你的mapping文件和混淆过的堆栈信息
  4. 点击ReTrace!

如图:
ProGuard工具

命令行还原

  1. 需要你的ProGuard的mapping文件和你想要还原的堆栈信息(如stacktrace.txt)
  2. 最简单的方法就是将这些文件拷贝到/tools/proguard/bin/目录
  3. 运行以下命令
//Windows
retrace.bat -verbose mapping.txt stacktrace.txt > out.txt

//Mac\Linux
retrace.sh -verbose mapping.txt stacktrace.txt > out.txt

参考

  • https://tech.meituan.com/2018/04/27/mt-proguard.html
  • https://stuff.mit.edu/afs/sipb/project/android/sdk/android-sdk-linux/tools/proguard/docs/index.html#manual/introduction.html
  • https://developer.android.com/studio/build/shrink-code.html
发布了47 篇原创文章 · 获赞 38 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/li0978/article/details/105336378