容易导致内存泄露的场景

作者: 元创造力 | 来源:发表于2017-06-22 16:40 被阅读191次

    1.资源性对象未关闭
    资源性对象(比如Cursor、File文件等)往往都用了一些缓冲,在不使用的时候,应该及时关闭它们,以便它们的缓存数据能够及时回收。它们的缓存不仅仅在JAVA虚拟机内,还存在JAVA虚拟机外,如果仅仅把它们的引用对象置为null,而不关闭它们,往往会造成内存泄露。因为有些资源性对象,比如SQLite Cursor,如果我们没有关闭它,而仅仅把它置为null,系统在回收它时也会关闭它,但是这样的效率太低了。因此对于资源性对象不再使用时,应该立刻调用它的close()函数,将其关闭,然后再置为null。在程序退出时,一定要确保其资源性对象已经关闭。
    因此,在编写资源文件读写时,都需要在finally中关闭资源性对象,避免在异常情况下资源对象未被释放的隐患。

    2.注册对象未注销
    如果事件注册后未注销,会导致观察者列表中维持着对象的引用,阻止垃圾回收,一般发生在注册广播接受器、注册观察者等。
    假设在Activity中,监听系统中的电话服务,以获取一些信息,可以在Activity中定义一个PhoneStateListener对象,同时将它注册到TelephonyManager服务中。对于Activity对象,理论上要求Activity在退出后该Activity对象就会被释放掉。
    但是如果在释放该Activity时,忘记取消之前注册的PhoneStateListener对象,则会导致Activity无法被GC回收。如果不断地进出这个Activity,则最终会由于大量的Activity对象没有办法被回收而引起频繁的GC情况,甚至导致OOM。

    3.类的静态变量持有大数据对象
    静态变量长期维持着对象的引用,阻止垃圾回收,如果静态变量持有大的数据对象,如Bitmap等,就很容易引起内存不足等问题。

    静态变量是在类被load的时候分配内存的,并且存在于方法区。当类被卸载的时候,静态变量被销毁。
    1).类在什么时候被加载?
    当我们启动一个app的时候,系统会创建一个进程,此进程会加载一个Dalvik VM的实例,
    然后代码就运行在DVM之上,类的加载和卸载,垃圾回收等事情都由DVM负责。也就是说在进程启动的时候,类被加载,静态变量被分配内存。
    2).类在什么时候被卸载?
    在进程结束的时候。
    

    4.非静态内部类的静态实例
    非静态内部类会维持一个到外部类实例的引用,如果非静态内部类的实例是静态的,就会间接长期维持着外部类的引用,阻止被系统回收。如下代码是一个非静态内部类的实现。

    public class LayoutPerActivity extends Activity {
            private static TestModule mTestModule = null;
            @override
            protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                this.getWindow().setBackgroundDrawable(null);
                setContentView(R.layout.activity_layout_per);
                if(null == mTestModule) {
                    mTestModule = new TestModule(this);
                }
            }
            class TestModule{
                private Context mContext = null;
                public TestModule(Context ctx) {
                    mContext = ctx;
                }
            }
        }
    

    在Activity内部创建了一个非静态内部类的静态实例mTestModule,每次启动Activity都会使用TestModule的数据,这样虽然避免了资源的重复创建,但是TestModule为非静态内部类默认持有外部类的引用,而这里又使用该非静态内部类创建了一个静态的实例,该实例的生命周期和应用一样长,这就导致该静态实例一直持有该Activity的引用,Activity的资源不能正常回收。
    可以这样避免内存泄露,即将内部类TestModule设为静态内部类或者将内部类TestModule抽取出来封装成一个单例,如果需要使用Context,就在没有特殊要求的情况下,使用Application Context,如果需要使用Activity Context,就记得用完后置空让GC回收,否则还是会内存泄露。

    5.Handler临时性内存泄露
    Handler通过发送Message与主线程交互是应用开发中非常常见的使用场景之一,Message发出之后存在于MessageQueue中,有些Message也不是马上就被处理到。在Message中存在一个target,它是Handler的一个引用,Message在Queue中存在的时间过长,就会导致Handler无法被回收。如果Handler是非静态的,则会导致Activity或者Service不会被回收。Handler发送Message与主线程交互的实现代码如下:

    public class LayoutPerActivity extends Activity {
            Handler mHandler = new Handler(){
                @Override
                public void handleMessage(Message msg) {
                    super.handleMessage(msg);
                }
            };
            
            private void doGetDataAsyncTask() {
                Message msg = Message.obtain();
                mHandler.sendMessage(msg);
            }
        }
    

    由于mHandle是Handler的非静态匿名内部类的实例,所以它持有外部类Activity的引用,并且消息队列是在一个Looper线程中不断轮询处理消息,那就有一种情况,当这个Activity退出时,消息队列中还有未处理的消息或者正在处理的消息,并且消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄露。避免这种内存泄露需要修改以下三个地方;

    1.使用一个静态Handler内部类,
    2.对Handler持有的对象使用弱引用,这样在回收时,也可以回收Handler持有的对象
    3.在Activity的onDestroy或者onStop时,应该移除消息队列中的消息,避免Looper线程的消息队列中有待处理的消息需要处理。
    

    修改后的代码如下:

    public class LayoutPerActivity extends Activity {
            private NewHandler mHandler = new NewHandler(this);
            private static class NewHandler extends Handler{
                private WeakReference<Context> mContext = null;
                public NewHandler(Context context) {
                    mContext = new WeakReference<Context>(context);
                }
                @Override
                public void handleMessage(Message msg) {
                    super.handleMessage(msg);
                }
            }
    
            private void doGetDataAsyncTask() {
                Message msg = Message.obtain();
                mHandler.sendMessage(msg);
            }
            @Override
            protected void onDestroy() {
                super.onDestroy();
                mHandler.removeCallbacksAndMessages(null);
            }
            
        }
    

    6.容器中的对象没有清理造成的内存泄露
    通常把一些对象的引用加入集合中,在不需要该对象时,如果没有把它的引用从集合中清理掉,这个集合就会越来越大。如果这个集合是static,情况就更严重了。

    7.WebView造成的内存泄露
    webview的内存泄露的解决方法一般是为此webview单独开一个进程,使用AIDL与应用的主进程进行通信,当不再使用此webView的时候杀死此进程即可。

    附:检测内存泄露工具及集成方法
    工具:square公司开源的Leakcanary
    github地址:https://github.com/square/leakcanary.git
    集成方法

    添加依赖:
        debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
        releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
        testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1
    

    新建一个App类继承Application


    image.png

    在onCreate方法中添加如下代码

    if (LeakCanary.isInAnalyzerProcess(this)) {
          // This process is dedicated to LeakCanary for heap analysis.
          // You should not init your app in this process.
          return;
        }
        LeakCanary.install(this);
        // Normal app init code...
      }
    

    然后在清单文件中配置App类

    image.png

    就这样就集成好了
    下面写一个栗子来验证一下吧:
    在工程里新建一个Activity,取名字叫SecondActivity:
    在SecondActivity添加如下代码:

    public class SecondActivity extends AppCompatActivity {
        private static SecondActivity.TestModule mTestModule = null;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            this.getWindow().setBackgroundDrawable(null);
            setContentView(R.layout.activity_main);
            if(null == mTestModule) {
                mTestModule = new SecondActivity.TestModule(this);
            }
        }
        class TestModule{
            private Context mContext = null;
            public TestModule(Context ctx) {
                mContext = ctx;
            }
        }
    }
    

    然后在MainActivity中加一个按钮,用于打开SecondActivity

    image.png
    public class MainActivity extends AppCompatActivity implements View.OnClickListener{
        private Button noStaticInnerClass;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            this.getWindow().setBackgroundDrawable(null);
            setContentView(R.layout.activity_main);
            noStaticInnerClass = (Button) findViewById(R.id.no_static_inner_class);
            noStaticInnerClass.setOnClickListener(this);
        }
    
        @Override
        public void onClick(View view) {
            switch (view.getId()) {
                case R.id.no_static_inner_class:
                    startActivity(new Intent(this,SecondActivity.class));
                    break;
            }
        }
    }
    

    然后一直打开关闭SecondActivity,就会检测到内存泄露:

    image.png

    相关文章

      网友评论

        本文标题:容易导致内存泄露的场景

        本文链接:https://www.haomeiwen.com/subject/wndfcxtx.html