看大佬造轮子:如何写一个Mess 混淆Activity、View

混淆一切,把爱你的心藏在最深处

作者 Moonshot 日期 2018-11-11
看大佬造轮子:如何写一个Mess 混淆Activity、View

Mess

Mess是一个解决Android自带ProGuard无法混淆Activity、Service的gradle插件

ProGuard 为什么混淆不了Activity、Service、自定义View

ProGuard是用来优化、缩减、混淆的java代码,并非为Android专门设计的。而安卓的四大组件的是再manifest下注册的,自定义View在xml文件中使用。因此如果代码混淆了,但是xml不变,会导致错误。故实际上默认是keep住了四大组件和view的子类。
然而大佬们是不会满足这种情况的。那么假设没看过Mess库该怎么做呢。

核心目标是 混淆Activity等、相应xml替换成混淆后的数据

  • 混淆Activity、View等
    • 编译过程是如何keep住的,即使混淆文件没有配置
  • 修改替换xml的数据
    • 需要知道混淆前后的对应关系 读取mapping.txt信息
    • 读取XML寻找需要相应的地方进行替换 并回写回去 进行打包
    • 在编译期需要合适的task进行hook 获取上述需要的信息、和替换原始数据等操作

ProGuard 小科普

###使用ProGuard的意义

压缩(Shrink):检测并移除代码中无用的类、字段、方法和特性(Attribute)。
优化(Optimize):对字节码进行优化,移除无用的指令。
混淆(Obfuscate):使用a,b,c,d这样简短而无意义的名称,对类、字段和方法进行重命名。
预检(Preveirfy):在Java平台上对处理后的代码进行预检,确保加载的class文件是可执行的。

简而言之,优化代码,去除非必要代码即,及一定的程度的瘦包(去除多余代码,缩进命名a.b.c减少代码文件的大小),然后混淆降低代码的可读性,提高反编译的难度和成本。

###混淆的基本使用
一般Android项目中的配置是这样的。

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

其中指定了两个混淆规则的文件proguard-android.txtproguard-rules.pro

其实proguard-rules.pro就是工程app目录下创建时自动生成的、一般也是项目的混淆规则会在该目录下配置。

其中proguard-android.txt是安卓默认的混淆规则,一般再sdk的/tools/proguard/proguard-android.txt **路径下。

然后一般混淆后会有几个文件在/build/proguard/输出。

  • dump.txt 描述apk文件中所有类文件间的内部结构。
  • mapping.txt 列出了原始的类,方法,和字段名与混淆后代码之间的映 射。
  • seeds.txt 列出了未被混淆的类和成员
  • usage.txt 列出了从apk中删除的代码

一般也用不到,主要是在处理线上奔溃信息时,用来定位到原始代码可能需要用到。

###混淆规则和Keep语法
在实际开发中一般需要的就是对某些不能混淆的类,编写相应Keep语句。例如
-keep public class com.moonshot.loveu.bean.** 不混淆相应的bean类 因为一般会使用Gson等工具进行转化

具体的混淆规则和Keep语法就不深入。可查看proguard Keep选项 (不要问为什么要放英文网站,因为比较有逼格),也可自行查阅信息。

###不可混淆的类

混淆有这么多好处,那干脆就什么都混淆不就好了吗。如果可以当然是最好,但有些不可混淆,一混淆会代码代码上的错误。

除了一开始说的四大组件、View不可混淆等,还有很多不可混淆,例如Native方法,枚举等等,实际上我们并没有编写这些得keep规则,但实际运行时没有问题的,一部分原因就是因为在proguard-android.txt中都写好了。


# The support library contains references to newer platform versions.
# Don't warn about those in case this app is linking against an older
# platform version. We know about them, and they are safe.
-dontwarn android.support.**


#保留R下面的资源
-keep class **.R$* {*;}

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

#保留在Activity中的方法参数是view的方法,
#这样以来我们在layout中写的onClick就不会被影响
-keepclassmembers class * extends android.app.Activity{
public void *(android.view.View);
}

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

#保留我们自定义控件(继承自View)不被混淆
-keep public class * extends android.view.View{
*** get*();
void set*(***);
public <init>(android.content.Context);
public <init>(android.content.Context, android.util.AttributeSet);
public <init>(android.content.Context, android.util.AttributeSet, int);
}

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

#保留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();
}

其实这些跟实际开发也没有很大关系。一般实际开发中有些不能混淆的多是用到反射相关的类、方法、成员变量不可混淆。因为混淆后会反射找不到相应的类。

如何达到Mess的效果

前文说道主要有两个步骤 1.混淆Activity、View 2. 修改替换xml的数据
其中在最新的proguard-android.txt中并没有看见Keep Activity和View的代码,只keep住了部分方法。

所以我们需要去了解还有哪里读取设置了混淆文件的配置,或者混淆的配置最终再哪里设置。

哪怎么办呢,假设Mess如果没有开源怎么办呢?

那么只能再打包的源码源码搜索相关proguard相关的东西。

例如搜索proguard-android.txt。可以找到一个常量:TaskManager.DEFAULT_PROGUARD_CONFIG_FILE.

然后找到JackTransform 使用了这个常亮

com.android.build.gradle.internal.transforms.JackTransform (实际进行代码编译成dex的task)

然后发现其中有实际混淆配置文件的读取。

if (config.isMinifyEnabled()) {//如果开启混淆 上文gradle中 minifyEnabled 的配置

File sdkDir = scope.getGlobalScope().getSdkHandler().getAndCheckSdkFolder();
checkNotNull(sdkDir);
//默认的混淆配置和项目的混淆配置文件 即前文的proguard-android.txt 和 proguard-rules.pro
File defaultProguardFile = ProguardFiles.getDefaultProguardFile(
TaskManager.DEFAULT_PROGUARD_CONFIG_FILE, project);

Set<File> proguardFiles = config.getProguardFiles(true /*includeLibs*/,
ImmutableList.of(defaultProguardFile));
File proguardResFile = scope.getProcessAndroidResourcesProguardOutputFile();
proguardFiles.add(proguardResFile);
// for tested app, we only care about their aapt config since the base
// configs are the same files anyway.

if (scope.getTestedVariantData() != null) {
//和资源相关的混淆信息文件
proguardResFile = scope.getTestedVariantData().getScope()
.getProcessAndroidResourcesProguardOutputFile();
proguardFiles.add(proguardResFile);
}
options.setProguardFiles(proguardFiles);
options.setMappingFile(new File(scope.getProguardOutputFolder(), "mapping.txt"));
}

发现其中getProcessAndroidResourcesProguardOutputFile()的数据是默认配置也非项目配置的数据。

@Override
@NonNull
public File getProcessAndroidResourcesProguardOutputFile() {
return new File(globalScope.getIntermediatesDir(),
"/proguard-rules/" + getVariantConfiguration().getDirName() + "/aapt_rules.txt");
}

实际打包后,可以返现aapt_rules.txt中包含着和manifest、layout等资源文件中使用到的类的keep规则。

那么现在目标变化为,如何在合适的task修改或删除aapt_rules.txt,来达到编译的时候混淆四大组件的目标。
通过getProcessAndroidResourcesProguardOutputFile的引用,可以发现另一个task :ProcessAndroidResources 简而言之,该task会通过aapt命令进行资源文件的编译,生成asrc文件等操作。

Mess的流程

加下来再我们让我们看看Mess实际是如何完成上面的每一个步骤

variant.outputs.each { BaseVariantOutput output ->

//Hook ProcessAndroidResources Task
String taskName = "transformClassesAndResourcesWithProguardFor${variant.name.capitalize()}"
def proguardTask = project.tasks.findByName(taskName)
if (!proguardTask) {
return
}

boolean hasProcessResourcesExecuted = false
output.processResources.doLast {
if (hasProcessResourcesExecuted) {
return
}
hasProcessResourcesExecuted = true
//删除 aapt_rules.txt保证 activity、view被混淆
def rulesPath = "${project.buildDir.absolutePath}/intermediates/proguard-rules/${variant.dirName}/aapt_rules.txt"
File aaptRules = new File(rulesPath)
aaptRules.delete()
aaptRules.createNewFile()
aaptRules << ""
}

proguardTask.doFirst {
//隐藏依赖库的混淆配置
println "start ignore proguard components"
ext.ignoreProguardComponents.each { String component ->
Util.hideProguardTxt(project, component)
}
}

proguardTask.doLast {
// 读取maping.txt 、修改替换xml的数据
// 详情看RewriteComponentTask的实现,主要是文件的读取修改逻辑
println "proguard finish, ready to execute rewrite"
RewriteComponentTask rewriteTask = project.tasks.create(name: "rewriteComponentFor${variant.name.capitalize()}",
type: RewriteComponentTask
) {
apkVariant = variant
variantOutput = output
}
rewriteTask.execute()
}

proguardTask.doLast {
//恢复依赖库混淆配置
ext.ignoreProguardComponents.each { String component ->
Util.recoverProguardTxt(project, component)
}
}
}