案例分析
这篇文章,我们来看看使用 BufferedReader 在 Android 设备上读取文件的一个问题~
测试代码是这个样子的:
public class MyActivity extends Activity {
private static final int MSG_CODE_LOOP = 1024;
private static final int MSG_CODE_QUIT = 0;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == MSG_CODE_LOOP) {
readFile();
sendEmptyMessageDelayed(MSG_CODE_LOOP, 5000);
} else if (msg.what == MSG_CODE_QUIT) {
removeMessages(MSG_CODE_LOOP);
}
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
handler.sendEmptyMessage(MSG_CODE_LOOP);
}
private void readFile() {
File file = Environment.getExternalStoragePublicDirectory("tmp.txt");
FileReader fileReader = null;
BufferedReader bufferedReader = null;
try {
fileReader = new FileReader(file);
bufferedReader = new BufferedReader(fileReader);
String line;
while ((line = bufferedReader.readLine()) != null) {
Log.d("io-demo", line);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bufferedReader != null) {
bufferedReader.close();
}
if (fileReader != null) {
fileReader.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
代码很简单,就是每 5 秒读取一次 SDCard 根目录中的 tmp.txt 文件,并在日志中打印出文件的内容。
(我们这个 tmp.txt 文件很小,只有一行 "hello world")
初看,好像没什么问题,FileReader 和 BufferedReader 都在 finally 里关闭了。
我们把程序跑起来,看看内存占用怎么样?
这里写了一个脚本,每 120 秒取一下进程的内存占用:
Pss Privite
Total Dirty
------------------------------------
Dalvik Heap 1249 1036
TOTAL 6451 4880
Dalvik Heap 2042 1832 // 2 分钟后,heap 增加 807K
TOTAL 7243 5668
Dalvik Heap 2818 2608 // 4 分钟后,heap 增加 1569K
TOTAL 8043 6468
Dalvik Heap 3598 3392 // 6 分钟后,heap 增加 2349K
TOTAL 8847 7276
Dalvik Heap 4374 4168 // ...
TOTAL 9651 8080
Dalvik Heap 5150 4944
TOTAL 10447 8876
Dalvik Heap 5926 5720
TOTAL 11247 9676
Dalvik Heap 6675 6496
TOTAL 12003 10476
Dalvik Heap 7440 7272
TOTAL 12789 11272
Dalvik Heap 8217 8048
TOTAL 13586 12068
Dalvik Heap 8826 8660
TOTAL 14222 12728
结果真是让我们大吃一惊啊,Heap 平均每分钟涨 400 K,怎么会这样?
我们打开 Android Device Monitor,查看 Allocation Tracker:
截图一:
打开 BufferedReader.java 这个文件:
public BufferedReader(Reader in) {
this(in, 8192); // line 9
}
public BufferedReader(Reader in, int size) {
super(in);
if (size <= 0) {
throw new IllegalArgumentException("size <= 0");
}
this.in = in;
buf = new char[size]; // line 112
}
这段代码中,每 new 一个BufferedReader, 会分配 8192 个 char (112 行),因为 java 中 char 占 2 个 Byte(16位),8192 个 char 占用 16384 个 Byte,约等于 16400 Byte (约16K),与Device Monitor 中数据相符。
截图二:
按照截图一中的分析方法,我们来看看 InputStreamReader.java
// InputStreamReader.java
private final ByteBuffer bytes = ByteBuffer.allocate(8192); // line 47
// ByteBuffer.java
public static ByteBuffer allocate(int capacity) {
if (capacity < 0) {
throw new IllegalArgumentException("capacity < 0: " + capacity);
}
return new ByteArrayBuffer(new byte[capacity]);// line 56
}
由代码可知,ByteBuffer.java 分配了一个 size 为 8192 的 byte 数组,Java 中每 byte 类型为 8 位,即每一个 byte 占用一个字节(Byte),8192 个 byte 占用内存为 8208 Byte(约8K),符合预期。
我们终于知道,这些增长的内存是哪里来的了。
建议
- 不要在 Java 层,做频繁地 文件 操作,可以在 native 层,用 C 语言来处理;
- 推而广之,我们的分析也应证了 “不要在 Java 中频繁地分配对象” 这句话。
备注
- 测试数据来自为 Smartisan T1,另在 Note3 上,heap 也会持续增加;
- 文中截图,只选取了 BufferedReader 和 ByteBuffer 相关的内存分配,其它因为读取文件而分配的对象,读者可以自行在工具中查看;
- 文中 heap 持续增加的情况,并不是每台手机上都出现,在Nexus 7 平板上,heap 会在 6 分钟左右趋于平稳,数据如下:
Dalvik Heap 1075 716
TOTAL 6428 4960
Dalvik Heap 1684 1060
TOTAL 7370 5416
Dalvik Heap 1804 1180
TOTAL 7538 5584
Dalvik Heap 1820 1196 // 从 6 分钟开始, heap 保持在 1820K
TOTAL 7566 5612
Dalvik Heap 1820 1196
TOTAL 7574 5620
Dalvik Heap 1820 1196
TOTAL 7578 5624
Dalvik Heap 1820 1196
TOTAL 7578 5624
Dalvik Heap 1820 1196
TOTAL 7609 5652