记一次自定义View的RxBus内存泄漏问题

1,877 阅读7分钟

在Android项目中自定义View是非常常见的,因为一些特殊的需求,我在自定义View中使用了RxBus,后来发现使用RxBus造成了内存泄漏的问题,特此记录。

1. 背景

产品要求实现一个可以播放一组SVGA动画的组件(本文中组件名叫做SVGAListView),支持服务端动态下发任意多个SVGA链接,客户端进行解析并显示和播放。

任意多个svga?不存在的,毕竟Android手机差异性过大,而且是一个组件,项目中会有N多个地方使用,必须要控制,最后跟产品协商最多只能支持5个,而且在列表中只播放一次。

2. 实现方案

在测试过程中,为了提高用户体验,产品又加了需求:希望在首页Tab切换时(ViewPage + Fragment实现),可以重新播放当前Fragment的所有SVGAListView中的SVGA。当时第一时间想到的是使用RxBus,在Fragment显示时,通知当前页面的所有SVGA进行播放。具体实现方案如下:

  1. SVGAListView的构造函数和onAttachedToWindow 进行RxBus的注册,在onDetachedFromWindow中进行RxBus解注册。
  2. SVGAListView中实现一个方法handleSvgaEvent(SvgaEvent event)播放当前View中的所有SVGA。
  3. 在BaseFragment中的setUserVisibleHint中通知当前页面中所有的SVGAListView中的SVGA进行播放

核心代码如下:

1. 在SVGAListView的构造函数和onAttachedToWindow 进行RxBus的注册,在onDetachedFromWindow中进行RxBus解注册。

SVGAListView.java

public class SVGAListView extends FrameLayout {

    public SVGAListView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext = context;
        RxBus.get().register(this);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        startAllSvga();
        if (!RxBus.get().hasRegistered(this)) {
            RxBus.get().register(this);
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        pauseAllSvga();
        if (RxBus.get().hasRegistered(this)) {
            RxBus.get().unregister(this);
        }
    }
}

2. 在SVGAListView中实现一个方法handleSvgaEvent(SvgaEvent event)播放当前View中的所有SVGA。

public class SVGAListView extends FrameLayout {

    @Subscribe
    public void handleSvgaEvent(handleSvgaEvent event) {
        Fragment viewInFragment = event.getWeakReferenceContext().get();
        if (viewInFragment == null || viewInFragment.getActivity() == null
                || viewInFragment.getActivity().isFinishing() || viewInFragment.getActivity().isDestroyed()) {
            return;
        }

        if (mContext != null && viewInFragment.getActivity() == mContext) {
            startAllSvga();
        }
    }
}

3. 在BaseFragment中的setUserVisibleHint中通知当前页面中所有的SVGAListView中的SVGA进行播放

BaseFragment.java

@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
    super.setUserVisibleHint(isVisibleToUser);
    this.isVisibleToUser = isVisibleToUser;
    if (isVisibleToUser && isResumed()) {
	// Fragment可见时,通知当前页面的所有SVGAListView中的所有SVGA进行播放
	RxBus.get().post(new SvgaEvent(this));
    }
}

SvgaEvent.java

public class SvgaEvent {

    private WeakReference<Fragment> weakReferenceContext;

    public SvgaEvent(Fragment context) {
        this.weakReferenceContext = new WeakReference<>(context);
    }

    public WeakReference<Fragment> getWeakReferenceContext() {
        return weakReferenceContext;
    }
}

3. 发现问题

在使用过程中,发现在LeakCanary中报有关于该组件的内存泄漏问题,主要堆栈信息如下:

内存泄漏.png

由上图可以看出是SubscriberEvent.target中持有了SVGAListView,SVGAListView中又MainActivity的引用,导致内存泄漏问题。 我反复确定了一下逻辑,按照程序的正常逻辑来走在View的onAttachedToWindow注册,在onDetachedFromWindow是不会发生这种情况的。经过分析以及和小伙伴的讨论,感觉可能的原因有以下猜测:

猜测1: 是否会存在在MainActivity被销毁是,某些组件没有来得及走onAttachedToWindow,导致RxBus没有解注册,持有了MainActivity,导致MainActivity不能及时释放,导致报出了内存泄漏? 猜测2: 在一些极端情况下,自定义View会先执行onDetachedFromWindow,然后才执行onAttachedToWindow,导致RxBus不能够解注册,从而导致内存泄漏(该猜测是小伙伴在一篇博客中看到的,具体链接还没有,具体在什么情况下,会发生这种情况,暂时也还未知)。

4. 解决方案

由于我也没有去深入了解过RxBus的底层实现,而且,通过内存泄漏的堆栈来看RxBus中注册事件是持有被监听对象的强引用,在自定义View中不能找到一个合适的时机去解注册,所以决定放弃使用RxBus的使用,通过自定义观察者模式实现相关的功能(还有一个原因是公司决定底层库需要禁用RxBus😂)。

自定义观察者模式实现逻辑如下:

  1. 定义观察者模式SvgaListViewSubject,实现观察者的管理,注册,解注册的相关接口和方法
  2. 新增一个Observer接口,定义观察者需要执行的方法
  3. SvgaListView实现Observer接口,并实现相关的方法
  4. 在合适的地方或实际(如BaseFragment)中执行notify方法,通知观察者执行相应的方法

相关代码如下:

1. 定义观察者模式SvgaListViewSubject,实现观察者的管理,注册,解注册的相关接口和方法

SvgaListViewSubject.java

public class SvgaListViewSubject {
    private static SvgaListViewSubject INSTANCE = new SvgaListViewSubject();

    /* observerMap 存储某一个Activity中,所有的SvgaListView对象
     * key ActivityName
     * value SvgaListView(Observer)
     */
    private static Map<String, List<WeakReference<Observer>>> observerMap = new HashMap<>();
    /**
     * 一个Observer Key 的白名单
     * 默认情况下,所有SvgaListView都会被加入到监听者模式中,该白名单下的SvgaListView不会被加入到白名单中,同时也不会触发观察者模式的监听
     */
    private static Set<String> whiteList = new HashSet<>();;

    private SvgaListViewSubject() {
    }

    public static SvgaListViewSubject getInstance() {
        return INSTANCE;
    }

    /**
     * 忽略当前Activity的观察者模式监听事件
     */
    public void ignoreObserver(Activity activity) {
        if (activity != null) {
            whiteList.add(activity.getClass().getName());
        }
    }

    /**
     * 撤销忽略的观察者模式,恢复为默认情况(所有SvgaListView会自动被添加到观察者列表中)
     */
    public void removeIgnoreObserver(Activity activity) {
        if (activity != null) {
            whiteList.remove(activity.getClass().getName());
        }
    }

    /**
     * 新增一个被观察者, 一般在SvgaListView attach 时调用
     *
     * @param context       SvgaListView组件所在的上下文 这里只对在Activity中的SvgaListView组件处理
     * @param weakReference SvgaListView组件的弱引用
     */
    public void addObserver(Context context, WeakReference<Observer> weakReference) {
        if (context instanceof Activity) {
            String key = context.toString();
            if (isInWhiteList(key)) {
                return;
            }
            if (observerMap.get(key) == null) {
                ArrayList<WeakReference<Observer>> arrayList = new ArrayList<>();
                arrayList.add(weakReference);
                observerMap.put(key, arrayList);
            } else {
                observerMap.get(key).add(weakReference);
            }
        }
    }

    /**
     * 判断当前Activity是否在{@link #whiteList}中
     */
    private boolean isInWhiteList(String key) {
        for (String className : whiteList) {
            if (key.startsWith(className)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 移除一个观察者,一般在SvgaListView detach 时调用
     */
    public void removeObserver(Context context, WeakReference<Observer> weakReference) {
        if (context instanceof Activity) {
            String key = context.toString();
            if (observerMap.get(key) != null) {
                observerMap.get(key).remove(weakReference);
                if (observerMap.get(key).size() == 0) {
                    observerMap.remove(key);
                }
            }
        }
    }

    /**
     * 移除当前Activity中所有的被观察者,一般在Activity销毁时调用
     * 在SvgaListView在执行detach 方法时,会检查当前Activity中是否还有Observer,如果没有,则会自动移除掉当前Activity的key
     *
     * @param activity SvgaListView所在的页面
     */
    public void removeActivityObservers(Activity activity) {
        if (activity != null) {
            String key = activity.toString();
            if (observerMap.get(key) != null) {
                observerMap.remove(key);
            }
        }
    }

    /**
     * 移除所有被观察者
     */
    public void removeAllObservers() {
        for (String key : observerMap.keySet()) {
            observerMap.remove(key);
        }
    }

    /**
     * 通知所有的被观察者
     */
    public void notifyAllObservers() {
        for (Map.Entry<String, List<WeakReference<Observer>>> entry : observerMap.entrySet()) {
            for (WeakReference<Observer> weakObserver : entry.getValue()) {
                if (weakObserver.get() != null) {
                    weakObserver.get().update();
                }
            }
        }
    }

    /**
     * 通知特定Activity下的所有SvgaListView对象
     */
    public void notifyObserversByActivity(Activity activity) {
        if (activity != null) {
            String key = activity.toString();
            List<WeakReference<Observer>> observers = observerMap.get(key);
            if (observers != null) {
                for (WeakReference<Observer> weakObserver : observers) {
                    if (weakObserver.get() != null) {
                        weakObserver.get().update();
                    }
                }
            }
        }
    }
}

2. 新增一个Observer接口,定义观察者需要执行的方法

Observer.java

public interface Observer {

    void update();
}

3. SvgaListView实现Observer接口,并实现相关的方法

SVGAListView.java

public class SVGAListView extends FrameLayout implements Observer {

    private WeakReference<Observer> thisWeakReference;

    public HeaderDressView() {
	thisWeakReference = new WeakReference<>((Observer) this);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        startAllSvga();
 	SvgaListViewSubject.getInstance().addObserver(mContext, thisWeakReference);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        pauseAllSvga();
        SvgaListViewSubject.getInstance().removeObserver(mContext, thisWeakReference);
    }

    @Override
    public void update() {
        if (isShown()) {
            showAllDecorators();
            startAllSvga();
        }
    }
}

默认情况下在SvgaListView的onDetachedFromWindow将将自己从观察者中移除了,为了避免问题,我们还可以在BaseActivityonDestory方法中调用SvgaListViewSubject#removeActivityObservers方法去移除当前Activity中所有的观察者。(这波操作只是为了保险起见,不是必要的操作,而且因为我们的SvgaListViewSubject中保存的观察者,key是Activity的类名,value也是一个弱引用,是不会造成内存泄漏 或 引起其他问题的。)

4. 在合适的地方或实际(如BaseFragment)中执行notify方法,通知观察者执行相应的方法

BaseFragment.java

@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
    super.setUserVisibleHint(isVisibleToUser);
    this.isVisibleToUser = isVisibleToUser;
    if (isVisibleToUser && isResumed()) {
	SvgaListViewSubject.getInstance().notifyObserversByActivity(getActivity());
    }

}

5. 总结

  1. 在自定义View或者组件化过程中尽量不使用RxBus,通过自定义观察者模式来实现相关功能,避免造成类似的问题。
  2. 对应RxBus,现在还是只局限在会使用的阶段,后续需要去了解源码,掌握RxBus的实现原理,可以从根本上解决一些问题。

本次问题并没有从RxBus入手解决内存泄漏问题,而是绕开了RxBus去自定义观察者模式去实现。原因是我对RxBus的底层还不是很熟悉,晚点有时间会去探究一下RxBus的源码,看看能不能在这个层面解决该问题,或者有相关大佬熟悉RxBus源码,辛苦给出一个可能发生该问题的原因。👀