在Android项目中自定义View是非常常见的,因为一些特殊的需求,我在自定义View中使用了RxBus,后来发现使用RxBus造成了内存泄漏的问题,特此记录。
1. 背景
产品要求实现一个可以播放一组SVGA动画的组件(本文中组件名叫做SVGAListView),支持服务端动态下发任意多个SVGA链接,客户端进行解析并显示和播放。
任意多个svga?不存在的,毕竟Android手机差异性过大,而且是一个组件,项目中会有N多个地方使用,必须要控制,最后跟产品协商最多只能支持5个,而且在列表中只播放一次。
2. 实现方案
在测试过程中,为了提高用户体验,产品又加了需求:希望在首页Tab切换时(ViewPage + Fragment实现),可以重新播放当前Fragment的所有SVGAListView中的SVGA。当时第一时间想到的是使用RxBus,在Fragment显示时,通知当前页面的所有SVGA进行播放。具体实现方案如下:
- 在
SVGAListView的构造函数和onAttachedToWindow进行RxBus的注册,在onDetachedFromWindow中进行RxBus解注册。 - 在
SVGAListView中实现一个方法handleSvgaEvent(SvgaEvent event)播放当前View中的所有SVGA。 - 在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中报有关于该组件的内存泄漏问题,主要堆栈信息如下:
由上图可以看出是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😂)。
自定义观察者模式实现逻辑如下:
- 定义观察者模式
SvgaListViewSubject,实现观察者的管理,注册,解注册的相关接口和方法- 新增一个Observer接口,定义观察者需要执行的方法
- SvgaListView实现Observer接口,并实现相关的方法
- 在合适的地方或实际(如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将将自己从观察者中移除了,为了避免问题,我们还可以在BaseActivity的onDestory方法中调用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. 总结
- 在自定义View或者组件化过程中尽量不使用RxBus,通过自定义观察者模式来实现相关功能,避免造成类似的问题。
- 对应RxBus,现在还是只局限在会使用的阶段,后续需要去了解源码,掌握RxBus的实现原理,可以从根本上解决一些问题。
本次问题并没有从RxBus入手解决内存泄漏问题,而是绕开了RxBus去自定义观察者模式去实现。原因是我对RxBus的底层还不是很熟悉,晚点有时间会去探究一下RxBus的源码,看看能不能在这个层面解决该问题,或者有相关大佬熟悉RxBus源码,辛苦给出一个可能发生该问题的原因。👀