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

相關文章
相關標籤/搜索