0%

[Android] FD leak

分享實際遇到的案例。

至於什麼是FD leak? 此文章已有詳細介紹https://juejin.cn/post/7058098018683191310,就不贅述。
可簡單理解成打開thread, cursor, 檔案, io等,皆會產生FD檔,當超過系統限制(基本是1024)時,便會造成錯誤。

偵測

要知道問題發生在哪,有沒有解決,需要每段時間去偵測FD數量變化。
參考https://blog.csdn.net/qunimaode/article/details/126996454

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
public static void logFdCount() {
int count = 0;
try {
Process process = Runtime.getRuntime().exec("ls /proc/" + android.os.Process.myPid() + "/fd");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
String s = bufferedReader.readLine();
while (s != null) {
s = bufferedReader.readLine();
count++;
}
bufferedReader.close();

bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
s = bufferedReader.readLine();
while (s != null) {
s = bufferedReader.readLine();
count++;
}
bufferedReader.close();
process.destroy();
} catch (IOException ex) {
Log.e("Mike", "Exception: " + ex.getLocalizedMessage(), ex);
}
Log.d("Mike", "fd count=" + count);
}

Thread

有用Executors.newSingleThreadExecutor(),但對於每次使用MyService時,都建一次新的,是沒有達成共用的。當api呼叫的量達到一定程度,便會造成FD leak。

1
2
3
4
5
6
7
8
9
10
11
public static MyService getMyService(String baseUrl) {
OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(baseUrl)
.client(httpBuilder.build())
.addConverterFactory(GsonConverterFactory.create())
.callbackExecutor(Executors.newSingleThreadExecutor())
.build();

return retrofit.create(MyService.class);
}

當時的解法是共用threadPool,這樣即便有打向不同服務的api,也可共用和重複利用thread。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static final Executor THREAD_POOL_EXECUTOR = Executors.newCachedThreadPool(new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);

@Override
public Thread newThread(@NonNull Runnable runnable) {
return new Thread(runnable, "work-" + mCount.getAndIncrement());
}
});

public static MyService getMyService(String baseUrl) {
OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(baseUrl)
.client(httpBuilder.build())
.addConverterFactory(GsonConverterFactory.create())
.callbackExecutor(THREAD_POOL_EXECUTOR)
.build();

return retrofit.create(MyService.class);
}

Thread + Looper

為了讓ProgressDialog不因主執行緒做太多事而卡頓,改放至副執行緒。

1
2
3
4
5
6
7
8
9
10
public static void showLoadingDialog(Activity activity) {
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
progressDialog = ProgressDialog.show(activity, null, "Loading...");
Looper.loop();
}
}).start();
}

當呼叫Looper.loop()便會讓執行緒卡著,持續等待接收message,就算事後dismiss ProgressDialog,一樣不會終止。要避免這樣的寫法。

通則是UI操作要維持在主執行緒,業務邏輯就該放在副執行緒。