案例分析
這篇文章,我們來看看使用 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 文件,并在日志中打印出文件的內(nèi)容。
(我們這個 tmp.txt 文件很小,只有一行 "hello world")
初看,好像沒什么問題,F(xiàn)ileReader 和 BufferedReader 都在 finally 里關(guān)閉了。
我們把程序跑起來,看看內(nèi)存占用怎么樣?
這里寫了一個腳本,每 120 秒取一下進程的內(nèi)存占用:
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
結(jié)果真是讓我們大吃一驚啊,Heap 平均每分鐘漲 400 K,怎么會這樣?
我們打開 Android Device Monitor,查看 Allocation Tracker:
截圖一:

screen_shot_a.png
打開 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 中數(shù)據(jù)相符。
截圖二:

screen_shot_b.png
按照截圖一中的分析方法,我們來看看 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 數(shù)組,Java 中每 byte 類型為 8 位,即每一個 byte 占用一個字節(jié)(Byte),8192 個 byte 占用內(nèi)存為 8208 Byte(約8K),符合預期。
我們終于知道,這些增長的內(nèi)存是哪里來的了。
建議
- 不要在 Java 層,做頻繁地 文件 操作,可以在 native 層,用 C 語言來處理;
- 推而廣之,我們的分析也應證了 “不要在 Java 中頻繁地分配對象” 這句話。
備注
- 測試數(shù)據(jù)來自為 Smartisan T1,另在 Note3 上,heap 也會持續(xù)增加;
- 文中截圖,只選取了 BufferedReader 和 ByteBuffer 相關(guān)的內(nèi)存分配,其它因為讀取文件而分配的對象,讀者可以自行在工具中查看;
- 文中 heap 持續(xù)增加的情況,并不是每臺手機上都出現(xiàn),在Nexus 7 平板上,heap 會在 6 分鐘左右趨于平穩(wěn),數(shù)據(jù)如下:
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