R8 踩坑,Gson 反序列化结果为空字符串

背景

在跟进线上日志上报系统时,发现上报的日志内容全部为空(c 字段内为空)。

1
{"c":"{}","f":5,"l":1615895427708,"n":"main","i":2,"m":true}

分析过程

测试环境没问题, 线上有问题,不由自主的想到可能是混淆的原因。但是一般混淆也只会导致反序列化出现问题,而不是序列化。

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
package com.*.common.log.bean;

import com.*.common.utils.Time8Utils;
import com.google.gson.annotations.SerializedName;

import java.util.UUID;

public class NormalMsgEntity {
@SerializedName("i")
private final String id = UUID.randomUUID().toString().replace("-", "");
@SerializedName("t")
private final String timestamp;
@SerializedName("l")
private String label;
@SerializedName("p")
private int level;
@SerializedName("c")
private String file;
@SerializedName("f")
private String function;
@SerializedName("n")
private int line;
@SerializedName("m")
private String message;
@SerializedName("d")
private String metadata;

public NormalMsgEntity() {
timestamp = Time8Utils.getDateTimeStringForUTC();
}

public String getId() {
return id;
}

public String getTimestamp() {
return timestamp;
}

public String getLabel() {
return label;
}

public NormalMsgEntity setLabel(String label) {
this.label = label;
return this;
}

public int getLevel() {
return level;
}

public NormalMsgEntity setLevel(int level) {
this.level = level;
return this;
}

public String getFile() {
return file;
}

public NormalMsgEntity setFile(String file) {
this.file = file;
return this;
}

public String getFunction() {
return function;
}

public NormalMsgEntity setFunction(String function) {
this.function = function;
return this;
}

public int getLine() {
return line;
}

public NormalMsgEntity setLine(int line) {
this.line = line;
return this;
}

public String getMessage() {
return message;
}

public NormalMsgEntity setMessage(String message) {
this.message = message;
return this;
}

public String getMetadata() {
return metadata;
}

public NormalMsgEntity setMetadata(String metadata) {
this.metadata = metadata;
return this;
}
}

看一眼 bean 的定义,似乎也没什么问题,定位到生成日志内容的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public String formatMessage(int level, String tag, String message, @Nullable Map<String, String> mateData) {
MethodInfoEntity methodInfo = StackTraceUtils.getMethodInfo();

NormalMsgEntity normalMsgEntity = new NormalMsgEntity();
if (mateData != null && !mateData.isEmpty()) {
normalMsgEntity.setMetadata(mGson.toJson(mateData));
}

normalMsgEntity
.setLabel(tag)
.setLevel(level)
.setFile(methodInfo.getFileName())
.setFunction(methodInfo.getMethodName())
.setLine(methodInfo.getLineNumber())
.setMessage(message);
return mGson.toJson(normalMsgEntity);
}

该方法做的事情很简单, 生成了几个对象,并且对相应的属性赋值,最终通过 gson#toJson 将对象序列化为 Json 字符串。

仔细端详一下,猜测将对象反序列化为 Json 字符串时,因为某种原因导致反序列化失败或者结果为“”

无论是序列化还是反序列化,Gson 都会先遍历对象的所有 Field,然后通过反射调用 getter or setter 方法亦或是直接给 Field 赋值。

打个断点到 gson#toJson 方法, 先不跟到调用栈内部, 我们自己通过反射获取下 Field,wtf! 竟然是空的。

debug

这 TM 发生了什么事情,难道编译器把我们的字段去掉了?把我们的 APK 反编译下,看看编译后的代码。

解决前编译结果

还真是,NormalMsgEntity 下的所有 Field 都不见了。

“测试包没问题,正式包有问题,编译结果与源码不一致”,这个很容易想到是Android Studio 的 R8 作祟。

当您使用 Android Gradle 插件 3.4.0 或更高版本构建项目时,该插件不再使用 ProGuard 执行编译时代码优化,而是与 R8 编译器协同工作,处理以下编译时任务:
代码缩减(即摇树优化):从应用及其库依赖项中检测并安全地移除不使用的类、字段、方法和属性(这使其成为了一个对于规避 64k 引用限制非常有用的工具)。例如,如果您仅使用某个库依赖项的少数几个 API,那么缩减功能可以识别应用不使用的库代码并仅从应用中移除这部分代码。如需了解详情,请转到介绍如何缩减代码的部分。
资源缩减:从封装应用中移除不使用的资源,包括应用库依赖项中不使用的资源。此功能可与代码缩减功能结合使用,这样一来,移除不使用的代码后,也可以安全地移除不再引用的所有资源。如需了解详情,请转到介绍如何缩减资源的部分。
混淆:缩短类和成员的名称,从而减小 DEX 文件的大小。如需了解详情,请转到介绍如何对代码进行混淆处理的部分。
优化:检查并重写代码,以进一步减小应用的 DEX 文件的大小。例如,如果 R8 检测到从未采用过给定 if/else 语句的 else {} 分支,则会移除 else {} 分支的代码。如需了解详情,请转到介绍代码优化的部分。

所以结论就是正式包开启了代码优化,而NormalMsgEntity 符合代码优化条件,在编译时我们定义的 Field 都被优化掉了,然后导致序列化成了空字符串。

解决方案

so, 有三种解决办法

  1. 防止该类被优化

    1
    keep class com.aftership.common.log.bean.NormalMsgEntity {*;}
  2. 保证每一个 filed 显式的使用到, 最简单的办法就是通过 idea 生成 toString方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Override
    public String toString() {
    return "NormalMsgEntity{" +
    "id='" + id + '\'' +
    ", timestamp='" + timestamp + '\'' +
    ", label='" + label + '\'' +
    ", level=" + level +
    ", file='" + file + '\'' +
    ", function='" + function + '\'' +
    ", line=" + line +
    ", message='" + message + '\'' +
    ", metadata='" + metadata + '\'' +
    '}';
    }
  3. 关闭 R8 代码优化

    1
    2
    3
    4
    5
    6
    7
    // gradle.properties   
    android.enableR8=false

    // build.gradle
    proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    // ↓↓↓↓↓↓↓↓↓↓ change ↓↓↓↓↓↓↓↓↓↓
    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

为了安全,我们这里选择方案2,反编译后的结果如下,**Filed** 都正常显示了。

解决后编译结果

参考资料

Shrink, obfuscate, and optimize your app

作者

sadhu

发布于

2021-03-17

更新于

2021-03-17

许可协议