阅读 1361

如何既装逼又优雅的设计一个模块化的MVP架构

一、前言

该篇文章会从架构模式、模块化设计、网络框架几个方面来分别谈如何去设计一个优秀的模块化的MVP架构 项目地址已在 Github 开源:使用 Java 构建的一个模块化的 MVP 的项目,欢迎 Star,谢谢~

1.1 特点功能

  • 项目架构使用了符合绝大部分项目使用的 MVP 架构模式,并使用模块化设计方便多人维护
  • 使用了目前最流行的 RxJava2+Retrofit2+Okhttp3 作为网络框架搭建
  • 支持多BaseUrl(多服务器地址)访问接口,开发者可自行扩展
  • 同时支持表单提交、raw提交,并自动解析提交数据和返还数据
  • 支持 header 的动态添加
  • 支持对前后端约定的业务码进行统一形式(逻辑处理、通知view)处理,如 code=311,则是对Token过期处理等
  • 支持请求(请求参数、header)、响应信息(响应状态、响应的json数据)的打印
  • 支持自定义网络请求异常信息
  • 支持 https 证书的自定义配置
  • 使用比 RxLifecycle 库更加优秀的 AutoDispose 库作为解决在 View 生命周期的变化中造成的内存泄漏问题
  • 使用 Arouter 库作为项目路由,作为模块之间的通信桥梁
  • 使用 腾讯X5内核浏览器 替代原生WebView加载网页
  • 项目中封装了一个带状态的 RecyclerView,如错误页面、空页面状态展示,开发者可以根据项目情况自行扩展使用

二、MVP 的架构模式

2.1 MVP 的优势和缺点

这是一个老生常谈的问题了,MVP 是 MVC 模式的进化版本,是当前 Android 开发者使用最多的架构模式,MVP 区别于 MVC 最大的一个优点就是切断了 V层 和 M层的直接通信,由 P层负责处理逻辑同时持有 M层和 V层实例,进而通知 V层更新UI,极大地降低了代码的耦合性,方便后期维护,且增强了代码的可读性,但是由于 V层和 P层需要接口约定,相比 MVC 会多出很多类,使得开发者觉得提高了的代码复杂度及学习成本。

2.2 如何设计一个 MVP 的模型

2.2.1 先从 View 层的设计说起

一般我们新建一个项目无论是何模式都会写一个 BaseActivity/BaseFragment 类作为View 层的基类,这里往往会写一个 View 层常用到的方法和根据产品初始化一些通用属性配置,在 MVP 模式中,由于 P层持有 V层实例,一般会调用一些常用的方法通知 V层,比如弹 Toast、Dialog 、状态变化等操作。所以我们一般会把这类 P层常用到 通知 V 层的方法会写成一个接口的形式到 BaseView 里面,如下:

public interface BaseView {
    void showToast(String msg);
    void onError(String errorMsg);
    void onLoadIng(String tip);
    void loadCompleted();
    void loadError(Object errorMsg);
    Context getContext();
    <T>AutoDisposeConverter<T> bindLifecycle();
}
复制代码

然后 BaseActivity/BaseFragment 实现 BaseView 接口,并且默认实现接口内的所有方法,方便 P层调用。

2.2.2 再谈 Presenter 层的设计

由于 Presenter 需要和 View 层通信,所以 Presenter 需要持有 View 层实例 所以 我们可以写一个BasePresenter 类,并且在构造函数中传入 View接口实例,View用泛型约束,使得 View 接口是 BaseView 的子类接口,并在该类中可以写一些常用到的Presenter 通知 View 的方法,或者子 Presenter 需要常处理的逻辑。

public class BasePresenter<V extends BaseView> {
    protected V view;
    public BasePresenter(V view){
        this.view = view;
    }
    protected String formatUrl(String needFormatUrl,String...params){
        if(needFormatUrl!=null&&params.length>0){
            return String.format(needFormatUrl,params);
        }
        return null;
    }
}
复制代码

传统的 MVP 模式写相比 MVC 模式一般都会多写三个类,其中包括 View 层接口、Presenter 层接口、Presenter 层实例类,把 View 接口和 Presenter 接口分开写会降低代码的可读性和增加类文件冗余,这里可做优化,写一个 Contact 接口类把 Presenter 层接口和 View 层接口关联起来,如下:

public interface VideoListContact {
    interface View extends BaseView{
        void setVideoList(List<VideoListResult.ItemListBean> dataList);
    }
    interface Presenter {
        void getVideoList(int id);
    }
}
复制代码

然后写 Presenter 的实例类,进行真实的逻辑处理和通知 View 层更新UI:

public class VideoListPresenter extends BasePresenter<VideoListContact.View> implements VideoListContact.Presenter{

    public VideoListPresenter(VideoListContact.View view) {
        super(view);
    }

    @Override
    public void getVideoList(int id) {
        HashMap<String,Object> params = new HashMap<>();
        params.put("id",id+"");
        params.put("udid","d2807c895f0348a180148c9dfa6f2feeac0781b5");
        BaobabFormRequestClient.getInstance().executePost(UrlConstant.POST_CATEGORIES_VIDEO_LIST, params, new AppObserver<VideoListResult>(view) {
            @Override
            public void onNext(VideoListResult videoListResult) {
                super.onNext(videoListResult);
                view.setVideoList(videoListResult.getItemList());
            }
        });
    }
}
复制代码

2.2.3 MVP 设计架构图

mvp_model.png

三、如何把项目模块化

3.1 模块化的介绍

模块化一般是基于业务来对项目进行拆解而分成不同的模块,如订单模块、资讯模块、个人中心模块、聊天模块。 (题外话:组件化一般是以细分功能拆解以达到组件高重用,避免重复造轮子,如支付模块、通信模块)

3.2 模块化的优点

  • 基于业务的模块可以在多个项目中重复使用,适合多个项目但是业务逻辑类似的场景,可以复用模块
  • 每个模块可以单独编译和调试,可以提高项目的开发效率和方便开发者进行功能测试
  • 每个模块相互独立,方便多人开发之间造成的代码冲突,且更加明确自己的开发任务,可以提高开发效率

3.3 模块化的常规步骤

3.3.1 构思好各个业务模块并且不要相互依赖,各业务模块都是相互解耦的,可设计成以下关系

模块架构图.png

3.3.2 定义业务模块是否单独编译和运行

在项目的 gradle.properties 文件下中添加 isRunAlone 字段来标识模块是否可单独编译。

isRunAlone=false
复制代码

添加完毕后,同步项目后进行下一步

3.3.3 在所有业务模块的 build.gradle 构建文件中,都对 isRunAlone 的值分别做逻辑判断处理,如下图

if (isRunAlone.toBoolean()) {
    apply plugin: 'com.android.application'
} else { 
    apply plugin: 'com.android.library'
}
...
...
android {
    ...
    ...
    //根据是否模块化运行判断加载不同的 AndroidManifest 活动清单配置文件
    sourceSets {
        main {  
            if (isRunAlone.toBoolean()) {
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
                java {
                    // 全部 module 一起编译时剔除 debug 目录
                    exclude '**/debug/**'
                }
            }
        }
    }
}
复制代码

以上代码可以看出,我们在 isRunAlone=true 时候,我们可以创建一个模块独立运行时候加载的清单文件(AndroidManifest.xml, 如上图中的路径(src/main/debug/AndroidManifest.xml),当模块被独立运行的时候,清单文件就会加载你指定地址的文件。

至此,基本的模块化配置就完成了,当我们把 isRunAlone 设置成 true ,并同步项目后,就可以运行任何一个业务模块了

模块运行效果图.png

四、打造一个功能强大的网络框架

目前 Android 开发最流行的的网络框架当属 RxJava+Retrofit+Okhttp 三刀流,下面我从配置灵活性、易用性、开发效率、内存泄露问题、状态信息可追踪性等方面去进行网络框架的封装 下面我将用我写此 blog 时候最新版本的 Rxjava2 + RxAndroid2 +Retrofit2 +Okhttp3 + Gson 开源库来对网络框架层进行封装 先上一个封装完后的接口请求代码

HashMap<String,Object> params = new HashMap<>();
        params.put("id",id+"");
        params.put("udid","d2807c895f0348a180148c9dfa6f2feeac0781b5");
        BaobabFormRequestClient.getInstance().executePost(UrlConstant.POST_CATEGORIES_VIDEO_LIST, params, new AppObserver<VideoListResult>(view) {
            @Override
            public void onNext(VideoListResult videoListResult) {
                super.onNext(videoListResult);
                view.setVideoList(videoListResult.getItemList());
            }
        });
复制代码

再上一个我封装整个请求 Client 的整体架构思路 UML 图:

base_net_model

4.1 先从一次网络请求谈起

当我们进行网络请求的时候,最关键是请求的url、请求方式、请求体、响应体这四个信息

4.1.1 请求 url

这里一般来说都是一个服务器url + 接口 path 的组合,服务器 url 在有的公司可能是多个的,接口 path 一般来说是多个的,所以这里我们要考虑到服务器地址(BaseUrl)可配置性,接口 path,由开发者传入,且可动态添加到请求 url 中

4.1.2 请求方式

有过一些经验的开发者都知道,请求接口我们常用到如 get、post、put、delete 等等的请求方式,这里我们可以定义一个枚举规范开发者需要什么样的请求方式,并在 Retrofit 服务接口(ApiService)中定义该请求方式对应的方法

4.1.3 请求体

有的公司的产品请求接口使用的是表单提交,有的是使用 Raw(JSON)形式提交,不同的提交方式要求开发者传递的内容不同,这一点也要考虑到框架层的设计中去

4.1.4 响应体

响应体这个基本上都是统一的后端返回给前端 json 字符串,每次响应结果都需要开发者对 json 进行实体解析,所以解析这一高频操作的步骤应该是被设计到框架层面的,开发者只需要告诉框架层需要解析成什么样的实体,这个时候就应该用到泛型,并通过 ParameterizedType 方式拿到泛型的具体类,这里要注意到,拿到具体类后要判断这个泛型类是否是一个集合类,对于类和集合,Gson 对它们的解析方式是不一样的

4.2 代码层的讲解

4.2.1 配置公共属性,定义需灵活配置的属性参数

4.1 中咱们提到了那些在请求 Client 中我们可能会用到多服务器地址,所以在构建 Retrofit 时候 BaseUrl 可以写成抽象方法由子类去实现。

public abstract class BaseNetClient{
    @NotNull
    protected abstract String getBaseApiUrl();//服务器地址

    protected abstract Cache getCache();//缓存对象
    protected Cache defaultCahe;
    private static final int HTTP_TIMEOUT_SECONDS = 10;//超时时间(s)
    private static final int READ_TIMEOUT_SECONDS = 10;//超时时间(s)
    protected static final long DEFAULT_HTTP_CACHE_LONG_SIZE = 1024 * 1024 * 100;//最大缓存大小
    private Retrofit mRetrofit;
    protected ApiService apiService;
    protected Gson mGson;

    protected BaseNetClient() {
        mGson = new GsonBuilder().disableHtmlEscaping().create();
        defaultCahe = new Cache(new File(BaseApplication.getApplication().getCacheDir(), Constant.HTTP_CACHE),DEFAULT_HTTP_CACHE_LONG_SIZE);
        OkHttpClient okHttpClient = new OkHttpClient.Builder().cache(getCache()==null?defaultCahe:getCache())
                .sslSocketFactory(SSLFactory.getDefaultSSLSocketFactory(),SSLFactory.getX509TrustManager())
                .hostnameVerifier((hostname, session) -> true)//直接设置证书验证结果 若证书验证不通过 设置true即可通过
                .retryOnConnectionFailure(true)//设置失败重连
                .addInterceptor(new NetLogInterceptor())//设置网络日志
                .connectTimeout(HTTP_TIMEOUT_SECONDS, TimeUnit.SECONDS)
                .readTimeout(READ_TIMEOUT_SECONDS,TimeUnit.SECONDS)
                .proxy(Proxy.NO_PROXY)//禁用代理使用
                .build();
        mRetrofit = new Retrofit.Builder()
                .client(okHttpClient)
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .baseUrl(getBaseApiUrl())
                .build();
        apiService = mRetrofit.create(ApiService.class);
    }
    public enum RequestType {
        GET, POST
    }
    public  RequestBody getRequestBodyFromObject(Object object) {
        RequestBody requestBody = RequestBody.create(
                okhttp3.MediaType.parse("application/json; charset=utf-8"),
                mGson.toJson(object)
        );
        return requestBody;
    }
}
复制代码

ApiService 的代码如下:

public interface ApiService {
    @GET("{url}")
    Observable<ResponseBody> executeGet(
            @Path(value = "url",encoded = true) String url,
            @QueryMap Map<String, Object> maps);
    @GET("{url}")
    Observable<ResponseBody> executeGet(
            @Path(value = "url",encoded = true) String url,
            @Body RequestBody requestBody);
    @GET("{url}")
    Observable<ResponseBody> executeGet(
            @Path(value = "url",encoded = true) String url);



    @POST("{url}")
    Observable<ResponseBody> executePost(
            @Path(value = "url",encoded = true) String url);

    @POST("{url}")
    Observable<ResponseBody> executePost(
            @Path(value = "url",encoded = true) String url,
            @QueryMap Map<String, Object> maps);

    @POST("{url}")
    Observable<ResponseBody> executePost(@Path(value = "url",encoded = true) String url, @Body RequestBody requestBody);

    @GET("{url}")
    Observable<ResponseBody> executeGetWithHeader(
            @HeaderMap Map<String, String> headers,
            @Path(value = "url",encoded = true) String url,
            @QueryMap Map<String, Object> maps);

    @GET("{url}")
    Observable<ResponseBody> executeGetWithHeader(
            @HeaderMap Map<String, String> headers,
            @Path(value = "url",encoded = true) String url,
            @Body RequestBody requestBody);

    @GET("{url}")
    Observable<ResponseBody> executeGetWithHeader(
            @HeaderMap Map<String, String> headers,
            @Path(value = "url",encoded = true) String url);


    @POST("{url}")
    Observable<ResponseBody> executePostWithHeader(
            @HeaderMap Map<String, String> headers,
            @Path(value = "url",encoded = true) String url,
            @FieldMap Map<String, Object> maps);

    @POST("{url}")
    Observable<ResponseBody> executePostWithHeader(
            @HeaderMap Map<String, String> headers,
            @Path(value = "url",encoded = true) String url,
            @Body RequestBody requestBody);


    @POST("{url}")
    Observable<ResponseBody> executePostWithHeader(
            @HeaderMap Map<String, String> headers,
            @Path(value = "url",encoded = true) String url);

}
复制代码

4.2.2 定义不同请求体的 Client

4.1中也提到说,我们请求时候可能是表单提交,也可能是Raw提交,我们可以通过方法的重载或者分成多个类文件去处理这个逻辑,并且在这个类中负责处理请求体的转换、请求操作、请求结果监听、请求结果转换操作。

/**
 * 创建时间:2019/8/6
 * 创建人:anthony.wang
 * 功能描述:表单提交的请求类
 */
public abstract class FormRequestClient extends BaseNetClient {


    public <T>Observable executeGet(String url,Map<String,Object> params, AppObserver<T> observer){
        return requestData(RequestType.GET,null,url,params,observer);
    }
    public <T>Observable executePost(String url,Map<String,Object> params, AppObserver<T> observer){
        return requestData(RequestType.POST,null,url,params,observer);
    }
    public <T>Observable executeGetWithHeader(Map<String,String> headerMap,String url,Map<String,Object> params, AppObserver<T> observer){
        return requestData(RequestType.GET,headerMap,url,params,observer);
    }
    public <T>Observable executePostWithHeader(Map<String,String> headerMap,String url,Map<String,Object> params, AppObserver<T> observer){
        return requestData(RequestType.POST,headerMap,url,params,observer);
    }

    private <T>Observable requestData(RequestType requestType,Map<String,String> headerMap, String url, Map<String, Object> params, AppObserver<T> observer) {
        if (requestType == null) {
            requestType = RequestType.GET;
        }
        if(params == null){
            params = new HashMap<>();
        }
        Observable<ResponseBody> requestObservable = null;
        switch (requestType) {
            case GET:
                if(headerMap!=null){
                    requestObservable = apiService.executeGetWithHeader(headerMap,url, params);
                }else{
                    requestObservable = apiService.executeGet(url, params);
                }

                break;
            case POST:
                if(headerMap!=null) {
                    requestObservable = apiService.executePostWithHeader(headerMap,url, params);
                }else{
                    requestObservable = apiService.executePost(url, params);

                }
                break;
        }
        if (requestObservable != null) {
            //若订阅需要绑定View层生命周期则走此方法 防止内存泄漏的问题
            if(observer.getAutoDisposeConverter()!=null){
                requestObservable
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .doOnDispose(() -> observer.onDispose())
                        .as(observer.getAutoDisposeConverter())
                        .subscribe(new SubscribeObserver(observer));
            }else{
                requestObservable
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe(new SubscribeObserver(observer));
            }
        }
        return requestObservable;
    }

}
复制代码

4.2.3 以上代码中 SubscribeObserver 是最终的观察者,负责通知订阅事件的状态和信息,代码如下:

public class SubscribeObserver implements Observer<ResponseBody> {
    private AppObserver appObserver;
    public SubscribeObserver(AppObserver appObserver){
        this.appObserver = appObserver;
    }
    @Override
    public void onSubscribe(Disposable d) {
        appObserver.onSubscribe(d);
    }

    @Override
    public void onNext(ResponseBody responseBody) {
        try {
            String json = responseBody.string();
            appObserver.onNext(appObserver.getEntityData(json));
        } catch (Exception e) {
            appObserver.onError(e);
        }
    }

    @Override
    public void onError(Throwable e) {
        appObserver.onError(e);
    }

    @Override
    public void onComplete() {
        appObserver.onComplete();
    }
}

复制代码

具体 ChildClient 单例的编写,实现抽象父类的方法,配置baseurl参数等

public class WanAndroidFormRequestClient extends FormRequestClient {
    protected WanAndroidFormRequestClient() {
        super();
    }

    public static WanAndroidFormRequestClient getInstance() {
        return WanAndroidFormRequestClient.SingleHolder.instance;
    }

    private static class SingleHolder {
        public static final WanAndroidFormRequestClient instance = new WanAndroidFormRequestClient();
    }
    @NotNull
    @Override
    protected String getBaseApiUrl() {
        return Protocols.WAN_ANDROID_BASE_RELEASE_API_URL;
    }

    @Override
    protected Cache getCache() {
        return null;
    }
}

复制代码

4.2.4 添加监听接口请求日志信息

先上效果图

我们有时候需要监听接口的请求参数、请求头、请求成功响应体等等信息方便观察和调试,要想监听这些信息的话我们可以在配置 OkHttpClient 时候通过 addInterceptor() 方法添加到拦截器里面,拦截器写法如下:

/**
 * 创建时间:2019/8/6
 * 创建人:anthony.wang
 * 功能描述:该拦截器主要复制打印请求参数和响应参数等信息 方便开发者调试
 */
public class NetLogInterceptor implements Interceptor {
    private final Charset UTF8 = Charset.forName("UTF-8");
    private boolean isOpenLog = true;

    public void setOpenLog(boolean openLog) {
        isOpenLog = openLog;
    }
    public NetLogInterceptor(){}
    public NetLogInterceptor(boolean isOpenLog){
        this.isOpenLog = isOpenLog;
    }

    @NotNull
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();

        String method = request.method();
        HttpUrl requestUrl = request.url();
        RequestBody requestBody = request.body();

        if(isOpenLog){
            if (requestBody != null) {
                Buffer bufferRequest = new Buffer();
                requestBody.writeTo(bufferRequest);
                logRetrofitRequest(
                        method,
                        requestUrl == null ? null : requestUrl.toString(),
                        bufferRequest.readString(UTF8),
                        request.headers()
                );
            } else {
                logRetrofitRequest(
                        method,
                        requestUrl == null ? null : requestUrl.toString(),
                        BaseApplication.getApplication().getResources().getString(
                                R.string.no_request_body
                        ),
                        request.headers()
                );
            }
        }


        Response response = chain.proceed(request);

        ResponseBody responseBody = response.body();
        long contentLength = responseBody.contentLength();

        if (bodyEncoded(response.headers())) {
            //HTTP (encoded body omitted)
        } else {
            BufferedSource source = responseBody.source();
            source.request(Long.MAX_VALUE);
            Buffer buffer = source.buffer();

            Charset charset = UTF8;
            MediaType contentType = responseBody.contentType();
            if (contentType != null) {
                try {
                    charset = contentType.charset(UTF8);
                } catch (UnsupportedCharsetException e) {
                    return response;
                }
            }

            if (!isPlaintext(buffer)) {
                return response;
            }
            if (contentLength != 0&&isOpenLog) {
                String result = buffer.clone().readString(charset);
                logRetrofitResponseSuccess(
                        response.request().url().toString(),
                        result,
                        response.headers()
                );
            }

        }
        return response;
    }
    private void logRetrofitRequest(
            String method,
            String url,
            String request,
            Headers headers
    ) {
        if (TextUtils.isEmpty(url)) {
            return;
        }
        if (TextUtils.isEmpty(request)) {
            return;
        }
        // FORMAT STRING
        String requestText = String.format(
                Constant.NET_REQUEST_STRING,
                method,
                url,
                request
        );
        if(headers!=null&&headers.size()>0){
            StringBuffer sbHeader = new StringBuffer();
            for (int i =0;i<headers.size();i++) {
                sbHeader.append(headers.name(i)+":"+headers.get(headers.name(i))+"\n");
            }
            if(sbHeader.toString().trim().isEmpty()){
                Logger.t(Constant.NET_LOG_TAG).d(String.format(Constant.NET_REQUEST_HEADER, BaseApplication.getApplication().getResources().getString(
                        R.string.no_request_header
                )));
            }else{
                Logger.t(Constant.NET_LOG_TAG).d(String.format(Constant.NET_REQUEST_HEADER,sbHeader.toString()));
            }
        }
        // LOG
        Logger.t(Constant.NET_LOG_TAG).d(requestText);
    }
    private void logRetrofitResponseSuccess(
            String url,
            String response,
            Headers headers
    ) {
        if (TextUtils.isEmpty(url)) {
            return;
        }
        if (TextUtils.isEmpty(response)) {
            return;
        }
        // FORMAT STRING
        String responseText = String.format(
                Constant.NET_RESPONSE_SUCESS_STRING,
                url,
                response
        );
        if(headers!=null&&headers.size()>0){
            StringBuffer sbHeader = new StringBuffer();
            for (int i =0;i<headers.size();i++) {
                sbHeader.append(headers.name(i)+":"+headers.get(headers.name(i))+"\n");
            }
            if(sbHeader.toString().trim().isEmpty()){
                Logger.t(Constant.NET_LOG_TAG).d(String.format(Constant.NET_RESPONSE_HEADER, BaseApplication.getApplication().getResources().getString(
                        R.string.no_response_header
                )));
            }else{
                Logger.t(Constant.NET_LOG_TAG).d(String.format(Constant.NET_RESPONSE_HEADER,sbHeader.toString()));
            }

        }
        // 打印json日志
        if(validate(responseText)){
            Logger.t(Constant.NET_LOG_TAG).json(responseText);
        }else{
            Logger.t(Constant.NET_LOG_TAG).d(responseText);
        }

    }
    private boolean isPlaintext(Buffer buffer) {
        try {
            Buffer prefix = new Buffer();
            long byteCount = buffer.size() < 64 ? buffer.size() : 64;
            buffer.copyTo(prefix, 0, byteCount);
            for (int i = 0; i < 16; i++) {
                if (prefix.exhausted()) {
                    break;
                }
                int codePoint = prefix.readUtf8CodePoint();
                if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
                    return false;
                }
            }
            return true;
        } catch (EOFException e) {
            return false;
        }
    }
    private boolean validate(String jsonStr) {
        JsonElement jsonElement;
        try {
            jsonElement = new JsonParser().parse(jsonStr);
        } catch (Exception e) {
            return false;
        }
        if (jsonElement == null) {
            return false;
        }
        return jsonElement.isJsonObject();
    }
    private boolean bodyEncoded(Headers headers) {
        String contentEncoding = headers.get("Content-Encoding");
        return contentEncoding != null && !contentEncoding.equalsIgnoreCase("identity");
    }
}
复制代码

4.2.5 使用 AutoDispose 解决 RxJava 内存泄漏

一般来说,在 MVP 框架中,网络请求都是在 Presenter 的实现类的某个方法中进行,接口响应成功获得响应结果后通知 View层(Activity/Fragment)刷新UI,请求到响应这一系列操作在 RxJava 响应式框架中是属于订阅关系,具体代码在 FormRequestClient.java 和 RawRequestClient.java 中都有,部分关键代码如下:

 ...
 ...
 if (requestObservable != null) {
            //若订阅需要绑定View层生命周期则走此方法 防止内存泄漏的问题
            if(observer.getAutoDisposeConverter()!=null){
                requestObservable
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .doOnDispose(() -> observer.onDispose())
                        .as(observer.getAutoDisposeConverter())
                        .subscribe(new SubscribeObserver(observer));
            }else{
                requestObservable
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe(new SubscribeObserver(observer));
            }
        }
...
...
复制代码

其中 requestObservable 作为请求时候的被观察者,SubscribeObserver 对象被 subscribe 调用作为观察者绑定到 requestObservable 的被观察者对象上,每一次的网络请求就有一堆请求响应的事件处于订阅关系,响应结果出来后通知 View 层刷新UI,这个时候可能就会造成内存泄露,比如在 A 页面请求接口想要改变当前页面某个TextView 的值,在请求发出后响应结果前,跳转到 B 页面,此刻 A 页面可能被销毁,但是刚刚的请求响应的订阅关系还存在,这个时候当响应成功后,就会造成内存泄露的问题。 为了防止以上问题出现,我们可以使用用当前较为流行的 AutoDispose 框架 来作为处理订阅关系与 View 生命周期变化的矛盾,当然除了 AutoDispose 框架 目前还有一个流行的框架它就是 RxLifecycle 框架,至于为什么我选用 AutoDispose 框架 而不用 RxLifecycle 框架,他两又有什么区别,这个就不展开说了,可以参考一下这篇博文 Android架构中添加AutoDispose解决RxJava内存泄漏 下面我就直说如何在 MVP 中使用 AutoDispose 的经典步骤 第一步:封装 RxLifecycleUtils

public class RxLifecycleUtils {

    private RxLifecycleUtils() {
        throw new IllegalStateException("Can't instance the RxLifecycleUtils");
    }

    public static <T> AutoDisposeConverter<T> bindLifecycle(LifecycleOwner lifecycleOwner) {
        return AutoDispose.autoDisposable(
                AndroidLifecycleScopeProvider.from(lifecycleOwner)
        );
    }
}

复制代码

第二步:写一个接口返回 AutoDisposeConverter,让 View 基层基层实现并绑定返回 AutoDisposeConverter 实例。这里可以向上滑动看看文章前面 2.1 谈到的 View 层设计,这里的目的是为了让 Presenter 层能够拿到所对应的 View 层的 AutoDisposeConverter 实例,从而能让订阅关系能够绑定 View 层的生命周期,避免内存泄漏问题,关键代码如下:

public abstract class BaseActivity<P extends BasePresenter> extends AppCompatActivity implements BaseView {
...
    @Override
    public <T> AutoDisposeConverter<T> bindLifecycle() {
        return RxLifecycleUtils.bindLifecycle(this);
    }
...
}

复制代码

第三步:在 Presenter 的实例类的方法中,进行请求时候,若需要绑定 View 的生命周期,就给 RxJava 响应式框架请求的响应式数据类型(Observable)如下代码使用:

WanAndroidFormRequestClient.getInstance().executeGet(UrlConstant.GET_BANNER_JSON, null, new AppObserver<BannerResult>(view,true) {
            @Override
            public void onNext(BannerResult bannerResults) {
                super.onNext(bannerResults);
                view.onBanner(bannerResults.getData());
            }

        });
复制代码

若有的接口不需要绑定 View 的生命周期,如接口埋点不需要通知 view 层修改,则如下代码使用:

WanAndroidFormRequestClient.getInstance().executeGet(UrlConstant.GET_BANNER_JSON, null, new AppObserver<BannerResult>(view,false) {
            @Override
            public void onNext(BannerResult bannerResults) {
                super.onNext(bannerResults);
                view.onBanner(bannerResults.getData());
            }

        });
复制代码

关键的点就是传入 AppObserver 对象的构造函数的值不同,来决定是否需要绑定 View 生命周期来处理订阅关系,这里贴 AppObserver 的部分相关核心代码如下:

public abstract class AppObserver<T> extends BaseObserver<T> {

...
    private BaseView view = null;
    private boolean needBindLifeCycle = true;//标识是否需要绑定view生命周期
    private String loadTips;
...
...
    public AppObserver(BaseView view, boolean needBindLifeCycle) {
        this.view = view;
        this.needBindLifeCycle = needBindLifeCycle;
    }

    public AppObserver(BaseView view, String loadTips, boolean needBindLifeCycle) {
        this.view = view;
        this.loadTips = loadTips;
        this.needBindLifeCycle = needBindLifeCycle;
    }


    //获取绑定view层生命周期的 AutoDisposeConverter 实例
    public <R> AutoDisposeConverter<R> getAutoDisposeConverter() {
        if (view != null && needBindLifeCycle && view.bindLifecycle() != null) {
            return view.bindLifecycle();
        }
        return null;
    }

    public BaseView getView() {
        return view;
    }
    public void onDispose(){//被取消订阅后会被调用
        if (view != null) {
            view.loadCompleted();
        }
    }
    @Override
    protected void onError(ApiException ex) {
        Logger.t(Constant.NET_LOG_TAG).e(String.format(Constant.NET_EXCEPTION_STRING, ex.getDisplayMessage()));
        if (view != null) {
            view.loadError(ex);
        }

    }

    @Override
    public void onSubscribe(Disposable d) {
        if (loadTips != null && view != null) {
            if(view!=null){
                view.onLoadIng(loadTips == null ? view.getContext().getString(R.string.on_loading) : loadTips);
            }

        }
    }

    @Override
    public void onComplete() {
        if (view != null) {
            view.loadCompleted();
        }

    }
    ...
}

复制代码

五、总结

在设计一个基础框架的时候,我们除了要考虑到项目的每一种应用场景还要考虑到代码的 “高内聚,低耦合”,对抽象、接口、重载、多态、泛型善加利用,充分考虑代码的复用性、灵活性、安全性,对于模块框架层来说最好做到开发者每次传入参数都能拿到想要的出参,这个过程不需要再次进行多余的处理,而且这个过程开发者写的代码量越简约越好。

上面提到的我设计的框架我也开源到了 Github 上面:使用 Java 构建的一个模块化的 MVP 的项目 除此之外,我还写了一个 Kotlin 版本的框架:使用 Kotlin 构建的一个模块化的 MVP 的项目

有任何问题疑问欢迎留言,或者在 Github 上提出 issue,觉得不错的话烦请给个 Star 。哈哈,谢谢~~

关注下面的标签,发现更多相似文章
评论