最近阅读《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;
}
|
书中给出的优化方案为结合 ByteArrayOutputStream
或 BufferedOutputStream
做 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 哪来的? 这就要看 ObjectOutputStream
的 writeObject
的骚操作了:
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 操作, ObjectOutputStream
的 write
操作内部又是调用的构造时传入的 OutputStream
, 所以就直接造成多次调用 FileOutputStream
的 write
。
修复
使用 ByteArrayOutputStream
或 BufferedOutputStream
+ 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 同样可以这样优化