Android内存管理及内存泄漏分析(二)

4、常见内存泄漏

这是一个老生常谈的一个问题了,但我还是先对Java中的内存泄漏做一个定义:

Java中的内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。

在C++中,内存泄漏的范围更大一些。有些对象被分配了内存空间,然后却不可达,由于C++中没有GC,这些内存将永远收不回来。在Java中,这些不可达的对象都由GC负责回收,因此程序员不需要考虑这部分的内存泄露。

对于Java中的内存泄漏,我总结为三点:static、线程 和 系统(外部)资源申请。

对于JVM内部而言,它的垃圾回收机制真的做的非常不错,我总觉得Java的出现是程序员的一次体力解放,大家再也不用去关心那个让RD们睡不好觉的指针问题;而JVM内部的内存泄漏,追究起原因来,我觉得(瞎估的)90%是static静态变量引起的、10%是while(true)的线程造成的,大家想一下,我们平时发现的内存泄漏,是不是大都是注册了监听没释放,或者是声明了一个大对象的static为了方便传递数据,结果忘了置空;对于static这样的变量,我们声明时要非常小心,我的意见是,不是非常必要的情况下,能不用尽量不要用。

外部资源申请一般指的是打开一些文件、设备、数据库等,这些系统都会给我们提供一些开销,如果我们没能及时关闭掉这些设备,则会造成不必要的内存开销。

Android上的内存泄漏会更具体一些,有些也会很隐蔽,我们来具体分析一下,这也是每个Android程序员面试都会考到的一个题。

(1)activity泄漏

这是我们平时最关心的泄漏,因为Activity在Android的四大组件中持的有资源最多,一个Activity没回收,会导致它里面的无数个View都无法被回收到。

<1>在需要Context的地方传入Activity,导致被静态变量持有,这种情况大家应该碰到的很多。

public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
        this.context = context;
    }
    public static synchronized AppManager getInstance(Context context) {
        if (instance == null) {
            instance = new AppManager(context);
        }
        return instance;
    }

....

// 使用时
AppManager am = AppManager.getInstance(activity);

上面这种情况,activity就被静态变量instance持有了,除非appmanager这个单例释放了。

这种情况有两个解决办法:

1、AppManager am = AppManager.getInstance(activity.getApplicationContext()); // 不建议

2、private AppManager(Context context) {
    this.context = context.getApplicationContext();
}

建议使用第二种方法,因为这是从根上进行的解决,而第一种方法是需要调用者来保证解决,这种方法很不靠谱,而且从严格意义上说,调用都使用的根本没错,他传进去的Activity就是一个Context。

<2>静态变量Activity

这是Activity使用的大忌。

class MyActivity extends Activity {
    public static Activity mActivity;
    protected void onCreate(...) {
        ....
        mActivity = this;
    }
}

这种写法一般都是程序员偷懒,想通过static的属性快速获取一个Activity的实例,但这种情况下一般使用的人不知道什么时候去把mActivity置空,很容易泄漏。

<3>Static view或static Drawable

class MyActivity extends Activity {
    public static View mView;
    public static Drawable mDrawable;
}

这个跟<2>有点相似,都是程序员偷懒干出来的事情,只要这个view是这个activity中创出来的或者用activit inflate进来的,那View中的context就是Activity,所以view在,Activity就不会被回收;Drawable这个实际上是Android 2.3及以前的一个Bug,我们看代码:

//View.setBackground方法:
publicvoidsetBackgroundDrawable(Drawable background) {
    ....
    background.setCallback(this);
    .......
}

// Drawable中的setCallback方法
public final void setCallback(Callback cb) {
    mCallback = cb;

<4>非static的内部类

直接看代码

非静态内部类

非静态内部类,只要不在赋值给静态变量,在类的内部使用起来还是非常方便的,因为它可以直接调用外部类的变量和方法。但如果我们在类的外部去new 一个这样的对象,我们应该怎么写呢?

MainActivity.TestResource inner = (new MainActivity()).new TestResource();

从这里可以看出,一个非静态的内部类对象是必须依附一个外部类对象存在的,这个时候如果内部类对象被静态变量持有,或者被传出去注册在哪里,就会导致外部类,比如这里的Activity无法回收。

<5>匿名内部类,我们最容易忽略的泄漏

Thread造成的匿名内部类泄漏

再看一种:

AppManager.getInstance(mContext).registerStateChangedListener(new     AppStateChangedListener() {
    ....
});

上面一种是线程造成的泄漏,线程不停,资源不释放;后一个是单例造成的;这两种匿名内部类没有变量持有,基本是必泄漏的。

<6>Handler泄漏

还是先看代码

内部匿名Handler造成的泄漏

我们去看一下handler的源码找一下原因:

Message的target

每个Message在放入looper里面时,都会为这个message指定一个target,而这个target就是Handler,如果这个handler是一个内部类,就会造成对应的外部类泄漏。

我们看一下这个问题在网上的解决办法:

弱引用法解决
官方提供的方法解决(用的不对,不要参考)

思考两个问题:弱引用是解决内存泄漏的首先方法吗?这里使用的官方的方法会出什么问题?

(2)注册和反注册

平时往单例中注册一些监听,正常都要在适当的时候进行反注册,除非这个监听是要伴随着整个进程的生命周期,这个比较容易理解,也是静态持有导致。

另一种注册和反注册的情况是receiver,Receiver正常是注册到系统中了,那到底被谁持有了呢

被loadedApk持有receiver

关于loadedApk这个类是在ActivityThread中初始化的,具体它的作用,可以在网上查找一下。

对于注册到localBroadcastManager中的Receiver,就更简单了,因为这个receiver在app内部使用,所以它就是一个类似往单例中注册listener的形式,必须反注册的。

(3)资源对象没关闭造成的内存泄露

这种情况的内存泄漏,就是我们开始说的向系统申请资源后没释放的情况,常见的是流和数据库未关闭,对于这种情况的细节就不做代码分析了,我的理解是:

linux对于每个设备等都是以文件来对待的,所以不管是文件还是设备,在打开时,系统都会为它创建一定的buffer,这个buffer是要占用内存空间的,如果没有关闭对应的流,这个buffer空间是一直被占用的。

(4)Bitmap的recycle不调会导致泄漏吗?

这一项是打了问号的,即到底Bitmap会不会造成内存泄漏呢?我们来一点点分析:

对于Bitmap的recycle()方法需不需要调用,网上的说法一般是这样的:

bitmap 2.3以前调,以后不调

那,如果在2.3及以下,不显式的调用recycle(),是不是就内存泄漏了呢?

我觉得不会,因为官方对recycle()的解释里面,从没说必须要调,只是推荐。

官方2.3及以下图片内存管理的说明
2.3源码中对recycle()的说明

从这两份文档来看,官方的意思应该是,这是一个高级调用,平时是不需要显式调用的,gc回回收这部分内存的,但2.3及以下,如果你确认一个bitmap的确不用了,还是调一下recycle比较好。

有点把人搞糊涂了,一般大家都认为2.3以下bitmap内存是native的堆中,gc收集不到,所以会引发一些OOM,但文档里又说GC会收集这些内存,让我们不用担心,到底是怎么一会事儿呢?

我们还是看源码吧,看源码能解决我们所有的疑惑,每次去看源码时,总能想来来Linus的那句话,好像是"Talk is cheap. Show me the code.",还有"Read the Fucking Source Code"。

Bitmap的源码
bitmap构造函数中初始化这样一个对象
nativeDestructor方法中释放内存

源码地址:
https://android.googlesource.com/platform/frameworks/base.git/+/7f9f99ea11051614a7727dfb9f9578b518e76e3c/graphics/java/android/graphics/Bitmap.java
https://android.googlesource.com/platform/frameworks/base/+/android-2.2.1_r2/core/jni/android/graphics/Bitmap.cpp
https://chromium.googlesource.com/chromium/src/+/ae2c20f398933a9e86c387dcc465ec0f71065ffc/skia/sgl/SkBitmap.cpp

到了这里,大家可以再回过头思考一个问题,为什么2.3及以下的bitmap内存也不会泄露,可大家还总是会说2.3的图片分配在native,容易造成OOM呢?

5、MAT分析内存泄漏

对于内存问题的分析,AndroidStudio也提供了dump工具,但功能与mat比起来还是要弱很多,所以我平时还是习惯使用MAT来进行分析。

打开 DDMS 工具,在左边 Devices 视图页面选中“Update Heap”图标,然后在右边切换到 Heap 视图,点击 Heap 视图中的“Cause GC”按钮,到此为止需检测的进程就可以被监视。

ddms

Heap视图中部有一个Type叫做data object,即数据对象,也就是我们的程序中大量存在的类类型的对象。在data object一行中有一列是“Total Size”,其值就是当前进程中所有Java数据对象的内存总量,一般情况下,这个值的大小决定了是否会有内存泄漏。可以这样判断:

进入某应用,不断的操作该应用,同时注意观察data object的Total Size值,正常情况下Total Size值都会稳定在一个有限的范围内,也就是说由于程序中的的代码良好,没有造成对象不被垃圾回收的情况。

所以说虽然我们不断的操作会不断的生成很多对象,而在虚拟机不断的进行GC的过程中,这些对象都被回收了,内存占用量会会落到一个稳定的水平;反之如果代码中存在没有释放对象引用的情况,则data object的Total Size值在每次GC后不会有明显的回落。随着操作次数的增多Total Size的值会越来越大,直到到达一个上限后导致进程被杀掉。

MAT分析hprof来定位内存泄露的原因所在

这是出现内存泄露后使用MAT进行问题定位的有效手段。

A)Dump出内存泄露当时的内存镜像hprof,分析怀疑泄露的类:

dump内存

注意:这里dump出来的hprof文件,要想直接查看,是需要在eclipse中安装mat插件的;这也带来一个问题,要想方便查看,是要打开eclipse的,但eclipse与androidstudio是不兼容的,打开了一个,另一个的adb就连不上,这块的确比较麻烦。

B)使用OQL,查询内存中的对象:

使用OQL

我们在查询内存泄漏时,一般优先是看Activity,它持有的内存是四大组件中最多的,也是我们平时最容易出现的内存泄漏,为了快速查找出这类的对象,我们可以使用OQL来写。

C)分析这些持有引用的对象的GC路径

查询引用关系

D)逐个分析每个对象的GC路径是否正常

分析引用持有路径

从这个路径可以看出是一个antiRadiationUtil工具类对象持有了MainActivity的引用导致MainActivity无法释放。此时就要进入代码分析此时antiRadiationUtil的引用持有是否合理(如果antiRadiationUtil持有了MainActivity的context导致节目退出后MainActivity无法销毁,那一般都属于内存泄露了)。

E)其它的使用:分析持有此类对象引用的外部对象

查询外部引用
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏...
    _痞子阅读 1,611评论 0 8
  • 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,...
    宇宙只有巴掌大阅读 2,357评论 0 12
  • Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏...
    apkcore阅读 1,214评论 2 7
  • 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,...
    DreamFish阅读 789评论 0 5
  • 主目录见:Android高级进阶知识(这是总目录索引)[written by 无心追求] Activity内部类...
    ZJ_Rocky阅读 1,978评论 0 2