在开发Android应用时,我们经常需要将数据进行持久化。对于少量的数据,Android提供了轻量的,以XML格式文件保存的SharedPreferences工具。对于大量的,且需要进行增删改查操作的数据,Android则提供了SQLite数据库。有时,我们希望对内存里的某些结构体数据(比如某个类的实例、ArrayList等)进行持久化,这时,使用SharedPreferences则过于繁琐,而使用数据库则太“大材小用”,其实我们可以直接将其序列化到磁盘上,等到要用时再反序列化。如下面的demo,我们将size为1000的ArrayList<Data>用ObjectOutputStream序列化到磁盘(作为demo,为了简单,直接将IO操作放到主线程,下同):
public class MainActivity extends Activity {
public static class Data implements Serializable {
private static final long serialVersionUID = 1L;
public String str1 = "aa";
public String str2 = "bb";
public String str3 = "cc";
public int int1 = 1;
public int int2 = 2;
public int int3 = 3;
public long long1 = 1L;
public long long2 = 2L;
public long long3 = 3L;
public InnerStruct is = new InnerStruct();
}
public static class InnerStruct implements Serializable {
private static final long serialVersionUID = 1L;
public float f = 1.0f;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FileOutputStream fos = null;
ObjectOutputStream oos = null;
try {
fos = new FileOutputStream(getApplication().getFilesDir().getAbsolutePath() + "/test.bin");
oos = new ObjectOutputStream(fos);
ArrayList<Data> data = new ArrayList<Data>();
for (int i = 0; i < 1000; i++) {
data.add(new Data());
}
oos.writeObject(data);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
运行程序,我们可以看到test.bin文件被正确地写到了/data/data/com.example.iotest/files目录下,大小为67313字节:
但是,这是新手的写法,稍微有点经验的程序员看到这些代码都会指出其IO性能问题:写文件次数过多!是吗?我们来看一下到底写了多少次文件:
这是用Traceview工具获取的数据,在解释这个数据之前,我们需要先从源码层面了解ObjectOutputStream在序列化对象时都干了哪些事情。ObjectOutputStream的主要功能是将复杂的对象进行拆解,真正写文件是由FileOutputStream的write(byte[] buffer, int byteOffset, int byteCount)方法进行,之后的调用路径如下图所示:
我们可以看到JAVA层的最后一个方法是libcore.io.Posix的writeBytes(FileDescriptor fd, Object buffer, int offset, int byteCount),之后就通过JNI调用了native的方法。libcore.io.Posix对应的C++文件是libcore_io_Posix.cpp(源码:http://androidxref.com/4.0.4/xref/libcore/luni/src/main/native/libcore_io_Posix.cpp#),writeBytes对应的C++方法是Posix_writeBytes(JNIEnv* env, jobject, jobject javaFd, jbyteArray javaBytes, jint byteOffset, jint byteCount),如下图所示:
在Posix_writeBytes方法里最后调用了GNU的libc库函数write (int filedes, const void *buffer, size_t size),这里是操作系统层面写文件的地方。
我们回过头来看看Traceview的数据,就可以发现写文件的次数达到了惊人的19052次!
那么我们应该怎样写才会减少写文件的次数呢?答案是用BufferedOutputStream做缓存,再批量写入文件,如下所示:
public class MainActivity extends Activity {
public static class Data implements Serializable {
private static final long serialVersionUID = 1L;
public String str1 = "aa";
public String str2 = "bb";
public String str3 = "cc";
public int int1 = 1;
public int int2 = 2;
public int int3 = 3;
public long long1 = 1L;
public long long2 = 2L;
public long long3 = 3L;
public InnerStruct is = new InnerStruct();
}
public static class InnerStruct implements Serializable {
private static final long serialVersionUID = 1L;
public float f = 1.0f;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FileOutputStream fos = null;
BufferedOutputStream bos = null;
ObjectOutputStream oos = null;
try {
fos = new FileOutputStream(getApplication().getFilesDir().getAbsolutePath() + "/test.bin");
bos = new BufferedOutputStream(fos);
oos = new ObjectOutputStream(bos);
ArrayList<Data> data = new ArrayList<Data>();
for (int i = 0; i < 1000; i++) {
data.add(new Data());
}
oos.writeObject(data);
oos.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bos != null) {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
这个写法写文件的次数是多少呢?我们用Traceview来看一下:
现在就只有9次了。
我们可以对比一下这两种方法写文件的耗时,无缓存的方法和有缓存的方法各执行10次(单位:ms):
无缓存 | 600 | 456 | 466 | 483 | 473 | 525 | 456 | 470 | 531 | 490 |
有缓存 | 98 | 126 | 116 | 133 | 100 | 105 | 122 | 117 | 79 | 132 |
无缓存方法执行的平均时间为:495ms,有缓存方法执行的平均时间为:112.8ms,时间减少77.2%。
BufferedOutputStream其实很简单,内部的数据结构就是一个byte数组,一个int的计数器:
其默认的buffer大小为8K:
当写文件时,如果buffer已经装不下了,就先将buffer里的数据写入文件,如果可以装下,则将数据先暂时放到buffer里: