内存泄露的根本原因
当一个对象处于可以被回收状态时,却因为该对象被其他暂时不可被回收的对象持有引用,而导致不能被回收,如此一来,该对象所占用的内存被回收以作他用,这部分内存就算是被泄露掉了。简单来说,就是该丢掉的垃圾还占着有用的空间没有被及时丢掉
内存泄露示例
最近在项目中遇到一个有点“隐蔽”的内存泄露,是通过集成的内存泄露检测工具 LeakCanary 检测出来的。我们的项目中使用了第三方自动更新的sdk,并且写了AutoUpdateManager这个单例的类来统一管理自动更新相关功能。自动更新方法的代码如下:
public Observable<UpdateVersionInfo> checkUpdate(final Context context, final boolean bForce) {
return NetManager.getInstance().getCheckVersionUpdateObservable()
.observeOn(Schedulers.newThread())
.flatMap(new Func1<CheckVersionUpdateRsp, Observable<UpdateVersionInfo>>() {
@Override
public Observable<UpdateVersionInfo> call(CheckVersionUpdateRsp rsp) {
if (rsp.isNeedUpdate()) {
return getUpdateVersionInfo(context, bForce);
} else {
// 云校园后台禁止检查更新
return Observable.empty();
}
}
}).observeOn(AndroidSchedulers.mainThread());
}
private Observable<UpdateVersionInfo> getUpdateVersionInfo(final Context context, final boolean bForce) {
return Observable.create(new Observable.OnSubscribe<UpdateVersionInfo>() {
@Override
public void call(final Subscriber<? super UpdateVersionInfo> subscriber) {
IFlytekUpdateListener updateListener = new IFlytekUpdateListener() {
@Override
public void onResult(int updateStatus, UpdateInfo updateInfo) {
if (hasUpdate && bForce) {
// 当前缓存有更新信息且无需考虑是否wifi,则直接将缓存的更新信息返回
subscriber.onNext(updateVersionInfo);
subscriber.onCompleted();
return;
}
if (updateStatus == UpdateErrorCode.OK && updateInfo != null && !(updateInfo.getUpdateType() == UpdateType.NoNeed)) {
//弹出更新dialog
hasUpdate = true;
AutoUpdateManager.this.updateInfo = updateInfo;
AutoUpdateManager.this.updateVersionInfo = new UpdateVersionInfo(updateInfo.getUpdateDetail(), updateInfo.getUpdateVersion());
subscriber.onNext(updateVersionInfo);
subscriber.onCompleted();
} else {
Timber.d("获取更新信息失败或者当前无更新的版本");
hasUpdate = false;
subscriber.onNext(null);
subscriber.onCompleted();
}
}
};
if (bForce) {
// forceUpdate指不区分wifi和移动网络均返回更新信息, 当前只使用这种方式
updateAgent.forceUpdate(context, updateListener);
} else {
updateAgent.autoUpdate(context, updateListener);
}
}
});
}
上面代码中的checkUpdate方法在我们的MainActivity中被如下调用:
AutoUpdateManager.getInstance().checkUpdate(MainApplication.getContext(), true)
.observeOn(AndroidSchedulers.mainThread())
.compose(this.<AutoUpdateManager.UpdateVersionInfo>bindToLifecycle())
.subscribe(new Action1<AutoUpdateManager.UpdateVersionInfo>() {
@Override
public void call(AutoUpdateManager.UpdateVersionInfo updateVersionInfo) {
showUpdateDialog(updateVersionInfo);
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable throwable) {
}
});
代码看起来比较多,另外因为使用了RxJava来写网络回调,并且有所嵌套,所以对象之间的引用不能一下子理清楚。
分析之前,我们需要理解一句话:(非静态)内部类(包括匿名内部类)天然持有自己外部类的(隐式)引用。这句话其实很容易理解,但很多时候会被忽略。在java基础中,我们肯定学过如果A类中有一个内部类C,那么C的对象可以通过new A.C()来获取,所以C天然持有A的引用。只不过我们有时通过自动导入包名后,就不用在前面加上“A.”了。
LeakCanary工具检测到MainActivity有泄露,而且打印出了堆栈信息,分析发现从updateAgent.forceUpdate(context, updateListener)方法进入sdk里面的代码,层层追踪,发现最终传进去的updateListener参数被赋值给一个c类中的c变量,而这个c变量持有MainActivity的引用导致MainActivity的泄露。
public class c implements b {
private HashMap<Long, c.a> a = new HashMap();
private com.iflytek.autoupdate.c.c.c b;
private IFlytekUpdateListener c;
private com.iflytek.autoupdate.a.a d;
private static c e = null;
public static c a(IFlytekUpdateListener var0, com.iflytek.autoupdate.a.a var1) {
if(e == null) {
e = new c(var0, var1);
}
return e;
}
……
// updateListener最终传入了这个方法
public void a(IFlytekUpdateListener var1) {
this.c = var1;
}
}
而很明显,c类是一个单例。
单例的内存泄露是最普遍的,因为单例对象本身是静态的,它的生命周期是跟应用程序的生命周期一样长的,所以单例中的成员变量如果持有了外部对象的引用(诸如Activity之类需要被即时回收的对象)而没有被即使释放,则一定会产生内存泄露的。
那么问题来了,为什么c变量(即updateListener)会持有MainActivity的引用呢?我们回到updateListener被new出来的地方看:
IFlytekUpdateListener updateListener = new IFlytekUpdateListener() {
@Override
public void onResult(int updateStatus, UpdateInfo updateInfo) {
if (hasUpdate && bForce) {
// 当前缓存有更新信息且无需考虑是否wifi,则直接将缓存的更新信息返回
subscriber.onNext(updateVersionInfo);
subscriber.onCompleted();
return;
}
if (updateStatus == UpdateErrorCode.OK && updateInfo != null && !(updateInfo.getUpdateType() == UpdateType.NoNeed)) {
//弹出更新dialog
hasUpdate = true;
AutoUpdateManager.this.updateInfo = updateInfo;
AutoUpdateManager.this.updateVersionInfo = new UpdateVersionInfo(updateInfo.getUpdateDetail(), updateInfo.getUpdateVersion());
subscriber.onNext(updateVersionInfo);
subscriber.onCompleted();
} else {
Timber.d("获取更新信息失败或者当前无更新的版本");
hasUpdate = false;
subscriber.onNext(null);
subscriber.onCompleted();
}
}
};
我们发现在updateListener的回调方法中,它引用了subscriber,而这个subscriber是RxJava中的观察者(Subscriber本身继成于Rx中的Observer)。在MainActivity中我们调用checkUpdate方法,订阅了此方法返回的被观察者(Observable),通过subscribe方法传进去了一个观察者Subscriber,当然这里的写法没有直接new一个Subscriber,而是new了两个Action1对象,其实在subscribe方法中,通过这两个对象,创建出了Subscriber的一个子类:
/**
* A Subscriber that forwards the onXXX method calls to callbacks.
* @param <T> the value type
*/
public final class ActionSubscriber<T> extends Subscriber<T> {
final Action1<? super T> onNext;
final Action1<Throwable> onError;
final Action0 onCompleted;
public ActionSubscriber(Action1<? super T> onNext, Action1<Throwable> onError, Action0 onCompleted) {
this.onNext = onNext;
this.onError = onError;
this.onCompleted = onCompleted;
}
@Override
public void onNext(T t) {
onNext.call(t);
}
@Override
public void onError(Throwable e) {
onError.call(e);
}
@Override
public void onCompleted() {
onCompleted.call();
}
}
而这个ActionSubscriber也最终被传入Observable.OnSubscribe类的回调call方法中,即上面提到的updateListener对象的回调方法里所引用的subscriber对象。最终通过层层传递,MainActivity的引用被sdk的内部单例c类所持有了。而且sdk没有提供释放引用的方法,于是导致了MainActivity的内存泄露。
那么可能有人同我一样会产生疑问,在Activity中我们经常对各种view设置点击事件,View.OnClickListener()其实就是Activity的匿名内部类,它也持有了Activity的引用,而我们也从来没有在Activity销毁之前调用view.setOnClickListener(null)这样的代码。那为什么不会产生内存泄露呢?
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// do something
}
});
那是因为Activity中的View都是附着在Activity自身的视图中的,当Activity销毁时,ActivityManager会自动销毁相关的视图,View也随之销毁,自然不会再持有Activity的引用了。
如何防止内存泄露
管理好对象的生命周期,及时释放掉无用的对象。比如我们在Activity启动时注册了一个BroadcastReceiver,但在Activity销毁时没有及时进行反注册,程序就会打印出错误的log提示。
E/ActivityThread: Activity holenzhou.com.aboutfragment.SecondActivity has leaked IntentReceiver holenzhou.com.aboutfragment.TestReceiver@7e54937 that was originally registered here. Are you missing a call to unregisterReceiver()?
还有我们最常用的Handler,有时编译器会提示我们Handler可能会造成内存泄露,建议我们将它声明为静态内部类,同时持有外部Activity的弱引用。
/**
* Instances of static inner classes do not hold an implicit
* reference to their outer class.
*/
private static class MyHandler extends Handler {
private final WeakReference<SampleActivity> mActivity;
public MyHandler(SampleActivity activity) {
mActivity = new WeakReference<SampleActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
SampleActivity activity = mActivity.get();
if (activity != null) {
// ...
}
}
}
private final MyHandler mHandler = new MyHandler(this);
但其实有更简单的解决方法,就是在Activity的onStop方法中,通过下面的api将Handler相关的所有的Callbacks和Messages移除掉,只不过这种方式很容易被忘记。类似的还有Android的Timer类。
handler.removeCallbacksAndMessages(null);
有些我们自定义的对象需要我们自己手动释放相关引用。比如我们定义了一些单例对象后,持有了外部Activity的引用,就需要在适当的时候释放相关的引用。
还有我们经常见到的网络请求框架里面都会有一个cancel方法取消发送除去的网路请求,其实也是为了方便我们在Activity销毁前及时移除网络请求的异步回调,防止造成内存泄露。