最近在做一個項目的內存優化時,偶然發現一個以前沒有注意到的問題,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方法即可。