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分析步驟如下:

  1. 啟動應用,然后按返回鍵退出應用。
  2. Android MonitorMemory Monitor界面點擊Initate GC
  3. 點擊Dump Java Heap生成hprof文件。生成完畢Android Studio會自動打開。
  4. 選擇Package Tree View視圖,并點擊Class Name按升序排序,這樣可以迅速找到要分析的程序的包名。
  5. 發現MainActivity的實例個數為1,即沒有被GC回收,發生內存泄露。

分析

為什么會出現內存泄露?從HPROF ViewerReference Tree來看,
GC Root(Depth為0)是LocationManager的內部類GnssStatusListenerTransport成員mGnssHandlermGnssHandlerGnssStatusListenerTransport的內部類GnssHandler的實例,所以mGnssHandler隱式持有外部類GnssStatusListenerTransport實例的引用,而GnssStatusListenerTransport的成員mGnssNmeaListener又指向了MainActivityOnNmeaMessageListener匿名內部類實例,從而導致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中又持有LocationManagerServiceProxy對象,這樣LocationManagerLocationManagerService就可以雙向通信。

這里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);
}

可以看到在GnssStatusListenerTransportIGnssStatusListener的接口實現里,主要是將LocationManagerService回傳的事件通過mGnssHandler進行異步轉發。mGnssHandlerGnssStatusListenerTransport的內部類GnssHandler的實例,它在GnssStatusListenerTransport的構造函數中被創建。這里我們注意到,mGnssHandler的構造與外部傳入的Handler對象有關。如果外部傳入了Handler對象,則mGnssHandler綁定到外部傳入的Handler對象所綁定的消息隊列,如果外部傳入的Handler對象為null,則mGnssHandler綁定到調用addNmeaListener方法所在的線程的消息隊列。接下來,我們看GnssHandlerhandleMessage實現,這里的實現比較簡單,就直接將事件通知給對應的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泄露。

思考

分析到這里就完了嗎?顯然不是。我們可以進一步思考:

  1. mGnssHandler投遞的那些可能造成內存泄露的Message也沒有使用delay的方式投遞,也就是說,Activity退出過不了多久,這些Message就會被處理完,即 內存泄露一段時間后就會恢復正常,MainActivity又可以給被回收了。但事實確實如此嗎?通過測試我們可以發現,即使過了很長時間,MainActivity依然回收不了。

  2. 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
  1. Handler內存泄露的問題早在2009年就被提出來了,為什么現在Android發展到了7.0,還會出現這種問題。我們查看Android源代碼中LocationManager的提交歷史,發現GnssHandler的機制(或者類似機制)在LocationManager內部已歷經幾個Android版本,難道就一直沒人發現這個問題嗎?我們在Google Issue Tracker中搜索LocationManager leak,發現確實有一些相關的issue,但這些issue不知道為何最后要么不了了之,要么被Google沒有任何解釋就直接關閉了。
  2. 我們在Android的源碼里搜索Handler,看Android內置程序如何使用Handler時會發現,在Android源碼內部有些地方處理了泄露,如TV內部使用WeakHandler,有些地方沒有處理泄露,即在Android源碼內部對Handler的處理并未統一。
  3. 另外我們在StackOverflow上發現這篇帖子,于是嘗試將程序改為:
// MainActivity
@Override
  protected void onResume() {
      super.onResume();
      registerNmeaListener();
  }

  @Override
  protected void onPause() {
      super.onPause();
      unregisterNmeaListener();
  }

竟然意外的發現內存泄露消失了。但是我們知道按返回建退出應用時,onPauseonStoponDestroy是在同一個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

使用SoftReferenceApplication 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采用了設計模式里的裝飾模式,ContextImplContextWrapper(Activity和Application的父類)真正做事情的類,同時ContextImpl內部持有Out Context,即ContextWrapper的引用。從SystemServiceRegistry可以看出,LocationManager與ContextWrapper是一一對應的關系,即使用LocationManager的每一個Activity都會創建一個LocationManager的實例。在我們這個場景中LocationManager持有ContextImpl的引用,ContextImpl持有Out Context,即MainActivityApplication的引用,從而導致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方法即可。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容