前言
本篇文章适合有Android开发基础,了解MVP开发模式的读者。
笔者觉得MVP开发模式在Android上的应用还不够完美,因为Presenter内部持有着View,而在Android中实现View接口的一般都是需要释放的资源,比如Activity,Fragment等,为了避免内存泄漏,一般是采用弱引用来保存View,或者在生命周期结束的时候把View置为空。
笔者一直在思考有没有办法让Presenter内部不持有这些需要释放的资源?最终java的动态代理给了笔者灵感,写了一个通信库:stream
下面将介绍如何利用这个库来解耦Presenter和View。以及库的实现原理。
新写法
这边通过一个简化的登陆例子来说明新写法。
为了提高可读性,省略了Model层,Presenter接口,简化了写法。
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原理分析
在开始之前先对关键名词做一下解释:
- 流接口
继承了
FStream
接口的接口,即上述例子中的ILoginView
接口 - 流对象
实现流接口类的对象,即上述例子中的
MainActivity
对象
现在来分析一下库内部是如何通信的,首先我们得知道下面几个代码被触发后内部发生了什么:
1. 注册流对象后发生了什么?
FStreamManager.getInstance().register(this);
复制代码
上面代码执行之后,库内部做了以下事情:
搜索流对象实现的所有流接口,并把流接口和流对象做一个映射。
搜索流接口的规则是,从流对象类开始,不断往上搜索父类。(读者不用担心性能问题,内部有做处理)
就上述例子来说,传进去的this
是MainActivity
对象,由于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();
的时候,有两种情况:
-
没有任何与之对应的流对象
那么会根据返回值类型返回对应的值,比如数字类型返回0,布尔类型返回false,对象类型返回null
-
有一个或者多个与之对应的流对象
那么默认会用最后一个注册的流对象的目标方法的返回值当做代理对象方法最终的返回值
第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