LocationManager内存泄露
最近在做一个项目的内存优化时,偶然发现一个以前没有注意到的问题,LocationManager引起内存泄露,于是就想探究下泄露的Root Cause并整理出来,希望其他开发人员使用时也能够注意。
问题
我们先看下面的示例代码(Android 7.0):
// MainActivity.java
@Override
protected void onStart() {
super.onStart();
registerNmeaListener();
}
@Override
protected void onStop() {
super.onStop();
unregisterNmeaListener();
}
private void registerNmeaListener() {
if (mOnNmeaMessageListener == null) {
mOnNmeaMessageListener = new OnNmeaMessageListener() {
@Override
public void onNmeaMessage(String message, long timestamp) {
}
};
mLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
mLocationManager.addNmeaListener(mOnNmeaMessageListener);
}
}
private void unregisterNmeaListener() {
if (mOnNmeaMessageListener != null) {
mLocationManager.removeNmeaListener(mOnNmeaMessageListener);
mOnNmeaMessageListener = null;
}
}
这是一段很常规的Android代码,我们在项目中通常都会这么实现。但如果使用Memory Monitor来查看堆内存,就发现会有内存泄露。
使用Memory Monitor
分析步骤如下:
- 启动应用,然后按
返回
键退出应用。 - 在
Android Monitor
的Memory Monitor
界面点击Initate GC
。 - 点击
Dump Java Heap
生成hprof
文件。生成完毕Android Studio
会自动打开。 - 选择
Package Tree View
视图,并点击Class Name
按升序排序,这样可以迅速找到要分析的程序的包名。 - 发现
MainActivity
的实例个数为1,即没有被GC回收,发生内存泄露。
分析
为什么会出现内存泄露?从HPROF Viewer的Reference Tree
来看,
GC Root(Depth为0)是LocationManager
的内部类GnssStatusListenerTransport
成员mGnssHandler
,mGnssHandler
是GnssStatusListenerTransport
的内部类GnssHandler
的实例,所以mGnssHandler
隐式持有外部类GnssStatusListenerTransport
实例的引用,而GnssStatusListenerTransport
的成员mGnssNmeaListener
又指向了MainActivity
的OnNmeaMessageListener
匿名内部类实例,从而导致MainActivity
泄露。简单概括就是:mGnssHandler->GnssStatusListenerTransport->mGnssNmeaListener->MainActivity
。
我们可以来看一下LocationManager的源码,位于$SOURCEROOT/frameworks/base/location/java/android/location/LocationManager.java:
private final HashMap<OnNmeaMessageListener, GnssStatusListenerTransport> mGnssNmeaListeners =
new HashMap<>();
/**
* Adds an NMEA listener.
*
* @param listener a {@link OnNmeaMessageListener} object to register
*
* @return true if the listener was successfully added
*
* @throws SecurityException if the ACCESS_FINE_LOCATION permission is not present
*/
@RequiresPermission(ACCESS_FINE_LOCATION)
public boolean addNmeaListener(OnNmeaMessageListener listener) {
return addNmeaListener(listener, null);
}
/**
* Adds an NMEA listener.
*
* @param listener a {@link OnNmeaMessageListener} object to register
* @param handler the handler that the listener runs on.
*
* @return true if the listener was successfully added
*
* @throws SecurityException if the ACCESS_FINE_LOCATION permission is not present
*/
@RequiresPermission(ACCESS_FINE_LOCATION)
public boolean addNmeaListener(OnNmeaMessageListener listener, Handler handler) {
boolean result;
if (mGpsNmeaListeners.get(listener) != null) {
// listener is already registered
return true;
}
try {
GnssStatusListenerTransport transport =
new GnssStatusListenerTransport(listener, handler);
result = mService.registerGnssStatusCallback(transport, mContext.getPackageName());
if (result) {
mGnssNmeaListeners.put(listener, transport);
}
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
return result;
}
/**
* Removes an NMEA listener.
*
* @param listener a {@link OnNmeaMessageListener} object to remove
*/
public void removeNmeaListener(OnNmeaMessageListener listener) {
try {
GnssStatusListenerTransport transport = mGnssNmeaListeners.remove(listener);
if (transport != null) {
mService.unregisterGnssStatusCallback(transport);
}
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
添加listener的时候会先判断下是否已经添加过,如果添加过了,就直接返回,如果没有,则使用传入的OnNmeaMessageListener对象和Handler对象构造一个对应的GnssStatusListenerTransport
对象,并将其注册到LocationManagerService
端,同时记录在本地mGnssNmeaListeners
表示的HashMap中。
移除listener时,先将listener从本地HashMap中移除,同时将其从LocationManagerService
注销掉。
注册到LocationManagerService
和从LocationManagerService
注销的过程与我们这里分析的问题关联不大,所以就不分析了。我们主要来看GnssStatusListenerTransport
类。Android 7.0对LocationManager做了较大改动,主要是增加了对GPS以外的其他卫星定位系统的支持,统称为GNSS(Global Navigation Satellite System)。这里为了流程清晰,我们把7.0兼容之前老版本的代码删除了。
// This class is used to send Gnss status events to the client's specific thread.
private class GnssStatusListenerTransport extends IGnssStatusListener.Stub {
private final GnssStatus.Callback mGnssCallback;
private final OnNmeaMessageListener mGnssNmeaListener;
private class GnssHandler extends Handler {
public GnssHandler(Handler handler) {
super(handler != null ? handler.getLooper() : Looper.myLooper());
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case NMEA_RECEIVED:
synchronized (mNmeaBuffer) {
int length = mNmeaBuffer.size();
for (int i = 0; i < length; i++) {
Nmea nmea = mNmeaBuffer.get(i);
mGnssNmeaListener.onNmeaMessage(nmea.mNmea, nmea.mTimestamp);
}
mNmeaBuffer.clear();
}
break;
case GpsStatus.GPS_EVENT_STARTED:
mGnssCallback.onStarted();
break;
case GpsStatus.GPS_EVENT_STOPPED:
mGnssCallback.onStopped();
break;
case GpsStatus.GPS_EVENT_FIRST_FIX:
mGnssCallback.onFirstFix(mTimeToFirstFix);
break;
case GpsStatus.GPS_EVENT_SATELLITE_STATUS:
mGnssCallback.onSatelliteStatusChanged(mGnssStatus);
break;
default:
break;
}
}
}
private final Handler mGnssHandler;
// This must not equal any of the GpsStatus event IDs
private static final int NMEA_RECEIVED = 1000;
private class Nmea {
long mTimestamp;
String mNmea;
Nmea(long timestamp, String nmea) {
mTimestamp = timestamp;
mNmea = nmea;
}
}
private final ArrayList<Nmea> mNmeaBuffer;
GnssStatusListenerTransport(GnssStatus.Callback callback) {
this(callback, null);
}
GnssStatusListenerTransport(GnssStatus.Callback callback, Handler handler) {
mOldGnssCallback = null;
mGnssCallback = callback;
mGnssHandler = new GnssHandler(handler);
mOldGnssNmeaListener = null;
mGnssNmeaListener = null;
mNmeaBuffer = null;
mGpsListener = null;
mGpsNmeaListener = null;
}
GnssStatusListenerTransport(OnNmeaMessageListener listener) {
this(listener, null);
}
GnssStatusListenerTransport(OnNmeaMessageListener listener, Handler handler) {
mOldGnssCallback = null;
mGnssCallback = null;
mGnssHandler = new GnssHandler(handler);
mOldGnssNmeaListener = null;
mGnssNmeaListener = listener;
mGpsListener = null;
mGpsNmeaListener = null;
mNmeaBuffer = new ArrayList<Nmea>();
}
@Override
public void onGnssStarted() {
if (mGpsListener != null) {
Message msg = Message.obtain();
msg.what = GpsStatus.GPS_EVENT_STARTED;
mGnssHandler.sendMessage(msg);
}
}
@Override
public void onGnssStopped() {
if (mGpsListener != null) {
Message msg = Message.obtain();
msg.what = GpsStatus.GPS_EVENT_STOPPED;
mGnssHandler.sendMessage(msg);
}
}
@Override
public void onFirstFix(int ttff) {
if (mGpsListener != null) {
mTimeToFirstFix = ttff;
Message msg = Message.obtain();
msg.what = GpsStatus.GPS_EVENT_FIRST_FIX;
mGnssHandler.sendMessage(msg);
}
}
@Override
public void onSvStatusChanged(int svCount, int[] prnWithFlags,
float[] cn0s, float[] elevations, float[] azimuths) {
if (mGnssCallback != null) {
mGnssStatus = new GnssStatus(svCount, prnWithFlags, cn0s, elevations, azimuths);
Message msg = Message.obtain();
msg.what = GpsStatus.GPS_EVENT_SATELLITE_STATUS;
// remove any SV status messages already in the queue
mGnssHandler.removeMessages(GpsStatus.GPS_EVENT_SATELLITE_STATUS);
mGnssHandler.sendMessage(msg);
}
}
@Override
public void onNmeaReceived(long timestamp, String nmea) {
if (mGnssNmeaListener != null) {
synchronized (mNmeaBuffer) {
mNmeaBuffer.add(new Nmea(timestamp, nmea));
}
Message msg = Message.obtain();
msg.what = NMEA_RECEIVED;
// remove any NMEA_RECEIVED messages already in the queue
mGnssHandler.removeMessages(NMEA_RECEIVED);
mGnssHandler.sendMessage(msg);
}
}
}
GnssStatusListenerTransport继承自IGnssStatusListener.Stub
,熟悉Binder
机制的同学都知道,Stub
类展开之后的形式是Stub extends Binder(implements IBinder) implements IGnssStatusListener(implements IInterface)
,它是Binder
通信的本地对象,将一个Binder
本地对象传给另一个进程,另一个进程会拿到一个Binder
通信的Proxy
对象,这样另一个进程就可以通过Proxy
对象调用本地对象的方法了,而LocationManager中又持有LocationManagerService
的Proxy
对象,这样LocationManager和LocationManagerService
就可以双向通信。
这里IGnssStatusListener
主要提供了5个方法供LocationManagerService
回调以便通知相应的GNSS事件,其源码位于$SOURCEROOT/frameworks/base/location/java/android/location/IGnssStatusListener.aidl:
/*
* Copyright (C) 2008, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.location;
import android.location.Location;
/**
* {@hide}
*/
oneway interface IGnssStatusListener
{
void onGnssStarted();
void onGnssStopped();
void onFirstFix(int ttff);
void onSvStatusChanged(int svCount, in int[] svidWithFlags, in float[] cn0s,
in float[] elevations, in float[] azimuths);
void onNmeaReceived(long timestamp, String nmea);
}
可以看到在GnssStatusListenerTransport
对IGnssStatusListener
的接口实现里,主要是将LocationManagerService
回传的事件通过mGnssHandler
进行异步转发。mGnssHandler
是GnssStatusListenerTransport
的内部类GnssHandler
的实例,它在GnssStatusListenerTransport
的构造函数中被创建。这里我们注意到,mGnssHandler
的构造与外部传入的Handler
对象有关。如果外部传入了Handler
对象,则mGnssHandler
绑定到外部传入的Handler
对象所绑定的消息队列,如果外部传入的Handler
对象为null
,则mGnssHandler
绑定到调用addNmeaListener
方法所在的线程的消息队列。接下来,我们看GnssHandler
的handleMessage实现,这里的实现比较简单,就直接将事件通知给对应的listener。
看到这里,估计大家也发现了,这里的mGnssHandler
可能会引发内存泄露,因为在调用LocationManager.removeNmeaListener时并没有任何清除与mGnssHandler
关联的Message
的操作。Handler可能引起内存泄露请参考http://android-developers.blogspot.com/2009/01/avoiding-memory-leaks.html。
对应到我们当前的场景,发生泄露的一个可能情况是LocationManagerService
不断给GnssStatusListenerTransport
对象发送信息,这些信息被mGnssHandler
封装成Message
投递到mGnssHandler
绑定的消息队列里,这里就是主线程消息队列,当我们在主线程调用LocationManager.removeNmeaListener方法时,mGnssHandler
可能已经往主线程的消息队列里投递了N多个消息。也就是说在主线程的消息队列里面,Activity.onDestroy的消息后面有很多mGnssHandler
投递的消息。这些mGnssHandler
投递的位于Activity.onDestroy之后的消息如果在Activity退出时没有被清除的话,就会发生Activity退出了,但是引用链Message->mGnssHandler->GnssStatusListenerTransport->mGnssNmeaListener->MainActivity
还在,导致MainActivity
泄露。
思考
分析到这里就完了吗?显然不是。我们可以进一步思考:
-
mGnssHandler
投递的那些可能造成内存泄露的Message也没有使用delay的方式投递,也就是说,Activity退出过不了多久,这些Message就会被处理完,即 内存泄露一段时间后就会恢复正常,MainActivity
又可以给被回收了。但事实确实如此吗?通过测试我们可以发现,即使过了很长时间,MainActivity
依然回收不了。 -
Memory Monitor
显示GC Root是mGnssHandler
,所以很可能不是Message->mGnssHandler
导致泄露。Android中的GC Root主要包括如下几类,可以参考,mGssHandler
属于哪一类?如果有人知道,也请告诉我一下(参考2017/07/02更新)。
- references on the stack
- Java Native Interface (JNI) native objects and memory
- static variables and functions
- threads and objects that can be referenced
- classes loaded by the bootstrap loader
- finalizers and unfinalized objects
- busy monitor objects
- Handler内存泄露的问题早在2009年就被提出来了,为什么现在Android发展到了7.0,还会出现这种问题。我们查看Android源代码中LocationManager的提交历史,发现
GnssHandler
的机制(或者类似机制)在LocationManager内部已历经几个Android版本,难道就一直没人发现这个问题吗?我们在Google Issue Tracker中搜索LocationManager leak,发现确实有一些相关的issue,但这些issue不知道为何最后要么不了了之,要么被Google没有任何解释就直接关闭了。 - 我们在Android的源码里搜索Handler,看Android内置程序如何使用Handler时会发现,在Android源码内部有些地方处理了泄露,如TV内部使用WeakHandler,有些地方没有处理泄露,即在Android源码内部对Handler的处理并未统一。
- 另外我们在StackOverflow上发现这篇帖子,于是尝试将程序改为:
// MainActivity
@Override
protected void onResume() {
super.onResume();
registerNmeaListener();
}
@Override
protected void onPause() {
super.onPause();
unregisterNmeaListener();
}
竟然意外的发现内存泄露消失了。但是我们知道按返回
建退出应用时,onPause,onStop和onDestroy是在同一个Message里处理的。具体可以参见ActivityThread.performDestroyActivity
,源码位于$SOURCEROOT/frameworks/base/core/java/android/app/ActivityThread.java:
private ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing,
int configChanges, boolean getNonConfigInstance) {
ActivityClientRecord r = mActivities.get(token);
Class<? extends Activity> activityClass = null;
if (localLOGV) Slog.v(TAG, "Performing finish of " + r);
if (r != null) {
activityClass = r.activity.getClass();
r.activity.mConfigChangeFlags |= configChanges;
if (finishing) {
r.activity.mFinished = true;
}
performPauseActivityIfNeeded(r, "destroy");
if (!r.stopped) {
try {
r.activity.performStop(r.mPreserveWindow);
} catch (SuperNotCalledException e) {
throw e;
} catch (Exception e) {
if (!mInstrumentation.onException(r.activity, e)) {
throw new RuntimeException(
"Unable to stop activity "
+ safeToComponentShortString(r.intent)
+ ": " + e.toString(), e);
}
}
r.stopped = true;
EventLog.writeEvent(LOG_AM_ON_STOP_CALLED, UserHandle.myUserId(),
r.activity.getComponentName().getClassName(), "destroy");
}
if (getNonConfigInstance) {
try {
r.lastNonConfigurationInstances
= r.activity.retainNonConfigurationInstances();
} catch (Exception e) {
if (!mInstrumentation.onException(r.activity, e)) {
throw new RuntimeException(
"Unable to retain activity "
+ r.intent.getComponent().toShortString()
+ ": " + e.toString(), e);
}
}
}
try {
r.activity.mCalled = false;
mInstrumentation.callActivityOnDestroy(r.activity);
if (!r.activity.mCalled) {
throw new SuperNotCalledException(
"Activity " + safeToComponentShortString(r.intent) +
" did not call through to super.onDestroy()");
}
if (r.window != null) {
r.window.closeAllPanels();
}
} catch (SuperNotCalledException e) {
throw e;
} catch (Exception e) {
if (!mInstrumentation.onException(r.activity, e)) {
throw new RuntimeException(
"Unable to destroy activity " + safeToComponentShortString(r.intent)
+ ": " + e.toString(), e);
}
}
}
mActivities.remove(token);
StrictMode.decrementExpectedActivityCount(activityClass);
return r;
}
既然是在同一个Message里处理,那为何在onPause中移除listener不会造成泄露,而在onStop中移除listener就会造成内存泄露?
解决
虽然现在我们还有很多暂时无法解答的问题,但对出现的问题我们还是有要解决方案。
在onPause移除listener
从之前分析来看,如果业务满足在onPause中移除listener的情况则可以使用此方法完美解决。若业务不满足在onPause中移除listener的情况(因为进入onPause时Activity可能没有被完全遮挡,所以底层视图还是需要更新,因此对于这种情况,我们不能移除listener),我们只能采用work around的方式。
反射
可以通过反射拿到mGnssHandler
的引用,然后移除所有相关的消息,并将GnssStatusListenerTransport
内部mGnssNmeaListener
的引用置null
。
使用SoftReference和Application Context
我们可以实现一个足够小的静态的OnNmeaMessageListener内部类并持有外部MainActivity
的软引用,然后将OnNmeaMessageListener接收到的所有事件转发给外部的MainActivity
来处理,以保持内部类足够小。
使用SoftReference的方式很好理解,但为什么要同时使用Application Context呢?这是因为GnssStatusListenerTransport无法被回收会导致LocationManager无法回收,而LocationManager持有调用getSystemService的调用者的Context。在我们这个场景中就是MainActivity
,因此还是会导致MainActivity
泄露。使用Application Context使得整个应用只会构造一个LocationManager,这点可以从SystemServiceRegistry
源码来看,源码位于$SOURCEROOT/frameworks/base/core/java/android/app/SystemServiceRegistry.java:
registerService(Context.LOCATION_SERVICE, LocationManager.class,
new CachedServiceFetcher<LocationManager>() {
@Override
public LocationManager createService(ContextImpl ctx) {
IBinder b = ServiceManager.getService(Context.LOCATION_SERVICE);
return new LocationManager(ctx, ILocationManager.Stub.asInterface(b));
}});
我们知道Context采用了设计模式里的装饰模式,ContextImpl
是ContextWrapper
(Activity和Application的父类)真正做事情的类,同时ContextImpl
内部持有Out Context,即ContextWrapper
的引用。从SystemServiceRegistry可以看出,LocationManager与ContextWrapper
是一一对应的关系,即使用LocationManager的每一个Activity都会创建一个LocationManager的实例。在我们这个场景中LocationManager持有ContextImpl的引用,ContextImpl持有Out Context,即MainActivity
或Application
的引用,从而导致MainActivity
泄露,而统一使用Application Context则不会有这个问题。
好了,到此我们整个LocationManager泄露的问题就说完了。对于前面还无法的解答的问题,我会继续分析。如果有人知道答案,也请告诉我一声。
Updated 2017/07/02
当我们使用上述反射机制解决了MainActivity
的内存泄露时,android.location
包的内存泄露还是存在的。此时我们通过Memory Monitor
来进一步分析android.location
包的堆内存情况,如下图:
GC Root是GnssStatusListenerTransport
,并且除了FinalizerReference
,没有其他引用指向它。FinalizerReference
是Android framework的一个隐藏类,主要用来实现Java的finalize机制。所有重写finalize()方法的类对象,最后都会被FinalizerReference类的静态变量引用,所以当它们没有强引用时不会被虚拟机立即回收,而是GC会将这些重写了finalize()方法的对象压入到ReferenceQueue中。同时会有一个守护线程Finalize Daemon
来真正处理调用他们的finalize
函数,实现垃圾回收。所以重写了finalize()方法的类对象需要至少经过两轮GC才有可能被释放,具体释放时机不确定。这与我们前面介绍Android GC Root有一类是finalizers and unfinalized objects
不谋而合。
但是我们在GnssStatusListenerTransport
并没有发现finalize()
被重写,这到底是怎么回事呢?相信大家一定也猜到了:在父类里重写了。我们依次查看GnssStatusListenerTransport
的父类,发现Binder类重写了finalize()
方法:
protected void finalize() throws Throwable {
try {
destroy();
} finally {
super.finalize();
}
}
到此,我们终于找到了LocationManager泄露的Root Cause。Android系统要解决这个问题,可以在GnssStatusListenerTransport
类中添加一个cleanup的方法来清除所有的外部引用,然后在移除listener之后调用一下cleanup方法即可。