前言
今天发现自定义View的get/set方法并没有被混淆,导致各种自定义组件的接口,以及内部各种代码逻辑,在逆向视角可读性很高,像是直接开源了。
我从没配置让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时候抛个异常,以免埋下逆天大坑。
但这是我自己做着玩的项目,核心问题已解决,其他的边边角角做的再好只也是浪费时间了。
欢迎讨论。