Android的垃圾回收与内存泄露
标签(空格分隔): Android
我们知道App都有一个UI线程,也叫主线程,那是Android框架帮我们创建的,这里注意的是不是每个activity对应一个UI主线程,而是一个App。
为Message都在一个队列中,拥有它下一个对象的引用非常重要,这里的写法其实跟我们上学时学习的队列类似,每一个实体类都拥有下一个的引用,这样就构成了一个队列
内存泄漏的基本知识请见博客一
如何高效使用handler避免内存泄漏请见博客二
Looper造成内存泄漏的总结为:
1、因为Looper里面的MesageQueue持有外部传进来的runnable引用,runnable又持有handler的引用,handler持有activity的引用//这种情况在用handler开启一个耗时的后台操作时,这时开启的线程也会持有handler的引用,在关闭`Activity`的时候停掉你的后台线程。线程停掉了,就相当于切断了`Handler`和外部连接的线,`Activity`自然会在合适的时候被回收。
2、在使用handler.handleMessage()这个方法时,实际上是一个回调的过程
,在message的内部handleMessage()将肯定是持有Handler的引用,而handler又持有activity的引用
会有一条链`MessageQueue -> Message -> Handler -> Activity`,由于它的引用导致你的`Activity`被持有引用而无法被回收`
weakHandler博客
WeakHandler的实现原理:
WeakHandler的思想是将Handler和Runnable做一次封装,我们使用的是封装后的WeakHandler,但其实真正起到handler作用的是封装的内部,而封装的内部对handler和runnable都是用的弱引用。
自己的理解:###
因为内部封装了的handler与runnable都是弱引用,所以实际上供调用者使用的是封装之后的weakHandler与weakRunnable,所虽然在Looper持有的weakhandler与weakRunnable,而weakHandler与weakRunnable又分别持有弱引用的handler与弱引用的runnable,所以当被封装了的handler与runnabl被回收时,weakHandler内部的弱引用handler与弱引用的runnable也会被回收,这时Looper持有的weakHandler内部已经没有了handler与runnable,只是一个空客,所以不会内存泄露。
以下的内容参考博客
内存泄漏潜在危害非常大,比如无意泄漏了一个Drawable,它可能只有几百K的占用,但是由于它一般会引用View,就意味着同时泄漏了View,Context,Activity 以及 Activity中的resource,这个内存的泄漏就非常可观了。
安卓中很容易出现这种连锁的引用泄露
造成内存泄露的情况有下面两种:##
1、try/catch/finally中网络文件等流的没有手动关闭###
- HTTP
- File
- ContendProvider
- Bitmap
- Uri
- Socket
2、onDestroy() 或者 onPause()中未及时关闭对象###
- 线程泄漏:当你执行耗时任务,在onDestroy()的时候考虑调用Thread.close(),如果对线程的控制不够强的话,可以使用RxJava自动建立线程池进行控制,并在生命周期结束时取消订阅;
- Handler泄露:当退出activity时,要注意所在Handler消息队列中的Message是否全部处理完成,可以考虑removeCallbacksAndMessages(null)手动关闭
- 广播泄露:手动注册广播时,记住退出的时候要unregisterReceiver() 第三方SDK/开源框架泄露:ShareSDK,
- JPush等第三方SDK需要按照文档控制生命周期,它们有时候要求你继承它们丑陋的activity,其实也为了帮你控制生命周期
- 各种callBack/Listener的泄露,要及时设置为Null,特别是static的callback
- EventBus等观察者模式的框架需要手动解除注册
- 某些Service也要及时关闭,比如图片上传,当上传成功后,要stopself()
- Webview需要手动调用WebView.onPause()以及WebView.destory()
static class/method/variable 的区别,你真的懂了吗?##
(1). Static inner class 与 non static inner class 的区别
static inner class即静态内部类,它只会出现在类的内部,在某个类中写一个静态内部类其实同你在IDE里新建一个.java 文件是完全一样的。
此处输入图片的描述
可以看到,在生命周期中,埋下了内存泄漏的隐患,如果它的生命周期比activity更长,那么可能会发生泄露,更可怕的是,有可能会产生难以预防的空指针问题。这个泄露的例子,详见内存管理(2)的文章
(2). static inner method
静态内部方法,也就是虚函数:可以被直接调用,而不用去依赖它所在的类,比如你需要随机数,只用调用Math.random()即可,而不用实例化Math这个对象。在工具类(Utils)中,建议用static修饰方法。static方法的调用不会泄露内存。
(3). static inner variable
慎重使用静态变量,静态变量是被分配给当前的Class的,由类的所有实例共享,而不是一个独立的实例,当ClassLoader停止加载这个Class时,它才会回收。在Android中,需要手动置空才会卸掉ClassLoader,才能出现GC。
当你旋转屏幕后,Drawable就会泄露。
匿名内部类实际上就是non-static inner class,所以也会有non-static inner class的缺点##
单例模式(Singleton)是不是内存泄漏?##
在单例模式中,只有一个对象被产生,看起来一直占用了内存,但是这个不意味就是浪费了内存,内存本来就是用来装东西的,只要这个对象一直被高效的利用就不能叫做泄露。但是也不要偷懒,一个劲的全整成了单例,越多的单例会让内存占用过多,放在Application中初始化的内容也越多,意味着APP打开白屏的时间会更久,而且软件维护起来也变得复杂。
好的例子:GlobalContext,SmsReceiver动态注册,EventBus
为什么大神喜欢用static final来修饰常数?##
static由于是所有实例共享的,说到共享一定要加锁,万一某个实例更改它后,其它的实例也会受到影响,所以加入final作为永久只读锁以防止常数被修改。
下面的话什么意思,看不懂???
全局变量生命周期是classloader,有坑。你的activity在finish后变量并不会改变。这个在面试中经常遇到,问你经过多次计算后,static的值是多少。比如在Android中有个坑,最常见的就是把一个sharedpreference赋值给一个static变量,然后又把sharedpreference改变后,再次调用这个static变量,就发现变量并没有改变,这个在debug中很难发现。
顺便说下final吧##
final 变量:是只读的;
final 方法:是不能继承或者重写的。
final 引用:引用不能修改,但是对象本身的属性可以修改;
final class:不可继承;
final不会让代码速度更快
Bitmap的使用##
使用前注意配置Bitmap的Config,比如长宽,参数(565, 8888),格式;
使用中注意缓存;
使用后注意recycle以清理native层的内存。
2.3以后的bitmap不需要手动recycle了,内存已经在java层了。同时,Bitmap还有别人做好的轮子,比如PhotoView,Picasso,就可以方便的解决OOM问题。
线程泄露可能是最严重的泄露问题##
例如在activity中开了一个线程去上传图片,完成之后弹出toast,但是在还没有上传完成之前,点击了推出了activity,注意上传线程是还在跑,当上传完成之后,却发现window没了,toast弹不出所以抛出异常
所以应该在activity退出的时候把上传线程也要停止了。
Context与ApplicationContext##
Context的生命周期是一个Activiy,而ApplicationContext的生命周期是整个程序。我们最要注意的就是Context的内存泄露。
在Activiy的UI中要使用Context,而在其他的地方比如数据库、网络、系统服务的需要频繁调用Context的情况时,要使用ApplicationContext,以防止内存泄露。
为什么ApplicationContext不会内存泄漏???
其他的小技巧##
1、Listview的item泄露###
这个是入门问题了,加入ViewHolder可以减少findViewById的时间,或者使用RecyclerView,来解决“滑动很卡”的问题。这个实质也是一个单例。
2、StringBuilder###
首先说一下String,StringBuilder,StringBuffer的区别:
- 字符串是不可变的,因为字符串都是常量!如果你试着改变它们的值,另一个对象被创建,而StringBuffer和StringBuilder是可变的,这样他们就可以改变它们的值
- StringBuffer是线程安全的。当应用程序只需要运行在单个线程则最好使用StringBuilder。StringBuilder比StringBuffer更有效率,因为StringBuffer其实是给StringBuilder加了同步锁;其实你使用Log.d(TAG,"xx"+"yy")这类写法后,编译器生成的代码已经自动帮你变成StringBuilder了
StringBuffer原理分析:###
将字符串拼接时(不管是字面常量也好,或者是变量,方法调用的结果也好),即用“+”将多个字符串拼接时,实际上都是变成StringBuilder。如果一个字符串(不管是字面常量也好,或者是变量,方法调用的结果也好)
new StringBuilder().append( string_exp ).append( any_exp ).toString()
如果表达式里有多个+号的话,后面相应也会多多几个StringBuilder.append的调用,最后才是toString方法。
StringBuilder(String)这个构造方法会分配一块16个字符的内存缓冲区。因此,如果后面拼接的字符不超过16的话,StringBuilder不需要再重新分配内存,不过如果超过16个字符的话StringBuilder会扩充自己的缓冲区。最后调用toString方法的时候,会拷贝StringBuilder里面的缓冲区,新生成一个String对象返回。
所以在在我们经常将一些基本数据类型转化成字符串时,例如经常是这样做的:String text=100+"";
虽然可以将整数100转化成“100”字符串,但是一个StringBuilder对象,一个char[16]数组,一个String对象,一个能把输入值存进去的char[]数组。这样是很浪费内存的,所以推荐使用String.valueOf,即String text=String.valueOf(100);
这样至少StringBuilder对象省掉了。
有的时候或许你根本就不需要转化基础类型。比如,你正在解析一个字符串,它是用单引号分隔开的。最初你可能是这么写的:
final int nextComma = str.indexOf("'");
或者是这样
final int nextComma = str.indexOf('\'');
- 同时,使用字符串进行逻辑运算是相当缓慢的,,不建议,因为JVM将字符串转换为字节码的StringBuffer。浪费大量的开销将从字符串转换为StringBuffer然后再返回字符串
综上所述:尽量使用StringBuilder,而不用String来累加字符串
多用基本类型##
使用int而不用Integer,较少的对象花销。在Android中使用sparseArrayMap取代HashMap就是把key变成了int,而一定程度上减小了内存占用。
Native代码不受GC控制##
使用弱引用##
使用弱引用可以防止一定程度的无意引用造成的泄露,比如在Handler中使用弱引用作为参数,当销毁的时候就有可能不会发生泄露。
但是弱引用随时可能为null,使用前需要判断是否为空