LocationManager内存泄露

Posted on Jul 1, 2017

最近在做一个项目的内存优化时,偶然发现一个以前没有注意到的问题,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方法即可。