FyListen 在 MVP 中的内存优化表现
本文只是分享个人开源框架的内存优化测试,你可以直接跳到最后,参考内存泄漏的分析过程!
项目地址:
https://github.com/StudyNoteOfTu/fylisten2-alpha1
由于使用到 AOP,所以直接导入依赖是是用不了的。需要将该模块和主模块进行合并,并做AspectJ的配置。
FyListen是否能够应用到项目中,需要测试。本文测试的结果为:【可以使用,而且符合开闭原则】
本次实验分析,我选择分析它在MVP架构中的内存优化表现,我将通过以下场景过程进行模拟:
-
Activity 实现了 LifecyclePublisher 接口,并将 onDestroy() 生命周期暴露出去
-
Presenter 层使用 RxJava 来控制 Model层和View层
-
Presenter 层的 Rxjava 利用 FyListen 对传入的 View层进行生命周期监听
- compose()操作符,发现生命周期结束,立马执行 dispose(),停止上游的异步任务,最快速度释放资源
-
View的任务是:从网络上下载图片,并保存到本地
- Model层1:retrofit从网络下载图片(模拟卡顿10s)
- Model层2:引用Context,进行本地文件写入
-
RxJava 极大地化简了 Model 层代码
-
如果让Model层自行做异步,再通过接口回调,则需要在Model层的接口中加上一个类似:
public interface IDownloadModel{ public interface OnDownloadListener{ void onFinish(String path); } }
-
由于使用RxJava,自动切换到子线程执行,所以完全可以让 Model 层的方法进行同步的返回,只需要由 Presenter 层将最后的结果回调给 View 层(如果View层没有销毁的话/dispose()没有被调用)
-
同时,为了尽可能地简化代码,以表现出 FyListen 生命周期监听的优越性,我们不做:
- 不在View层对Presenter进行解绑 - 减少了 View 层 onDestroy() 中的代码量
- 不在Presenter层对FyListen做发布者释放
- 和 unregisterPublisher() 方法的设计初衷保持一致:这个方法只用来发布者主动要求不再发布,不代表发布者生命周期结束!
1. 代码结构:
Model层
-
FileStoreModelImpl - 本地文件写入工具,负责将下载图片的byte[]写入本地文件 - 模拟本地io
public class FileStoreModelImpl implements IFileStoreModel { FileOutputStream fos; /** * 取消文件存储或者读取的工作 */ @Override public void cancel() { try { if (fos != null) { fos.close(); } } catch (IOException e) { e.printStackTrace(); } } /** * 开始文件存储 */ @Override public String storeFile(Context context, String filename, String filepath, byte[] filebytes) { try { //由于读写暂时没用到context,我们使用是GCROOT的局部变量,来对context作一个强引用 Context c = context; File file = new File(filepath+File.separator+filename); if (file.exists()){ file.delete(); } File parentFile = file.getParentFile(); if (!parentFile.exists()){ parentFile.mkdirs(); } //本地文件写入 file.createNewFile(); fos = new FileOutputStream(file); fos.write(filebytes); fos.flush(); fos.close(); } catch (IOException e) { e.printStackTrace(); } return filepath+File.separator+filename; } }
-
PictureDownloadImpl - 网络图片下载工具 - 模拟耗时的网络请求
public class PictureDownloadImpl implements IPictureDownloadModel { //将 Observable交出去,让presenter来调度该异步任务 @Override public Observable<ResponseBody> download(String url) { //使用retrofit进行下载 Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://img-blog.csdnimg.cn/") .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .build(); IPictureDownloadService downloadServiceProxy = retrofit.create(IPictureDownloadService.class); //模拟网络卡顿,等待时间长达1000秒 try { Thread.sleep(1000_000); } catch (InterruptedException e) { e.printStackTrace(); } return downloadServiceProxy.download(url); } //由于将异步任务交出有 Presenter层来管理,所以observable任务的停止有rxjava来控制,这里不作实现 @Override public void cancel() { } }
Presenter层
-
BasePresenter - 绑定的同时开启生命周期监听,当LifecyclePublisher生命周期来到 onDestroy()的时候,进行解绑:
public class BasePresenter<T> implements Listener { //所有presenter都需要传入IView 所以我们抽出Base层 并且用弱引用来绑定IView //解决内存泄漏问题,我们暂时采用弱引用方式 // 还是得等到内存不足的时候才让gc来收 protected WeakReference<T> mViewRef; //根除内存泄漏 --- 绑定 //进行绑定 public void attachView(T view){ mViewRef = new WeakReference<T>(view); //进行监听,监听目标退出时,自动回调并进行解绑!!! if (view instanceof LifecyclePublisher){ //如果是个生命周期发布者,就监听它 //如果项目中并没有使用LifecyclePublisher,即view没有实现LifecyclePublisher,就不会进到这里 FyListen.registerListenerAnyway((LifecyclePublisher)view,this); } } //进行解绑 public void detachView(){ //手动地打断弱引用,而非让gc来 mViewRef.clear(); } /** * 监听到onDestroy()的时候,自动解绑 * @param p 生命周期发布者 */ @Override public void onDestroy(LifecyclePublisher p) { detachView(); } @Override public void onError(LifecyclePublisher p, String error) { //一般不会使用到,这是LifecyclePublisher绑定出问题时候的回调 } }
-
PictureDownloadPresenter - 具体表现层,负责控制Model层进行图片下载与保存,并将结果回调给View层。
- Model层分别为下载器和文件存储工具,将任务细化解耦,便于后期维护
- 使用 RxJava 按顺序执行下载和存储这两个步骤,如果任务成功完成,将回调,如果任务中断,则直接结束方法
public class PictureDownloadPresenter<T extends IPictureDownloadView> extends BasePresenter<T> { //下载器 IPictureDownloadModel pictureDownloadModel = new PictureDownloadImpl(); //文件工具-存储byte[]或者stream到本地,再次之前,请自行处理权限申请 IFileStoreModel fileModel = new FileStoreModelImpl(); //presenter发起电影下载以及文件存储工作,任务分工明确,使用两个model //rxjava将两个先后执行的异步任务整合起来,得到最终结果后,回到主线程,回调UI public void downloadPicture(String url) { if (mViewRef.get() != null) { mViewRef.get().beginDownload(); if (pictureDownloadModel != null && fileModel != null) { //开启监听,以及线程切换 //1.执行下载任务 //2.下载任务执行之后,保存文件 //3.主线程回调任务结果 // compose(CommonTransformer.listen(mViewRef.get())): // 传入的mViewRef.get()如果是LifecyclePublisher,就会进行生命周期监听注册 //当view的onDestroy()被监听到时,会自动调用dispose()停止rxjava的工作 //如果不是,就自行根据原项目结构进行内存泄漏处理 Disposable subscribe = Observable .create((ObservableOnSubscribe<String>) emitter -> emitter.onNext(url)) .flatMap(new Function<String, Observable<String>>() { @Override public Observable<String> apply(String s) throws Exception { return pictureDownloadModel.download(s) .map(new Function<ResponseBody, String>() { @Override public String apply(ResponseBody responseBody) throws Exception { //获取返回结果中的文件,通过filemodel进行存储,分工明确 byte[] bytes = responseBody.bytes(); //这些model都同步执行任务,提交异步交给rxjava来完成 String filePath = Environment.getExternalStorageDirectory().getPath(); String fileName = "test.jpg"; return fileModel.storeFile((Context) mViewRef.get(), fileName, filePath, bytes); } }); } }).compose(CommonTransformer.listen(mViewRef.get())) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<String>() { @Override public void accept(String s) throws Exception { if (mViewRef.get() != null) { //主线程回调下载结果(返回文件下载路径) mViewRef.get().pictureDownloaded(s); } } }); } } } }
-
RxJava使用 compose()操作符来插入生命周期监听
public class CommonTransformer<T> implements Listener, ObservableTransformer<T,T> { CompositeDisposable compositeDisposable = new CompositeDisposable(); //rxjava监听到生命周期发布者发起了 onDestroy //就会回调到这里,将rxjava的任务取消! @Override public void onDestroy(LifecyclePublisher p){ Log.e("TAG","publisher ondestroyed"); if (!compositeDisposable.isDisposed()){ compositeDisposable.dispose(); } } @Override public void onError(LifecyclePublisher p, String error) { Log.e("TAG","publisher error"); } @Override public ObservableSource<T> apply(Observable<T> upstream) { return upstream.doOnSubscribe(new Consumer<Disposable>() { @Override public void accept(Disposable disposable) throws Exception { compositeDisposable.add(disposable); } }); } public static <T> CommonTransformer<T> listen(Object o){ CommonTransformer<T> transformer = new CommonTransformer<>(); if (o instanceof LifecyclePublisher){ //如果object是lifecyclePublisher,就注册,否则什么也不做 Log.e("TAG","listen to lifecycle publisher"); FyListen.registerListenerAnyway((LifecyclePublisher) o,transformer); } return transformer; } }
View层
常见MVP架构,通常会使用BaseActivity来进行Presenter的绑定和解绑,由于 FyListen 符合设计思想:对拓展开放,对修改关闭。这表现在:
- 只需要在原项目的BaseActivity中实现LifecyclePublisher这个空接口
- 在onDestry()上填上@LifecycleTrace(Status.ON_DESTROY) 注解
对原本项目几乎没有任何修改!
-
BaseActivity - MVP架构下的Activity基类
public abstract class BaseLifecyclePublisherActivity<V, T extends BasePresenter<V>> extends AppCompatActivity implements LifecyclePublisher { //表示层的引用 public T mPresenter; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); //由于监听者使用的是 registerListenerAnyway() // 所以甚至不需要发布者主动进行注册 // 更加符合了对修改关闭,对拓展开放的设计原则 // FyListen.registerPublisher(this); mPresenter = createPresenter(); mPresenter.attachView((V)this); } protected abstract T createPresenter(); //对外暴露onDestroy()的生命周期回调 @LifecycleTrace(Status.ON_DESTROY) @Override protected void onDestroy() { super.onDestroy(); //没有FyListen的时候,未解决内存泄漏 //你需要手动在这里解绑 //mPresenter.detachView(); } }
-
MainActivity - 下载图片并展示的活动
一个View对应一个Presenter,一个Presenter可以根据业务数据要求,找到不同的Model层进行数据获取,并将获取到的数据处理完后,回调View层更新UI的接口。通过RxJava,我们不再需要在View层写类似 runOnUIThread()的代码。
public class MainActivity extends BaseLifecyclePublisherActivity<IPictureDownloadView, PictureDownloadPresenter<IPictureDownloadView>> implements IPictureDownloadView,LifecyclePublisher { Button btnDownload; ImageView ivPicture; //图片地址 String picUrl = "eb102bfc59ae457798bbf13c381a4af1.jpeg"; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initViews(); } //绑定表示层 //如果未来表示层逻辑需要替换,在这里替换需要的表示层即可 @Override protected PictureDownloadPresenter<IPictureDownloadView> createPresenter() { return new PictureDownloadPresenter<>(); } private void initViews() { btnDownload = findViewById(R.id.btn_download); btnDownload.setOnClickListener(v->{ mPresenter.downloadPicture(picUrl); }); ivPicture = findViewById(R.id.imageview); } //----------UI 回调--------------- @Override public void pictureDownloaded(String filepath) { //假设我们下载了一个图片,我们把它展现出来 Glide.with(this).load(filepath).into(ivPicture); } @Override public void beginDownload() { //通知用户开始下载了! Toast.makeText(this, "开始下载图片,并存储到本地", Toast.LENGTH_SHORT).show(); } }
2. 测试开始 - 测试成功
我们从LauncherActivity进入到MainActivity后,点击按钮进行下载图片,如果一直等待,图片将会展示出来,这是正常运行的情况:
为了测试在图片下载过程中,点击返回键,Activity是否能够正常被释放,我将图片下载的延时设置为100s,我们来做以下操作:
- 点击下载图片
- 点击返回键
- 查看Profiler中内存情况:
- 点击下载图片时候的内存情况
- 点击返回后的内存情况
- 主动GC后的内存情况
1. 点击下载图片时候的内存情况:
由于MainActivity在前台运行,所以自然没有被释放
2. 在图片还在下载过程中(模拟1000秒等待),点击返回
我们观察到,虽然点了返回,但是MainActivity仍然没有被释放:
我们注意到,如果一个对象只被弱引用所持有的,也不会立即被释放,需要等到GC到来的时候,才被释放。我们来看一下 FyListen 是否成功将其他引用全部断开:
可以看到,现在剩下唯一的GCROOT只有 WeakHashMap 对它持有引用,而且是弱引用。我们可以等待GC的到来,也可以通过下面这个按钮让GC提前到来:(我在这里卡了很久,一直在想为什么WeakHashMap没有释放对MainActivity的引用,后来才想起来,这只是个弱引用,只是GC还没来而已!)
通知GC清理战场后,图片还没下载好,但是我们惊喜的发现,Activity被释放了!
本人又进行了额外强度大一点的测试:连续开关MainActivity多次,导致内存中有很多的MainActivity实例,通知GC时,全都被回收,过程同上,不贴图了。
3. 为什么会这样呢?
WeakHashMap的原理我后续会另外整理一份文档和大家分享。我们只需要知道,WeakHashMap对key是个弱引用,key被回收后,value会在 get()、put()等方法中被主动置空清除。
我们主要来分析 FyListen 作为核心角色,在这里面发挥的作用,具体使用参考FyListen 的接口文档:
1. 代码优化
FyListen 可以被用在原本就构建好的MVP工程,你只需要做两件事:
- 让BaseActivity或者Activity实现 LifecyclePublisher 接口
- 在Presenter层实现一个监听者,这里的可能性很多,我上面是在一个RxJava的项目中的表现,但这并不影响没有使用 RxJava 框架的项目,你只需要做到:实现一个Listener接口的实例,让它监听View层的LifecyclePublisher实例
- 例如本文分析时,通过Listener实例,通过dispose()让RxJava的异步任务停止,从而释放引用
- 你也可以在Presenter层任意一个合适的地方,对View层注册监听。
- 如果你的项目比较特殊,View和Presenter是多对多,FyListen的回调中也明确了是谁发来的生命周期通知。
- 你不再需要在 BaseActivity 的 onDestroy() 中对 Presenter层进行解绑,所有解绑工作都放在了 Presenter层的Listener实例的 onDestroy(LifecyclePublisher p)生命周期回调之中
2. 优化原理
首先,FyListen的监听者可以主动要求实现了 LifecyclePublisher接口的发布者进行生命周期发布注册,这使得你不用在 BaseActivity 的onCreate() 中加入registerPublisher()这串代码进行发布者注册。(对修改关闭设计)
//监听者在注册监听时,只需要有发布者的引用,就可以让发布者被动的去注册生命周期发布。
//如果这个发布者此时已经被释放了,将不会注册成功,并回调监听者的 onError()方法
FyListen.registerListenerAnyway((LifecyclePublisher) o,listener);
其次,FyListen由于监听到了LifecyclePublisher的onDestroy()生命周期,会通知所有仍然存活着的监听者。监听者们可以通 onDestroy(LifecyclePublisher p)的回调,判断是否需要退出,如果要退出,执行一些任务关闭的方法:
例如本例中,监听到onDestroy,主动调用dispose()关闭 RxJava 的工作
public class CommonTransformer<T> implements Listener, ObservableTransformer<T,T> {
//作为Listener,发布者通知 ON_DESTROY 的时候,回调到这个方法
@Override
public void onDestroy(LifecyclePublisher p){
//通过 dispose() 来关闭rxjava的工作
if (!compositeDisposable.isDisposed()){
compositeDisposable.dispose();
}
}
}
除了关闭执行的任务,还需要在FyListen管理着中,将View层的引用断开,但是这个步骤,由WeakHashMap帮我们完成了,因为本身就是弱引用。
需要注意的是,本项目中,认为Model层是长生命周期,View层是短生命周期,所以不需要处理View对Model层的引用(准确来说,是不需要处理 FyListen的map对Model层的强引用,Model层的引用会在View层释放之后的某一个时间被回收。如果有性能要求,你也可以在监听者的onDestroy()中,进行解绑监听,提前释放引用)。如果你的项目中,View层是长生命周期,而Model层工具是短声明周期,请注意调用 unregisterListener(LifecyclePublisher target, Listener listener)
,将Model层解放出来,使之可以被回收!