美文网首页
记一个OOM Case的分析

记一个OOM Case的分析

作者: wbo4958 | 来源:发表于2017-05-30 10:46 被阅读175次

    这篇文章记录一个OOM的case, 在LeanbackLauncher里不停的切换语言会导致OOM

    转载请标明来处: http://www.jianshu.com/p/1c324e766689

    LeanbackLauncher切换语言会触发 updateLocale
    它的具体调用链

    updateLocale
      updateLocales()
        updatePersistentConfiguration
          updateConfigurationLocked
            ensureActivityConfigurationLocked
              relaunchActivityLocked()
    

    relaunchActivityLocked会destroy LeanbackLauncher.MainActivity, 因此可以猜测Activity没有被GC掉。

    MAT分析hprof文件

    通过MAT分析dump出来的hprof文件,发现 NvAccStClient 有多个实例,如下所示

    Class Name                                                                                        | Ref. Objects | Shallow Heap | Ref. Shallow Heap | Retained Heap
    --------------------------------------------------------------------------------------------------------------------------------------------------------------------
                                                                                                      |              |              |                   |              
    class com.nvidia.shieldtech.NvHookHelper @ 0x7290d020 System Class                                |           13 |           24 |             4,368 |           728
    '- mContext com.google.android.leanbacklauncher.LauncherApplication @ 0x12c3ce40                  |           13 |           32 |             4,368 |            80
       '- mLoadedApk android.app.LoadedApk @ 0x12c16900                                               |           13 |          112 |             4,368 |           880
          '- mServices android.util.ArrayMap @ 0x12c3c220                                             |           13 |           32 |             4,368 |           392
             '- mArray java.lang.Object[8] @ 0x12cfb2e0                                               |           13 |           48 |             4,368 |           328
                '- [1] android.util.ArrayMap @ 0x12cd3be0                                             |           13 |           32 |             4,368 |           280
                   '- mArray java.lang.Object[36] @ 0x12e0fba0                                        |           13 |          160 |             4,368 |           160
                      |- [20] com.nvidia.NvAccSt.NvAccStCapture$NvAccStClient$1 @ 0x12d4f2c0          |            1 |           16 |               336 |            16
                      |- [24] com.nvidia.NvAccSt.NvAccStCapture$NvAccStClient$1 @ 0x12e210a0          |            1 |           16 |               336 |            16
                      |- [2] com.nvidia.NvAccSt.NvAccStCapture$NvAccStClient$1 @ 0x12e595c0           |            1 |           16 |               336 |            16
                      |- [6] com.nvidia.NvAccSt.NvAccStCapture$NvAccStClient$1 @ 0x12e5e8a0           |            1 |           16 |               336 |            16
                      |- [12] com.nvidia.NvAccSt.NvAccStCapture$NvAccStClient$1 @ 0x12c50710          |            1 |           16 |               336 |            16
                      |- [16] com.nvidia.NvAccSt.NvAccStCapture$NvAccStClient$1 @ 0x12fb44c0          |            1 |           16 |               336 |            16
                      |- [8] com.nvidia.NvAccSt.NvAccStCapture$NvAccStClient$1 @ 0x12ce7290           |            1 |           16 |               336 |            16
                      |- [22] com.nvidia.NvAccSt.NvAccStCapture$NvAccStClient$1 @ 0x12fe5940          |            1 |           16 |               336 |            16
                      |- [14] com.nvidia.NvAccSt.NvAccStCapture$NvAccStClient$1 @ 0x12f32ef0          |            1 |           16 |               336 |            16
                      |- [10] com.nvidia.NvAccSt.NvAccStCapture$NvAccStClient$1 @ 0x12c342c0          |            1 |           16 |               336 |            16
                      |- [4] com.nvidia.NvAccSt.NvAccStCapture$NvAccStClient$1 @ 0x12e51ea0           |            1 |           16 |               336 |            16
                      |- [0] com.nvidia.NvAccSt.NvAccStCapture$NvAccStClient$1 @ 0x130b8370           |            1 |           16 |               336 |            16
                      |- [18] com.nvidia.NvAccSt.NvAccStCapture$NvAccStClient$1 @ 0x12fe5550          |            1 |           16 |               336 |            16
                      |  '- this$1 com.nvidia.NvAccSt.NvAccStCapture$NvAccStClient @ 0x131027e0       |            1 |           32 |               336 |            88
                      |     '- this$0 com.nvidia.NvAccSt.NvAccStCapture @ 0x131043f8                  |            1 |           40 |               336 |           160
                      |        '- mContext com.android.internal.policy.DecorContext @ 0x12d676d0      |            1 |           48 |               336 |            48
                      |           '- mPhoneWindow com.android.internal.policy.PhoneWindow @ 0x1300fbb0|            1 |          360 |               336 |        15,192
                      '- Total: 13 entries                                                            |              |              |                   |          
    

    可以看出应该是对NvAccStClient的引用导致 LeanbackLauncher.MainActivity 没有被GC.

    原因分析

    通过 openGrok 快速查看 NvAccStClient 的调用关系发现NvAccStClient 是定义在NvAccStCapture里的私有变量。

    NvAccStCapture.java

    private NvAccStClient mNvAccStClient = new NvAccStClient();
                      
    public NvAccStCapture(Context context) {
        mContext = context;
        mNvAccStClient.connect();
    }
    

    NvAccStClient.java

    boolean connect() {
        boolean ret = false;
        Intent intent = new Intent(
                    "com.nvidia.NvAccSt.START_SERVICE");
        intent.setClassName("com.nvidia.NvAccSt",
                            "com.nvidia.NvAccSt.NvAccStService");
        try {
            ret = mContext.bindServiceAsUser(intent, mConnection,
                    Context.BIND_AUTO_CREATE, UserHandle.CURRENT_OR_SELF);
        } catch (SecurityException e) {
            e.printStackTrace();
        }
        return ret;
    }
    

    而 NvAccStCapture 是在ViewRootImpl里初始化的,每一个Activity都对应一个ViewRootImpl, 因此如果NvAccSt被引用了就会导致ViewRootImpl不会被GC,从而导致Activity不能被GC,

    public ViewRootImpl(Context context, Display display) {
        mContext = context;
        if (NvAccStCapture.isEnabled(mContext)) {
            mNvAccStCapture = new NvAccStCapture(mContext);
        } else {
            mNvAccStCapture = null;
        }
    }       
    

    那么问题来了,NvAccSt是怎么被引用的呢?

    回到 NvAccStClient里的 connect函数

    boolean connect() {
        boolean ret = false;
        Intent intent = new Intent(
                    "com.nvidia.NvAccSt.START_SERVICE");
        intent.setClassName("com.nvidia.NvAccSt",
                            "com.nvidia.NvAccSt.NvAccStService");
        try {
            ret = mContext.bindServiceAsUser(intent, mConnection,
                    Context.BIND_AUTO_CREATE, UserHandle.CURRENT_OR_SELF);
        } catch (SecurityException e) {
            e.printStackTrace();
        }
        return ret;
    }
    

    如果 com.nvidia.NvAccSt 这个apk不存在,那么bindServiceAsUser就会 fail,
    接着看下 bindServiceAsUser

        /** @hide */
        @Override
        public boolean bindServiceAsUser(Intent service, ServiceConnection conn, int flags,
                UserHandle user) {
            return bindServiceCommon(service, conn, flags, mMainThread.getHandler(), user);
        }
    
    
        private boolean bindServiceCommon(Intent service, ServiceConnection conn, int flags, Handler
                handler, UserHandle user) {
            IServiceConnection sd;
            if (conn == null) {
                throw new IllegalArgumentException("connection is null");
            }
            if (mPackageInfo != null) {
                sd = mPackageInfo.getServiceDispatcher(conn, getOuterContext(), handler, flags);
            } else {
                throw new RuntimeException("Not supported in system context");
            }
            //因为apk都不存在,那么就会返回fail
            validateServiceIntent(service);
            try {
                IBinder token = getActivityToken();
                if (token == null && (flags&BIND_AUTO_CREATE) == 0 && mPackageInfo != null
                        && mPackageInfo.getApplicationInfo().targetSdkVersion
                        < android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
                    flags |= BIND_WAIVE_PRIORITY;
                }
                service.prepareToLeaveProcess(this);
                int res = ActivityManagerNative.getDefault().bindService(
                    mMainThread.getApplicationThread(), getActivityToken(), service,
                    service.resolveTypeIfNeeded(getContentResolver()),
                    sd, flags, getOpPackageName(), user.getIdentifier());
                if (res < 0) {
                    throw new SecurityException(
                            "Not allowed to bind to service " + service);
                }
                return res != 0;
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }
    

    接着看getServiceDispatcher

        public final IServiceConnection getServiceDispatcher(ServiceConnection c,
                Context context, Handler handler, int flags) {
            synchronized (mServices) {
                LoadedApk.ServiceDispatcher sd = null;
                ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher> map = mServices.get(context);
                if (map != null) {
                    sd = map.get(c);
                }
                if (sd == null) {
                    sd = new ServiceDispatcher(c, context, handler, flags);
                    if (map == null) {
                        map = new ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher>();
                        mServices.put(context, map);
                    }
                    map.put(c, sd);
                } else {
                    sd.validate(context, handler);
                }
                return sd.getIServiceConnection();
            }
        }
    

    发现 getServiceDispatcher 不管要绑定的service是否存在,直接生成一个ServiceDispatcher, 然后保存到mServices里.

    特别注意, mContext是global Application context, 它与Activity是不一样的,一个APK只有一个 Application Context

    这样的引用链如下

    Activity -> Décor view -> ViewRootImpl -> mContext -> mLoadedApk -> mServices -> mArrayMap (holding mConnection)
    

    即生成每个Activity时,都会向Global Application Context加入ServiceConnection, 这样每个 Activity都有被Application Context所hold住的引用,而Application Context的生命周期最长,这样当Activity就不会被GC,多打开几次Activity,就会导到OOM了。

    解决方案,当只要 bindServiceAsUser fail了也要unbind一次。

    问题衍生

    可以试下,如果在一般的Activity里去bindService, 即使bind失败了,不去unbind,也不导致OOM,为什么呢?

    因为在Activity里bindService,它对应的ServiceConnection是保存在对应的Activity的mContext里的,而不是Global Application Context里,这里是有本质的区别的。

    因为Activity是destroy的时候会自己unbindService

    handleDestroyActivity
      scheduleFinalCleanup
        performFinalCleanup
          removeContextRegistrations (LoadedApk)
    
        public void removeContextRegistrations(Context context,
                String who, String what) {
            final boolean reportRegistrationLeaks = StrictMode.vmRegistrationLeaksEnabled();
            synchronized (mReceivers) {
                ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher> rmap =
                        mReceivers.remove(context);
                if (rmap != null) {
                    for (int i = 0; i < rmap.size(); i++) {
                        LoadedApk.ReceiverDispatcher rd = rmap.valueAt(i);
                        IntentReceiverLeaked leak = new IntentReceiverLeaked(
                                what + " " + who + " has leaked IntentReceiver "
                                + rd.getIntentReceiver() + " that was " +
                                "originally registered here. Are you missing a " +
                                "call to unregisterReceiver()?");
                        leak.setStackTrace(rd.getLocation().getStackTrace());
                        Slog.e(ActivityThread.TAG, leak.getMessage(), leak);
                        if (reportRegistrationLeaks) {
                            StrictMode.onIntentReceiverLeaked(leak);
                        }
                        try {
                            ActivityManagerNative.getDefault().unregisterReceiver(
                                    rd.getIIntentReceiver());
                        } catch (RemoteException e) {
                            throw e.rethrowFromSystemServer();
                        }
                    }
                }
                mUnregisteredReceivers.remove(context);
            }
    
            synchronized (mServices) {
                //Slog.i(TAG, "Receiver registrations: " + mReceivers);
                ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher> smap =
                        mServices.remove(context);
                if (smap != null) {
                    for (int i = 0; i < smap.size(); i++) {
                        LoadedApk.ServiceDispatcher sd = smap.valueAt(i);
                        ServiceConnectionLeaked leak = new ServiceConnectionLeaked(
                                what + " " + who + " has leaked ServiceConnection "
                                + sd.getServiceConnection() + " that was originally bound here");
                        leak.setStackTrace(sd.getLocation().getStackTrace());
                        Slog.e(ActivityThread.TAG, leak.getMessage(), leak);
                        if (reportRegistrationLeaks) {
                            StrictMode.onServiceConnectionLeaked(leak);
                        }
                        try {
                            ActivityManagerNative.getDefault().unbindService(
                                    sd.getIServiceConnection());
                        } catch (RemoteException e) {
                            throw e.rethrowFromSystemServer();
                        }
                        sd.doForget();
                    }
                }
                mUnboundServices.remove(context);
                //Slog.i(TAG, "Service registrations: " + mServices);
            }
        }
    

    从 removeContextRegistrations 里可以看出,在destroy一个activity的最后,会unbindService, unregisterReceiver, 防止内存泄露。

    参考

    相关文章

      网友评论

          本文标题:记一个OOM Case的分析

          本文链接:https://www.haomeiwen.com/subject/pzalfxtx.html