This page looks best with JavaScript enabled

优化 ObjectOutputStream 的使用

 ·  ☕ 2 min read

最近阅读《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 同样可以这样优化


Yang
WRITTEN BY
Yang
Developer