公司的项目基本该有的功能都做完了,之前都是在赶工,改需求,妈的需求改得还真TM频繁,从开始做的时候就改到要发布,这种情况已经持续一年多了,一直到现在,害得每次都赶工,着实是蛋疼,真他妈的不想干了。因为之前都是赶功能,根本就没时间对app进行内存优化,现在没什么事做了,就来优化下内存。
对于android手机而言,内存是很宝贵的,不像PC一样。手机内存本来就不多,如果开发上还不注意节约内存的开销,很容易导致可用内存变得越来越少,到你的app的使用内存超过向系统申请内存时,系统就不得不把你的app进程给kill掉。所以我们为了不给系统增加太多的压力和不让我们app给系统干掉,还是优化下内存开销吧。
- 什么是内存泄露
在java中,如果一个对象没有可用价值了,但又被其他引用所指向,那么这个对象对于gc来说就不是一个垃圾,
所以不会对其进行回收,但是我们认为这应该是个垃圾,应该被gc回收的。这个对象得不到gc的回收,
就会一直存活在堆内存中,占用内存,就跟我们说的霸着茅坑不拉屎的道理是一样的。这样就导致了内存的泄露。
所以我们在开发中就应该尽量避免内存泄露,让app使用更加流畅。
- 内存泄露检测工具
1、MAT,下载地址: MAT ,这个工具功能很强大,但是学习成本比较高,我用了几遍就不想用了,实在是麻烦,每次都要导出.hprof文件,然后通过命令行把.hprof转换成MAT可以识别的文件,里面的功能也要学习断时间才行。
2、LeakCanary, 下载地址:LeakCanary ,这个工具实在是太刁了,方便,使用简单,在通知栏通知内存泄露,我非常喜欢。以下讲解下他的使用,当然,你也可以看官方文档。
1.1、首先我们在build.gradle中的dependencies中添加以下代码
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1' // or 1.4-beta1
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' // or 1.4-beta1
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' // or 1.4-beta1
debugCompile 是在调试的时候使用的,releaseCompile 是在release.apk包中不使用的,这样配置也就不用我们修改任何代码了,我们正式打的包是不会出现leakcanary那些提示的。
1.2、在Application中的onCreat方法添加代码:
LeakCanary.install(this);
经过以上配置就可以监听activity是否存在内存泄露了,什么代码都不用加了。如果我们需要监听某个对象是否存在内存泄露,我们可以这样做:
RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
refWatcher.watch(testInstance);
- 内存泄露案例1
public class LeakActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
}
},1000 * 60);
}
}
以上代码很简单,handler在post了一个1分钟后执行的runnable,而这个runnable是挂载在主线程的。看似很平常,没什么不对,我们运行程序后,可以看到以下截图:
从上图我们可以看到,LeakActivity的对象发生了内存泄露,是由mMessageQueue这个对象导致的。当我们启动LeakActivity的时候,handler会把Runnable对象封装成一个Message,然后 post 这个Message进MessageQueue,等待60秒后执行。我们结束LeakActivity时,并没有取消掉MessageQueue里的Message,所以Message里的Runnable会一直等到1分钟结束后执行里面的run方法,在这过程中MessageQueue会一直持有Message的对象引用,然而Runnable是封装在这个Message的,所以他们之间的引用关系就像这样:MessageQueue->Message->Runnable->Handler->Activity。所以这就导致当前activity与MessageQueue一直有关联,导致LeakActivity的对象不能被gc回收,从而导致内存泄露。(关于Handler、Message、MessageQueue、Looper之间的工作原理可以去看下源码)
所以要避免上面的内存泄露,我们可以这样做,在activity的onDestroy方法中干掉handler的所有callback和message:
@Override
protected void onDestroy() {
super.onDestroy();
handler.removeCallbacksAndMessages(null);
}
这样就ok了。
在activity中开启的线程也是一样,如果activity结束了而线程还在跑,一样会导致activity内存泄露,因为"非静态内部类对象都会持有一个外部类对象的引用",你创建的线程就是activity中的一个内部类,持有activity对象的引用,当activity结束了,但线程还在跑,就会导致activity内存泄露。
上面的例子是很容易看出是否有内存泄露,那么接下来的例子就没那么容易看出来了,而且开发中使用的频率是很高的。
- 内存泄露案例2
LeakActivity1
public class LeakActivity1 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);
}
}
TestManager
public class TestManager {
private 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();
}
运行LeakActivity1后出现了内存泄露,如下图:
由上图我们可以看出,LeakActivity1还是内存泄露了。我按照上面标注的顺序说下,第1个中的leaks是内存泄露的意思,代表是LeakActivity1的对象instance内存泄露了,2中references是引用的意思,导致内存泄露是由LeakActivity1中的一个实现了MyListener的匿名类导致的,这里的引用就是指这个实现类的引用,3中的reference是指TestManager类中的listener这个引用,4中指出了最终导致内存泄露的根本源头为TestManger类中的INSTANCE。
上面的代码中,我们定义了个TestManager,并且使用单例模式,然后在activity实现MyListener接口,再通过testManager.registerListener(listener);注册个回调。我们开发中很经常这样干。但是我们这里的是单例,如下图,当程序执行LeakActivity1时,读到private TestManager testManager=TestManager.getInstance(); 这句代码时,首先会在栈内存中开辟内存存储testManager变量,然后读到TestManager.getInstance();时候,会加载TestManager类,因为TestManager中有static对象,static跟类的生命周期是一样的,类一加载,static就加载了,类一被销毁,static才会跟着销毁(static是存在方法区),这时候jvm会在方法区中存储变量INSTANCE,然后在堆内存开辟空间存放INSTANCE对象,然后把地址值付给INSTANCE变量,使INSTANCE变量就指向这个对象(类似c语言的指针),activity类也是这样的一种执行关系。
因为这是一个单例,当app进程被干掉的时候,堆内存中的INSTANCE对象才会被释放,所以INSTANCE对象的生命周期是很长的,LeakActivity1中,listener持有当前activity的对象,然后testManager.registerListener(listener);执行完,TestManager中的listener就持有activity中listener的对象,而TestManager中的INSTANCE是static的,生命周期长,activity销毁的时候INSTANCE依然还在,INSTANCE还在,那么TestManager类中的全局变量也还是存在的,所以TestManager中的listener变量还在,还一直持有LeakActivity1中的listener对象引用,所以最终是INSTANCE导致LeakActivity1内存泄露。
所以,要解决这个问题,可以这样做,在activity的onDestroy方法中注销注册的listener:
@Override
protected void onDestroy() {
testManager.unregisterListener();
super.onDestroy();
}
这样做后TestManager中的listener不再持有LeakActivity1中的listener对象引用,所以LeakActivity1被销毁后listener对象也可被回收了。
最终,问题又解决了,当然你也可以直接把INSTANCE置null。
- 总结
1、小心使用static
2、线程生命周期要跟activity同步
3、小心使用第三方jar包(我开发中就遇到过jar包中持有activity对象导致的内存泄露)
4、网络请求也是线程操作的,也应该与activity生命周期同步,在onDestroy的时候cancle掉请求
5、尽量使用application代替activity和context: Context context = activity.getApplication();这样就使得context不是指向Activity了,指向全局的application,这样就没内存泄露可说了。
等等......
不单单是activity会出现内存泄漏的,其他的类对象也可能会泄漏,对象回收不了,那么类中的其他变量值也会在,如果不处理,量一多,还是挺可怕的。今天就写到这了,公司现在就剩我一个人了,该回去吃饭了。