普遍的,不论大小Android应用都会配置 proguard 在release 编译的时候混淆自己的代码:

1
2
3
4
5
6
7
8
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

但无论 Proguard 还是 R8,他们的混淆字典默认都太简单了(too simple),只是 abcdefg 而已,反编译后还是很容易阅读的,如下所示:

1
2
3
4
5
6
7
8
final class b {
Object a;
aes b;
ada c;

b() {
}
}

所幸,Proguard 支持自定义字典:

1
2
3
-obfuscationdictionary dict.txt
-classobfuscationdictionary dict.txt
-packageobfuscationdictionary dict.txt

如果,有那么一个字典,里面都是形似“乱码”的字符,看起来不仅费眼睛,甚至电脑字体还没收录更佳(会显示成一个方框)。

万能的github 上还真有符合要求的。但是直接生成好的字典文件一直用也是有隐患的,举个例子,两个版本之间类、方法的个数差别不大,最终的混淆结果其实是很相似的,对比 mapping 之后,有可能一个方法前一个版本叫 aa,现在叫 ab 了。

而且翻看了部分实现方案,要么是字典文件里词汇量不够大,要么生成代码实现可能有其它bug。故而干脆自己撸起袖子几行代码搞定。

首先给 app build.gradle 加一个生成字典的任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
task genDict {
outputs.file('build/tmp/dict.txt')
doLast {
def r = new Random()
def start = r.nextInt(1000) + 0x0100
def end = start + 0x4000 // 如果字典太大了,可以将start~end范围缩小
def chars = (start..end)
.findAll { Character.isValidCodePoint(it) && Character.isJavaIdentifierPart(it) }
.collect { String.valueOf(Character.toChars(it)) }
int max = chars.size()
def startChars = []
def dict = []
// 筛选可用作java标识符开头的char
for (int i = 0; i < max; i++) {
char c = chars.get(i).charAt(0)
if (Character.isJavaIdentifierStart(c)) {
startChars << String.valueOf(c)
}
}
def startSize = startChars.size()
// 打乱顺序
Collections.shuffle(chars, r)
Collections.shuffle(startChars, r)
// 拼两个char为一个词,让字典更丰富
for (int i = 0; i < max; i++) {
def m = r.nextInt(startSize - 3)
def n = m + 3
(m..n).each { j ->
dict << (startChars.get(j) + chars.get(i))
}
}

def f = outputs.files.getSingleFile()
f.getParentFile().mkdirs()
f.withWriter("UTF-8") {
it.write(startChars.join(System.lineSeparator()))
it.write(dict.join(System.lineSeparator()))
}
}
}

手动执行./gradlew genDict,看看字典,都是这种字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19








چ







Ī


自动生成proguard字典也很简单:

1
2
3
4
5
6
7
8
9
afterEvaluate {
// 只给 release 包注册
android.applicationVariants.all { variant ->
if (variant.name.endsWith('Release'))
variant.javaCompileProvider.configure {
dependsOn 'genDict'
}
}
}

最后别忘了在 proguard rules 里配置字典文件

1
2
3
-obfuscationdictionary build/tmp/dict.txt
-classobfuscationdictionary build/tmp/dict.txt
-packageobfuscationdictionary build/tmp/dict.txt

混淆后效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Ƞ extends 칁<홳> {
public final 驀 치;

public Ƞ(驀 驀) {
this.치 = 驀;
}

public void 치(ꃑ ꃑ) {
}

public void 치(홳 홳) {
this.치.逥 = true;
}
}

对了,android gradle plugin 的版本是 3.4.1,gradle 版本 5.1.1,如果你用的和我不一样,前面所提到的代码(genDict)可能需要稍作修改。