最近阅读《Android移动性能实战》看到手机QQ测试团队给出的一个案列 「Object Ouput Stream 4000 多次的写操作」,
其原因就是直接使用了 ObjectOutputStream + FileOutputStream 做对象的序列化到磁盘。印象中我们的项目中也有这样的代码
SerializeUtil#serializeObject:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static <Obj extends Serializable> boolean serializeObject(Obj o, String path) {
ObjectOutputStream oo = null;
boolean success = true;
try {
oo = new ObjectOutputStream(new FileOutputStream(path));
oo.writeObject(o);
} catch (Exception ignore) {
success = false;
} finally {
try {
if (oo != null) {
oo.close();
}
} catch (Exception ignored) {
success = false;
}
}
return success;
}

书中给出的优化方案为结合 ByteArrayOutputStreamBufferedOutputStream 做 Object 的序列化。

复现问题

眼见为实,于是结合开源的 https://github.com/Tencent/matrix (据说可以检测到代码中调用底层IO的次数耗时等信息), 写个测试代码试试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class TestObject : Serializable {
val d = ByteArray(1024 * 30)
val s = ArrayList<String>()

init {
repeat(10000) {
s.add("$it")
}
}
}

fun writeObjectToFile(path: String, obj: Serializable) {
val oos = ObjectOutputStream(FileOutputStream(path))
oos.writeObject(obj)
oos.flush()
oos.close()
}

直接在主线程执行(目前 matrix 只能检测主线程上的 IO 操作), 结果有了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"path": "/sdcard/test.obj",
"size": 99785,
"op": 10040,
"buffer": 1024,
"cost": 31,
"opType": 2,
"opSize": 99785,
"thread": "main",
"stack": "writeObjectToFile(TestActivity.kt:94)",
"repeat": 0,
"tag": "io",
"type": 2,
"time": 1571364341671
}

结果说明:

  • size: 写入的数据量
  • op: 操作次数
  • type: 1:read 2: write
  • buffer: 操作使用的 buffer size

竟然有 10040 次的写入操作(调用底层 libjavacore.so 的 write). 而且写入的 buffer 只有 1024. 但是 buffer 1024, size 99785, 写入次数不是应该为 99785/1024 = 98 次吗?

原因分析

10040 哪来的? 这就要看 ObjectOutputStreamwriteObject 的骚操作了:

writeObject 最终会走到 ObjectOutputStream#defaultWriteFields 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int numObjFields = desc.getNumObjFields();
// 获取 Field 数量
if (numObjFields > 0) {
ObjectStreamField[] fields = desc.getFields(false);
Object[] objVals = new Object[numObjFields];
int numPrimFields = fields.length - objVals.length;
desc.getObjFieldValues(obj, objVals);
for (int i = 0; i < objVals.length; i++) {
// 为每个 Field 执行 writeObject0
try {
writeObject0(objVals[i], fields[numPrimFields + i].isUnshared());
} finally {...}
}
}

writeObject0 方法:

1
2
3
4
5
6
7
8
9
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {...}

可见 ObjectOutputStream 实际会为每个字段执行具体的 write 操作, ObjectOutputStreamwrite 操作内部又是调用的构造时传入的 OutputStream, 所以就直接造成多次调用 FileOutputStreamwrite

修复

使用 ByteArrayOutputStreamBufferedOutputStream + ObjectOutputStream + FileOutputStream 的组合,能够先将 ObjectOutputStream 全部写入到位于内存的
ByteArrayOutputStream, 然后通过 ByteArrayOutputStream 一次写入到 FileOutputStream 中, 最终就只会有 1 次的底层 write 操作调用.

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
public static <Obj extends Serializable> boolean serializeObject(Obj o, String path) {
ObjectOutputStream oo = null;
ByteArrayOutputStream bao;
FileOutputStream fos = null;
boolean success = true;
try {
bao = new ByteArrayOutputStream();
oo = new ObjectOutputStream(bao);
oo.writeObject(o);
oo.flush();
fos = new FileOutputStream(path);
bao.writeTo(fos);
bao.flush();
fos.flush();
} catch (Exception ignore) {
success = false;
} finally {
try {
if (oo != null) {
oo.close();
}
if (fos != null) {
fos.close();
}
} catch (Exception ignored) {
success = false;
}
}
return success;
}

ObjectInputStream 同样可以这样优化