(Android 9.0)关于Cursor的内存泄露问题总结

前言

Android中数据的持久化经常会使用到SqliteDatabase或是ContentProvider,查询数据的时候可以通过SqliteDatabase.query或是ContentResolver.query方法来获取一个Cursor对象,这个Cursor对象里面就包含了我们所要查找的数据。在之前的认知里Cursor在使用完毕后必须要主动调用close来关闭释放资源。最近发现项目里有很多地方并没有做回收处理,编译器也没有任何提示。为了搞清楚这个问题,把以前写的Demo找了出来,并结合ContentProvider和SqliteDatabase相关源码来分析一下具体原因。

SqliteDatabase

SqliteDatabase中提供的query开头的查询方法最后都会通过queryWithFactory方法来查询,而queryWithFactory最终会调用rawQueryWithFactory来执行查询,我们直接来跳转到rawQueryWithFactory方法。

    public Cursor rawQueryWithFactory(
            CursorFactory cursorFactory, String sql, String[] selectionArgs,
            String editTable, CancellationSignal cancellationSignal) {
        acquireReference();
        try {
            SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(this, sql, editTable,
                    cancellationSignal);
            return driver.query(cursorFactory != null ? cursorFactory : mCursorFactory,
                    selectionArgs);
        } finally {
            releaseReference();
        }
    }

发现最后返回的地方会去调用SQLiteDirectCursorDriver的query方法,继续跳。

    public Cursor query(CursorFactory factory, String[] selectionArgs) {
        final SQLiteQuery query = new SQLiteQuery(mDatabase, mSql, mCancellationSignal);
        final Cursor cursor;
        try {
            // 把所有数据绑定到SQLiteQuery的对象query中
            query.bindAllArgsAsStrings(selectionArgs);
            // 封装成Cursor对象
            if (factory == null) {
                cursor = new SQLiteCursor(this, mEditTable, query);
            } else {
                cursor = factory.newCursor(mDatabase, this, mEditTable, query);
            }
        } catch (RuntimeException ex) {
            query.close();
            throw ex;
        }

        mQuery = query;
        return cursor;
    }

SQLiteDirectCursorDriver.query方法中会先把所有数据绑定到SQLiteQuery的对象query中,然后根据CursorFactory对象是否为空来决定如何创建Cursor对象,如果为空则直接new一个SQLiteCursor对象,如果非空则需要实现CursorFactory接口类的newCursor方法来返回一个Cursor对象。

    public interface CursorFactory {
        /**
         * See {@link SQLiteCursor#SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)}.
         */
        public Cursor newCursor(SQLiteDatabase db,
                SQLiteCursorDriver masterQuery, String editTable,
                SQLiteQuery query);
    }

最后,由SQLiteCursor类的构造方法把相关数据封装起来并创建Cursor对象。

    public SQLiteCursor(SQLiteCursorDriver driver, String editTable, SQLiteQuery query) {
        if (query == null) {
            throw new IllegalArgumentException("query object cannot be null");
        }
        if (StrictMode.vmSqliteObjectLeaksEnabled()) {
            mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace();
        } else {
            mStackTrace = null;
        }
        mDriver = driver;
        mEditTable = editTable;
        mColumnNameMap = null;
        mQuery = query;

        mColumns = query.getColumnNames();
    }

**Object类是类层次结构的根类,每个类都使用Object作为父类,Object类中定义了finalize方法,当Java触发GC就会回调finalize方法。**SQLiteCursor类中重写了finalize方法,在这个方法中会主动调用Cursor.close方法来释放资源。

    /**
     * Release the native resources, if they haven't been released yet.
     */
    @Override
    protected void finalize() {
        try {
            // if the cursor hasn't been closed yet, close it first
            if (mWindow != null) {
                if (mStackTrace != null) {
                    String sql = mQuery.getSql();
                    int len = sql.length();
                    StrictMode.onSqliteObjectLeaked(
                        "Finalizing a Cursor that has not been deactivated or closed. " +
                        "database = " + mQuery.getDatabase().getLabel() +
                        ", table = " + mEditTable +
                        ", query = " + sql.substring(0, (len > 1000) ? 1000 : len),
                        mStackTrace);
                }
                close();
            }
        } finally {
            super.finalize();
        }
    }

ContentProvider

ContentProvider一般是用于存储应用间可共享的数据,应用可以通过ContentResolver提供的方法来对数据进行增删改查操作,同SqliteDatabase仍然从ContentResolver的query方法入手。

    public final @Nullable Cursor query(final @RequiresPermission.Read @NonNull Uri uri,
            @Nullable String[] projection, @Nullable Bundle queryArgs,
            @Nullable CancellationSignal cancellationSignal) {
            ...
            // Wrap the cursor object into CursorWrapperInner object.
            final IContentProvider provider = (stableProvider != null) ? stableProvider
                    : acquireProvider(uri);
            final CursorWrapperInner wrapper = new CursorWrapperInner(qCursor, provider);
            stableProvider = null;
            qCursor = null;
            return wrapper;
            ...
    }

相较于SqliteDatabase的查找过程,使用ContentResolver查找相对简单,在ContentResolver.query方法中经过一些处理后会返回一个CursorWrapperInner类对象。从其构造方法可知,这个CursorWrapperInner对象包含了IContentProvider接口对象的信息。

        CursorWrapperInner(Cursor cursor, IContentProvider contentProvider) {
            super(cursor);
            mContentProvider = contentProvider;
            mCloseGuard.open("close");
        }

同样,在CursorWrapperInner类中重写了Object类的finalize方法,在这个方法中同样会调用close方法来释放资源。

        @Override
        public void close() {
            mCloseGuard.close();
            super.close();

            if (mProviderReleased.compareAndSet(false, true)) {
                ContentResolver.this.releaseProvider(mContentProvider);
            }
        }

        @Override
        protected void finalize() throws Throwable {
            try {
                if (mCloseGuard != null) {
                    mCloseGuard.warnIfOpen();
                }

                close();
            } finally {
                super.finalize();
            }
        }

总结

从对SqliteDatabase和ContentResolver相关源码的分析可以看出,两种方法最终返回的Cursor类对象都重写了Object类的finalize方法,即使不主动调用Cursor.close方法也不会造成内存泄露。虽然不会造成泄露,但是会消耗IO资源有可能导致native运行异常,所以最好还是手动调用Cursor.close方法及时回收内存。

如果不小心忘记关闭Cursor了怎么办?有什么方法来快速确定是否有泄漏?可以通过一下几种方法来快速定位问题。

  • 编写代码的时候注意编译器提示

如下图所示,如果我把cursor.close注销掉编译器就会提示我需要调用close()方法来释放资源。

  • 编写完代码跑一遍Lint

跑Lint的时候,生成的报告中会有相应的警报,可以直接定位问题代码。

  • 在项目中集成leakcanary

leakcanary github地址:https://github.com/square/leakcanary

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值