接下来,让我们进入native方法查看对应实现。
JNIEXPORT void JNICALL FileOutputStream_open0(JNIEnv *env, jobject this, jstring path, jboolean append) { fileOpen(env, this, path, fos_fd, O_WRONLY | O_CREAT | (append ? O_APPEND : O_TRUNC)); }
在fileOpen方法中,通过handleOpen生成native层的文件描述符(fd),这个fd就是这个所谓对面的文件描述符。
void fileOpen(JNIEnv *env, jobject this, jstring path, jfieldID fid, int flags) { WITH_PLATFORM_STRING(env, path, ps) { FD fd; //...... fd = handleOpen(ps, flags, 0666); if (fd != -1) { SET_FD(this, fd, fid); } else { throwFileNotFoundException(env, path); } } END_PLATFORM_STRING(env, ps); } FD handleOpen(const char *path, int oflag, int mode) { FD fd; RESTARTABLE(open64(path, oflag, mode), fd);//调用open,获取fd if (fd != -1) { //...... if (result != -1) { //...... } else { close(fd); fd = -1; } } return fd; }
到这里就结束了吗?
回到开始,FileOutputStream构造方法中初始化了Java层的文件描述符类 FileDescriptor,目前这个对象中的文件描述符的值还是初始的-1,所以目前它还是一个无效的文件描述符,native层完成fd创建后,还需要把fd的值传到 Java层。
我们再来看SET_FD这个宏的定义,在这个宏定义中,通过反射的方式给Java层对象的成员变量赋值。由于上文内容可知,open0是对象的jni方法,所以宏中的this,就是初始创建的FileOutputStream在Java层的对象实例。
#define SET_FD(this, fd, fid) \ if ((*env)->GetObjectField(env, (this), (fid)) != NULL) \ (*env)->SetIntField(env, (*env)->GetObjectField(env, (this), (fid)),IO_fd_fdID, (fd))
而fid则会在native代码中提前初始化好。
static void FileOutputStream_initIDs(JNIEnv *env) { jclass clazz = (*env)->FindClass(env, "java/io/FileOutputStream"); fos_fd = (*env)->GetFieldID(env, clazz, "fd", "Ljava/io/FileDescriptor;"); }
收,到这里FileOutputStream的初始化跟进就完成了,我们已经找到了底层fd初始化的路径。Android的IO操作还有其他的流操作类,大致流程基本类似,这里不再细述。
并不是不关闭就一定会导致文件描述符泄漏,在流对象的析构方法中会调用close方法,所以这个对象被回收时,理论上也是会释放文件描述符。但是最好还是通过代码控制释放逻辑。
3.3 SQLite泄漏
在日常开发中如果使用数据库SQLite管理本地数据,在数据库查询的cursor使用完成后,亦需要调用close方法释放资源,否则也有可能导致内存和文件描述符的泄漏。
public void get() { db = ordersDBHelper.getReadableDatabase(); Cursor cursor = db.query(...); while (cursor.moveToNext()) { //...... } if(flag){ //某种原因导致retrn return; } //不调用close,fd就会泄漏 cursor.close(); }
按照理解query操作应该会导致文件描述符泄漏,那我们就从query方法的实现开始分析。
然而,在query方法中并没有发现文件描述符相关的代码。
经过测试发现,moveToNext 调用后才会导致文件描述符增长。通过query方法可以获取cursor的实现类SQLiteCursor。
public Cursor query(CursorFactory factory, String[] selectionArgs) { final SQLiteQuery query = new SQLiteQuery(mDatabase, mSql, mCancellationSignal); final Cursor cursor; //...... if (factory == null) { cursor = new SQLiteCursor(this, mEditTable, query); } else { cursor = factory.newCursor(mDatabase, this, mEditTable, query); } //...... }
在SQLiteCursor的父类找到moveToNext的实现。getCount 是抽象方法,在子类SQLiteCursor实现。
@Override public final boolean moveToNext() { return moveToPosition(mPos + 1); } public final boolean moveToPosition(int position) { // Make sure position isn't past the end of the cursor final int count = getCount(); if (position >= count) { mPos = count; return false; } //...... }
getCount 方法中对成员变量mCount做判断,如果还是初始值,则会调用fillWindow方法。
@Override public int getCount() { if (mCount == NO_COUNT) { fillWindow(0); } return mCount; } private void fillWindow(int requiredPos) { clearOrCreateWindow(getDatabase().getPath()); //...... }
clearOrCreateWindow 实现又回到父类 AbstractWindowedCursor 中。
protected void clearOrCreateWindow(String name) { if (mWindow == null) { mWindow = new CursorWindow(name); } else { mWindow.clear(); } }
在CursorWindow的构造方法中,通过nativeCreate方法调用到native层的初始化。
public CursorWindow(String name, @BytesLong long windowSizeBytes) { //...... mWindowPtr = nativeCreate(mName, (int) windowSizeBytes); //...... }
在C++代码中会继续调用一个native层CursorWindow的create方法。
static jlong nativeCreate(JNIEnv* env, jclass clazz, jstring nameObj, jint cursorWindowSize) { //...... CursorWindow* window; status_t status = CursorWindow::create(name, cursorWindowSize, &window); //...... return reinterpret_cast<jlong>(window); }
在CursorWindow的create方法中,我们可以发现fd创建相关的代码。
status_t CursorWindow::create(const String8& name, size_t size, CursorWindow** outCursorWindow) { String8 ashmemName("CursorWindow: "); ashmemName.append(name); status_t result; int ashmemFd = ashmem_create_region(ashmemName.string(), size); //...... }
ashmem_create_region 方法最终会调用到open函数打开文件并返回系统创建的文件描述符。这部分代码不在赘述,有兴趣的可以自行查看 。
native完成初始化会把fd信息保存在CursorWindow中并会返回一个指针地址到Java层,Java层可以通过这个指针操作c++层对象从而也能获取对应的文件描述符。
3.4 InputChannel 导致的泄漏
WindowManager.addView
通过WindowManager反复添加view也会导致文件描述符增长,可以通过调用removeView释放之前创建的FD。
private void addView() { View windowView = LayoutInflater.from(getApplication()).inflate(R.layout.layout_window, null); //重复调用 mWindowManager.addView(windowView, wmParams); }
WindowManagerImpl中的addView最终会走到ViewRootImpl的setView。
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { //...... root = new ViewRootImpl(view.getContext(), display); //...... root.setView(view, wparams, panelParentView); }
setView中会创建InputChannel,并通过Binder机制传到服务端。
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { //...... //创建inputchannel if ((mWindowAttributes.inputFeatures & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) { mInputChannel = new InputChannel(); } //远程服务接口 res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mWinFrame, mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);//mInputChannel 作为参数传过去 //...... if (mInputChannel != null) { if (mInputQueueCallback != null) { mInputQueue = new InputQueue(); mInputQueueCallback.onInputQueueCreated(mInputQueue); } //创建 WindowInputEventReceiver 对象 mInputEventReceiver = new WindowInputEventReceiver(mInputChannel, Looper.myLooper()); } }
addToDisplay是一个AIDL方法,它的实现类是源码中的Session。最终调用的是 WindowManagerService 的 addWIndow 方法。
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, Rect outFrame, Rect outContentInsets, Rect outStableInsets, DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel, InsetsState outInsetsState, InsetsSourceControl[] outActiveControls) { return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outFrame, outContentInsets, outStableInsets, outDisplayCutout, outInputChannel, outInsetsState, outActiveControls, UserHandle.getUserId(mUid)); }
WMS在 addWindow 方法中创建 InputChannel 用于通讯。
public int addWindow(Session session, IWindow client, int seq, LayoutParams attrs, int viewVisibility, int displayId, Rect outFrame, Rect outContentInsets, Rect outStableInsets, Rect outOutsets, DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel) { //...... final boolean openInputChannels = (outInputChannel != null && (attrs.inputFeatures & INPUT_FEATURE_NO_INPUT_CHANNEL) == 0); if (openInputChannels) { win.openInputChannel(outInputChannel); } //...... }
在 openInputChannel 中创建 InputChannel ,并把客户端的传回去。
void openInputChannel(InputChannel outInputChannel) { //...... InputChannel[] inputChannels = InputChannel.openInputChannelPair(name); mInputChannel = inputChannels[0]; mClientChannel = inputChannels[1]; //...... }
InputChannel 的 openInputChannelPair 会调用native的 nativeOpenInputChannelPair ,在native中创建两个带有文件描述符的 socket 。
int socketpair(int domain, int type, int protocol, int sv[2]) { //创建一对匿名的已经连接的套接字 int rc = __socketpair(domain, type, protocol, sv); if (rc == 0) { //跟踪文件描述符 FDTRACK_CREATE(sv[0]); FDTRACK_CREATE(sv[1]); } return rc; }
WindowManager 的分析涉及WMS,WMS内容比较多,本文重点关注文件描述符相关的内容。简单的理解,就是进程间通讯会创建socket,所以也会创建文件描述符,而且会在服务端进程和客户端进程各创建一个。另外,如果系统进程文件描述符过多,理论上会造成系统崩溃。
四、如何排查
如果你的应用收到如下这些崩溃堆栈,恭喜你,你的应用存在文件描述符泄漏。
- abort message 'could not create instance too many files'
- could not read input file descriptors from parcel
- socket failed:EMFILE (Too many open files)
- ...
文件描述符导致的崩溃往往无法通过堆栈直接分析。道理很简单: 出问题的代码在消耗文件描述符同时,正常的代码逻辑可能也同样在创建文件描述符,所以崩溃可能是被正常代码触发了。
4.1 打印当前FD信息
遇到这类问题可以先尝试本体复现,通过命令 ‘ls -la /proc/$pid/fd' 查看当前进程文件描述符的消耗情况。一般android应用的文件描述符可以分为几类,通过对比哪一类文件描述符数量过高,来缩小问题范围。
4.2 dump系统信息
通过dumpsys window ,查看是否有异常window。用于解决 InputChannel 相关的泄漏问题。
4.3 线上监控
如果是本地无法复现问题,可以尝试添加线上监控代码,定时轮询当前进程使用的FD数量,在达到阈值时,读取当前FD的信息,并传到后台分析,获取FD对应文件信息的代码如下。
if (Build.VERSION.SDK_INT >= VersionCodes.L) { linkTarget = Os.readlink(file.getAbsolutePath()); } else { //通过 readlink 读取文件描述符信息 }
4.4 排查循环打印的日志
除了直接对 FD相关的信息进行分析,还需要关注logcat中是否有频繁打印的信息,例如:socket创建失败。
以上就是详解Android 文件描述符的详细内容,更多关于Android文件描述符的资料请关注其它相关文章!
标签:SQLite
相关阅读 >>
Sqlite 入门教程四 增删改查 有讲究
andriodstudio利用listview和数据库实现简单学生管理
android 中自定义contentprovider与contentobserver的使用简单实例
更多相关阅读请进入《Sqlite》频道 >>

数据库系统概念 第6版
本书主要讲述了数据模型、基于对象的数据库和XML、数据存储和查询、事务管理、体系结构等方面的内容。