Android学习系列(32)--App调试内存泄露之Cursor篇

2024-05-12 07:38

本文主要是介绍Android学习系列(32)--App调试内存泄露之Cursor篇,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

    最近在工作中处理了一些内存泄露的问题,在这个过程中我尤其发现了一些基本的问题反而忽略导致内存泄露,比如静态变量,cursor关闭,流关闭,线程,定时器,反注册,bitmap等等,我稍微统计并总结了一下,当然了,这些问题这么说起来比较笼统,接下来我会根据问题,把一些实例代码贴出来,一步一步分析,在具体的场景下,用行之有效的方法,找出泄露的根本原因,并给出解决方案。

    现在,就从cursor关闭的问题开始把,谁都知道cursor要关闭,但是往往相反,人们却常常忘记关闭,因为真正的应用场景可能并非理想化的简单。
1. 理想化的cursor关闭

1
2
3
4
// Sample Code
Cursor cursor = db.query();
List<String> list = convertToList(cursor);
cursor.close();

    这是最简单的cursor使用场景,如果这里的cursor没有关闭,我想可能会引起万千口水,一片骂声。
    但是实际场景可能并非如此,这里的cursor可能不会关闭,至少有以下两种可能。

2. Cursor未关闭的可能
     (1). cursor.close()之前发生异常。
     (2). cursor需要继续使用,不能马上关闭,后面忘记关闭了。

3. Cursor.close()之前发生异常
     这个很容易理解,应该也是初学者最开始碰到的常见问题,举例如下:

1
2
3
4
5
6
7
8
9
try 
     Cursor c = queryCursor(); 
     int  a = c.getInt( 1 ); 
     ......
     // 如果出错,后面的cursor.close()将不会执行
     ......
     c.close(); 
catch  (Exception e) { 

  正确写法应该是:

1
2
3
4
5
6
7
8
9
10
11
12
13
Cursor c;
try 
     c = queryCursor(); 
     int  a = c.getInt(1); 
     ......
     // 如果出错,后面的cursor.close()将不会执行
     //c.close(); 
catch  (Exception e) { 
finally {
     if  (c !=  null ) {
         c.close();
     }
} 

    很简单,但是需要时刻谨记。

4. Cursor需要继续使用,不能马上关闭
    有没有这种情况?怎么办?
    答案是有,CursorAdapter就是一个典型的例子。
    CursorAdapter示例如下:

1
2
3
4
5
6
mCursor = getContentResolver().query(CONTENT_URI, PROJECTION,
null null null );
mAdapter =  new  MyCursorAdapter( this , R.layout.list_item, mCursor);
setListAdapter(mAdapter);
// 这里就不能关闭执行mCursor.close(),
// 否则list中将会无数据

5. 这样的Cursor应该什么时候关闭呢?
    这是个可以说好回答也可以说不好回答的问题,那就是在Cursor不再使用的时候关闭掉。
    比如说,
    上面的查询,如果每次进入或者resume的时候会重新查询执行。
    一般来说,也只是这种需求,很少需要看不到界面的时候还在不停地显示查询结果,如果真的有,不予讨论,记得最终关掉就OK了。
    这个时候,我们一般可以在onStop()方法里面把cursor关掉(同时意味着你可能需要在onResume()或者onStart()重新查询一下)。

1
2
3
4
5
6
@Override
protected  void  onStop() {
     super .onStop();
     // mCursorAdapter会释放之前的cursor,相当于关闭了cursor
     mCursorAdapter.changeCursor( null );
}

  我专门附上CursorAdapter的changeCursor()方法源码,让大家看的更清楚,免得不放心changeCursor(null)方法:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
  * Change the underlying cursor to a new cursor. If there is an existing cursor it will be
  * closed.
  *
  * @param cursor The new cursor to be used
  */
public  void  changeCursor(Cursor cursor) {
     Cursor old = swapCursor(cursor);
     if  (old !=  null ) {
         old.close();
     }
}
/**
  * Swap in a new Cursor, returning the old Cursor.  Unlike
  * {@link #changeCursor(Cursor)}, the returned old Cursor is <em>not</em>
  * closed.
  *
  * @param newCursor The new cursor to be used.
  * @return Returns the previously set Cursor, or null if there wasa not one.
  * If the given new Cursor is the same instance is the previously set
  * Cursor, null is also returned.
  */
public  Cursor swapCursor(Cursor newCursor) {
     if  (newCursor == mCursor) {
         return  null ;
     }
     Cursor oldCursor = mCursor;
     if  (oldCursor !=  null ) {
         if  (mChangeObserver !=  null ) oldCursor.unregisterContentObserver(mChangeObserver);
         if  (mDataSetObserver !=  null ) oldCursor.unregisterDataSetObserver(mDataSetObserver);
     }
     mCursor = newCursor;
     if  (newCursor !=  null ) {
         if  (mChangeObserver !=  null ) newCursor.registerContentObserver(mChangeObserver);
         if  (mDataSetObserver !=  null ) newCursor.registerDataSetObserver(mDataSetObserver);
         mRowIDColumn = newCursor.getColumnIndexOrThrow( "_id" );
         mDataValid =  true ;
         // notify the observers about the new cursor
         notifyDataSetChanged();
     else  {
         mRowIDColumn = - 1 ;
         mDataValid =  false ;
         // notify the observers about the lack of a data set
         notifyDataSetInvalidated();
     }
     return  oldCursor;
}

6. 实战AsyncQueryHandler中Cursor的关闭问题
    AsyncQueryHandler是一个很经典很典型的分析Cursor的例子,不仅一阵见血,能举一反三,而且非常常见,为以后避免。
    AsyncQueryHandler文档参考地址:
    http://developer.android.com/reference/android/content/AsyncQueryHandler.html
    下面这段代码是Android2.3系统中Mms信息主页面ConversationList源码的一部分,大家看看Cursor正确关闭了吗?

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
26
27
28
29
30
31
32
33
34
private  final  class  ThreadListQueryHandler  extends  AsyncQueryHandler {
     public  ThreadListQueryHandler(ContentResolver contentResolver) {
         super (contentResolver);
     }
     @Override
     protected  void  onQueryComplete( int  token, Object cookie, Cursor cursor) {
         switch  (token) {
         case  THREAD_LIST_QUERY_TOKEN:
             mListAdapter.changeCursor(cursor);
             setTitle(mTitle);
             ... ...
             break ;
         case  HAVE_LOCKED_MESSAGES_TOKEN:
             long  threadId = (Long)cookie;
             confirmDeleteThreadDialog( new  DeleteThreadListener(threadId, mQueryHandler,
                     ConversationList. this ), threadId == - 1 ,
                     cursor !=  null  && cursor.getCount() >  0 ,
                     ConversationList. this );
             break ;
         default :
             Log.e(TAG,  "onQueryComplete called with unknown token "  + token);
         }
     }
}
@Override
protected  void  onStop() {
     super .onStop();
     mListAdapter.changeCursor( null );
}

    大家觉得有问题吗?
    主要是两点:
    (1). THREAD_LIST_QUERY_TOKEN分支的Cursor正确关闭了吗?
    (2). HAVE_LOCKED_MESSAGES_TOKEN分支的Cursor正确关闭了吗?
    根据前面的一条条分析,答案是:
    (1). THREAD_LIST_QUERY_TOKEN分支的Cursor被传递到了mListAdapter了,而mListAdapter在onStop里面使用changeCursor(null),当用户离开当前Activity,这个Cursor被正确关闭了,不会泄露。
    (2). HAVE_LOCKED_MESSAGES_TOKEN分支的Cursor(就是参数cursor),只是作为一个判断的一个条件,被使用后不再使用,但是也没有关掉,所以cursor泄露,在StrictMode监视下只要跑到这个地方都会抛出这个错误:

1
2
3
4
E/StrictMode(639): A resource was acquired at attached stack trace but never released. See java.io.Closeable  for  information on avoiding resource leaks.
E/StrictMode(639): java.lang.Throwable: Explicit termination method  'close'  not called
E/StrictMode(639): at dalvik. system .CloseGuard. open (CloseGuard.java:184)
... ...

  在Android4.0 JellyBean中谷歌修正了这个泄露问题,相关代码如下:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
private  final  class  ThreadListQueryHandler  extends  ConversationQueryHandler {
     public  ThreadListQueryHandler(ContentResolver contentResolver) {
         super (contentResolver);
     }
     @Override
     protected  void  onQueryComplete( int  token, Object cookie, Cursor cursor) {
         switch  (token) {
         case  THREAD_LIST_QUERY_TOKEN:
             mListAdapter.changeCursor(cursor);
             ... ...
             break ;
         case  UNREAD_THREADS_QUERY_TOKEN:
             // 新增的UNREAD_THREADS_QUERY_TOKEN分子和HAVE_LOCKED_MESSAGES_TOKEN分支也是类似的情况,cursor在jellybean中被及时关闭了
             int  count =  0 ;
             if  (cursor !=  null ) {
                 count = cursor.getCount();
                 cursor.close();
             }
             mUnreadConvCount.setText(count >  0  ? Integer.toString(count) :  null );
             break ;
         case  HAVE_LOCKED_MESSAGES_TOKEN:
             @SuppressWarnings ( "unchecked" )
             Collection<Long> threadIds = (Collection<Long>)cookie;
             confirmDeleteThreadDialog( new  DeleteThreadListener(threadIds, mQueryHandler,
                     ConversationList. this ), threadIds,
                     cursor !=  null  && cursor.getCount() >  0 ,
                     ConversationList. this );
             // HAVE_LOCKED_MESSAGES_TOKEN分支中的cursor在jellybean中被及时关闭了
             if  (cursor !=  null ) {
                 cursor.close();
             }
             break ;
         default :
             Log.e(TAG,  "onQueryComplete called with unknown token "  + token);
         }
     }
}
@Override
protected  void  onStop() {
     super .onStop();
     mListAdapter.changeCursor( null );
}

  是不是小看了AsyncQueryHandler,谷歌在早期的版本里面都有一些这样的代码,更何况不注意的我们呢,实际上网上很多使用AsyncQueryHandler举例中都犯了这个错误,看完这篇文章后,以后再也不怕AsyncQueryHandler的cursor泄露了,还说不定能解决很多你现在应用的后台strictmode的cursor not close异常问题。

7. 小结
    虽然我觉得还有很多cursor未关闭的情况没有说到,但是根本问题都是及时正确的关闭cursor。
    内存泄露cursor篇是我工作经验上的一个总结,专门捋清楚后对我自己对大家觉得都很有帮助,让复杂的问题本质化,简单化!

这篇关于Android学习系列(32)--App调试内存泄露之Cursor篇的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/982005

相关文章

Android 悬浮窗开发示例((动态权限请求 | 前台服务和通知 | 悬浮窗创建 )

《Android悬浮窗开发示例((动态权限请求|前台服务和通知|悬浮窗创建)》本文介绍了Android悬浮窗的实现效果,包括动态权限请求、前台服务和通知的使用,悬浮窗权限需要动态申请并引导... 目录一、悬浮窗 动态权限请求1、动态请求权限2、悬浮窗权限说明3、检查动态权限4、申请动态权限5、权限设置完毕后

golang内存对齐的项目实践

《golang内存对齐的项目实践》本文主要介绍了golang内存对齐的项目实践,内存对齐不仅有助于提高内存访问效率,还确保了与硬件接口的兼容性,是Go语言编程中不可忽视的重要优化手段,下面就来介绍一下... 目录一、结构体中的字段顺序与内存对齐二、内存对齐的原理与规则三、调整结构体字段顺序优化内存对齐四、内

Android里面的Service种类以及启动方式

《Android里面的Service种类以及启动方式》Android中的Service分为前台服务和后台服务,前台服务需要亮身份牌并显示通知,后台服务则有启动方式选择,包括startService和b... 目录一句话总结:一、Service 的两种类型:1. 前台服务(必须亮身份牌)2. 后台服务(偷偷干

Java深度学习库DJL实现Python的NumPy方式

《Java深度学习库DJL实现Python的NumPy方式》本文介绍了DJL库的背景和基本功能,包括NDArray的创建、数学运算、数据获取和设置等,同时,还展示了如何使用NDArray进行数据预处理... 目录1 NDArray 的背景介绍1.1 架构2 JavaDJL使用2.1 安装DJL2.2 基本操

使用C/C++调用libcurl调试消息的方式

《使用C/C++调用libcurl调试消息的方式》在使用C/C++调用libcurl进行HTTP请求时,有时我们需要查看请求的/应答消息的内容(包括请求头和请求体)以方便调试,libcurl提供了多种... 目录1. libcurl 调试工具简介2. 输出请求消息使用 CURLOPT_VERBOSE使用 C

Android kotlin语言实现删除文件的解决方案

《Androidkotlin语言实现删除文件的解决方案》:本文主要介绍Androidkotlin语言实现删除文件的解决方案,在项目开发过程中,尤其是需要跨平台协作的项目,那么删除用户指定的文件的... 目录一、前言二、适用环境三、模板内容1.权限申请2.Activity中的模板一、前言在项目开发过程中,尤

Linux内存泄露的原因排查和解决方案(内存管理方法)

《Linux内存泄露的原因排查和解决方案(内存管理方法)》文章主要介绍了运维团队在Linux处理LB服务内存暴涨、内存报警问题的过程,从发现问题、排查原因到制定解决方案,并从中学习了Linux内存管理... 目录一、问题二、排查过程三、解决方案四、内存管理方法1)linux内存寻址2)Linux分页机制3)

Java循环创建对象内存溢出的解决方法

《Java循环创建对象内存溢出的解决方法》在Java中,如果在循环中不当地创建大量对象而不及时释放内存,很容易导致内存溢出(OutOfMemoryError),所以本文给大家介绍了Java循环创建对象... 目录问题1. 解决方案2. 示例代码2.1 原始版本(可能导致内存溢出)2.2 修改后的版本问题在

大数据小内存排序问题如何巧妙解决

《大数据小内存排序问题如何巧妙解决》文章介绍了大数据小内存排序的三种方法:数据库排序、分治法和位图法,数据库排序简单但速度慢,对设备要求高;分治法高效但实现复杂;位图法可读性差,但存储空间受限... 目录三种方法:方法概要数据库排序(http://www.chinasem.cn对数据库设备要求较高)分治法(常

Redis多种内存淘汰策略及配置技巧分享

《Redis多种内存淘汰策略及配置技巧分享》本文介绍了Redis内存满时的淘汰机制,包括内存淘汰机制的概念,Redis提供的8种淘汰策略(如noeviction、volatile-lru等)及其适用场... 目录前言一、什么是 Redis 的内存淘汰机制?二、Redis 内存淘汰策略1. pythonnoe