在Android
应用程序中,经常会出现内存泄露的问题,可能一两次的启动没有对应用产生什么崩溃呀之类的问题,但是长时间使用或者频繁退出进入使用,因为内存泄露,应用超过了系统允许的内存范围就会出现ANR,卡顿,OOM等问题。有经验和注重性能的程序员们都会在编程过程中,注意代码的编写,避免内存泄漏。这边文章也是我自己边学习边总结边应用。总结下来,给自己解疑,好记性不如烂笔头,用了之后再总结一下,真的更有助于对知识的吸收,更希望能够帮助跟我一样还在学习路上的人解疑,有什么不对的地方,多多提出来哈!开篇啦!
为什么会有内存泄露?
Android
程序中,对象与对象之间存在引用的关系,当一个对象因为被其他对象引用,在用完之后无法GC回收一直存在内存中的时候就会出现内存泄漏,因为对象无法被回收,所以一直存在内存里面,随着使用时间的增长,同一个对象在内存中存在很多对象,占用了应用的内存越来越大。
所以内存泄露的原因就是本该回收的对象没有被GC回收,一直存留在内存中。
从这点我们也可以知道我们的要解决内存泄露的方法就是让应该回收的对象可以正常的被回收。
在这里我们简单的提一下那些对象需要由GC来回收。方法里面的定义的局部变量存储在栈中,变量使用完之后就会自动释放回收变量。而new出来的对象都存放在堆里面,有Java回收器回收。
对象引用
在一起学习什么时候对象之间会存在引用关系之前,首先来将一下Android中四种引用的弱引用,强引用,软引用,虚引用。
弱引用
关键词WeakReference
.定义。弱引用当只有弱引用在引用时,GC会回收该弱引用。强引用
直接new的对象都是强引用,强引用,只有没有对象在引用他的时候,才会释放掉。软引用
关键词SoftReference
定义。软引用的比较少,如果一个对象只具有软引用,那么在内存空间足够时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被使用。虚引用
PhantomReference
引用。
对象的引用这个概念之前也知道一些,但是经过这次对内存泄露的学习之后,对于自己以前不知道的引用的情况有了新的理解。下面我们来一起学习下引用吧。对了,这是基础知识的讲解。对于引用有深刻理解的同仁们可以直接跳下一节。
首先我们首先来看看最简单的引用的情况:
假设有两个类,类A和类B。
① class A {
private B b;
public A() {
b = new B();
}
}
class B{
}
如上述代码所示, 在A里面创建了一个B的实例,并且赋给了b,这样A就引用b。因为b是A的成员变量所以当A释放的时候,b也会被释放。
② 当我们在A中分别定义一个内部静态类和一个内部非静态类,以及添加一个外部接口。RefenrenceListener如下:
interface RefenrenceListener {
void onClick();
}
类A修改成这个样子:
class A{
private C c ;
private D d;
// 非静态匿名内部类会引用外部类
class C {
}
//静态匿名内部类不会引用外部类
static class D{
}
public A(){
c = new C();
//拿有外部类的引用
d = new D(); //不引用外部类
setRefenrenceListener(new RefenrenceListenr(){
void onClick(){
Log.i("doris", "onclick");
} );
} //匿名内部类RefenrenceListener会有外部类的引用
}
/* 静态的匿名内部类成员变量不引用外部类 */
private static RefenrenceListenr mRefenrenceListenr = new RefenrenceListenr() {
@Override
public void onclick() {
}
};
}
/***关于引用关系可以用个方法获取到某个对象的引用情况
用法: getAllFieldName(c.getClass())***/
private void getAllFieldName(Class<?> clazz) {
System.out.println("className: ======> " + clazz.getSimpleName());
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
System.out.println(field.getName());
}
}
从上面总结可以得到:
- 静态匿名内部类成员变量不引用外部类。其他的非静态匿名类成员变量,以及匿名内部类局部变量都会引用外部类。
- 非静态内部类一定引用外部类。
- 静态内部类不引用外部类
- 方法内的局部内部类和匿名内部类都会引用外部类。
- 当某个对象是某个类的成员变量的时候那么就一定引用了这个对象。
从上面我们知道了,对象之间引用关系的形成,但是并不是所有的应用都会引起内存泄露。主要看,引用对象与被引用对象那个生命周期比较长,如果引用对象生命周期长于本来应该释放的对象的话,那就会引起内存泄露。注意这句话,不管是那种引用其实都不是百分之百的会引起内存泄露,只有当引用对象生命周期够长的时候才会引起内存泄露。 曾经以为只要有非静态内部类就一定会引起内存泄露、而且当我知道回调也引用外部类的时候,我开始想那这内存泄露是避免不了了,总不能不用回调吧。是不是很傻。还好学习更多,让我知道了更多。所以记住哦!!!!
常见内存泄露的原因
- 非静态匿名内部类和非静态内部类会引用外部类。
- 回调有可能会引起内存泄露,如果回调对象被静态对象引用或者其他原因引用而无法释放,就会导致内存泄露。
- Dialog有可能引发泄露
- 非静态Handler引用外部类引起内存泄露
- 线程,动画等无限循环执行,引用了需要释放的对象,也会引起内存泄露
- 静态成员集合类和静态View对象 以及静态的非静态成员变量
- 单例类
- 资源未关闭导致的泄露。BroadcastReceiver未解除注册
当然要记住 不是说有强引用就一定会引起内存泄露,关键看可能泄露的对象和引用他的对象那个生命力更能顽强。所以我们要做的就是保证不该被强引用的不被强引用, 被强引用了也不会释放。
经典案例
下面我将举例一些常见的内存泄露的例子,并给出相应的解决方法。
- 最最经典的Handler内存泄露
在程序中,我们通常会在一个类里面定义个内部类Handler用来对消息的处理,从而达到异步处理。如果我们定义一个非静态匿名内部类的话,那么Handler就会持有外部类的引用,假设外部类是Activity。那么就会导致当Activity退出之后,Handler如果还持有消息的话,就会无法释放Activity。所以我们应该定义一个静态的内部Handler类。但是我们要使用外部类的方法以及变量那该怎么办呢,那也好办这个时候就要用到弱引用了。弱引用就是当对象只被弱引用引用的时候,会被GC回收。
代码如下:
static class MyHandler extends WeakHandler<MainActivity> {
public MyHandler(MainActivity owner) {
super(owner);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (getOwner() == null) {
return;
}
if (msg.what == MSG_SHOW_REPLAYEPG_WINDOW) {
if (getOwner().mInfoBar != null) {
getOwner().mInfoBar.dismisss();
}
if (getOwner().mLookBackView == null) {
getOwner().mLookBackView = new LookBackView(getOwner());
getOwner().mLookBackView.setOnScheduleListItemListener(getOwner().mOnListItemClickListener);
}
getOwner().mLookBackView.showLookBackWindow();
}
}
}
其实追其根本,非静态的Handler之所以会导致内存泄漏就是因为,非静态的内部类会引用外部类,当外部类销毁,而Handler仍然有消息要处理,未销毁的时候,外部类就无法销毁而引发了Activity的内存泄漏。所以触类旁通,所有的非静态的内部类如果引用了Activity,都有可能引起内存泄漏,这个时候我们就应该像Handler这样处理,改成静态类并且使用弱引用引用。如果可以当然能不引用,外部类就不引用。
- Thread
在线程中可能发引发内存泄漏的原因有,应用退出之后,线程没有停止,并且引用了资源,导致资源无法释放,从而引起内存泄漏,所以要注意线程的停止以及资源的释放。 这里就不列举代码了。 - 单例
单例中比较容易引发内存泄漏的情况是静态变量引用了本需要被释放掉的资源,比如Activity,导致Activity无法释放。下面举例说明:
public class ServerManager {
public static ServerManager mServerManager;
private Context mContext;
private ServerManager(Context context) {
mContext = context;
}
public static ServerManager getInstance(Context context) {
if (mServerManager == null) {
mServerManager = new ServerManager(context);
}
return mServerManager;
}
}
外部调用:
public class MainActivity extends AppCompatActivity {
private Button mBtnNotification;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ServerManager serverManager = ServerManager.getInstance(this);
}
}
上面的context引用有问题,问题在哪里呢,单例类ServerManager的实例是一个静态变量,然后引用了context。context是一个指向MainActivity类的对象,那么
回调被别的静态对象引用。当MainActivity销毁的时候就出现问题了,由于mServerManager是一个静态变量所以一直存在于内存中,并持有mainactivity的引用,所以MainActvity无法释放。这是一个典型的以及我们很容易出错的地方,但是也是很容易修改的地方,那就是将MainActivity中的this改成
getApplicationContext()
,从而ServerManager就不会影响Activity的释放。
这里也引申除了一个新的点,那就是能用ApplicationContext的地方,千万不要用Activity的context,这样就可以避免不必要的内存泄漏。静态成员变量引用类引发的内存泄漏
静态变量如果引用了资源,也会导致资源无法释放。
总结
其实内存泄漏的本质,就是要释放的对象由于被GC-ROOt引用而导致无法释放,这就有可能引起内存泄漏,之所以说有可能还是因为,也要看要释放的对象和引用他的对象那个对象生命长。如果引用他的GC-root对象的生命长于要释放的对象,那么就会引发内存泄漏。 而解决内存泄漏的方法的本质就是1.避免造成可能引发内存泄漏的情况。2.当不可避免的要引用的时候,记得在使用完之后及时的释放资源,切换引用链中导致内存泄漏的关键点 3.图片资源等用完之后记得及时释放。
这是我自己学习查阅了相关的内存泄漏以及实践之后的一些总结,如果有哪里写的不对的话,欢迎指出,以防他继续遗丑万年。哈哈。
最后贴出我觉得写的很不错的文章,虽然人家已经写的很不错了,但是自己总结一番对自己吸收这个知识是很有帮助的。所以总结的博客 ,不只是为了给更多的人看,其实也是个人知识体系构建的过程。你也可以一起来建立,贵在坚持,简单的一句加油,共勉!
参考链接:
Android内存泄露案例分析