Android 内存泄漏分析总结

GitHub地址

https://github.com/realxz/MemoryLeak
GitHub 代码只包含泄漏情况,不包括修改后的代码,大家可以下载下来后,自行修改。

什么是内存

Android 系统我们的 APP 分配的内存大小是有限的,我现在用的手机小米 4c 为我自己开发的应用 分配的256MB的内存大小,不同的手机型号,不同的 ROM 分配的内存大小不一定一样,这里面所提 到的内存一般是指 Android 手机的 RAM。

RAM 包含寄存器,栈,堆,静态存储区域,常量池。通常我们所说的 Android 内存泄漏中的内存, 指的是其中的堆内存。一般来来说,我们 new 出来的对 象都会存储在堆内存中,这部分的内存由 GC 进行回收管理。

GC 是什么,它如何进行内存管理

GC 指垃圾回收器 「Garbage Collection」。Java 使用 GC 进行内存回收理,不用我们手动释放内 存,提升了我们的开发效率。那GC回收对象的依据是什么呢 ?简单的说,对于一个对象,若果不存 在从 GC 根节点到该对象的引用链 (从根节点不可到达的 (从根节点不可到达的),那么对于 GC 来说这个对象就是需要被回收的,反之该对象是从根节点可到达的,那么这个对象就不会被 GC 回 收。

根节点:在 Java 中可以作为根节点的对象有很多,这块内容我理解的不是很到位。我很简单的把它理解为 Android 应用的主线程,存活的子线程,栈中的对象以及静态属性引用的对象。
注意:这里的引用是指强引用,在 Java 当中存在4种引用类型分别是「强引用」、「软引用」、「弱引用」、「虚引用」。如果没有特别指定,我们所说的引用都是指强引用,GC 不会回收具有 强引用的对象。

什么是内存泄漏

我们已经知道了,如果某个对象,从根节点可到达,也就是存在从根节点到该对象的引用链,那么该对象是不会被 GC 回收的。如果说这个对象已经不会再被使用到了,是无用的,我们依然持有他的引用的话,就会造成内存泄漏,例如 一个长期在后台运行的线程持有 Activity 的引用,这个时 候 Activity 执行了 onDestroy 方法,那么这个 Activity 就是从根节点可到达并且无用的对象, 这个 Activity 对象就是泄漏的对象,给这个对象分配的内存将无法被回收。

内存泄漏的影响

  • 内存很宝贵,即使从效率,责任的角度上,我们也应该降低内存的使用,减少内存的浪费。
  • 内存泄漏导致可用内存越来越少,最终导致OOM。
  • 可用内存减少,GC 被触发,虽然 GC 可以帮助我们回收无用内存,但是频繁的触发 GC 也会影响性能,可能造成程序卡顿。

如何查找、定位内存泄漏

  • MAT「Memory AnalysisTools」,网上有很多的使用教程,个人感觉使用较繁琐,需要耐心分析和一定的经验才能定位出内存泄漏。
  • LeakCanary,Square 公司开源作品,使用方便,可以直接定位到泄漏的对象,并且给出调用链。

内存泄漏事例

非静态内部类

package com.example.xiezhen.memoryleak;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;

public class InnerThreadActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_inner_thread);

        RunningThread runningThread = new RunningThread();
        runningThread.start();
    }

    class RunningThread extends Thread {
        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(1000*5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public void closeActivity(View view) {
        this.finish();
    }

}

当我们运行这段代码的时候,LeakCanary 会帮我们检测出来内存泄漏,如图:

![image_1bbluquud1ndr12k41mdnpbmiv9.png-59.9kB][1]

在 Java 中,内部类会隐式的持有外部类的引用。我们可以很清楚的看见 RunningThread 对象持有 了 InnerThreadActivity 的引用,由于 RunningThread 线程会一直运行下去,我 finish 掉当前 的 Activity 就会导致 InnerThreadActivity 实例发生泄漏。我们可以采用静态内部类的方式来解 除这种内存泄漏的隐患,代码如下:

private static class RunningThread extends Thread {
    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000 * 5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

注意:尽量使用静态内部类来替代内部类,同时避免让长期运行的任务( 线程 )持有 Activity的引用。

匿名内部类

package com.example.xiezhen.memoryleak;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;

public class AnonymousThreadActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_anonymous_thread);
        Thread anonymousThread = new Thread() {
            @Override
            public void run() {
                while (true) {
                    //do something
                    try {
                        Thread.sleep(60 * 1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    return;
                }
            }
        };
        anonymousThread.start();
    }

    public void closeActivity(View view) {
        this.finish();
    }
}

LeakCanary 同样会检测出内存泄漏,如图:

![image_1bbluvkf51um53c04fucmr1g8cm.png-69.7kB][2]

在 Java 中,匿名内部类和非静态内部类一样,都会持有外部类的引用。上面的代码正式由于 Thread 的匿名类持有了 AnonymousThreadActivity 的引用,并且匿名类的运行时间长达 1 分钟, 在这段时间内,我 finish 掉了 Activity 导致了内存泄漏,解决方式和非静态内部类的方法一样,使用静态内部类来代替匿名内部类,这里就不贴代码了。

Handler 内存泄漏

package com.example.xiezhen.memoryleak;

import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

public class HandlerActivity extends AppCompatActivity {
    private TextView tvShowMessage;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler);
        tvShowMessage = (TextView) findViewById(R.id.tv_show_message);
        MemoryLeakHandler handler = new MemoryLeakHandler();
        handler.sendMessageDelayed(Message.obtain(), 1000 * 10);
    }

    class MemoryLeakHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            tvShowMessage.setText("MemoryLeak");
            Toast.makeText(HandlerActivity.this, "memory leak", Toast.LENGTH_SHORT).show();
        }
    }

    public void closeActivity(View view) {
        this.finish();
    }
}

![image_1bblv16hj148t1bvmjge13jd5if13.png-79.1kB][3]

LeakCanary 为我们展示了内存泄漏的引用链,这段代码泄漏的原因也是因为非静态内部类持有了外部类的引用。图中的引用链涉及到 Android 中的消息机制 「Handler」、「MessageQueue」、 「Looper」。大致叙述一下,我们的 MemoryLeakHandler 因为内部类的关系会持有 HandlerActivity 实例的引用,我们使用 Handler 来发送消息,这个Handler 会被消息中 target 属性引用,这个 Message 会在我们主线程的消息队 列中存活 10 秒钟,在这段时间内,我 finish 掉当前 Activity 就会造成内存泄漏,并且依然会弹出 Toast 尽管我们已经开不见这个 Activity了。

解决方案依然是采用静态内部类来替代非静态内部类,并且使用 WeakReference 来引用 Activity,如果对象只存在弱引用的话,GC 是会回收这部分内存的。

package com.example.xiezhen.memoryleak;

import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

import java.lang.ref.WeakReference;

public class HandlerActivity extends AppCompatActivity {
    private TextView tvShowMessage;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler);
        tvShowMessage = (TextView) findViewById(R.id.tv_show_message);
        MemoryLeakHandler handler = new MemoryLeakHandler(this);
        handler.sendMessageDelayed(Message.obtain(), 1000 * 10);
    }

    private static class MemoryLeakHandler extends Handler {
        private WeakReference<HandlerActivity> weakReference;

        public MemoryLeakHandler(HandlerActivity activity) {
            this.weakReference = new WeakReference<HandlerActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            HandlerActivity activity = weakReference.get();
            if (activity != null) {
                activity.tvShowMessage.setText("MemoryLeak");
                Toast.makeText(activity, "memory leak", Toast.LENGTH_SHORT).show();
            }
        }
    }


    public void closeActivity(View view) {
        this.finish();
    }
}

单例/静态引用

static volatile EventBus defaultInstance;
public static EventBus getDefault() {
    if (defaultInstance == null) {
        synchronized (EventBus.class) {
            if (defaultInstance == null) {
                defaultInstance = new EventBus();
            }
        }
    }
    return defaultInstance;
}
package com.example.xiezhen.memoryleak;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;

public class EventBusActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_event_bus);
        EventBus.getDefault().register(this);
        EventBusThread thread=new EventBusThread();
        thread.start();

    }

    public void closeActivity(View view) {
        this.finish();
    }

    private static class EventBusThread extends Thread {
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            EventBus.getDefault().post("eventbus");
        }
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void receiveMessage(String message) {
        Toast.makeText(this, message, Toast.LENGTH_LONG).show();
    }

}

EventBus 我相信大家都不陌生,我们一般在使用 EventBus 的时候,都会使用EventBus.getDefault( ) 方法来获取一个 EventBus 单例,这个单例是静态的,全局可访问的。上面的代码我们在获取 EventBus 单例后调用 register 方法,将 EventBusActivity 注册到 EventBus 中,这个时候 EventBus 就会持有 Activity 的引用,由于单例是静态的,生命周期和整个 App 生命周期 一致,如果我们不调用 unRegister 方法的话,EventBusActivity 实例就会泄漏。上述代码中,我特意没有去调用 unRegister 方法,我们来看看 LeakCanary 的结果:

![image_1bblv9o5chuh4m6i6m1l8t1mnc1g.png-88kB][4]

  • 不要让我们的对象被静态属性所引用,这很容易造成内存泄漏。
  • 一般来说我们在使用注册方法的时候,library 都会提供相对应的解除注册方法,不要忘了调用!

Activity Context & Application Context

package com.example.xiezhen.memoryleak;

import android.content.Context;
import android.content.pm.PackageInfo;

/**
* Created by xiezhen on 2017/3/16.
*/

public class CommonHelper {
    private Context context;
    private static CommonHelper commonHelper = null;

    private CommonHelper(Context context) {
        this.context = context;
    }

    public static CommonHelper getCommonHelper(Context context) {
        if (commonHelper == null) {
            commonHelper = new CommonHelper(context);
        }
        return commonHelper;
    }

    public int getVersionCode() {
        PackageInfo packInfo = getPackageInfo(context);
        if (packInfo != null) {
            return packInfo.versionCode;
        } else {
            return -1;
        }
    }

    public  String getVersionName() {
        PackageInfo packInfo = getPackageInfo(context);
        if (packInfo != null) {
            return packInfo.versionName;
        } else {
            return "";
        }
    }


    private PackageInfo getPackageInfo(Context context) {
        try {
            return context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
        } catch (Exception e) {
            return null;
        }
    }


}
package com.example.xiezhen.memoryleak;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

public class ContextActivity extends AppCompatActivity {

    private TextView tvVersionName;
    private TextView tvVersionCode;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_context);
        tvVersionName = (TextView) findViewById(R.id.tv_version_name);
        tvVersionCode = (TextView) findViewById(R.id.tv_version_code);
    }

    @Override
    protected void onResume() {
        super.onResume();
        setVersionCode(CommonHelper.getCommonHelper(this).getVersionCode());
        setVersionName(CommonHelper.getCommonHelper(this).getVersionName());
    }

    private void setVersionName(String versionName) {
        tvVersionName.setText(versionName);
    }

    private void setVersionCode(int versionCode) {
        tvVersionCode.setText(String.valueOf(versionCode));
    }

    public void closeActivity(View view) {
        this.finish();
    }

}

我写了一个工具类,来获取 App 的 Version Code 和 Version Name,这段代码同样会导致内存泄漏,下面是 LeakCanary 的泄露图。

![image_1bblvctrqieh39h9asvsm9p91t.png-60.1kB][5]

发现泄露了一个 Context 实例,在调用 CommonHelper 中的方法时候的时候,我们将 ContextActivity 作为一个 Context 对象传递了进去,Context 对象的引用被长期持有导致内存泄漏。处理这种泄漏的方法很简单,使用 Application Context 来代替 Activity Context 即可,Application Context 在整个 App 生命周期内适用。

结论

一般来说,内存泄漏都是因为泄漏对象的引用被传递到该对象的范围之外,或者说内存泄漏是因为持有对象的长期引用,导致对象无法被 GC 回收。为了避免这种情况,我们可以选择在对象生命周期结束的时候,解除绑定,将引用置为空,或者使用弱引用。

  1. 由于 Context 导致内存泄漏。使用 Application Context 代替 Activity Context,避免长期持有 Context 的引用,引用应该和 Context 自身的生命周期保持一致。
  2. 由于非静态内部类、匿名内部类导致内存泄。它们会隐式的持有外部类的引用,一不小心长期持有该引用就会导致内存泄漏,使用静态内部类来代替它们。
  3. Handler 导致内存泄漏。原因和第二点一样,同样使用静态内部类的实现方式,同时对需要引用的对象/资源采用弱引用的方式。
  4. EventBus导致内存泄漏。EventBus 的单例特性,会长期持有注册对象的引用,一定要在对象生命周期结束的时候,接触注册,释放引用。同样对于系统提供的一些成对出现的方法,我们也需要成对的调用,例如 BroadcastReceiver 的 registerReceiver( ) 方法和 unRegisterReceiver( ) 方法。
  5. 线程导致内存泄漏。我们经常会执行一些长期运行的任务,避免在这些任务中持有 Activity 对象的引用,如果持有了引用的话,我们应该在对象生命周期结束的时候,释放引用。

参考链接

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

推荐阅读更多精彩内容

  • Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏...
    _痞子阅读 1,625评论 0 8
  • 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,...
    宇宙只有巴掌大阅读 2,360评论 0 12
  • Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏...
    apkcore阅读 1,217评论 2 7
  • 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,...
    DreamFish阅读 790评论 0 5
  • 每日打卡。 来评论区记录下今天的收获和成长吧!
    树洞君阅读 725评论 20 1