Android - 解锁MVP新姿式

前言

本篇文章适合有Android开发基础,了解MVP开发模式的读者。java

笔者以为MVP开发模式在Android上的应用还不够完美,由于Presenter内部持有着View,而在Android中实现View接口的通常都是须要释放的资源,好比Activity,Fragment等,为了不内存泄漏,通常是采用弱引用来保存View,或者在生命周期结束的时候把View置为空。git

笔者一直在思考有没有办法让Presenter内部不持有这些须要释放的资源?最终java的动态代理给了笔者灵感,写了一个通讯库:streamgithub

下面将介绍如何利用这个库来解耦Presenter和View。以及库的实现原理。ide

新写法

这边经过一个简化的登录例子来讲明新写法。
为了提升可读性,省略了Model层,Presenter接口,简化了写法。post

View

public interface ILoginView extends FStream {
    /** * 登陆成功回调 */
    void onLoginSuccess();
}
复制代码

Presenter

public class LoginPresenter {
    private final ILoginView mLoginView;

    public LoginPresenter(String tag) {
        mLoginView = new FStream.ProxyBuilder().setTag(tag).build(ILoginView.class);
    }

    /** * 登陆方法 */
    public void login() {
        // 模拟请求接口
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                mLoginView.onLoginSuccess();
            }
        }, 2000);
    }
}
复制代码

Activity

public class MainActivity extends BaseActivity implements ILoginView {
    private final LoginPresenter mLoginPresenter = new LoginPresenter(toString());

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mLoginPresenter.login();
            }
        });
    }

    @Override
    public void onLoginSuccess() {
        Log.i(getClass().getSimpleName(), "onLoginSuccess");
    }
}
复制代码
public class BaseActivity extends AppCompatActivity implements FStream {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        FStreamManager.getInstance().register(this);
    }

    @Override
    public Object getTagForClass(Class<?> clazz) {
        return toString();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        FStreamManager.getInstance().unregister(this);
    }
}
复制代码

对比写法

对比发现,新写法和常规写法有几处不一样:性能

  • ILoginView接口继承了FStream接口
  • LoginPresenter中默认建立了一个实现ILoginView接口的对象,不须要外部传入View对象了,而是传入一个字符串标识
  • BaseActivity中多了一些stream相关的代码

看到这里读者是否有疑问,既然LoginPresenter中没有持有实现ILoginView接口的MainActivity对象,那它是怎么通知到MainActivity对象这边呢?
学习

stream原理分析

在开始以前先对关键名词作一下解释:ui

  • 流接口

    继承了FStream接口的接口,即上述例子中的ILoginView接口this

  • 流对象

    实现流接口类的对象,即上述例子中的MainActivity对象spa

如今来分析一下库内部是如何通讯的,首先咱们得知道下面几个代码被触发后内部发生了什么:

1. 注册流对象后发生了什么?

FStreamManager.getInstance().register(this);
复制代码

上面代码执行以后,库内部作了如下事情:

搜索流对象实现的全部流接口,并把流接口和流对象作一个映射。

搜索流接口的规则是,从流对象类开始,不断往上搜索父类。(读者不用担忧性能问题,内部有作处理)

就上述例子来讲,传进去的thisMainActivity对象,因为MainActivity实现了ILoginView接口,而且ILoginView接口继承了FStream接口,即ILoginView接口是一个流接口,因此最终会找到ILoginView接口。

找到以后,库内部会用Map<Class, List<FStream>>来保存流接口和流对象的映射关系,以下:

key value
ILoginView.class [MainActivity对象]

因为Map的Value是一个List,因此映射表中的“MainActivity对象”用中括号包裹,表示存到一个List中。

2. 建立代理对象后发生了什么?

mLoginView = new FStream.ProxyBuilder().setTag(tag).build(ILoginView.class);
复制代码

上面代码执行以后,库内部作了如下事情:

根据流接口class,建立一个流接口的代理对象,代理对象和传进来的tag关联,最后返回代理对象

就上述例子来讲,实际上mLoginView所指向的是一个代理对象,库内部建立代理对象的代码简化以下:

ILoginView proxy = (ILoginView) Proxy.newProxyInstance(ILoginView.class.getClassLoader(), new Class<?>[]{ILoginView.class}, new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return null;
    }
});
复制代码

熟悉java动态动态代理的读者对上面的代码应该不陌生,建立代理对象的时候会要求传入一个实现InvocationHandler接口的对象,InvocationHandler接口中只有一个invoke()方法,方法参数说明:

  • proxy,代理对象
  • method,代理对象的哪个方法被调用了
  • args,该方法的参数

3. 代理对象的方法被调用的时候发生了什么?

mLoginView.onLoginSuccess();
复制代码

上面代码执行以后,发生了如下操做:

会触发InvocationHandler对象的invoke()方法,在invoke()方法内,从Map中取出与当前流接口映射的全部流对象,并调用流对象的目标方法。

内部通知流对象的代码简化以下:

// 从保存映射关系的Map中取出对应的流对象
final List<FStream> list = MAP_STREAM.get(ILoginView.class);
for (FStream item : list) {
    // 触发已注册的流对象的目标方法
    method.invoke(item, args);
}
复制代码

到此为止,整个通讯流程已经理清了,具体的分发逻辑,有兴趣的读者能够看一下项目源码。

疑问

1. 若是流接口对应多个流对象,那么当代理对象方法被调用的时候,全部流对象都会被通知吗?

先看一下FStream内部的一个默认方法

default Object getTagForClass(Class clazz) {
    return null;
}
复制代码

实际上库内部在通知流对象以前,会先调用一下这个getTagForClass()方法返回一个tag,用返回的tag和代理对象的tag进行比较,只有tag相等,才会通知这个流对象。

若是代理对象未设置tag,则默认的tag为null
若是流对象未重写getTagForClass()方法,则默认返回的tag为null

因此默认状况下,流接口和它所映射的流对象的tag是相等的,都为null,表示代理对象的方法被调用的时候,全部流对象都会被通知。

库内部比较tag相等的规则以下:

private boolean checkTag(FStream stream) {
    // mTag为代理对象的tag
    final Object tag = stream.getTagForClass(mClass);
    if (mTag == tag)
        return true;
    
    return mTag != null && mTag.equals(tag);
}
复制代码

getTagForClass()方法的参数 “clazz”, 即建立代理对象时传入的流接口对应的class,在上述例子中,就是ILoginView.class,之因此须要这个参数,是由于流对象可能实现了多个流接口,多个流接口又对应多个不一样类型的代理对象,各个代理对象的tag可能又不一样。
这时候就能够经过class参数来判断是哪一个类型的代理对象方法被调用,而后返回对应的tag。

举个例子?

就上述例子中,咱们新增一个View接口,这个View的功能是显示隐藏进度框,改造以下:

View

public interface IProgressView extends FStream {
    void showProgress();

    void dismissProgress();
}
复制代码

Presenter

public class LoginPresenter implements ILoginPresenter {
    private final ILoginView mLoginView;
    // 改造:新增一个IProgressView
    private final IProgressView mProgressView;

    public LoginPresenter(String tag) {
        mLoginView = new FStream.ProxyBuilder().setTag(tag).build(ILoginView.class);

        // 改造:建立IProgressView接口对应的代理对象,注意这里传入的tag是hello字符串
        mProgressView = new FStream.ProxyBuilder().setTag("hello").build(IProgressView.class);
    }

    @Override
    public void login() {
        // 改造:显示进度框
        mProgressView.showProgress();
        
        // 延迟2秒后通知View,模拟请求接口
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                // 改造:隐藏进度框
                mProgressView.dismissProgress();
                mLoginView.onLoginFinish();
            }
        }, 2000);
    }
}
复制代码

Activity

// 改造:实现IProgressView接口
public class BaseActivity extends AppCompatActivity implements FStream, IProgressView {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        FStreamManager.getInstance().register(this);
    }

    @Override
    public Object getTagForClass(Class<?> clazz) {
        // 改造:若是clazz == IProgressView.class,返回hello字符串做为tag,这样流对象和代理对象的tag才能匹配
        if (clazz == IProgressView.class)
            return "hello";

        return toString();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        FStreamManager.getInstance().unregister(this);
    }

    @Override
    public void showProgress() {
        Log.i(getClass().getSimpleName(), "showProgress");
    }

    @Override
    public void dismissProgress() {
        Log.i(getClass().getSimpleName(), "dismissProgress");
    }
}
复制代码

因为mProgressView是一个代理对象,tag被设置为“hello”字符串,因此在重写getTagForClass()方法内须要判断,clazz == IProgressView.class时返回“hello”字符串做为tag,这样流对象和代理对象的tag匹配以后,流对象才能被通知到。

固然实际开发中建议同一个Activity中的tag保持一致,除非是有特殊需求的能够根据上述例子所示,返回相应的tag。

2. 若是流接口对应多个流对象,不想通知全部的流对象,怎么处理?

能够在建立代理对象的时候传入一个实现DispatchCallback接口的对象,用来处理是否继续分发的逻辑。

interface DispatchCallback {
        /** * 流对象的方法被通知以前触发 * * @param stream 流对象 * @param method 方法 * @param methodParams 方法参数 * @return true-中止分发,false-继续分发 */
        boolean beforeDispatch(FStream stream, Method method, Object[] methodParams);

        /** * 流对象的方法被通知以后触发 * * @param stream 流对象 * @param method 方法 * @param methodParams 方法参数 * @param methodResult 流对象方法被调用后的返回值 * @return true-中止分发,false-继续分发 */
        boolean afterDispatch(FStream stream, Method method, Object[] methodParams, Object methodResult);
    }
复制代码
mLoginView = new FStream.ProxyBuilder()
        .setTag(tag)
        .setDispatchCallback(new FStream.DispatchCallback() {
            @Override
            public boolean beforeDispatch(FStream stream, Method method, Object[] methodParams) {
                // 处理是否继续分发的逻辑
                return false;
            }

            @Override
            public boolean afterDispatch(FStream stream, Method method, Object[] methodParams, Object methodResult) {
                // 处理是否继续分发的逻辑
                return false;
            }
        })
        .build(ILoginView.class);
复制代码

3. 若是被调用的代理对象方法有返回值,那么最终的返回值怎么肯定?

这边简单改造一下上述例子:

View

public interface ILoginView extends FStream {
    void onLoginFinish();
    
    // 改造:返回须要登陆的用户名
    String getUserName();
}
复制代码

Presenter

public class LoginPresenter implements ILoginPresenter {
    private final ILoginView mLoginView;

    public LoginPresenter(String tag) {
        mLoginView = new FStream.ProxyBuilder().setTag(tag).build(ILoginView.class);
    }

    @Override
    public void login() {
        // 改造:得到用户名来登陆
        final String userName = mLoginView.getUserName();

        // 延迟2秒后通知View,模拟请求接口
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                mLoginView.onLoginFinish();
            }
        }, 2000);
    }
}
复制代码

调用mLoginView.getUserName();的时候,有两种状况:

  1. 没有任何与之对应的流对象

    那么会根据返回值类型返回对应的值,好比数字类型返回0,布尔类型返回false,对象类型返回null

  2. 有一个或者多个与之对应的流对象

    那么默认会用最后一个注册的流对象的目标方法的返回值当作代理对象方法最终的返回值

第2种状况中若是代理对象须要筛选返回值,那如何处理?

能够在建立代理对象的时候传入一个实现ResultFilter接口的对象,用来筛选返回值

interface ResultFilter {
    /** * 过滤返回值 * * @param method 方法 * @param methodParams 方法参数 * @param results 全部流对象的返回值 * @return */
    Object filter(Method method, Object[] methodParams, List<Object> results);
}
复制代码
mLoginView = new FStream.ProxyBuilder()
        .setTag(tag)
        .setResultFilter(new FStream.ResultFilter() {
            @Override
            public Object filter(Method method, Object[] methodParams, List<Object> results) {
                // 这边筛选第一个流对象的返回值做为最终的返回值
                return results.get(0);
            }
        })
        .build(ILoginView.class);
复制代码

结束语

文章比较长,须要耐心的看,才能理解整个内部的原理,固然stream库还不止解耦Presenter和View这一个使用场景,后面有时间会写一下stream库的更多使用场景。

关于这个库,有疑问的,或者须要探讨的能够和笔者联系,你们一块儿学习。 邮箱:565061763@qq.com

相关文章
相关标签/搜索