内存泄漏场景以及解决方法

什么是内存泄漏

由于程序的逻辑错误导致程序失去对该内存的控制,使得内存浪费。

简单的来说就是因为当程序需要不再使用该内存,释放内存的失败而产生的无用的内存消耗的时候。

内存分配策略

  • 静态分配:主要存放静态数据,全局 static 数据 和 常量。 且该块内存在编译的时候已经确定了,而且在程序运行期间都是存在的
  • 栈区:当方法被执行的时候,即他会被放在栈的顶部,且方法内的局部变量都会在栈内被创建,并且在方法执行完后回收,因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  • 堆区:又称为动态内存分配,主要储存对象(即new出来的对象),如果存在不使用的对象,就可以GC掉。

java是如何管理内存的

程序员通过new方法来申请内存的,new出的对象会放在堆中,但是对象的清楚是由GC来执行的,结论:对象的创建是由程序创建的,但是回收是通过GC来得到的,这减少了程序员的工作,也加重了JVM的负担,这也是 Java 程序运行速度较慢的原因之一。因为为了保证GC能够正确的释放对象,我们必须对每一个对象的运行状态进行监控,包括对象的申请、引用、被引用、赋值等,

总结: 监视对象为了GC能够准确及时的回收对象,而释放的原则就是该对象不在被引用。

Java中的内存泄漏

在java中内存泄漏的对象有两个特点:

  • 该对象是可达的
  • 该对象是无用的,即程序不再使用该对象

如果满足以上两点即判断为内存泄漏.

场景

单例:

public class AppManager {

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

如果传入的是APP的上下文是运行的,因为前文我们知道静态成员的生命周期是整个程序运行期间,但是如果我们传入的是Activity的对象的话,这就糟了,因为我们的单例的生命周期是整个runtime,但是同时持有Activity对象这样,activity是在整个程序运行期间是无法被GC掉的。这就照成了内存泄漏。

所以单例的时候需要上下文应该调用MyApplication.getContext(),传入上下文的引用。

静态Activity

静态View

这个不用多说,但是我们可以在onDestroy的时候把该静态view=null可以避免

匿名类/AsyncTask

public class MainActivity extends AppCompatActivity {
    void startAsyncTask() {
        new AsyncTask<Void, Void, Void>() {
            @Override protected Void doInBackground(Void... params) {
                while(true);
            }
        }.execute();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        View aicButton = findViewById(R.id.at_button);
        aicButton.setOnClickListener(new View.OnClickListener() {
            @Override 
            public void onClick(View v) {
                startAsyncTask();
            }
        });
    }
}

其中Activity代码中创建了匿名类AsyncTask,我们知道匿名类和内部类持有外部类的对象,而这里外部类就是Activity,所以如果AsyncTask没有被GC掉,Activity就会造成内存泄漏。

解决:
自定义静态 AsyncTask 类,并且让 AsyncTask 的周期和 Activity 周期保持一致,也就是在 Activity 生命周期结束时要将 AsyncTask cancel掉。

非静态内部类

public class MainActivity extends AppCompatActivity {

    private static TestResource mResource = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        if(mManager == null){
            mManager = new TestResource();
        }
        //...
    }

    class TestResource {
        //...
    }
}

上文创建了非静态内部类的单例,这样多次启动该actvity就避免了多次创建的浪费,但是该内部类是一个静态实例,他的生命周期是整个runtime,通过该内部类拥有该Activity应用导致该activity的资源无法被回收。

正确的做法为:

将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,请按照上面推荐的使用ApplicationContext。当然,Applicationcontext 不是万能的,所以也不能随便乱用,对于有些地方则必须使用 ActivityContext

Handler(最为常见)

我们一般为了解决ANR最常用的方法就是把耗时任务放在子线程做,我们就可以封装请求和回调,借助Handler交给子线程处理,但是Handler不是万能的,如果编码不规范会造成内存泄漏,我们知道HandlerLooper以及MessageQueue是关联的,如果通过Handelr发送messageMessageQueue中,等待被处理,在这期间messageQueue持有MessageHandler的引用,但是一般HandlerActvity的匿名类,这时候Handler持有的外部类Activity的引用,这时候都有可能导致内存泄漏。

public class SampleActivity extends Activity {

    private final Handler mLeakyHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // ...
        }
    }

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

        // Post a message and delay its execution for 10 minutes.
        mLeakyHandler.postDelayed(new Runnable() {
            @Override
            public void run() { /* ... */ }
        }, 1000 * 60 * 10);

        // Go back to the previous Activity.
        finish();
    }
}

在该 SampleActivity 中声明了一个延迟 10分钟 执行的消息 Message,mLeakyHandler 将其 push 进了消息队列 MessageQueue 里。当该 Activity 被 finish() 掉时,延迟执行任务的 Message 还会继续存在于主线程中,它持有该 Activity 的 Handler 引用,所以此时 finish() 掉的 Activity 就不会被回收了从而造成内存泄漏(因 Handler 为非静态内部类,它会持有外部类的引用,在这里就是指 SampleActivity)。

正确的做法为:
设置handler为内部静态类,同时通过弱引用的方式引入 Activity,避免直接将 Activity 作为 context 传进去,
推荐使用 "静态内部类 + WeakReference" 这种方式,每次使用前注意判空。

public class SampleActivity extends Activity {

    private static class MyHandler extends Handler {
        private final WeakReference<SampleActivity> mActivity;

        public MyHandler(SampleActivity activity) {
            mActivity = new WeakReference<SampleActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            SampleActivity activity = mActivity.get();
            if (activity != null) {                              // 每次使用前注意判空
                // ...
            }
        }
    }

    private final MyHandler mHandler = new MyHandler(this);

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

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

        // Post a message and delay its execution for 10 minutes.
        mHandler.postDelayed(sRunnable, 1000 * 60 * 10);

        // Go back to the previous Activity.
        finish();
    }
}

Thread

和上文的AsyncTask也是相同原因匿名类持有外部类的引用。

public class SampleActivity extends Activity {
    void spawnThread() {
        new Thread() {
            @Override public void run() {
                while(true);
            }
        }.start();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        View tButton = findViewById(R.id.t_button);
        tButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                spawnThread();
            }
        });
    }
}

错误做法2 : Thread设置为静态的,因为Thread位于GC根部,DVM会和所有的活动线程保持hard references关系,所以运行中的Thread绝不会被GC无端回收了。

所以正确的解决办法是在自定义静态内部类的基础上给线程加上取消机制,因此我们可以在Activity的onDestroy方法中将thread关闭掉。

Sensor Manager

public class SampleActivity extends Activity {
    void registerListener() {
           SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
           Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
           sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        View smButton = findViewById(R.id.sm_button);
        smButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                registerListener();
            }
        });
    }
}

我们通过Context调用getSystemService获取服务,这些服务运行在他们自己的进程执行一系列的后台任务(与硬件交互的接口),如果Context需要随时收到相关的通知,就必须把自己作为监听器注册进去,而这个服务就持有Activity的对象,如果我们忘记Activity前面销毁这个监听器,那么这样会导致内存泄漏

尽量避免使用的static成员变量

因为我们知道static的生命周期和app进程的生命周期一样长。

如果这个app是后台常住内存,这部分的内存也不会被释放,又因为我们手机app的内存管理机制,他们会首先回收掉内存较大的应用,因为现在大部分app都有保活的策略,这样会导致app不断的重启这是十分恐怖的事情。

正确的做法为:
懒初始化static变量,且尽量避免使用。

集合对象及时清除

我们通常会把一些对象的引用存入集合里,但是当我们不再使用的时候应该把它们的引用清理掉,不然会导致集合越来越大,如果该static的集合情况会跟严重。

正确的做法为:
在该Activity退出的时候,我们应该讲里面的东西先clear掉,然后把集合置为空。

webView

当我们不再使用webView的时候,我们应该调用他的destory()方法来销毁它,

正确的做法为:
为webView开启另外一个进程,通过AIDL与主线程进行通信,webView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。 ???????????????

资源没关闭

对于使用了BraodcastReceiverContentObserverFile游标 CursorStreamBitmap等资源的使用,及时关闭或者注销

总结

在开发中,内存泄漏一般情况不会导致app崩溃,但是他们耗尽大量的内存,这样我们GC的工作会更为频繁(因为可真真正正使用的内存减少了),因为GC是一个耗时耗能的操作,所以给我们的只管感受就是页面卡顿

详情看:Android 性能篇 -- 带你领略Android内存泄漏的前世今生

推荐阅读更多精彩内容