Android常见内存泄漏
标签(空格分隔): 性能优化
性能优化是一个大的范畴,如果有人问你在Android中如何做性能优化的,也许都不知道从哪开始说起。
首先要明白的是,为什么我们的App需要优化,最显而易见的时刻:用户say,什么*,刷这么久都没反应,取关卸载算了。
这跟什么有关,我们先苍白的反驳下,尼玛用户设备老旧网又烂,关我屁事,根本不用优化。可是,老板拍板了,施压给CTO,然后CTO又来找你:Y的今天必须给我想办法优化了,不然不准回家—引用自Stay
优化之前,首先我们需要了解一下跟进程相关的一些基本内容。
-
app进程为毛容易出现ooM.
这个是因为Android系统对dalvik的vm heapsize作了硬性限制,当java进程申请的java空间超过阈值时,就会抛出OOM异常(这个阈值可以是64M、32M、16M等,视机型而定),可以通过adb shell getprop | grep dalvik.vm.heapgrowthlimit查看此值。
也就是说,程序发生OMM并不表示RAM不足,而是因为程序申请的java heap对象超过了dalvik vm heapgrowthlimit。也就是说,在RAM充足的情况下,也可能发生OOM。
这样的设计似乎有些不合理,但是Google为什么这样做呢?这样设计的目的是为了让Android系统能同时让比较多的进程常驻内存,这样程序启动时就不用每次都重新加载到内存,能够给用户更快的响应。迫使每个应用程序使用较小的内存,移动设备非常有限的RAM就能使比较多的app常驻其中。
内存泄漏
内存泄漏跟内存溢出不是一个概念,内存泄漏:简单说来就是内存不在GC的掌控之中,内存泄漏的结果会造成内存溢出。
首先了解一下内存的集中策略:
静态的:内存在程序编译的时候就已经分配好,这块内存在程序整个运行过程中一直都存在,主要存放静态数据、全局的static数据和一些常量。
栈式的:在执行函数时,函数一些内部变量的存储都可以放在栈上创建,函数执行结束,这些存储单元自动释放。栈内存包括分配的运算,速度很快,因为内置在处理器里面。当然容量有限。
堆式的:也叫动态内存分配。有时候用malloc或者new来申请分配一个内存,在c/c++自己掌控内存,java没有什么方法解决。成员变量全部存储在堆中(包括基本数据类型,引用和引用的对象实体)–因为他们属于类,类对象最终是要被new出来的;局部变量的基本数据类型和引用存储于栈当中,引用的的对象实体存储在堆中—因为他们属于方法中的变量,随着方法结束生命周期
区别:
1. 堆式不连续的内存,堆空间比较大灵活。栈式一块连续的内存区域,大小是操作系统决定的。
2. 堆内存管理很麻烦,频繁的new/remove会造成大量的内存碎片,这样就会慢慢导致效率低下。对于栈内存的话,先进后出,进出完全不会产生碎片,运行效率高且稳定;
我们所讨论的内存泄漏,主要讨论堆内存,他存放的就是引用指向的对象实体。
- StrongReference强引用:回收时机,从不回收,使用:对象一般保存生命周期jvm停止的时候才会停止。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
- SoftReference 软银用:回收时机;当内存不足的时候,使用:软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。;生命周期:内存不足时终止。软引用可用来实现内存敏感的高速缓存
- WeakReference弱引用:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。,生命周期:GC后终止。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。构建非敏感数据的缓存 - PhatomReference虚引用:回收时机:在垃圾回收的时候,生命周期:GC后终止,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收
建议:
开发时,为了防止内存溢出,处理一些占用内存大且生命周期长的对象的时候,尽量使用软引用和弱引用。
/*******************下面看一些常见的内存泄漏例子******************/
-
-
public class CommUtil{
private static instance;
private Context context;
private CommUtil(Context context){
this.context = context;
}
public static CommUtils getInstance(Context context){
if(instance == null){
instance = new CommUtil(context);
}
return instance;
}
}
public class Main extends Activity{
@Override
public void onCreate(Bundle bundle){
CommUtil util = CommUtil.getInstance(this);
}
}
/**
分析:当MainActivity将要回收时,由于CommonUtil持有MainActivity的强引用,此时jvm根本不能回收(也就是内存泄露了),当MainActivity重走生命周期时,会重新创建一个MainActivity,此时堆内存中就会存在两个activity。
解决方案:能用Application的context就用Application的,因为CommUtil跟Application生命周期同生死
*/
- 非静态内部类引起内存泄露
public class Main extends Activity{
private int a = 1;
public void onCreate(Bundle bundle){
loadData();
}
}
public void loadData(){
new Thread(new Runnable() {
@Override
public void run() {
while(true){
try {
int b=a;
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
/**
分析:如果是现在这种情况,直接finish(),你以为Main释放了,其实没什么卵用。
因为匿名内部类持有Main的强引用,jvm同样不能回收Main,造成内存泄漏。
方案:将非静态内部类修改为静态内部类。(静态内部类不会隐士持有外部类的引用,但此时静态内部类不能直接引用外部类的成员变量和函数)
*/
private static class MyHandler extends Handler{
private WeakReference<MainActivity> mainActivity;//设置弱引用保存,当内存一发生GC的时候就会回收。
public MyHandler(MainActivity mainActivity) {
this.mainActivity = new WeakReference<MainActivity>(mainActivity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
MainActivity main = mainActivity.get();
if(main==null||main.isFinishing()){
return;
}
switch (msg.what){
case 0:
//加载数据
int b = main.a;
break;
}
}
};
- 不需要用的监听未移除会发生内存泄露
//add监听,放到集合里面
tv.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
@Override
public void onWindowFocusChanged(boolean b) {
//监听view的加载,view加载出来的时候,计算他的宽高等。
//计算完后,一定要移除这个监听
tv.getViewTreeObserver().removeOnWindowFocusChangeListener(this);
}
});
看一下源码实现:
public void addOnWindowFocusChangeListener(OnWindowFocusChangeListener listener) {
checkIsAlive();
if (mOnWindowFocusListeners == null) {
mOnWindowFocusListeners
= new CopyOnWriteArrayList<OnWindowFocusChangeListener>();
}
mOnWindowFocusListeners.add(listener);
}
此处可知道,其实listeners 被放在了CopyOnWriteArrayList中,所以如果不移除的话,CopyOnWriteArrayList一直会持有监听引用,造成内存泄漏
- 资源未关闭引起的内存泄露情况
比如:BroadCastReceiver、Cursor、Bitmap、IO流、自定义属性attribute attr.recycle()回收。
当不需要使用的时候,要记得及时释放资源。否则就会内存泄露。 - 无限循环动画
没有在onDestroy中停止动画,否则Activity就会变成泄露对象。
比如:轮播图效果。
以上就是我总结的常见的一些内存泄漏的情况。
知道了会存在内存泄漏,那我们怎么样去寻找当前项目中的可能存在的内存泄漏?
- 确定是否存在内存泄漏
- Android Monitors工具的内存分析
最直观的看内存增长情况,知道该动作是否发生内存泄露。重复执行某一操作,然后主动GC(initiate GC)后,观察此时内存的大小,如果有明显增长,肯定有内存泄漏的情况。 - 使用MAT内存分析工具
MAT分析heap的总内存占用大小来初步判断是否存在泄露。
我们反复执行某一个操作并同时执行GC排除可以回收掉的内存,注意观察histogram的object size值,正常情况下object Size值都会稳定在一个有限的范围内,也就是说由于程序中的的代码良好,没有造成对象不被垃圾回收的情况。反之如果代码中存在没有释放对象引用的情况,随着操作次数的增多Object Size的值会越来越大。那么这里就已经初步判断这个操作导致了内存泄露的情况。
- Android Monitors工具的内存分析
- 找怀疑对象(哪些对象属于泄露的)
MAT对比操作前后的hprof来定位内存泄露是泄露了什么数据对象。(这样做可以排除一些对象,不用后面去查看所有被引用的对象是否是嫌疑)
快速定位到操作前后所持有的对象哪些是增加了(GC后还是比之前多出来的对象就可能是泄露对象嫌疑犯)
技巧:Histogram中还可以对对象进行Group,比如选择Group By Package更方便查看自己Package中的对象信息。 - MAT分析hprof来定位内存泄露的原因所在。(哪个对象持有了上面怀疑出来的发生泄露的对象)
1)Dump出内存泄露“当时”的内存镜像hprof,分析怀疑泄露的类;
2)把上面2得出的这些嫌疑犯一个一个排查个遍。步骤:
(1)进入Histogram,过滤出某一个嫌疑对象类
(2)然后分析持有此类对象引用的外部对象(在该类上面点击右键List Objects—>with incoming references)
(3)再过滤掉一些弱引用、软引用、虚引用,因为它们迟早可以被GC干掉不属于内存泄漏(在类上面点击右键Merge Shortest Paths to GC Roots—>exclude all phantom/weak/soft etc.references)
(4)逐个分析每个对象的GC路径是否正常
此时就要进入代码分析此时这个对象的引用持有是否合理,这就要考经验和体力了!
另外还有LeakCanary,Lint分析工具都是非常好用的工具,对我们写出高质量的代码,减少内存泄漏都非常有用。
性能优化方面,请参考 stay写的:那些Android上的性能优化