本篇文章適合有Android開發基礎,瞭解MVP開發模式的讀者。java
筆者以爲MVP開發模式在Android上的應用還不夠完美,由於Presenter內部持有着View,而在Android中實現View接口的通常都是須要釋放的資源,好比Activity,Fragment等,爲了不內存泄漏,通常是採用弱引用來保存View,或者在生命週期結束的時候把View置爲空。git
筆者一直在思考有沒有辦法讓Presenter內部不持有這些須要釋放的資源?最終java的動態代理給了筆者靈感,寫了一個通訊庫:streamgithub
下面將介紹如何利用這個庫來解耦Presenter和View。以及庫的實現原理。ide
這邊經過一個簡化的登錄例子來講明新寫法。
爲了提升可讀性,省略了Model層,Presenter接口,簡化了寫法。post
public interface ILoginView extends FStream {
/** * 登陸成功回調 */
void onLoginSuccess();
}
複製代碼
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);
}
}
複製代碼
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
對象這邊呢?
學習
在開始以前先對關鍵名詞作一下解釋:ui
繼承了
FStream
接口的接口,即上述例子中的ILoginView
接口this
實現流接口類的對象,即上述例子中的
MainActivity
對象spa
如今來分析一下庫內部是如何通訊的,首先咱們得知道下面幾個代碼被觸發後內部發生了什麼:
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中。
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()
方法,方法參數說明:
mLoginView.onLoginSuccess();
複製代碼
上面代碼執行以後,發生瞭如下操做:
會觸發
InvocationHandler
對象的invoke()
方法,在invoke()
方法內,從Map中取出與當前流接口映射的全部流對象,並調用流對象的目標方法。
內部通知流對象的代碼簡化以下:
// 從保存映射關係的Map中取出對應的流對象
final List<FStream> list = MAP_STREAM.get(ILoginView.class);
for (FStream item : list) {
// 觸發已註冊的流對象的目標方法
method.invoke(item, args);
}
複製代碼
到此爲止,整個通訊流程已經理清了,具體的分發邏輯,有興趣的讀者能夠看一下項目源碼。
先看一下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的功能是顯示隱藏進度框,改造以下:
public interface IProgressView extends FStream {
void showProgress();
void dismissProgress();
}
複製代碼
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);
}
}
複製代碼
// 改造:實現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。
能夠在建立代理對象的時候傳入一個實現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);
複製代碼
這邊簡單改造一下上述例子:
public interface ILoginView extends FStream {
void onLoginFinish();
// 改造:返回須要登陸的用戶名
String getUserName();
}
複製代碼
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