Android混淆介绍

版权声明:本文为博主原创文章,转载请声明出处。 https://blog.csdn.net/asd501823206/article/details/88861803

Android混淆

官方文档

要尽可能减小 APK文件,您应该启用压缩来移除发布构建中未使用的代码和资源。此页面介绍如何执行该操作,以及如何指定要在构建时保留或舍弃的代码和资源。代码压缩通过ProGuard提供,ProGuard会检测和移除封装应用中未使用的类、字段、方法和属性,包括自带代码库中的未使用项(这使其成为以变通方式解决 64k 引用限制的有用工具)。ProGuard还可优化字节码,移除未使用的代码指令,以及用短名称混淆其余的类、字段和方法。混淆过的代码可令您的APK难以被逆向工程,这在应用使用许可验证等安全敏感性功能时特别有用。资源压缩通过适用于 Gradle的Android插件提供,该插件会移除封装应用中未使用的资源,包括代码库中未使用的资源。它可与代码压缩发挥协同效应,使得在移除未使用的代码后,任何不再被引用的资源也能安全地移除。本文介绍的功能依赖下列组件:SDK Tools 25.0.10 或更高版本 适用于 Gradle 的 Android 插件 2.0.0 或更高版本

ProGuard用于要尽可能的减小APK文件大小,在编译时移除未使用的代码和资源文件,同时通过配置文件来选择需要保留的代码和文件。
ProGuard会检测和移除应用中未使用到类、字段、方法和属性。ProGuard还可以优化字节码:移除未使用的代码指令、用短名称(或是自定义的混淆方式)混淆其余的类、字段和方法。
混淆过的代码可以使包的体积减小,而且更加难以理解,在APK被反编译时,逆向工程也很难将代码还原。


压缩代码

启用ProGuard需要在build.gradle中添加minifyEnabled true

Note:代码压缩会拖慢构建速度,因此在调试过程中应该尽可能的避免使用ProGuard。但是最终测试时一定要开启ProGuard,因为ProGuard会将未自定义保留的代码重新命名,会导致注解、反射等功能出现问题。

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

除了 minifyEnabled 属性外,还有用于定义 ProGuard 规则的 proguardFiles 属性:

getDefaultProguardFile(‘proguard-android.txt’) 方法可从 Android SDK tools/proguard/ 文件夹获取默认的 ProGuard 设置。

提示:要想做进一步的代码压缩,请尝试使用位于同一位置的 proguard-android-optimize.txt 文件。它包括相同的 ProGuard规则,但还包括其他在字节码一级(方法内和方法间)执行分析的优化,以进一步减小 APK 大小和帮助提高其运行速度。

proguard-rules.pro 文件用于添加自定义ProGuard规则。默认情况下,该文件位于模块根目录(build.gradle 文件旁)。

要添加更多各构建变体专用的 ProGuard 规则,请在相应的 productFlavor 代码块中再添加一个 proguardFiles 属性。例如,以下 Gradle 文件会向 flavor2 产品定制添加 flavor2-rules.pro。现在 flavor2 使用所有三个 ProGuard 规则,因为还应用了来自 release 代码块的规则。

android {
    ...
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                   'proguard-rules.pro'
        }
    }
    productFlavors {
        flavor1 {
        }
        flavor2 {
            proguardFile 'flavor2-rules.pro'
        }
    }
}

每次构建时 ProGuard 都会输出下列文件:

dump.txt:说明 APK 中所有类文件的内部结构。
mapping.txt:提供原始与混淆过的类、方法和字段名称之间的转换。
seeds.txt:列出未进行混淆的类和成员。
usage.txt:列出从 APK 移除的代码。

这些文件保存在 /build/outputs/mapping/release/ 中。


自定义要保留的代码

对于某些情况,默认 ProGuard配置文件(proguard-android.txt)足以满足需要,ProGuard会移除所有(并且只会移除)未使用的代码。不过,ProGuard难以对许多情况进行正确分析,可能会移除应用真正需要的代码。举例来说,它可能错误移除代码的情况包括:

当应用引用的类只来自 AndroidManifest.xml 文件时
当应用调用的方法来自 Java 原生接口 (JNI) 时
当应用在运行时(例如使用反射或自检)操作代码时

测试应用应该能够发现因不当移除的代码而导致的错误,但您也可以通过查看/build/outputs/mapping/release/ 中保存的 usage.txt 输出文件来检查移除了哪些代码。

要修正错误并强制 ProGuard 保留特定代码,请在 ProGuard 配置文件中添加一行 -keep 代码。例如:

-keep public class MyClass

或者,您可以向您想保留的代码添加@Keep注解。在类上添加@Keep可原样保留整个类。在方法或字段上添加它可完整保留方法/字段(及其名称)以及类名称。请注意,只有在使用注解支持库时,才能使用此注解。

在使用 -keep 选项时,有许多事项需要考虑;如需了解有关自定义配置文件的详细信息,请阅读 ProGuard 手册。问题排查一章概述了您可能会在混淆代码时遇到的其他常见问题。


解码混淆过的堆叠追踪

在 ProGuard 压缩代码后,读取堆叠追踪变得困难(即使并非不可行),因为方法名称经过了混淆处理。幸运的是,ProGuard 每次运行时都会创建一个 mapping.txt 文件,其中显示了与混淆过的名称对应的原始类名称、方法名称和字段名称。ProGuard 将该文件保存在应用的 /build/outputs/mapping/release/ 目录中。

Note:您每次使用 ProGuard 创建发布构建时都会覆盖 mapping.txt 文件,因此您每次发布新版本时都必须小心地保存一个副本。通过为每个发布构建保留一个 mapping.txt 文件副本,您就可以在用户提交的已混淆堆叠追踪来自旧版本应用时对问题进行调试。

要自行将混淆过的堆叠追踪转换成可读的堆叠追踪,请使用 retrace 脚本(在 Windows 上为 retrace.bat;在 Mac/Linux 上为 retrace.sh)。它位于 /tools/proguard/ 目录中。该脚本利用 mapping.txt 文件和您的堆叠追踪生成新的可读堆叠追踪。使用 retrace 工具的语法如下:

retrace.bat|retrace.sh [-verbose] mapping.txt [<stacktrace_file>]

例如:

retrace.bat -verbose mapping.txt obfuscated_trace.txt

如果您不指定堆叠追踪文件,retrace 工具会从标准输入读取。


压缩资源

资源压缩只与代码压缩协同工作。代码压缩器移除所有未使用的代码后,资源压缩器便可确定应用仍然使用的资源。这在您添加包含资源的代码库时体现得尤为明显 - 您必须移除未使用的库代码,使库资源变为未引用资源,才能通过资源压缩器将它们移除。

要启用资源压缩,请在 build.gradle 文件中将 shrinkResources 属性设置为 true(在用于代码压缩的 minifyEnabled 旁边)。例如:

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

Note:资源压缩器目前不会移除 values/ 文件夹中定义的资源(例如字符串、尺寸、样式和颜色)。这是因为 Android 资源打包工具 (AAPT) 不允许 Gradle 插件为资源指定预定义版本。

(压缩资源无法单独启用,要与minifyEnabled一同使用)


自定义要保留的资源

如果您有想要保留或舍弃的特定资源,请在您的项目中创建一个包含 标记的 XML 文件,并在 tools:keep 属性中指定每个要保留的资源,在 tools:discard 属性中指定每个要舍弃的资源。这两个属性都接受逗号分隔的资源名称列表。您可以使用星号字符作为通配符。

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"
    tools:discard="@layout/unused2" />

将该文件保存在项目资源中,例如,保存在 res/raw/keep.xml。构建不会将该文件打包到 APK 之中。

指定要舍弃的资源可能看似愚蠢,因为您本可将它们删除,但在使用varient时,这样做可能很有用。例如,如果您明知给定资源表面上会在代码中使用(并因此不会被压缩器移除),但实际不会用于给定varient,就可以将所有资源放入公用项目目录,然后为每个varient创建一个不同的 keep.xml 文件。构建工具也可能无法根据需要正确识别资源,这是因为编译器会添加内联资源 ID,而资源分析器可能不知道真正引用的资源和恰巧具有相同值的代码中的整数值之间的差别。


启用严格引用检查

正常情况下,资源压缩器可准确判定系统是否使用了资源。不过,如果您的代码调用 Resources.getIdentifier()(或您的任何库进行了这一调用 - AppCompat 库会执行该调用),这就表示您的代码将根据动态生成的字符串查询资源名称。当您执行这一调用时,默认情况下资源压缩器会采取防御性行为,将所有具有匹配名称格式的资源标记为可能已使用,无法移除。

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

img_%1d只有在运行时才能确定引用的是哪一个资源文件,因此在编译器无法判断出img_开头的文件哪些是未被引用的资源。

这些是默认情况下启用的安全压缩模式的示例。但您可以停用这一“有备无患”处理方式,并指定资源压缩器只保留其确定已使用的资源。要执行此操作,请在 keep.xml 文件中将 shrinkMode 设置为 strict,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:shrinkMode="strict" />

如果您确已启用严格压缩模式,并且代码也引用了包含动态生成字符串的资源(如上所示),则必须利用 tools:keep 属性手动保留这些资源。


合并重复资源

默认情况下,Gradle 还会合并同名资源,例如可能位于不同资源文件夹中的同名可绘制对象。这一行为不受 shrinkResources 属性控制,也无法停用,因为在有多个资源匹配代码查询的名称时,有必要利用这一行为来避免错误。

只有在两个或更多个文件具有完全相同的资源名称、类型和限定符时,才会进行资源合并。Gradle 会在重复项中选择其视为最佳选择的文件(根据下述优先顺序),并只将这一个资源传递给 AAPT,以供在 APK 文件中分发。

Gradle 会在下列位置寻找重复资源:

与主源集关联的主资源,一般位于 src/main/res/ 中
变体叠加,来自varient和flavor
库项目依赖项

Gradle 会按以下级联优先顺序合并重复资源:

依赖项 → 主资源 → 构建风味 → 构建类型

例如,如果某个重复资源同时出现在主资源和flavor中,Gradle 会选择flavor中的重复资源。

如果完全相同的资源出现在同一源集中,Gradle 无法合并它们,并且会发出资源合并错误。如果您在 build.gradle 文件的 sourceSet 属性中定义了多个源集,则可能会发生这种情况,例如,如果 src/main/res/ 和 src/main/res2/ 包含完全相同的资源,就可能会发生这种情况。


排查资源压缩问题

当您压缩资源时,Gradle Console 会显示它从应用软件包中移除的资源的摘要。例如:

:android:shrinkDebugResources
Removed unused resources: Binary resource data reduced from 2570KB to 1711KB: Removed 33%
:android:validateDebugSigning

Gradle 还会在 /build/outputs/mapping/release/(ProGuard 输出文件所在的文件夹)中创建一个名为 resources.txt 的诊断文件。该文件包括诸如哪些资源引用了其他资源以及使用或移除了哪些资源等详情。


ProGuard

上面的部分来自于官方文档中,本节主要解释一些ProGuard中的配置的涵义

-optimizationpasses:optimizationpasses表示proguard对你的代码进行迭代优化的次数,首先要明白optimization 会对代码进行各种优化,每次优化后的代码还可以再次优化,所以就产生了优化次数的问题,这里面的 passes 应该翻译成’次数’而不是’通道’。楼上默认写5,应该是做Android的,关于Android里面为什么写 5 ,因为作者本来写99,但是每次迭代时间都很长团队成员天天抱怨,就改成5了,迭代会在最后一次无法优化的时候停止,也就是虽然你写着99,但是可能就优化了几次,一般情况下迭代10次左右的时候代码已经不能再次优化了

–dontskipnonpubliclibraryclasses:不忽略library里面非public修饰的类。忽略library里面非public修饰的类可以加快ProGuard的处理速度和降低ProGuard的使用内存。一般而言,library里的非公开类是不能被程序使用的,忽略掉这些类可以加快混淆速度。

-optimizations:指定混淆是采用的算法,后面的参数是一个过滤器,这个过滤器是谷歌推荐的算法,一般不做更改

-dontwarn:不打印指定类的警告信息

-keepclasseswithmembers:指定的类和类成员被保留,假如指定的类成员存在的话。

-keepclassmembers {modifier} {class_specification}:保护指定类的成员

-keepattributes {attribute_name,…}:保护给定的可选属性,例如LineNumberTable, LocalVariableTable, SourceFile, Deprecated, Synthetic, Signature, and InnerClasses.


附录

1.推荐几个比较好(丧心病狂)的混淆字典:RockyQu/ProguardDictionaryhqzxzwb/ProguardDictionaryGenerator

2.附上一个可用的proguard-rules (文件中的包名都被替换为了packagename,请自行更改)

# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

#-------------------------------------------- 公共配置 Start ---------------------------------------#

# 混淆的压缩比例,0-7
-optimizationpasses 5

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

# 指定混淆是采用的算法
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*

#优化时允许访问并修改有修饰符的类和类的成员
-allowaccessmodification

#将文件来源重命名为“SourceFile”字符串
-renamesourcefileattribute SourceFile

#保留行号
-keepattributes SourceFile,LineNumberTable

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

# 避免混淆泛型
-keepattributes Signature

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

#混淆时不使用大小写混合类名
-dontusemixedcaseclassnames

#不跳过library中的非public的类
-dontskipnonpubliclibraryclasses

##打印混淆的详细信息
#-verbose

#保留native方法的类名和方法名
-keepclasseswithmembernames class * {
    native <methods>;
}

#--------------------------------------------- 公共配置 End ----------------------------------------#

#--------------------------------------------- 应用配置 Start --------------------------------------#

#所有实体类都保持住(使用Gson转换实体类时,如果实体类字段名被混淆,则无法正常转换)
-keep public class packagename.data.** {*;}
-keep public class packagename.http.base.** {*;}

# 保持哪些类不被混淆
-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 com.android.vending.licensing.ILicensingService

# DataBinding
-dontwarn android.databinding.**
-keep class android.databinding.** { *; }

# 保持自定义控件类不被混淆
-keepclasseswithmembers class * {
   public <init>(android.content.Context, android.util.AttributeSet);
}
-keepclasseswithmembers class * {
   public <init>(android.content.Context, android.util.AttributeSet, int);
}
-keepclassmembers class * extends android.app.Activity {
   public void *(android.view.View);
}

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

#保持Parcelable不被混淆
-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();
}
-keepclassmembers class * {
   public <init> (org.json.JSONObject);
}

-keep public class packagename.R$*{
public static final int *;
}

#-------------------------------------------- 应用配置 End -----------------------------------------#

#-------------------------------------------- 第三方包 start ---------------------------------------#

#Gson
-keepattributes Signature
-keepattributes *Annotation*
-keep class sun.misc.Unsafe { *; }
-keep class com.google.gson.stream.** { *; }
-keep class com.google.gson.examples.android.model.** { *; }
-keep class com.google.gson.* { *;}
-dontwarn com.google.gson.**

# UMeng Statistics
-keep class com.umeng.** {*;}
-keepclassmembers class * {
   public <init> (org.json.JSONObject);
}
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

# Glide
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
  **[] $VALUES;
  public *;
}

# EventBus
-keepattributes *Annotation*
-keepclassmembers class * {
    @org.greenrobot.eventbus.Subscribe <methods>;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
# Only required if you use AsyncExecutor
-keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent {
    <init>(java.lang.Throwable);
}

# Retrofit 2.X
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
-keepattributes Signature
-keepattributes Exceptions

-keepclasseswithmembers class * {
    @retrofit2.http.* <methods>;
}

# OkHttp
-keepattributes Signature
-keepattributes *Annotation*
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-dontwarn okhttp3.**

# RxJava

-keep class rx.schedulers.Schedulers {
    public static <methods>;
}
-keep class rx.schedulers.ImmediateScheduler {
    public <methods>;
}
-keep class rx.schedulers.TestScheduler {
    public <methods>;
}
-keep class rx.schedulers.Schedulers {
    public static ** test();
}

# Tencent
-keep class com.tencent.mm.opensdk.** {*;}
-keep class com.tencent.wxop.** {*;}
-keep class com.tencent.mm.sdk.** {*;}

# Matisse
-dontwarn com.bumptech.glide.**
-dontwarn com.bumptech.glide.**

#--------------------------------------------- 第三方包 End ----------------------------------------#

# 忽略警告,否则打包可能会不成功
-ignorewarnings



参考资料

1.官方文档-压缩代码和资源

2.HansChen_-ProGuard代码混淆详细攻略

3.凌枫清扬-Android Studio 代码混淆

猜你喜欢

转载自blog.csdn.net/asd501823206/article/details/88861803