Android 笔记 :内存泄漏、内存溢出、内存优化

一、释义

内存泄漏:如果一个无用对象(不需要再使用的对象)仍然被其他对象持有引用,造成该对象无法被系统回收,以致该对象在堆中所占用的内存单元无法被释放而造成内存空间浪费,这中情况就是内存泄露。
或者说长生命周期对象持有短生命周期对象的引用,造成短生命周期对象无法回收而造成内存空间浪费。
内存溢出:系统会给每个App分配内存空间也就是heap size值,当app占用的内存加上申请的内存超过这个系统分配的内存限额,最终导致OOM(OutOfMemory)使程序崩溃。

二、内存溢出

常见原因:
1.大量的内存泄漏
2.加载对象过大、如bitmap;
3.相应资源过多,来不及加载。
4.启动参数内存值设定的过小
举例:

1.堆溢出

Java堆中存放:对象、数组。下面以不断创建对象为例:

  ArrayList list = newArrayList();
    while(true){
        list.add(new HeapLeak.method());
    }

2.栈溢出

栈中存储:基本数据类型,对象引用,方法等。下面以无限递归创建方法和申请栈空间为例,

void method(){
    method();
}

3.常量区溢出

常量区代表运行时每个class文件中的常量表。它包括几种常量:编译期的数字常量、方法或者域的引用(在运行时解析)。

下面以不断添加Stirng为例:

    int count = 0;
    ArrayList list = newArrayList();
    while (true)
        list.add(String.valueOf(count++).intern());

说明:intern()就是查看方法区中有没有这个字符串,没有的话就加进去,如果这里不用intern(),字符串是存在堆里的,会报heapOutOfMemory.

4.方法区溢出

从Java官方API中我们知道,方法区存放每个Class的结构,比如说运行时常量池、域、方法数据、方法体、构造函数、包括类中的专用方法、实例初始化、接口初始化。

Java的反射和动态代理可以动态产生Class。

三、内存泄漏

影响:

应用可用的内存减少,增加了堆内存的压力
降低了应用的性能,比如会触发更频繁的 GC
严重的时候可能会导致内存溢出错误,即 OOM Error

常见举例及原因

1.单例导致内存泄露

因为单例的静态特性使得它的生命周期同应用的生命周期一样长,如果一个对象已经没有用处了,但是单例还持有它的引用,那么在整个应用程序的生命周期它都不能正常被回收,从而导致内存泄露。
如果我们在调用getInstance(Context context)方法的时候传入的context参数是Activity、Service等上下文,就会导致内存泄露。

2.静态变量导致内存泄露

静态变量存储在方法区,它的生命周期从类加载开始,到整个进程结束。一旦静态变量初始化后,它所持有的引用只有等到进程结束才会释放。

比如下面这样的情况,在Activity中为了避免重复的创建info,将sInfo作为静态变量:

public class MainActivity extends AppCompatActivity {

    private static Info sInfo;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (sInfo != null) {
            sInfo = new Info(this);
        }
    }
}

class Info {
    public Info(Activity activity) {
    }
}

Info作为Activity的静态成员,并且持有Activity的引用,但是sInfo作为静态变量,生命周期肯定比Activity长。所以当Activity退出后,sInfo仍然引用了Activity,Activity不能被回收,这就导致了内存泄露。

3.非静态内部类导致内存泄露(Handler,Thread,AsyncTask)

非静态内部类(包括匿名内部类)默认就会持有外部类的引用,当非静态内部类对象的生命周期比外部类对象的生命周期长时,就会导致内存泄露。
3.1handler
非静态内部类导致的内存泄露在Android开发中有一种典型的场景就是使用Handler,实例代码:

public class MainActivity extends AppCompatActivity {

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

   private void start() {
       Message msg = Message.obtain();
       msg.what = 1;
       mHandler.sendMessage(msg);
   }

   private Handler mHandler = new Handler() {
       @Override
       public void handleMessage(Message msg) {
           if (msg.what == 1) {
               //do something
           }
       }
   };

}
泄漏原因:mHandler会作为成员变量保存在发送的消息msg中,即msg持有mHandler的引用,而mHandler是Activity的非静态内部类实例,即mHandler持有Activity的引用,那么我们就可以理解为msg间接持有Activity的引用。当Activity退出后,msg可能仍然存在于消息对列MessageQueue中未处理或者正在处理,那么这样就会导致Activity无法被回收,以致发生Activity的内存泄露。
3.2线程造成的内存泄漏

new Thread(new Runnable() {
        @Override
        public void run() {
            SystemClock.sleep(10000);
        }
    }).start();

对于线程造成的内存泄漏:Runnable都是一个匿名内部类,因此它们对当前Activity都有一个隐式引用。如果Activity在销毁之前,任务还未完成, 那么将导致Activity的内存资源无法回收,造成内存泄漏。

4.未取消注册或回调导致内存泄露

见于广播,注册观察则模式的时候,如果不及时取消也会造成内存泄露。

5.未取消Timer、TimerTask导致内存泄露

当我们Activity销毁的时,有可能Timer还在继续等待执行TimerTask,它持有Activity的引用不能被回收,因此当我们Activity销毁的时候要立即 cancelTimer

6.资源未关闭或释放

见于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap、数据库操作,使用后未关闭,造成对象中一直被占用而得不到释放。

7.webview

在加载网页后会长期占用内存而不能被释放,webview的callback 也持有activity的引用,所以造成无法释放内存。

8.属性动画造成内存泄露

动画同样是一个耗时任务,比如在Activity中启动了属性动画(ObjectAnimator),但是在销毁的时候,没有调用cancle方法,虽然我们看不到动画了,但是这个动画依然会不断地播放下去,动画引用所在的控件,所在的控件引用Activity,这就造成Activity无法正常释放。

9.集合中的对象未清理造成内存泄露

如果一个对象放入到ArrayList、HashMap等集合中,这个集合就会持有该对象的引用。当我们不再需要这个对象时,也并没有将它从集合中移除,这样只要集合还在使用(而此对象已经无用了),这个对象就造成了内存泄露。并且如果集合被静态引用的话,集合里面那些没有用的对象更会造成内存泄露了。

四、内存泄漏解决办法

1.单例模式内存泄漏

对于生命周期比Activity长的对象如果需要应该使用ApplicationContext,在需要使用Context参数的时候先考虑Application.Context.

2.静态变量

静态持有很多时候都有可能因为其使用的生命周期不一致而导致内存泄露,所以我们在新建静态持有的变量的时候需要多考虑一下各个成员之间的引用关系,并且尽量少地使用静态持有的变量,以避免发生内存泄露。当然,我们也可以在适当的时候讲静态量重置为null,使其不再持有引用,这样也可以避免内存泄露。

3.非静态内部类/匿名内部类 持有外部引用

比如handler改为静态内部类

 private static class MyHandler extends Handler {

    private WeakReference<MainActivity> activityWeakReference;

    public MyHandler(MainActivity activity) {
        activityWeakReference = new WeakReference<>(activity);
    }

    @Override
    public void handleMessage(Message msg) {
        MainActivity activity = activityWeakReference.get();
        if (activity != null) {
            if (msg.what == 1) {
                // 做相应逻辑
            }
        }
    }
}

mHandler通过弱引用的方式持有Activity,当GC执行垃圾回收时,遇到Activity就会回收并释放所占据的内存单元。这样就不会发生内存泄露了。

上面的做法确实避免了Activity导致的内存泄露,发送的msg不再已经没有持有Activity的引用了,但是msg还是有可能存在消息队列MessageQueue中,所以更好的是在Activity销毁时onDestroy里面移除消息队列中所有消息和所有的Runnable。

  @Override
 protected void onDestroy() {
     super.onDestroy();
     mHandler.removeCallbacksAndMessages(null);
     mHandler = null;
 }

4.未取消注册或回调解决办法

activity中存在registerXXX()方法,bindxxx()等,都要去查看是否要在ondestory方法中进行unregisterxx()或者unbind(),或者取消回调。

5.Timer Timetask

Activity销毁的时候要立即cancel掉timer和timerTask.

@Override
protected void onDestroy() {
    super.onDestroy();
      if (mTimer != null) {
        mTimer.cancel();
        mTimer.purge();
        mTimer = null;
    }
    if (mTimerTask != null) {
        mTimerTask.cancel();
        mTimerTask = null;
    }
}

6.未关闭资源

见于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap、数据库操作,应该在完成操作或者Activity销毁时及时关闭或者注销。

7.webview

在销毁WebView之前需要先将WebView从父容器中移除,然后在销毁WebView.

  @Override
protected void onDestroy() {
    super.onDestroy();
    // 先从父控件中移除WebView
    mContainer.removeView(mWebView);
    mWebView.stopLoading();
    mWebView.getSettings().setJavaScriptEnabled(false);
    mWebView.clearHistory();
    mWebView.removeAllViews();
    mWebView.destroy();
}

8.属性动画,repeatCount为无限模式

在销毁的时候,调用cancle方法;

   @Override
protected void onDestroy() {
    super.onDestroy();
    //结束动画
   anim.cancle();
}

.9.集合

在使用集合时要及时将不用的对象从集合remove,或者clear集合,以避免内存泄漏。

五、内存优化

1.操作习惯代码方面:

除了上述需要注意的减少或防止内存泄漏的写法,还有总结书以下几点
1.使用StringBuffer而不建议去使用String进行字符串拼接
2.对static关键字不要滥用:static变量所指向的内存引用,如果不把它设置为null,GC是永远不会回收这个对象的。
3.少用或不用枚举:占用内存多(Enums often require more than twice as much memory as static constants.)。

  1. 对需求不多的第三方库拆分出自己需要的部分。
    5.在引用组件Activity,Fragment时,优先考虑使用弱引用。
    6.在使用异步操作时注意Activity销毁时,需要清空任务列表,如果有使用集合,将集合清空并置空,释放相应的资源。
    7.动态回收内存、对象复用,避免频繁创建销毁对象;
    8.选用合理的数据结构,比如根据需求选择arraylist/linkedlist

2.布局方面

1.优化布局(这一句废话)
a.选择性能消耗较小的布局(FrameLayout、LinearLayout < RelativeLayout。)
b.减少布局嵌套层级
尽量用最少的布局层级完成布局,去除无用布局。

c.使用<include>标签 复用某些模块布局
一个程序一般为了风格统一,很多界面会有共通的UI,<include>标签可以将一个指定的布局文件加载到当前的布局文件中。我们可以定义这个通用UI,使用<include>标签添加这个通用UI的引用。
d.使用<merge>标签
<merge>标签一般与<include>标签一起使用以减少布局层数,如上面的layout_a是一个竖直方向的线性布局,而layout_b也是一个竖直方向的线性布局,很明显layout_a的线性布局是多余的,这时候就可以使用<merge>标签包裹去掉一层布局。
e.使用<ViewStub>实现View的延迟加载
ViewStub继承自View,不可见且大小为0,可以做到在使用的时候再加载,提高程序初始化性能。

2.不要设置过多层的background,

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容