Android developers: you eat a full meal in the history of the most confusing dinner Android

webp

Android in the daily development process, the confusion is an essential skill of our development App. As long as we lived through App packaged on-line process, more or less need to understand the basic operation of some code obfuscation. So, what is confusing in the end? What are its benefits? How the specific effects? But wait, let's look at each explore its "unique" charm.


Like friends trouble spots followers, your support is my greatest motivation! I will share the knowledge and Android regularly resolution, the topic will be constantly updated BATJ interview, welcome to come to discuss the exchange, if any good articles are also welcome to contribute.

About confusion

Code obfuscation ( Obfuscated code ) is the program code into a behavioral code difficult to read and understand some rules.

Benefits of confusion

Benefits confusion is its purpose: to make APK difficult to reverse engineering, that greatly increases the cost of decompilation. In addition, Android among the "confusion" is also able to remove unnecessary resources when packaging, significantly reduced the volume of APK. Finally, also in varying ways to avoid common Android in 64k limit the number of method references.

Let's look at the structure before and after contrast APK confusion:

webp

webp

As can be seen from the above two pictures: After obfuscated APK in our package name, class name, member name and so is replaced by a random, meaningless names, increasing the degree of difficulty to read and understand the code, improve decompile costs. Careful junior partner may well be noticed: before and after the confusion APK volume actually decreases from 2.7M to 1.4M, nearly double the volume down! Really that magical? Haha, really is so amazing, so we slowly to uncover the mystery of it.

Confusion among Android

在 Android 中,我们平常所说的"混淆"其实有两层意思,一个是 Java 代码的混淆,另外一个是资源的压缩。其实这两者之间并没有什么关联,只不过习惯性地放在一起来使用。那么,说了这么多,Android 平台上到底该如何开启混淆呢?

启用混淆

......

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

以上就是开启混淆的基本操作了,通过 minifyEnabled 设置为 true 来开启混淆。同时,可以设置 shrinkResourcestrue 来开启资源的压缩。不难看出,我们一般在打 release 包时才启用混淆,因为混淆会增加额外的编译时间,所以不建议在 debug 模式下启用。此外,需要注意的是:只有在启用混淆的前提下开启资源压缩才会有效!以上代码中的 proguard-android.txt 表示 Android 系统为我们提供的默认混淆规则文件,而 proguard-rules.pro 则是我们想要自定义的混淆规则,至于如何自定义混淆规则我们将在接下来会讲到。

代码混淆

其实,Java 平台为我们提供了 Proguard 混淆工具来帮助我们快速地对代码进行混淆。根据 Java 官方介绍,Proguard 对应的具体中文定义如下:

  • 它是一个包含代码文件压缩优化混淆校验等功能的工具

  • 它能够检测并删除无用的类、变量、方法和属性

  • 它能够优化字节码并删除未使用的指令

  • 它能够将类、变量和方法的名字重命名为无意义的名称从而达到混淆效果

  • 最后,它还会校验处理后的代码,主要针对 Java 6 及以上版本和 Java ME

资源压缩

Android 中,编译器为我们提供了另外一项强大的功能:资源的压缩。资源压缩能够帮助我们移除项目及依赖仓库中未使用到的资源,有效地降低了apk包的大小。由于资源压缩与代码混淆是协同工作,所以,如果需要开启资源的压缩,切记要先开启代码混淆,否则会出现以下问题:

ERROR: Removing unused resources requires unused code shrinking to be turned on. See http://d.android.com/r/tools/shrink-resources.html for more information.Affected Modules: app

自定义要保留的资源

当我们开启了资源压缩之后,系统会默认替我们移除所有未使用的资源,假如我们需要保留某些特定的资源,可以在我们项目中创建一个被 <resources> 标记的 XML 文件(如 res/raw/keep.xml),并在 tools:keep 属性中指定每个要保留的资源,在 tools:discard 属性中指定每个要舍弃的资源。这两个属性都接受逗号分隔的资源名称列表。同样,我们可以使用字符 * 作为通配符。如:

<?xml version="1.0" encoding="utf-8"?><resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@layout/activity_video*,@layout/dialog_update_v2"
    tools:discard="@layout/unused_layout,@drawable/unused_selector" />

启用严格检查模式

正常情况下,资源压缩器可准确判定系统是否使用了资源。不过,如果您的代码(包含库)调用 Resources.getIdentifier(),这就表示您的代码将根据动态生成的字符串查询资源名称。这时,资源压缩器会采取防御性行为,将所有具有匹配名称格式的资源标记为可能已使用,无法移除。例如,以下代码会使所有带 img_ 前缀的资源标记为已使用:

String name = String.format("img_%1d", angle + 1);
res = getResources().getIdentifier(name, "drawable", getPackageName());

这时,我可以开启资源的严格审查模式,只会保留确定已使用的资源。

移除备用资源

Gradle 资源压缩器只会移除未被应用引用的资源,这意味着它不会移除用于不同设备配置的备用资源。必要时,我们可以使用 Android Gradle 插件的 resConfigs 属性来移除您的应用不需要的备用资源文件(常见的有用于国际化支持的 strings.xml,适配用的 layout.xml 等):

android {
    defaultConfig {
        ...        //保留中文和英文国际化支持
        resConfigs "en", "zh"
    }
}

自定义混淆规则

品尝完了以上"配菜",下面让我们来品味一下本文的"主菜":自定义混淆规则。首先,我们来了解一下常见的混淆命令。

keep 命令

这里说的 keep 命令指的是一系列以 -keep 开头的命令,它主要用来保留 Java 中不需要进行混淆的元素。以下是常见的 -keep 命令:

  • -keep

    作用:保留指定的类和成员,防止被混淆处理。例如:

    # 保留包:com.moos.media.entity 下面的类以及类成员-keep public class com.moos.media.entity.**# 保留类:NumberProgressBar-keep public class com.moos.media.widget.NumberProgressBar {*;}
  • -keepclassmembers

    作用:保留指定的类的成员(变量/方法),它们将不会被混淆。如:

    # 保留类的成员:MediaUtils类中的特定成员方法
    -keepclassmembers class com.moos.media.MediaUtils {
        public static *** getLocalVideos(android.content.Context);    public static *** getLocalPictures(android.content.Context);
    }
  • -keepclasseswithmembers

    作用:保留指定的类和其成员(变量/方法),前提是它们在压缩阶段没有被删除。与-keep 使用方式类似:

    # 保留类:BaseMediaEntity 的子类-keepclasseswithmembers public class * extends com.moos.media.entity.BaseMediaEntity{*;}# 保留类:OnProgressBarListener接口的实现类-keep public class * implements com.moos.media.widget.OnProgressBarListener {*;}
  • @Keep

    除了以上方式,你也可以选择使用 @Keep 注解来保留期望代码,防止它们被混淆处理。比如,我们通过 @Keep 修饰一个类来保留它不被混淆:

    @Keep
    data class CloudMusicBean(var createDate: String,
                              var id: Long,
                              var name: String,
                              var url: String,
                              val imgUrl: String)

    同样地,我们也可以让 @Keep 来修饰方法或者字段进而保留它们。

其他命令

  1. dontwarn

    -dontwarn 命令一般在我们引入新的 library 时会使用到,常用于处理 library 中无法解决的警告。如:

    -keep class twitter4j.** { *; }
    
    -dontwarn twitter4j.**
  2. 其他的命令用法可参考 Android 系统提供的默认混淆规则:

    #混淆时不生成大小写混合的类名-dontusemixedcaseclassnames#不跳过非公共的库的类-dontskipnonpubliclibraryclasses#混淆过程中记录日志-verbose#关闭预校验-dontpreverify#关闭优化-dontoptimize#保留注解-keepattributes *Annotation*#保留所有拥有本地方法的类名及本地方法名-keepclasseswithmembernames class * {
        native <methods>;
    }#保留自定义View的get和set方法-keepclassmembers public class * extends android.view.View {
       void set*(***);
       *** get*();
    }#保留Activity中View及其子类入参的方法,如: onClick(android.view.View)-keepclassmembers class * extends android.app.Activity {
       public void *(android.view.View);
    }#保留枚举-keepclassmembers enum * {
        **[] $VALUES;
        public *;
    }#保留序列化的类-keepclassmembers class * implements android.os.Parcelable {
      public static final android.os.Parcelable$Creator CREATOR;
    }#保留R文件的静态成员-keepclassmembers class **.R$* {
        public static <fields>;
    }
    
    -dontwarn android.support.**
    
    -keep class android.support.annotation.Keep-keep @android.support.annotation.Keep class * {*;}
    
    -keepclasseswithmembers class * {
        @android.support.annotation.Keep <methods>;
    }
    
    -keepclasseswithmembers class * {
        @android.support.annotation.Keep <fields>;
    }
    
    -keepclasseswithmembers class * {
        @android.support.annotation.Keep <init>(...);
    }

    更多混淆命令可以参考文章:Proguard 最全混淆规则说明 ,这里就不做详细讲解了。

混淆"黑名单"

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

枚举

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

被反射的元素

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

实体类

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

四大组件

Android 中的四大组件同样不应该被混淆。原因在于:

  1. 四大组件使用前都需要在 AndroidManifest.xml 文件中进行注册声明,然而混淆处理之后,四大组件的类名就会被篡改,实际使用的类与 manifest 中注册的类并不匹配,故而出错。

  2. 其他应用程序访问组件时可能会用到类的包名加类名,如果经过混淆,可能会无法找到对应组件或者产生异常。

JNI 调用的Java 方法

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

其他不应该被混淆的

  • 自定义控件不需要被混淆

  • JavaScript 调用 Java 的方法不应混淆

  • Java 的 native 方法不应该被混淆

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

混淆后的堆栈跟踪

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

混淆输出结果

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

  • dump.txt

    说明 APK 内所有类文件的内部结构。

  • mapping.txt

    提供混淆前后的内容对照表,内容主要包含类、方法和类的成员变量。

  • seeds.txt

    罗列出未进行混淆处理的类和成员。

  • usage.txt

    罗列出从 APK 中移除的代码。

恢复堆栈跟踪

了解完混淆构建完毕后输出的内容之后,我们现在就来看一下之前的问题:混淆处理后,StackTrace 定位困难。如何来恢复 StackTrace 的定位能力呢?系统为我们提供了 retrace 工具,结合上文提到的 mapping.txt 文件,就可以将混淆后的崩溃堆栈追踪信息还原成正常情况下的 StackTrace 信息。主要有两种方式来恢复 StackTrace,为了方便理解,我们以下面这段崩溃信息为例,借助两种方式分别来还原:

 java.lang.RuntimeException: Unable to start activity 
     Caused by: kotlin.KotlinNullPointerException
        at com.moos.media.ui.ImageSelectActivity.k(ImageSelectActivity.kt:71)
        at com.moos.media.ui.ImageSelectActivity.onCreate(ImageSelectActivity.kt:58)
        at android.app.Activity.performCreate(Activity.java:6237)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1107)
  1. 通过 retrace 脚本工具

    首先我们要进入到 Android SDK 路径的 /tools/proguard/bin 目录中,这里以 Mac 系统为例,可以看到如下内容:

webp

可以看到如上三个文件,而 proguardgui.sh 才是我们需要的 retrace 脚本(Windows系统下为 proguardgui.bat )。Windows 系统中只需要双击脚本 proguardgui.bat 即可运行,至于 Mac 系统,如果你没有做任何配置,只需要将 proguardgui.sh 脚本拖动到 Mac 自带的终端中,回车键即可运行。接着,我们会看到如下界面:

webp

选择 ReTrace 栏 ,并添加我们项目中混淆生成的 mapping.txt 文件所在位置,然后将我们的混淆后的崩溃信息复制到 Obfuscated stack trace 那一栏,点击 ReTrace! 按钮即可还原出我们的崩溃日志信息,结果如上图所示,我们之前的混淆日志:at com.moos.media.ui.ImageSelectActivity.k(ImageSelectActivity.kt:71) 被还原成了 at com.moos.media.ui.ImageSelectActivity.initView(ImageSelectActivity.kt:71)ImageSelectActivity.k 是我们混淆后的方法名,ImageSelectActivity.initView 则是最初未混淆前的方法名,借助于 ReTrace 工具的帮助,我们就可以像以前一样很快定位到崩溃代码区域了。

  1. 通过 retrace 命令行

    我们先要将崩溃信息复制到 txt 格式的文件(如:proguard_stacktrace.txt)中保存,然后执行以下命令即可(MAC系统):

    retrace.sh -verbose mapping.txt proguard_stacktrace.txt

    如果你是 windows 系统,可以执行以下命令:

    retrace.bat -verbose mapping.txt proguard_stacktrace.txt

    最终还原的结果和之前效果一样:

webp

也许你通过以上两种方式在对 stackTrace 进行恢复时,发现 Unknown Source 问题:

webp

It is noteworthy that, remember to add the following configuration in confusion rules to improve the efficiency of our StackSource find:

# Retain the source file name and the specific lines of code numbers -keepattributes SourceFile, LineNumberTable

In addition, every time we use ProGuard create a publication will cover all before you build version of the mapping.txtfile, so we must be careful to save a copy of every time you release a new version. Retained by a release build for each mapping.txtcopy of a file, we will be able to debug and fix any problems in the old version of the application submitted by users have been confused StackTrace.

Up gesture operation

After the introduction above, we know that, APK after the code obfuscates the package name, class name, member name is transformed into meaningless, incomprehensible name, increase the cost of decompilation. Android ProGuard provides us with the default "confuse dictionary", is about the form of the element name into English lowercase letters. So, we can define your own confusion dictionary? Secrecy, let's take a look at renderings:

webp

The wave action is not a little "outstanding" up? Haha, not guessing, is actually very simple, as long as the generated set of their own txtconfusion dictionary format, and then in the confusion rule Proguard-rules.proapplication you can look in:

webp

Confusion in this article use the dictionary can be viewed here and download

Of course, you can also customize their own to "confuse dictionary", increase the difficulty of decompilation.

All the way down, we can see the necessity and advantages of confusing technical point of view, it is still very worthy of our in-depth study and research, we bring you a taste of just the "tip of the iceberg." Due to my limited level of technology, if we find a problem or elaborate inappropriate, please point out and correct. Like my friends also trouble spots followers, your support is my greatest motivation! I will share the knowledge and Android regularly resolution, the topic will be constantly updated BATJ interview, welcome to come to discuss the exchange, if any good articles are also welcome to contribute.


Guess you like

Origin blog.51cto.com/13673213/2423637