Map导致的内存泄露

  • 1.关于这个内存泄露场景,有诸多不解,特此记录
  • 1.1.测试内存泄露操作,及解决办法
  • 1.2.为了找到泄露的原因,途中发现一个不解之处,推测
  • 2.Map内存泄露原因包含
  • 2.1.Map是长生命周期,value含有短生命周期对象的强引用
  • 2.2.value未被移除
  • 3.使用LeakCanary找Map的内存泄露
  • 4.结束


1.关于这个内存泄露场景,有诸多不解,特此记录

场景简介:Disposable 存到 CompositeDisposable 内,CompositeDisposable 存到静态的Map中,造成内存泄露。

注:Map在单例类内和静态Map效果等同,都不会回收。

LeakCanaryActivity :

public class LeakCanaryActivity extends BaseActivity implements LocationListener {

    private Disposable disposable;
    private CompositeDisposable compositeDisposableNew2;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.leak_activity);
        compositeDisposableNew2 = new CompositeDisposable();

        Observable.just("1231").subscribe(new Observer<String>() {
            @Override
            public void onSubscribe(Disposable d) {
                disposable = d;
            }

            @Override
            public void onNext(String s) {

            }

            @Override
            public void onError(Throwable e) {

            }

            @Override
            public void onComplete() {

            }
        });

        findViewById(R.id.tv_test_4).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                LeakTextManager.getInstance().create(disposable);
            }
        });
        
        findViewById(R.id.tv_test_5).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                CompositeDisposable compositeDisposableNew = new CompositeDisposable();
                LeakTextManager.getInstance().setCompositeDisposableNew(compositeDisposableNew, disposable);
            }
        });
        
        findViewById(R.id.tv_test_6).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                LeakTextManager.getInstance().setCompositeDisposableNew(compositeDisposableNew2, disposable);
            }
        });
    }

LeakTextManager :

public class LeakTextManager {

    private static LeakTextManager instance;

    private LeakTextManager() {
    }

    public static LeakTextManager getInstance() {
        if (instance == null) {
            instance = new LeakTextManager();
        }
        return instance;
    }

    private HashMap<Object, CompositeDisposable> mMaps = new HashMap<>();
    private CompositeDisposable compositeDisposableNew;

    public void create(Disposable disposable) {
        compositeDisposableNew = new CompositeDisposable();
        compositeDisposableNew.add(disposable);
        mMaps.put("da", compositeDisposableNew);
    }

    public void setCompositeDisposableNew(CompositeDisposable compositeDisposableNew, Disposable disposable) {
        this.compositeDisposableNew = compositeDisposableNew;
        compositeDisposableNew.add(disposable);
        mMaps.put("2211d", compositeDisposableNew);
    }

    public void log() {
        System.out.println("compositeDisposableNew ==" + compositeDisposableNew);
    }

}

1.1.测试内存泄露操作,及解决办法

有三个按钮,tv_test_4、tv_test_5、tv_test_6,点击其中一个,然后关闭Activity随即发生泄露。

2021-05-19 17:16:07.930 29687-31383/com.yoshin.tspsdk D/LeakCanary: ====================================
    HEAP ANALYSIS RESULT
    ====================================
    1 APPLICATION LEAKS
    
    References underlined with "~~~" are likely causes.
    Learn more at https://squ.re/leaks.
    
    Signature: 27470564cfe5c45d246f5b39a145dac1e5b801
    ┬───
    │ GC Root: Local variable in native code
    │
    ├─ android.os.HandlerThread instance
    │    Leaking: NO (PathClassLoader↓ is not leaking)
    │    Thread name: 'LeakCanary-Heap-Dump'
    │    ↓ HandlerThread.contextClassLoader
    ├─ dalvik.system.PathClassLoader instance
    │    Leaking: NO (LeakTextManager↓ is not leaking and A ClassLoader is never leaking)
    │    ↓ PathClassLoader.runtimeInternalObjects
    ├─ java.lang.Object[] array
    │    Leaking: NO (LeakTextManager↓ is not leaking)
    │    ↓ Object[].[1638]
    ├─ com.yoshin.tspsdk.leakcanary.LeakTextManager class
    │    Leaking: NO (a class is never leaking)
    │    ↓ static LeakTextManager.instance
    │                             ~~~~~~~~
    ├─ com.yoshin.tspsdk.leakcanary.LeakTextManager instance
    │    Leaking: UNKNOWN
    │    ↓ LeakTextManager.compositeDisposableNew
    │                      ~~~~~~~~~~~~~~~~~~~~~~
    ├─ io.reactivex.disposables.CompositeDisposable instance
    │    Leaking: UNKNOWN
    │    ↓ CompositeDisposable.resources
    │                          ~~~~~~~~~
    ├─ io.reactivex.internal.util.OpenHashSet instance
    │    Leaking: UNKNOWN
    │    ↓ OpenHashSet.keys
    │                  ~~~~
    ├─ java.lang.Object[] array
    │    Leaking: UNKNOWN
    │    ↓ Object[].[0]
    │               ~~~
    ├─ io.reactivex.internal.operators.observable.ObservableScalarXMap$ScalarDisposable instance
    │    Leaking: UNKNOWN
    │    ↓ ObservableScalarXMap$ScalarDisposable.observer
    │                                            ~~~~~~~~
    ├─ com.yoshin.tspsdk.leakcanary.LeakCanaryActivity$5 instance
    │    Leaking: UNKNOWN
    │    Anonymous class implementing io.reactivex.Observer
    │    ↓ LeakCanaryActivity$5.this$0
    │                           ~~~~~~
    ╰→ com.yoshin.tspsdk.leakcanary.LeakCanaryActivity instance
         Leaking: YES (ObjectWatcher was watching this because com.yoshin.tspsdk.leakcanary.LeakCanaryActivity received Activity#onDestroy() callback and Activity#mDestroyed is true)
         key = 2cab45c2-8bbc-466e-b799-48f728159486
         watchDurationMillis = 5164
         retainedDurationMillis = 163
    ====================================
    0 LIBRARY LEAKS
    
    Library Leaks are leaks coming from the Android Framework or Google libraries.
    ====================================
    METADATA
    
    Please include this in bug reports and Stack Overflow questions.
    
    Build.VERSION.SDK_INT: 29
    Build.MANUFACTURER: HUAWEI
    LeakCanary version: 2.2
    App process name: com.yoshin.tspsdk
    Analysis duration: 3369 ms
    Heap dump file path: /data/user/0/com.yoshin.tspsdk/files/leakcanary/2021-05-19_17-16-02_919.hprof
    Heap dump timestamp: 1621415767928
    ====================================

解决:
在退出activity之前把 “da”、“2211d” 从map中移除,使用的是 map.remove ,这样就不泄露了

1.2.为了找到泄露的原因,途中发现一个不解之处,推测

在查找泄露的途中,我修改了下面代码:

public void create(Disposable disposable) {
        compositeDisposableNew = new CompositeDisposable();
        compositeDisposableNew.add(disposable);
        mMaps.put("da", compositeDisposableNew);
    }

    public void setCompositeDisposableNew(CompositeDisposable compositeDisposableNew, Disposable disposable) {
        this.compositeDisposableNew = compositeDisposableNew;
        compositeDisposableNew.add(disposable);
        mMaps.put("2211d", compositeDisposableNew);
    }

改成:

public void create(Disposable disposable) {
        compositeDisposableNew = new CompositeDisposable();
        //compositeDisposableNew.add(disposable);
        mMaps.put("da", compositeDisposableNew);
    }

    public void setCompositeDisposableNew(CompositeDisposable compositeDisposableNew, Disposable disposable) {
        this.compositeDisposableNew = compositeDisposableNew;
        //compositeDisposableNew.add(disposable);
        mMaps.put("2211d", compositeDisposableNew);
    }

这个时候再进行测试,发现即使没有使用 map.remove ,内存泄露也不存在了。所以,我推测在不使用 map.remove 的情况下导致泄露的原因是: Disposable 被静态Map长期引用。

一般的类(CompositeDisposable )在被静态Map引用的时候,不会引起内存泄露,这是因为CompositeDisposable 并不和Activity的生命周挂钩;Disposable 被静态Map长期引用,引起内存泄露,是不是说明Disposable 其实是和Activity的生命周期有关,或者说隐式的持有Activity的引用呢?

Disposable 是个interface,不会找源代码,特此记录,希望大佬可以给指点~

2.Map内存泄露原因包含

2.1.Map是长生命周期,value含有短生命周期对象的强引用

Map是长生命周期:可以理解成Map是静态的,或者是在单例中;
短生命周期对象:指的是Activity或者Service或者其他,他的生命周期和上述的Map比,就是短生命周期
当短生命周期对象要回收,长生命周期持有短生命周期的强引用,就会让短生命周期对象的回收失败,因为Java中,只要一个对象被强引用引用,那么GC就不会回收它。

Map是短生命周期不会引起内存泄露:

public class MapTestModel {

    private Map<Object, View> map = new HashMap<>();
    private Map<Object, Context> mapC = new HashMap<>();
    public MapTestModel() {

    }

    public void add(View view) {
        map.put("haha", view);
    }
    public void addC(Context context) {
        mapC.put("haha", context);
    }

}

在Activity中:

MapTestModel mapTestModel = new MapTestModel();

    private void test_Model_view() {
        mapTestModel.add(textView);
        mapTestModel.addC(this);
    }

在Activity关闭的时候,这样不会引起内存泄露。

2.2.value未被移除

如我们上述的例子 1.1,因为Map中,含有Disposable ,所以需要使用 map.remove 。这种场景会有移除失败的风险:原因是Map的key是object,如果object发生变更,导致地址变更,那么移除的时候,就会移除失败。


3.使用LeakCanary找Map的内存泄露

当使用LeakCanary的时候,如果发现log上,有Map的value显示出来

├─ com.yoshin.tspsdk.leakcanary.LeakTextManager instance
    │    Leaking: UNKNOWN
    │    ↓ LeakTextManager.compositeDisposableNew
    │                      ~~~~~~~~~~~~~~~~~~~~~~

就可以考虑一下是不是上述原因。

因为存的这个value可能你都不知道是不是含有短生命周期的强引用~

4.结束

就这样了,等再有新的理解,再更新