你的自定义View的get/set可能没被混淆! 记一次自定义task破解混淆玄学问题

564 阅读5分钟

前言

今天发现自定义View的get/set方法并没有被混淆,导致各种自定义组件的接口,以及内部各种代码逻辑,在逆向视角可读性很高,像是直接开源了。

image.png

我从没配置让get/set不被minify??

摸索了一番,最终发现需要新增一个gradle任务才好解决这个问题。

发现没有相关的简中博客,Google大概搜了下也没搜到类似的内容,于是记录和分享一下解决历程。

摸索历程

谁让View的子类的get/set不被混淆

项目创建后,AndroidStudio默认的模板,混淆配置:

proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'

我个人习惯是从不写minify豁免规则,而是默认所有成员都会被minify,需要避免minify的地方标记@Keep注解。

那大概率是proguard-android-optimize.txt导致的问题,而这个文件位于Android SDK目录,SDK/tools/proguard

打开它,确实发现如下语句:

# keep setters in Views so that animations can still work.
# see http://proguard.sourceforge.net/manual/examples.html#beans
-keepclassmembers public class * extends android.view.View {
   void set*(***);
   *** get*();
}

抛开 是不是这里导致的 不谈,首先想的是SDK目录东西肯定不要动,毕竟是所有项目通用的文件,万一将来忘了这件事,要踩逆天大坑。

GPT给的没效果的方法

# 混淆所有其他类和方法 
-keepclassmembers class com.example.MyCustomView { 
    !<methods>; 
}

先是IDE提示错误abstract, final, native, private, protected, public, static, strictfp, synchronized, synthetic, transient or volatile expected, got '<methods>', 然后根据提示,把!<methods>; 改成!public <methods>;,测试无效,看来并不能覆盖另一个混淆规则。追问GPT之后回答越来越离谱,放弃。

修改proguard-android-optimize.txt没效果?

暂时找不到更好的办法,就先修改SDK/tools/proguard/proguard-android-optimize.txt凑合用,注释掉下方内容,结果竟然发现仍然没效果?

   void set*(***);
   *** get*();

无效,想着是AndroidStudio是把SDK目录的一些配置缓存了吧,或者是有什么BUG,毕竟从Android构建工具开发者视角来看,这些文件通常也不会改。于是各种Clean、重启AndroidStudio。问题并没解决。

开始怀疑getDefaultProguardFile这个函数了。 在build.gradle打印它的返回值:

android{
    //...
    buildTypes {
        defaultProguardFile = getDefaultProguardFile('proguard-android-optimize.txt')
        println("defaultProguardFile = " + defaultProguardFile)

defaultProguardFile = 省略/app/build/intermediates/default_proguard_files/global/proguard-android-optimize.txt-7.3.1

然后看这个文件内容,明明在SDK目录的proguard-android-optimize.txt文件注释掉的东西,但是这个文件仍然有:

# Keep setters in Views so that animations can still work.
-keepclassmembers public class * extends android.view.View {
    void set*(***);
    *** get*();
}

难道getDefaultProguardFile的参数proguard-android-optimize.txt被无视了,或者是,传入的文件名,并不是SDK目录刚修改的文件?于是随便写了个getDefaultProguardFile('xxx'),想通过AGP报错,是否会提示某某路径不存在xxx文件的,依此确认到底从哪个路径找的。

然而报错却是:

Supplied proguard configuration file name is unsupported. Valid values are: [proguard-android-optimize.txt, proguard-defaults.txt, proguard-android.txt]

这下不得不看AGP源码了。

哪来的混淆规则?看AGP源码

我这里是7.3.1的AGP版本,对应源码搜getDefaultProguardFile,定位到函数:

open fun getDefaultProguardFile(name: String): File {
    if (!ProguardFiles.KNOWN_FILE_NAMES.contains(name)) {
        dslServices
            .issueReporter
            .reportError(
                IssueReporter.Type.GENERIC, ProguardFiles.UNKNOWN_FILENAME_MESSAGE
            )
    }
    return ProguardFiles.getDefaultProguardFile(name, dslServices.buildDirectory)
}

其中KNOWN_FILE_NAMES是个数组,限制了这个函数只能输入三类内容,也就是刚刚的报错。代码:

    public enum ProguardFile {
        /** Default when not using the "postProcessing" DSL block. */
        DONT_OPTIMIZE("proguard-android.txt"),

        /** Variant of the above which does not disable optimizations. */
        OPTIMIZE("proguard-android-optimize.txt"),

        /**
         * Does not disable any actions, includes optimizations config. To be used with the new
         * "postProcessing" DSL block.
         */
        NO_ACTIONS("proguard-defaults.txt"),
        ;

        @NonNull public final String fileName;

        ProguardFile(@NonNull String fileName) {
            this.fileName = fileName;
        }
    }

    public static final Set<String> KNOWN_FILE_NAMES =
            Arrays.stream(ProguardFile.values()).map(pf -> pf.fileName).collect(Collectors.toSet());

回到刚才,继续找ProguardFiles类的getDefaultProguardFile

public static File getDefaultProguardFile(
        @NonNull String name, @NonNull DirectoryProperty buildDirectory) {
    if (!KNOWN_FILE_NAMES.contains(name)) {
        throw new IllegalArgumentException(UNKNOWN_FILENAME_MESSAGE);
    }

    return FileUtils.join(
            getDefaultProguardFileDir(buildDirectory),
            name + "-" + Version.ANDROID_GRADLE_PLUGIN_VERSION);
}

看来上文打印出来的proguard-android-optimize.txt-7.3.1就是这里拼接的了。

线索断了,但是从ProguardFiles类,可以观察到另一个方法:

    public static void createProguardFile(
            @NonNull String name, @NonNull File destination, @NonNull Boolean keepRClass)
             throws IOException {
        // 忽略
        
        append(sb, "proguard-header.txt");
        sb.append("\n");

        switch (proguardFile) {
            case DONT_OPTIMIZE:
                // 太长, 忽略
                break;
            case OPTIMIZE:
                sb.append(
                        "# Optimizations: If you don't want to optimize, use the proguard-android.txt configuration file\n"
                                + "# instead of this one, which turns off the optimization flags.\n");
                append(sb, "proguard-optimizations.txt");
                break;
            case NO_ACTIONS:
                sb.append(
                        "# Optimizations can be turned on and off in the 'postProcessing' DSL block.\n"
                                + "# The configuration below is applied if optimizations are enabled.\n");
                append(sb, "proguard-optimizations.txt");
                break;
        }

        sb.append("\n");
        append(sb, "proguard-common.txt");

        if (keepRClass) {
            String rFieldRule = "-keepclassmembers class **.R$* {\n" +
                    "    public static <fields>;\n" +
                    "}\n";
            sb.append(rFieldRule);
        }

        Files.asCharSink(destination, UTF_8).write(sb.toString());
    }

    private static void append(StringBuilder sb, String resourceName) throws IOException {
        sb.append(Resources.toString(ProguardFiles.class.getResource(resourceName), UTF_8));
    }
}

可以看到其中有个:

append(sb, "proguard-common.txt");

看过一些关于混淆的博客,没见过这proguard-common.txt文件??

他就在AGP源码中,com\android\build\gradle\proguard-common.txt,确实包含View子类的get/set混淆赦免代码:

# Keep setters in Views so that animations can still work.
-keepclassmembers public class * extends android.view.View {
    void set*(***);
    *** get*();
}

事到如今,这个玄学问题终于从科学角度解释完了。

再搜createProguardFile函数,可以看到他是在哪个任务中调用的:

@DisableCachingByDefault
abstract class ExtractProguardFiles : NonIncrementalGlobalTask() {

    // 忽略
    override fun doTaskAction() {
        for (name in ProguardFiles.KNOWN_FILE_NAMES) {
            val defaultProguardFile = ProguardFiles.getDefaultProguardFile(name, buildDirectory)
            if (!defaultProguardFile.isFile) {
                ProguardFiles.createProguardFile(name, defaultProguardFile, enableKeepRClass.get())
            }
        }
    }

这个任务生成的proguard-android-optimize.txt-7.3.1文件。而具体使用这个的文件地方,是构建过程中最慢的一个task:minifyReleaseWithR8

结论

有了以上分析过程,最终在build.gradle编写如下代码,在:minifyReleaseWithR8之前修改ExtractProguardFiles生成的混淆配置,就可以解决View子类的get/set没被混淆的问题:

// ... 忽略
File defaultProguardFile = null
android {
    // ... 
    buildTypes {
        defaultProguardFile = getDefaultProguardFile('proguard-android-optimize.txt')
        // ... 
        release {
            // ... 
            proguardFiles defaultProguardFile, 'proguard-rules.pro'
        }
        // ... 
    }
    // ... 
}
// ... 

tasks.register('minifyCustomViewGetSet') {
    doLast{
        def text = defaultProguardFile.getText()
        def oldRule = "-keepclassmembers public class * extends android.view.View {\n" +
                "    void set*(***);\n" +
                "    *** get*();\n" +
                "}\n"
        def newRule = "-keepclassmembers public class * extends android.view.View {\n" +
                "#    void set*(***);\n" +
                "#    *** get*();\n" +
                "}\n"

        println("contains oldRule: " + text.contains(oldRule))

        def newContent = text.replace(oldRule, newRule)
        defaultProguardFile.setText(newContent)
    }
}

// ... 

afterEvaluate{
    // ... 
    tasks.matching { it.name.startsWith('minify') && it.name.endsWith('WithR8') }.configureEach { minifyTask ->
        minifyTask.dependsOn minifyCustomViewGetSet
    }

}

假如某个AGP版本,被替换的内容变了怎么办?最好还是没找到oldRule时候抛个异常,以免埋下逆天大坑。 但这是我自己做着玩的项目,核心问题已解决,其他的边边角角做的再好只也是浪费时间了。

欢迎讨论。