Android内存优化-内存泄漏的几个场景以及解决方式

一.什么是内存泄漏

在Java程序中,如果一个对象没有利用价值了,正常情况下gc是会对其进行回收的,但是此时仍然有其他引用指向这个活在堆内存中的对象,那么gc就不会认为这个对象是一个垃圾,那么就不会对其进行回收,所以它会一直活在堆内存中占用内存,这就导致了内存泄漏。

总结一下,导致内存泄漏的原因就是有一些我们永远不会使用的对象,仍然有引用指向它(当然这是在强引用的情况下),那么就不满足gc回收的条件,从而一直活在堆内存中导致内存泄漏,这样的对象多了占用大量内存就会导致App发生oom。

举几个例子:比如使用EventBus,肯定是要执行register(),那么在Fragment或Activity finish的时候,一定不要忘记执行unregister()方法。

二.内存泄漏的常见场景以及解决方式

1.Activity中的Handler长期持有activity引用导致activity泄漏

public class MainActivity extends AppCompatActivity {

    private final Handler myHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            //doSomething
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        myHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                //doSomething
            }
        },60*10*1000);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

由于myHandler延时10分钟就会发送一条消息,当activity finish之后,延时发送的消息会在主线程的消息队列中存活10分钟直到被looper拿到然后给到handler处理。此消息(new Runnable)隐式持有其外部类handler的引用,myHandler又隐式的持有其外部类Activity的引用,直到消息被处理完之后,这个引用都不会被释放。因此Activity即使finish,但仍然不会被gc回收。 
引用的顺序MessageQueue->Message->Runnable->Handler->Activity,从这个引用链得到Activity与MessageQueue关联,所以Activity对象不能被gc回收,从而导致内存泄漏。

解决方式: 
为了解决Handler隐式的持有外部类引用,我们应当将Handler定义在一个新文件或在Activity中使用静态内部类。因为静态内部类不会持有外部类的引用,这样当Activity finish时,Handler不会持有Activity的引用就不会导致Activity内存泄漏。如果需要在Handler内部调用外部Activity的方法,正确的做法是让Handler持有一个Activity的弱引用(WeakReference),这样当gc扫描的时候,这个弱引用的对象就会被回收。 
解决了Handler隐式持有外部类Activity引用,Runnable在之前的代码中作为匿名内部类隐式持有Handler引用,所以我们在Activity内部定义一个静态变量引用Runnable类,这是因为匿名类的静态实例不会隐式持有他们外部类的引用。

public class MainActivity extends AppCompatActivity {

    private final MyHandler mHandler = new MyHandler(this);

    private static Runnable sRunnable = new Runnable() {
        @Override
        public void run() {
            //doSomething
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mHandler.postDelayed(sRunnable, 60 * 10);

        this.finish();
    }

    private static class MyHandler extends Handler {

        private final WeakReference<MainActivity> mActivity;

        public MyHandler(MainActivity activity) {
            this.mActivity = new WeakReference<MainActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = mActivity.get();

            if (activity != null) {
                //doSomething
            }

        }
    }
}
  • 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

或者

我们也可以这样做: 
在Activity的onDestroy方法中干掉handler中所有的callback和message:

@Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }
  • 1
  • 2
  • 3
  • 4
  • 5

2.非静态匿名内部类造成内存泄漏

在Android中最常见的操作就是当有耗时操作的时候我们不能在主线程执行这些操作,否则有可能造成ANR,主线程主要是UI操作的主战场。 
比如网络请求或者数据库查询这些耗时操作我们需要自己另外开启线程,在子线程中执行这些耗时操作。当我们需要开启的子线程比较少的时候,直接new Thread(Runnable)就可以了。如果你经常这样做的话就说明你没有注意到有可能会产生内存泄漏的问题。 
如果Activity结束了,而Thread还在跑,同样会导致Activity内存泄漏,这是因为new Thread作为非静态内部类对象都会隐式持有一个外部类对象的引用,我们所创建的线程就是Activity中的一个内部类,持有Activity对象的引用,所以当Activity 结束了,而子线程还在跑就会导致Activity内存泄漏。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        testThread();
    }

    private void testThread() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    SystemClock.sleep(1000);
                }
            }
        }).start();
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

new Thread()是匿名内部类,且非静态。所以会隐式持有外部类的一个引用,只要非静态匿名类对象没有被回收,Activity就不会被回收。

解决方式: 
同样把Thread定义为静态的内部类,这样就不会持有外部类的引用。

3.单例+依赖注入

LeakActivity.java

public class LeakActivity extends AppCompatActivity {

    private TestManager testManager = TestManager.getInstance();
    private MyListener listener=new MyListener() {
        @Override
        public void doSomeThing() {}
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        testManager.registerListener(listener);
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

TestManager.java

public class TestManager {

    private static final TestManager INSTANCE = new TestManager();
    private MyListener listener;

    public static TestManager getInstance() {
        return INSTANCE;
    }

    public void registerListener(MyListener listener) {
        this.listener = listener;
    }
    public void unregisterListener() {
        listener = null;
    }
}

interface MyListener {
    void doSomeThing();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

在LeakActivity中TestManager.getInstance()创建对象实例,TestManager中采用单例模式返回一个Testmanager实例变量。 
引用链:TestManager->listener->Activity

TestManager中的实例变量是static静态变量,静态变量和类的生命周期是一样的。类加载的时候,静态变量就被加载,类销毁时,静态变量也会随之销毁。 
因为INSTANCE是一个单例,所以和app的生命周期是一样的。当app进程销毁时,堆内存中的INSTANCE对象才会被释放,INSTANCE的生命周期非常的长。 
而又可以看到代码中Activity里面创建了listener非静态内部类,所以listener就持有外部类Activity的引用。随着testManager.registerListener(listener)执行,TestManager中的listener就持有Activity中listener对象,由此形成了一个引用链。关键在于INSTANCE是一个静态变量,往往Activity finish的时候,INSTANCE还活着,而INSTANCE依然持有Activity的引用,所以造成了Activity内存泄漏。

所以,解决方式:要在Activity的onDestroy()方法中注销注册的listener

@Override
    protected void onDestroy() {
        testManager.unregisterListener();
        super.onDestroy();
    }
  • 1
  • 2
  • 3
  • 4
  • 5

将TestManager中listener与Activity中的listener关联断开。

三.总结

出现内存泄露的主要原因是生命周期的不一致造成的:在Android中,长时间运行的任务和Acyivity生命周期进行协调会有点困难,如果你不加以小心的话会导致内存泄漏。 
内存泄漏的主要原因在于一个生命周期长的东西间接引用了一个生命周期短的东西,会造成生命周期短的东西无法被回收。反过来,如果是一个生命周期短的东西引用了一个生命周期长的东西,是不会影响生命周期短的东西被回收的。 
  对象都是有生命周期的,对象的生命周期有的是进程级别的,有的是Activity所在的生命周期,随Activity消亡;有的是Service所在的生命周期,随Service消亡。很多情况下判断对象是否合理存在的一个很重要的理由就是它实际的生命周期是否符合它本来的生命周期。很多Memory Leak的发生,很大程度上都是生命周期的错配,本来在随Activity销毁的对象变成了进程级别的对象,Memory Leak就无法避免了。

四.避免内存泄漏的一些技巧

  1. 使用静态内部类/匿名类,不要使用非静态内部类/匿名类.非静态内部类/匿名类会隐式的持有外部类的引用,外部类就有可能发生泄漏。而静态内部类/匿名类不会隐式的持有外部类引用,外部类会以正常的方式回收,如果你想在静态内部类/匿名类中使用外部类的属性或方法时,可以显示的持有一个弱引用。
  2. 不要以为Java永远会帮你清理回收正在运行的threads.在上面的代码中,我们很容易误以为当Activity结束销毁时会帮我们把正在运行的thread也结束回收掉,但事情永远不是这样的!Java threads会一直存在,只有当线程运行完成或被杀死掉,线程才会被回收。所以我们应该养成为thread设置退出逻辑条件的习惯。
  3. 适当的考虑下是否应该使用线程.Android应用框架设计了许多的类来简化执行后台任务,我们可以使用与Activity生命周期相关联的Loaders来执行简短的后台查询任务。如果一个线程不依赖与Activity,我们还可以使用Service来执行后台任务,然后用BroadcastReceiver来向Activity报告结果。另外需要注意的是本文讨论的thread同样使用于AsyncTasks,AsyncTask同样也是由线程来实现,只不过使用了Java5.0新增并发包中的功能,但同时需要注意的是根据官方文档所说,AsyncTask适用于执行一些简短的后台任务。
  4. 频繁的使用static关键字修饰 
    很多初学者非常喜欢用static类static变量,声明赋值调用都简单方便。由于static声明变量的生命周期其实是和APP的生命周期一样的(进程级别)。大量的使用的话,就会占据内存空间不释放,积少成多也会造成内存的不断开销,直至挂掉。static的合理使用一般用来修饰基本数据类型或者轻量级对象,尽量避免修复集合或者大对象,常用作修饰全局配置项、工具类方法、内部类。
  5. BitMap隐患 
    Bitmap的不当处理极可能造成OOM,绝大多数情况应用程序OOM都是因这个原因出现的。Bitamp位图是Android中当之无愧的胖子,所以在操作的时候必须小心。 
    及时释放recycle。由于Dalivk并不会主动的去回收,需要开发者在Bitmap不被使用的时候recycle掉。 
    设置一定的压缩率。需求允许的话,应该去对BItmap进行一定的缩放,通过BitmapFactory.Options的inSampleSize属性进行控制。如果仅仅只想获得Bitmap的属性,其实并不需要根据BItmap的像素去分配内存,只需在解析读取Bmp的时候使用BitmapFactory.Options的inJustDecodeBounds属性。 
    最后建议大家在加载网络图片的时候,使用软引用或者弱引用并进行本地缓存,推荐使用android-universal-imageloader或者xUtils。
  6. 引用地狱 
    Activity中生成的对象原则上是应该在Activity生命周期结束之后就释放的。Activity对象本身也是,所以应该尽量避免有appliction进程级别的对象来引用Activity级别的对象,如果有的话也应该在Activity结束的时候解引用。如不应用applicationContext在Activity中获取资源。Service也一样。 
    有的时候我们也会为了程序的效率性能把本来是Activity级里才用的资源提升到进程级别,比如ImageCache,或者其它DataManager等。 
    我只能说,空间和时间是相对的,有的时候需要牺牲时间换取空间,有的时候需要牺牲空间换取时间。内存是空间的存在,性能是时间的存在。完美的程序是在一定条件下的完美。
  7. BroadCastReceiver、Service 解绑 
    绑定广播和服务,一定要记得在不需要的时候给解绑。
  8. handler 清理 
    在Activity的onDestroy方法中调用 
    handler.removeCallbacksAndMessages(null); 
    取消所有的消息的处理,包括待处理的消息;
  9. Cursor及时关闭 
    在查询SQLite数据库时,会返回一个Cursor,当查询完毕后,及时关闭,这样就可以把查询的结果集及时给回收掉。
  10. I/O流 
    I/O流操作完毕,读写结束,记得关闭。
  11. 线程 
    线程不再需要继续执行的时候要记得及时关闭,开启线程数量不易过多,一般和自己机器内核数一样最好,推荐开启线程的时候,使用线程池。线程生命周期要跟activity同步。
  12. 网络请求也是线程操作的,也应该与activity生命周期同步,在onDestroy的时候cancle掉请求。

猜你喜欢

转载自blog.csdn.net/gvvbn/article/details/80243709