混淆在项目中的应用方案

27 阅读11分钟

混淆开关

用变量ALLOW_PROGUARD作为开关,ALLOW_PROGUARD为true时进行混淆,ALLOW_PROGUARD为false时不进行混淆。

  1. 在各个应用的mk文件中按照如下方式对应:


    ifeq ($(ALLOW_PROGUARD),true)
    #混淆
    LOCAL_PROGUARD_ENABLED := full obfuscation
    LOCAL_PROGUARD_FLAG_FILES := proguard.flags
    else
    #不混淆
    LOCAL_PROGUARD_ENABLED := disabled
    endif


  2. 开关的控制
    在vendor/chinatsp/scripts文件夹中添加make文件proguard_config.mk
    并在vendor/chinatsp/scripts中include这个proguard_config.mk文件。
    脚本中会设置ALLOW_PROGUARD的值:

    ALLOW_PROGUARD := true

    在编译之前进行source ./build/envsetup.sh的时候,会执行这个脚本。
    所以如果修改了ALLOW_PROGUARD的值,要重新执行source ./build/envsetup.sh。
    在每次执行make或make xxx命令时,会判断这个值,去删除或者创建mapping文件夹。

 

mapping文件的保存

混淆时会生成一个mapping.txt文件,其中列出了经过混淆处理的类、方法和字段名称与原始名称的映射关系。用来帮助调试问题。

混淆后的mapping文件统一放到/out/mapping/中的对应文件夹下,比如vendor/chinatsp/apps/Launcher的mapping文件,会放到/out/mapping/vendor/chinatsp/apps/Launcher 文件夹下。

项目上每天release版本的时候,会将/out/mapping/单独打包为一个压缩包,和版本一起发布,便于调试bug。

  1. mapping文件保存的目录的创建
    在vendor/chinatsp/scripts文件夹中添加make文件proguard_config.mk, 在这个make文件中,会对变量ALLOW_PROGUARD进行判断,如果要混淆,会将需要的mapping文件夹全部创建好;
    如果不混淆,会将之前残留的mapping文件夹删除。
    这个脚本在source ./build/envsetup.sh的时候执行。
    所以如果修改了ALLOW_PROGUARD的值,或者是添加了一个新的库,要重新执行source ./build/envsetup.sh。~~~~**
    **

    在每次执行make或make xxx命令时,会调用proguard_config.mk
    **
    **
  2. 指定mapping文件的保存路径
    在各个app的混淆规则文件proguard.flags中需要指定自己的mapping文件保存的路径,规则如下:
    -printmapping ../../../../out/mapping/vendor/chinatsp/apps/Launcher/mapping.txt

**
**

ProGuard作用

  1. 压缩/缩减(Shrinking):默认开启,用以减小应用体积,移除未被使用的类和成员,并且会在优化动作执行之后再次执行(因为优化后可能会再次暴露一些未被使用的类和成员)。
    -dontshrink 关闭压缩
    -printusage filename  可以将没有使用的代码列出来,输出到filename

  2. 优化(Optimization):默认开启,在字节码级别执行优化,让应用运行的更快。
    比如:
    如果代码从未采用过给定 if/else 语句的 else {} 分支,R8 可能会移除 else {} 分支的代码。
    如果代码只在一个位置调用某个方法,R8 可能会移除该方法并将其内嵌在这一个调用点。
    -dontoptimize  关闭优化
    -optimizationpasses n 表示proguard对代码进行迭代优化的次数,Android一般为5

  3. 混淆(Obfuscation):默认开启,增大反编译难度,类和类成员会被随机命名,除非用keep保护。
    -dontobfuscate 关闭混淆

    常用规则

Input/Output选项

-include filename递归地从给定的文件中读取配置选项。可以用来引用库中的proguard.flags文件
-skipnonpubliclibraryclasses指定在读取依赖库时跳过非公共类,以加快处理速度并减少ProGuard的内存使用。在默认情况下,ProGuard会同时读取依赖库中的非公共类和公共类。但是,如果非公共类不影响输入jar中的实际程序代码,那么它们通常是不相关的。忽略它们,然后加快ProGuard,而不影响输出。不幸的是,一些库,包括最近的JSE运行时库,包含由公共库类扩展的非公共库类。你不能使用这个选项。如果因为设置了这个选项而找不到类,ProGuard会打印出警告。
-dontskipnonpubliclibraryclasses指定不跳过依赖库中的非公共类。在版本4.5中,这是默认设置。
-dontskipnonpubliclibraryclassmembers指定不忽略包可见库类成员(字段和方法)。默认情况下,ProGuard在解析库中的类时会跳过这些类成员,因为程序类通常不会引用它们。但是,有时程序类与库类位于同一包中,并且它们确实引用其包可见的类成员。在这些情况下,实际读取类成员可能很有用,以确保所处理的代码保持一致。

 

Keep选项

使用-keep可以保留某些代码不被混淆或者移除等。

关键字描述举例
-keep-keep [,modifier,...] class_spec 指定作为代码入口点保留的类和类成员(字段和方法)。-keep class com.chinatsp.test.A保持类A,不包括内部的域和方法
-keepclassmembers-keepclassmembers [,modifier,...] class_specification指定要保留的类成员(如果它们的类也被保留)。 例如,保留实现可序列化接口的类的所有序列化字段和方法。-keepclassmembers class * implements java.io.Serializable { static final long serialVersionUID; private static final java.io.ObjectStreamField[] serialPersistentFields; !static !transient ; private void writeObject(java.io.ObjectOutputStream); private void readObject(java.io.ObjectInputStream); java.lang.Object writeReplace(); java.lang.Object readResolve(); }
-keepclasseswithmembers-keepclasseswithmembers [,modifier,...] class_specification指定要保留的类和类成员,条件是所有指定的类成员都存在。例如,保留所有具有main方法的应用程序,而不必显式地列出它们。-keepclasseswithmembers class * { public void main(**); }
-keepnames-keepnames class_specification是-keep,allowshrinking class_specification的缩写,它指定要保持名称的类和类成员(如果在压缩(shrinking)阶段未删除它们)。此字段仅用在混淆阶段。例如,保留实现Serializable接口的类的所有类名,以使处理后的代码与任何原始序列化的类保持兼容。完全不使用的类仍可以删除。-keepnames class * implements java.io.Serializable
-keepclassmembernames-keepclassmembernames class_specification是 -keep,classmembers,allowshrinkingclass_specification 的缩写,它指定要保留其名称的类成员(如果在压缩(shrinking)阶段未删除它们)。此字段仅用在混淆阶段。例如,在处理由JDK 1.2或更早版本编译的库时,保留合成类$方法的名称,因此混淆器可以在处理使用了这个处理后的库的应用程序时,再次检测到它(尽管ProGuard本身不需要 这个)。-keepclassmembernames class hello.Hello { public *; }
-keepclasseswithmembernames-keepclasseswithmembernamesclass_specification是keepclasseswithmembers,allowshrinkingclass_specification 的缩写。它指定要保留其名称的类和类成员,条件是所有指定的类成员在收缩阶段之后都存在。此字段仅用在混淆阶段。例如,保留所有native方法名称及其类的名称,以便处理后的代码仍可以与native库代码链接。 完全不使用的Native方法仍然可以删除。如果使用了类文件,但是没有使用其Native方法,则其名称仍将被混淆。-keepclasseswithmembernames class * { native ; }

对比

类和类成员-keep-keepnames
仅类成员-keepclassmembers-keepclassmembernames
如果拥有某成员,保留类和类成员-keepclasseswithmembers-keepclasseswithmembernames

移除是指在压缩(Shrinking)时是否会被删除。

 

类名的匹配规则

匹配类名称中的任何单个字符,但不匹配包分隔符。例如,“ com.example.Test?” 匹配“ com.example.Test1”和“ com.example.Test2”,但不匹配“ com.example.Test12”。
*匹配不包含包分隔符的类名的任何部分。例如,“ com.example.* Test ”匹配“ com.example.Test”和“ com.example.YourTestApplication”,但不匹配“ com.example.mysubpackage.MyTest”。或者,更一般而言,“ com.example.” 匹配"com.example”中的所有类,但不匹配其子包中的所有类。
**匹配类名的任何部分,可能包含任意数量的包分隔符。例如,“ com.example.**” 匹配在“ com.example”及其子包中的所有类。

 

类,类的域和方法等的匹配规则

extendsimplements表示类的继承和实现关系-keep public class * extends android.view.View 保持继承自View的类 -keep class * implements android.os.Parcelable保持实现了Parcelable接口的类
$匹配内部类-keep class com.chinatsp.test.A$B { *; } 保持A的内部类B
publicprivate各种关键字匹配对应的关键字-keep class com. chinatsp.test.A {public ;} 保持所有public的方法
匹配方法名称中的单个字符 
*匹配任意的域或者方法名-keep class com. chinatsp.test.* {*;}    保持com. chinatsp.test包下的所有类, 以及类中的所有域和方法
在相同选项中匹配第n个匹配的通配符。例如,"com.example.*Foo<1>" 可以匹配 "com.example.BarFooBar".
%匹配任何原始类型(“ boolean”,“ int”等),但不匹配“ void”。 
***匹配任何类型(原始或非原始,数组或非数组)。keepclassmembers class com.android.calendar.DayView {*** setAnimateDayHeight(...);}匹配类DayView 中名称是setAnimateDayHeight,返回值是任意类型,有任意个数的参数的方法。
匹配构造方法-keep class com. chinatsp.test.A {(); //保持所有构造方法} 
匹配所有域-keep class com. chinatsp.test.A {; //保持所有域} 
匹配所有方法-keep class com. chinatsp.test.A {; //保持所有方法} 
...匹配任何类型,任何个数的参数-keep class com. chinatsp.test.A {public void putString(...);} 匹配A中public的没有返回值,参数是任意个数,任何类型,名称是putString的方法。

缩减选项

-dontshrink-dontshrink不缩减。默认会缩减代码:删除所有未使用的类和类成员。 它仅保留由各种-keep选项列出的选项,以及它们直接或间接依赖的选项。它还会在每个优化步骤之后再次应用压缩步骤,因为某些优化可能会删除更多类和类成员。-dontshrink
-printusage-printusage [filename]指定列出输入类文件的无效(dead)代码。 该列表将打印到标准输出或给定的文件。仅在压缩时适用。例如,列出应用程序的未使用(unused)代码。然后从代码上将其删除来精简自己的代码。-printusage ~/tmp/random_output_file.txt
-whyareyoukeeping class_specification -whyareyoukeeping class_specification指定打印有关为什么在压缩(shrink)步骤中保留给定类和类成员的详细信息。 如果您想知道为什么输出中存在某些给定的元素,这可能会很有用。通常,可能有许多不同的原因。对于每个指定的类和类成员,此选项将最短的方法链打印到指定的种子(seed)或入口点(entry point)。在当前实现中,打印出的最短的链有时可能包含循环推演-这些不反映实际的压缩处理。如果指定了-verbose选项,则跟踪将包括完整的字段和方法签名。仅在压缩时适用。-whyareyoukeeping class * extends foo.bar { *; }

 

Optimization选项

-dontoptimize指定不优化输入类文件。默认情况下,ProGuard会优化所有代码。它内联并合并类和类成员,并在字节码级别优化所有方法。-dontoptimize
-optimizations-optimizations [optimization_filter]在更细粒度的级别上指定要启用和禁用的优化。仅在优化时适用。这是一个专家级选项。 
-optimizationpasses-optimizationpasses n指定要执行的优化次数。默认情况下,执行一次。多次执行可能会导致进一步的改进。如果在优化通过之后未发现任何改进,则优化结束。仅在优化时适用。-optimizationpasses 5
-assumenosideeffects-assumenosideeffects class_specification指定除了可能返回值之外,没有任何副作用的方法。仅在优化时适用。一般而言,做出假设可能很危险;您可以轻松地破坏已处理的代码。 仅当您知道自己在做什么时才使用此选项!例如,方法System.currentTimeMillis()返回一个值,但没有任何副作用。如果可以确定未使用返回值,则ProGuard可以在优化步骤中删除对此类方法的调用。ProGuard将分析您的程序代码以自动查找此类方法。它不会分析库代码,因此此选项可能有用。例如,可以指定方法System.currentTimeMillis(),以便删除对它的任何空闲调用。 一定要小心,也可以使用该选项删除日志记录代码。请注意,ProGuard将选项应用于指定方法的整个层次结构。

Obfuscation选项

-dontobfuscate指定不混淆输入类文件。默认情况下,ProGuard对代码进行模糊处理:它为类和类成员分配新的随机短名称。它删除仅对调试有用的内部属性,例如源文件名,变量名和行号。-dontobfuscate
-printmapping-printmapping [filename]指定为已重命名的类和类成员打印从旧名称到新名称的映射。映射将打印到标准输出或给定文件。仅在混淆时适用。例如,对于后续的增量混淆,或者如果想再次理解混淆的堆栈跟踪,则需要使用它。-printmapping ../../../../out/mapping/vendor/chinatsp/apps/Launcher/mapping.txt
-applymapping-applymapping filename指定一个之前进行ProGuard混淆时生成的混淆名称对照表,本次依照该对照继续混淆,不在表中的成员生成新的名字。映射可以引用输入类以及库类。此选项对于增量混淆(即处理现有代码段的附件或小补丁)很有用。但是如果代码的结构发生了根本变化,则ProGuard可能会打印出警告,指出应用映射会导致冲突。您可以通过在两次混淆运行中指定option-useuniqueclassmembernames来降低这种风险。只允许一个映射文件。仅在混淆时适用。基本不会用到
-useuniqueclassmembernames指定为具有相同名称的类成员分配相同的混淆名称,为具有不同名称的类成员分配不同的混淆名称(对于每个给定的类成员签名)。如果没有该选项,则可以将更多的类成员映射到相同的短名称,如“ a”,“ b”等。因此,该选项会稍微增加结果代码的大小,但可以确保保存的混淆名称映射始终是在后续的渐增混淆步骤中受到尊重。此选项仅在混淆时适用。实际上,如果您打算执行渐增模糊处理,则可能要完全避免缩减和优化,因为这些步骤可能会删除或修改代码中对以后添加至关重要的部分。 
-keepattributes-keepattributes [attribute_filter]指定要保留的所有可选属性。 可以使用一个或多个-keepattributes指令指定属性。可选过滤器是Java虚拟机和ProGuard支持的属性名称的逗号分隔列表。属性名称可以包含?,*和**通配符,并且可以在其前面加上!! (否定)。最后,如果您的代码依赖注释,则可能需要保留注释。仅在混淆时适用。例如,在处理库时,至少应保留Exceptions,InnerClasses和Signature属性。还应该保留SourceFile和LineNumberTable属性,以产生有用的混淆堆栈跟踪。**-keepattributes Exceptions, InnerClasses, Signature, Deprecated,**SourceFile, LineNumberTable, Annotation, EnclosingMethod

相关的关键字还有-applymapping, -obfuscationdictionary,   -classobfuscationdictionary,-packageobfuscationdictionary,-overloadaggressively,-useuniqueclassmembernames,-dontusemixedcaseclassnames,

-keeppackagenames, -flattenpackagehierarchy,-repackageclasses, -keepparameternames, -renamesourcefileattribute,-adaptclassstrings, -adaptkotlinmetadata, -adaptresourcefilenames, -adaptresourcefilecontents等,

但是这些并不常用,感兴趣的可以自己查看。

 www.guardsquare.com/en/products…

Preverification 选项

-dontpreverify指定不预先验证已处理的类文件。 默认情况下,如果类文件针对Java Micro Edition或Java 6或更高版本,则将进行预验证。对于Java Micro Edition,需要进行预验证,因此,如果指定此选项,则需要在已处理的代码上运行外部预验证器。对于Java 6,预验证是可选的,但从Java 7开始,它是必需的。仅当最终针对Android时才是不必要的,因此您可以将其关闭以减少处理时间。

 

General选项

-verbose指定在混淆期间写出更多信息。 一般不用设置
-dontnote-dontnote [class_filter]指定不打印在配置中的潜在错误或遗漏的注释,例如,类名中的错字或可能有用的缺失选项。这个选项过滤器是一个正则表达式; ProGuard不会打印与这个正则表达式相匹配的类(类名匹配)的注释。一般不用配置
-dontwarn-dontwarn [class_filter]指定根本不警告尚未解决的引用和其他重要问题。可选过滤器是一个正则表达式; ProGuard不会打印有关名称匹配的类的警告。忽视警告可能很危险。例如,如果确实需要未解析的类或类成员进行处理,则处理后的代码将无法正常运行。仅当您知道自己在做什么时才使用此选项!
-ignorewarnings指定打印有关未解决的引用和其他重要问题的任何警告,但在任何情况下都将继续处理。忽视警告可能很危险。例如,如果确实需要未解析的类或类成员进行处理,则处理后的代码将无法正常运行。仅当您知道自己在做什么时才使用此选项!
-printconfiguration-printconfiguration [filename]指定写出已解析的完整配置,包括包含的文件和替换的变量。结构被打印到标准输出或给定的文件。有时这对于调试配置或将XML配置转换为更具可读性的格式很有用。
-dump-dump [filename]指定在进行任何处理后写出类文件的内部结构。结构被打印到标准输出或给定的文件。例如,您可能想要写出给定jar文件的内容,而无需进行任何处理。

不能混淆的内容

有些代码在混淆后会出现异常,是需要保留,不能被混淆的。

jni方法不可混淆,因为这个方法需要和native方法保持一致-keepclasseswithmembernames class * { # 保持native方法不被混淆native ;}
反射用到的类不能混淆(否则反射可能出现问题)-keep class com.chinatsp.xxx.xxxxxx**-keep class com.chinatsp.xxx.xxxxxx { ;}*
AndroidMainfest中的类不混淆,所以四大组件和Application的子类和Framework层下所有的类默认不会进行混淆无,这些默认就是不混淆的
Parcelable的子类和Creator静态成员变量不混淆,否则会产生Android.os.BadParcelableException异常。**-keep class * implements Android.os.Parcelable { # 保持Parcelable不被混淆public static final Android.os.Parcelable$Creator ;*}
实现了Serializable进行序列化的类不能混淆,否则产生异常。-keepnames class * implements java.io.Serializable
使用enum类型时需要注意避免以下两个方法混淆,因为enum类的特殊性,以下两个方法会被反射调用。**-keepclassmembers enum * {****public static [] values();public static ** valueOf(java.lang.String);}
泛型不能混淆-keepattributes Signature
R文件内部的内容不能混淆-keepclassmembers class *.R$ { public static ; }不用自己设置,平台默认的配置文件中已经设置了
如果用到了类的getSimpleName()作为Log的tag,那么最好不要混淆这个类的名字,否则打印出来的Tag就是a,b,c,d这种名字了。或者不要使用getSimpleName(), 直接定义tag:private static final String TAG = "AllListFragment";-keepnames public class com.chinatsp.radio.list.view.AllListFragment;
如果添加fragment的时候,tag用的是类的getSimpleName(), 那么也不要混淆这个类的名字,否则findFragmentByTag的时候可能会找不到。或者不要使用getSimpleName(), 直接写字符串。-keepnames public class * extends android.app.Fragment

 

常用的三方库的混淆规则

 

EventBus接收的消息接口不能混淆,否则改接口会因为 unused而被移除,就收不到消息了-keepattributes Annotation -keepclassmembers class ** { @org.greenrobot.eventbus.Subscribe ; }
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 *; }If you're targeting any API level less than Android API 27, also include: -dontwarn com.bumptech.glide.load.resource.bitmap.VideoDecoder
RxJava RxAndroid从2.x版本开始,默认情况下,RxJava本身不需要任何ProGuard/R8设置,应该可以正常工作。不幸的是,它依赖的Reactive Streams 自1.0.3版本以来在其JAR中嵌入了Java 9类文件,可能会导致警告:Warning: org.reactivestreams.FlowAdaptersFlowPublisherFromReactive:cantfindsuperclassorinterfacejava.util.concurrent.FlowFlowPublisherFromReactive: can't find superclass or interface java.util.concurrent.FlowPublisher Warning: org.reactivestreams.FlowAdaptersFlowToReactiveProcessor:cantfindsuperclassorinterfacejava.util.concurrent.FlowFlowToReactiveProcessor: can't find superclass or interface java.util.concurrent.FlowProcessor Warning: org.reactivestreams.FlowAdaptersFlowToReactiveSubscriber:cantfindsuperclassorinterfacejava.util.concurrent.FlowFlowToReactiveSubscriber: can't find superclass or interface java.util.concurrent.FlowSubscriber Warning: org.reactivestreams.FlowAdaptersFlowToReactiveSubscription:cantfindsuperclassorinterfacejava.util.concurrent.FlowFlowToReactiveSubscription: can't find superclass or interface java.util.concurrent.FlowSubscription Warning: org.reactivestreams.FlowAdapters: can't find referenced class java.util.concurrent.Flow$Publisher 所以建议在应用程序的proguard配置文件中设置对应的-dontwarn条目。-dontwarn java.util.concurrent.Flow*
Gson ##---------------Begin: proguard configuration for Gson ---------- # 在处理字段时,Gson使用存储在类文件中的泛型类型信息。在默认情况下,# Proguard会删除这些信息,所以将其配置为保留所有这些信息。-keepattributes Signature# For using GSON @Expose annotation -keepattributes Annotation# Gson 特定的类 -dontwarn sun.misc.** #-keep class com.google.gson.stream.** { ; } # 将通过Gson序列化/反序列化的应用程序类-keep class com.google.gson.examples.android.model.* { ; } # 防止proguard从TypeAdapter、TypeAdapterFactory、JsonSerializer, JsonDeserializer # 实例中剥离接口信息(这样就可以在@JsonAdapter中使用它们) -keep class * implements com.google.gson.TypeAdapter -keep class * implements com.google.gson.TypeAdapterFactory -keep class * implements com.google.gson.JsonSerializer -keep class * implements com.google.gson.JsonDeserializer# Prevent R8 from leaving Data object members always null -keepclassmembers,allowobfuscation class * { @com.google.gson.annotations.SerializedName ; }##---------------End: proguard configuration for Gson ----------
android-support-v7-recyclerview如果在xml中用到了app:layoutManager="LinearLayoutManager",则需要配置; 如果没有,则不需要配置# When layoutManager xml attribute is used, RecyclerView inflates #LayoutManagers' constructors using reflection. -keep public class * extends android.support.v7.widget.RecyclerView$LayoutManager { public (...); }
android-support-core-ui # Make sure we keep annotations for ViewPager's DecorView -keepattributes Annotation
   

 

想要了解详细规则的,可以查看 www.guardsquare.com/en/products…