做Android开发多年,之前从来没有听过FD这个概念,线上遇到了一个极端的crash然后专门查资料学习了一下,在这里分享出来希望对大家有帮助。
文件描述符 FileDescriptor Android官方介绍
Instances of the file descriptor class serve as an opaque handle to the underlying machine-specific structure representing an open file, an open socket, or another source or sink of bytes. The main practical use for a file descriptor is to create a FileInputStream or FileOutputStream to contain it.
Applications should not create their own file descriptors.
使用有道翻译一下:
文件描述符类的实例充当底层特定于机器的结构的不透明句柄,该结构表示打开的文件、打开的套接字或另一个字节源或接收器。文件描述符的主要实际用途是创建一个FileInputStream或FileOutputStream来包含它。
应用程序不应该创建自己的文件描述符。
看谷歌介绍Fd,并没有提到fd有最大数的限制吧?因为Android系统是基于Linux内核创建的,而文件描述符(Fd)是Linux层的概念,所以谷歌没有提到这点,因为根本和Android没关系了,但是wtf 线上crash可不管你是Android还是Linux,你必须得要解决才行。
看来想掌握Android必须要深入理解Linux才行啊~
一个进程的文件描述符有上限,如果文件描述符超过上限的话就会爆出奇奇怪怪的Crash
无需root获取进程的文件描述符上限和进程当前的文件描述符大小(下面两个方法支持Android所有版本)
//获取当前进程的文件描述符上限
private int getFdMax() {
File fdFile = new File("/proc/" + Process.myPid() + "/limits");
try {
BufferedReader bfr = new BufferedReader(new FileReader(fdFile));
String line = bfr.readLine();
StringBuilder sb = new StringBuilder();
while (line != null) {
if (line.contains("Max open files")) {
sb.append(line);
break;
}
line = bfr.readLine();
}
bfr.close();
//这里的空格确实就是这么长,不要粘贴错了
String number = sb.toString().split(" ")[1];
return Integer.parseInt(number);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
//获取当前进程当前的文件描述符大小
private int getFdCount() {
File fdFile = new File("/proc/" + Process.myPid() + "/fd");
File[] files = fdFile.listFiles();
if (files != null) {
return files.length;
}
return 0;
}
现在问题来了,我们的文件描述符数量为什么会增加?根据谷歌的文档我们知道涉及文件相关的数据流套接字操作就会有文件描述符的增加,而我们的程序大部分情况下会把文件描述符数量再恢复:
比如 打开了很多文件 fd 100->200
那么很快 程序就会清理fd的数量 把 fd 200->100
这样保证我们的程序的fd始终处于一个正常的范围内,这样程序就不会crash了
那么为什么我们的fd会不断增加而不被系统回收呢???
这里有一个博客Android中FD泄漏的几种类型介绍的很详细,大家可以看下,这里面最常见的其实就是文件流不关闭。为此我写了一个demo:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fd);
Log.i("TAG", "getFdMax: " + getFdMax());
for (int i = 0; i < 100; i++) {
readFile(i);
Log.i("TAG", "getFdCount: " + getFdCount());
}
}
private void readFile(int i){
String filename = "temp" + i;
File file = new File(this.getCacheDir(),filename);
try{
file.createNewFile();
for (int j = 0; j < 100; j++) {
FileOutputStream out = new FileOutputStream(file);
}
} catch (Exception e){
}
}
我们看下日志:
2020-09-12 23:41:16.495 15093-15093/com.study.csdn I/TAG: getFdMax: 1024
2020-09-12 23:41:16.501 15093-15093/com.study.csdn I/TAG: getFdCount: 150
2020-09-12 23:41:16.508 15093-15093/com.study.csdn I/TAG: getFdCount: 250
2020-09-12 23:41:16.516 15093-15093/com.study.csdn I/TAG: getFdCount: 350
2020-09-12 23:41:16.521 15093-15093/com.study.csdn I/TAG: getFdCount: 450
2020-09-12 23:41:16.526 15093-15093/com.study.csdn I/TAG: getFdCount: 550
2020-09-12 23:41:16.531 15093-15093/com.study.csdn I/TAG: getFdCount: 650
2020-09-12 23:41:16.536 15093-15093/com.study.csdn I/TAG: getFdCount: 750
2020-09-12 23:41:16.541 15093-15093/com.study.csdn I/TAG: getFdCount: 850
2020-09-12 23:41:16.546 15093-15093/com.study.csdn I/TAG: getFdCount: 844
2020-09-12 23:41:16.551 15093-15093/com.study.csdn I/TAG: getFdCount: 868
2020-09-12 23:41:16.555 15093-15093/com.study.csdn I/TAG: getFdCount: 886
2020-09-12 23:41:16.560 15093-15093/com.study.csdn I/TAG: getFdCount: 908
2020-09-12 23:41:16.565 15093-15093/com.study.csdn I/TAG: getFdCount: 948
2020-09-12 23:41:16.567 15093-15093/com.study.csdn I/TAG: getFdCount: 0
2020-09-12 23:41:16.567 15093-15093/com.study.csdn I/TAG: getFdCount: 0
2020-09-12 23:41:16.567 15093-15093/com.study.csdn I/TAG: getFdCount: 0
2020-09-12 23:41:16.578 15093-15093/com.study.csdn I/TAG: getFdCount: 0
我们看到进程的文件描述符一直在增加从150涨到了948,然后接下来突破了最大上限1024直接无法显示只能显示成0了,最后进程被杀死,Error信息如下:
//最主要的其实就是这句 Bad file descriptor 说明文件描述符已近是错了。
2020-09-12 23:43:32.573 15210-15210/? E/Looper: Error adding epoll events for fd -1: Bad file descriptor
2020-09-12 23:43:32.575 15210-15210/? E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.study.csdn, PID: 15210
java.lang.RuntimeException: Failed to initialize display event receiver. status=-2147483648
at android.view.DisplayEventReceiver.nativeInit(Native Method)
at android.view.DisplayEventReceiver.<init>(DisplayEventReceiver.java:92)
at android.view.Choreographer$FrameDisplayEventReceiver.<init>(Choreographer.java:963)
at android.view.Choreographer.<init>(Choreographer.java:246)
at android.view.Choreographer.<init>(Unknown Source:0)
at android.view.Choreographer$1.initialValue(Choreographer.java:113)
at android.view.Choreographer$1.initialValue(Choreographer.java:107)
at java.lang.ThreadLocal.setInitialValue(ThreadLocal.java:180)
at java.lang.ThreadLocal.get(ThreadLocal.java:170)
at android.view.Choreographer.getInstance(Choreographer.java:272)
at android.view.ViewRootImpl.<init>(ViewRootImpl.java:536)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:346)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94)
at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3716)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2901)
at android.app.ActivityThread.-wrap11(Unknown Source:0)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1616)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:176)
at android.app.ActivityThread.main(ActivityThread.java:6651)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:824)
如果你自己使用手机测试的时候会发现自己的程序getFdMax不等于1024也并不会crash,因为笔者经过测试发现:
Android<=8.1的版本 getFdMax=1024
Android>=9.0的版本 getFdMax=32768 (可能是觉得Linux的上限太苛刻了,开始升级了最大限制)
解决这种crash的问题一般比较棘手
- Crash不好复现,因为需要泄露到上限才可以,时机要很特别
- 只能知道fd泄露了但是不知道是什么代码导致的泄露
所以我的建议是:
1 找到经常出现crash的页面
2 注释掉该页面相关的代码看看fd会不会不断上升,如果注释掉的代码fd就不上升了就说明该代码是罪魁祸首,否则慢慢注释。