不断学习,做更好的自己!💪
一、前言
内存优化可以说是性能优化中最重要的优化点之一,可以说,如果你没有掌握系统的内存优化方案,就不能说你对 Android 的性能优化有过多的研究与探索。
内存泄漏是一个缓慢积累的过程,一点一点的给你,温水煮青蛙一般,我们往往很难直观的看到,只能最后内存不够用了,程序奔溃了,才知道里面有大量的泄漏,但是到底是那些地方?估计是狼烟遍地,千疮百孔,都不知道如何下手。怎么办?最让人难受的是内存泄漏情况那么多,记不住,理解也不容易,关键是老会忘记。怎么办呢?老这么下去也不是事,总不能面试的时候突击,做项目的时候不知所措吧。所以一定要记住了解GC原理,这样才可以更准确的理解内存泄漏的场景和原因。不懂GC原理的可以先看一下这个JVM初探:内存分配、GC原理与垃圾收集器
本来GC的诞生是为了让java程序员更加轻松(这一点隔壁C++痛苦的一匹),java虚拟机会自动帮助我们回收那些不再需要的内存空间。通过引用计数法,可达性分析法等等方法,确认该对象是否没有引用,是否可以被回收。
有人会说真么强悍的功能看起来无懈可击啊,对,理论上可以达到消除内存泄漏,但是很多人不按常理出牌啊,往往很多时候,有的对象还保持着引用,但逻辑上已经不会再用到。就是这一类对象,游走于GC法律的边缘,我没用了,但是你又不知道我没用了,就是这么赖着不走,空耗内存。
因为有内存泄漏,所以内存被占用越来越多,那么GC会更容易被触发,GC会越来越频发,但是当GC的时候所有的线程都是暂停状态的,需要处理的对象数量越多耗时越长,所以这也会造成卡顿。
那么什么情况下会出现这样的对象呢? 基本可以分为以下四大类:
1. 集合类泄漏
集合类添加元素后,仍引用着集合元素对象,导致该集合中的元素对象无法被回收,从而导致内存泄露。
栗子:
static List<Object> mList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
Object obj = new Object();
mList.add(obj);
obj = null;
}
解决方案:
当mList没用的时候,我们如果不做处理的话,这就是典型的占着茅坑不拉屎,mList内部持有者众多集合元素的对象,不泄露天理难容啊。解决这个问题也超级简单。把mList清理掉,然后把它的引用也给释放掉。
mList.clear();
mList = null;
2. 单例/静态变量造成的内存泄漏
单例模式具有其 静态特性,它的生命周期 等于应用程序的生命周期,正是因为这一点,往往很容易造成内存泄漏。
栗子:
public class SingleInstance {
private static SingleInstance mInstance;
private Context mContext;
private SingleInstance(Context context){
this.mContext = context;
}
public static SingleInstance newInstance(Context context){
if(mInstance == null){
mInstance = new SingleInstance(context);
}
return sInstance;
}
}
当我们在Activity里面使用这个的时候,把我们Acitivty的context传进去,那么,这个单例就持有这个Activity的引用,当这个Activity没有用了,需要销毁的时候,因为这个单例还持有Activity的引用,所以无法GC回收,所以就出现了内存泄漏,也就是生命周期长的持有了生命周期短的引用,造成了内存泄漏。
所以我们要做的就是生命周期长的和生命周期长的玩,短的和短的玩。就好比你去商场,本来就是传个话的,话说完就要走了,突然保安过来非要拉着你的手,说要和你天长地久。只要商场在一天,他就要陪你一天。天呢?太可怕了。叔叔我们不约,我有我的小伙伴,我还要上学呢,你赶紧找你的保洁阿姨去吧。你在商场的生命周期本来可能就是1分钟,而保安的生命周期那是要和商场开关门一致的,所以不同生命周期的最好别一起玩的好。
解决方案:
public class SingleInstance {
private static SingleInstance mInstance;
private Context mContext;
private SingleInstance(Context context){
this.mContext = context.getApplicationContext();
}
public static SingleInstance newInstance(Context context){
if(mInstance == null){
mInstance = new SingleInstance(context);
}
return sInstance;
}
}
还有一个常用的地方就是 Toast。你应该知道和谁玩了吧。
3. 匿名内部类/非静态内部类
这里有一张宝图:
非静态内部类他会持有他外部类的引用,从图我们可以看到非静态内部类的生命周期可能比外部类更长,这就是二楼的情况一致了,如果非静态内部类的周明周期长于外部类,在加上自动持有外部类的强引用,我的乖乖,想不泄漏都难啊。
栗子:
public class TestActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
new MyAscnyTask().execute();
}
class MyAscnyTask extends AsyncTask<Void, Integer, String>{
protected String doInBackground(Void... params) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "";
}
}
}
我们经常会用这个方法去异步加载,然后更新数据。貌似很平常,我们开始学这个的时候就是这么写的,没发现有问题啊,但是你这么想一想,MyAscnyTask 是一个非静态内部类,如果他处理数据的时间很长,极端点我们用 sleep 100 秒,在这期间 Activity 可能早就关闭了,本来 Activity 的内存应该被回收的,但是我们知道非静态内部类会持有外部类的引用,所以 Activity 也需要陪着非静态内部类MyAscnyTask 一起天荒地老。好了,内存泄漏就形成了。
解决方案:
既然 MyAscnyTask 的生命周期可能比较长,那就把它变成静态,和 Application 玩去吧,这样 MyAscnyTask 就不会再持有外部类的引用了。两者也相互独立了。
public class TestActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
new MyAscnyTask().execute();
}
//改了这里 注意一下 static
static class MyAscnyTask extends AsyncTask<Void, Integer, String>{
protected String doInBackground(Void... params) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "";
}
}
}
说完非静态内部类,我再来看看匿名内部类,这个问题很常见,匿名内部类和非静态内部类有一个共同的地方,就是会只有外部类的强引用,所以这哥俩本质是一样的。但是处理方法有些不一样。但是思路绝对一样。换汤不换药。
栗子:
public class TestActivity extends Activity {
private TextView mText;
private Handler mHandler = new Handler(){
public void handleMessage(Message msg) {
super.handleMessage(msg);
//do something
mText.setText(" do someThing");
}
};
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
mText = findVIewById(R.id.mText);
// 匿名线程持有 Activity 的引用,进行耗时操作
new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
mHandler. sendEmptyMessageDelayed(0, 100000);
}
想必这两个方法是我们经常用的吧,很熟悉,也是这么学的,没感觉不对啊,老师就是这么教的,通过我们上面的分析,还这么想吗?关键是 耗时时间过长,造成内部类的生命周期大于外部类,对弈非静态内部类,我们可以静态化,至于匿名内部类怎么办呢?一样把它变成静态内部类,也就是说尽量不要用匿名内部类。完事了吗?很多人不注意这么一件事,如果我们在 handleMessage 方法里进行UI的更新,这个 Handler 静态化了和 Activity 没啥关系了,但是比如这个mText,怎么说?全写是 activity.mText,看到了吧,持有了 Activity 的引用,也就是说Handler费劲心思变成静态类,自认为不持有 Activity 的引用了,准确的说是不自动持有 Activity 的引用了,但是我们要做UI更新的时候势必会持有 Activity 的引用,静态类持有非静态类的引用,我们发现怎么又开始内存泄漏了呢?处处是坑啊,怎么办呢?我们这里就要引出弱引用的概念了。
引用分为强引用,软引用,弱引用,虚引用,强度一次递减。
- 强引用
我们平时不做特殊处理的一般都是强引用,如果一个对象具有强引用,GC 宁可 OOM 也绝不会回收它。看出多强硬了吧。 - 软引用(SoftReference)
如果内存空间足够,GC 就不会回收它,如果内存空间不足了,就会回收这些对象的内存。 - 弱引用(WeakReference)
弱引用要比软引用,更弱一个级别,内存不够要回收他,GC 的时候不管内存够不够也要回收他,简直是弱的一匹。不过 GC 是一个优先级很低的线程,也不是太频繁进行,所以弱引用的生活还过得去,没那么提心吊胆。 - 虚引用
用的甚少,我没有用过,如果想了解的朋友,可以自行谷歌百度。
所以我们用弱引用来修饰 Activity,这样 GC 的时候,该回收的也就回收了,不会再有内存泄漏了。很完美。
public class TestActivity extends Activity {
private TextView mText;
private MyHandler myHandler = new MyHandler(TestActivity.this);
private MyThread myThread = new MyThread();
private static class MyHandler extends Handler {
WeakReference<TestActivity> weakReference;
MyHandler(TestActivity testActivity) {
this.weakReference = new WeakReference<TestActivity>(testActivity);
}
public void handleMessage(Message msg) {
super.handleMessage(msg);
weakReference.get().mText.setText("do someThing");
}
}
private static class MyThread extends Thread {
public void run() {
super.run();
try {
sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
mText = findViewById(R.id.mText);
myHandler.sendEmptyMessageDelayed(0, 100000);
myThread.start();
}
//最后清空这些回调
protected void onDestroy() {
super.onDestroy();
myHandler.removeCallbacksAndMessages(null);
}
4. 资源未关闭造成的内存泄漏
- 网络、文件等流忘记关闭
- 手动注册广播时,退出时忘记 unregisterReceiver()
- Service 执行完后忘记 stopSelf()
- EventBus 等观察者模式的框架忘记手动解除注册