1. 程式人生 > >【架構】Android裡較為理想的全域性重新整理機制

【架構】Android裡較為理想的全域性重新整理機制

我們來談談客戶端介面的資料同步問題。

介面資料同步的需求

比如,下面的AB兩個介面中都顯示了學生Leslie的資訊,當我們在A介面修改學生學號時,我們希望回到B介面時,學生的資訊也能跟著改變,才能保證業務資料的正確和一致性。

這就涉及到資料的同步和重新整理問題。

主介面顯示了學生和老師的資訊

另一個介面也顯示了學生的資訊

重新整理資料時都要從資料來源再次請求資料嗎?

如今手機應用的資料幾乎都來自網路(或者本地資料庫)。假如我們在A介面上修改了學生的資訊並同步到網路,若回到B介面需要重新整理該學生的資訊,再次呼叫網路請求得到學生的資訊顯示在B介面,這是可以的,但會大大增加伺服器的負擔或影響應用的響應速度,而且當用戶到達一定的數量級,客戶端頻繁的網路請求遲早會把伺服器搞垮。

如何避免頻繁的網路請求同時也能實現客戶端介面資料的正確性和一致性的呢?

對於上述問題,大多應用的解決方法是把網路請求得到的業務資料快取在記憶體中,優先使用記憶體快取資料

這樣做的理由是:

我們假定一個正常的普通使用者,同一時刻只會在同一個裝置上開啟應用並進行業務資料操作。在這個大前提下,我們可以保證在同一時間內,不會有其他裝置對伺服器的同個賬號資料進行更新。 於是,我們可以在第一次請求網路資料時,將請求得到的資料存在記憶體中,當用戶對業務資料做出了操作,我們在呼叫API將業務資料同步到伺服器後,根據使用者的操作對於記憶體資料進行對應的更新,保證了記憶體資料和伺服器資料的一致,在之後,直接從記憶體資料來源取出資料供介面顯示即可

這樣的方式避免了頻繁、重複的API網路請求,同時使應用有較高的資料訪問和響應速度。壞處是如果業務資料量龐大,應用將會佔用大量的執行記憶體,可以通過優化來改善(我們日後再談)。

總結一下。我們對客戶端的業務資料進行重新整理時,可以優先考慮從記憶體中讀取快取好的業務資料,特殊情況下,才考慮從網路或本地資料庫請求資料。

客戶端資料同步機制的設計

上面討論好的資料儲存機制為全域性的資料重新整理提供了一些基礎。如何實現全域性的資料同步機制呢?

先上架構大致的示意圖。

架構的大致示意圖

看完示意圖你可能就大致明白了。

簡單來講。一個Actvity裡包含了多個Fragment,而每個Fragment介面如果有資料同步的需要,就委託Activity向一個叫FragmentSyncManager的類註冊一個特定型別

的監聽回撥(如學生資料、教師資料型別)。在之後的任何介面中,只要我們呼叫同步指令並指明要同步的資料型別,FragmentSyncManager就會找到對應資料型別的所有同步回撥,並全部執行同步重新整理。

原理很簡單,但把它們融進架構和妥善封裝是開發和程式設計的問題了。

接下來我們簡單分析一下架構的實現。

架構實現

1. 同步監聽的定義和監聽例項的管理

我們先看看同步監聽類的定義:

/**
 * 同步回撥類
 * @param <T> 這裡的泛型就是用來標識資料型別的,為SyncedObject的子類
 * 比如學生資料型別我們就指定T為SyncedObject.Student,教師資料型別就指定T為SyncedObject.Teacher
 */
public abstract class OnRefreshDataListener<T extends SyncedObject> {

    /**
     * 返回泛型的型別
     * 假如這一個監聽是用來重新整理學生資訊的,這個方法將會返回SyncedObject.Student的Class型別值
     */
    public final Class<?> getSyncedObjectClass() {
        return (Class) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
    }

    // onRefresh方法便是同步要做的事情
    // 當FragmentSyncManager接收到同步指令時,會找到指定的資料型別並呼叫對應同步監聽的這個方法
    public void onRefresh(T t) {
        // 這裡內容可以是從記憶體快取或網路得到資料並顯示在介面
    }
}

上面的程式碼中,泛型T是SyncedObject的子類,SyncedObject類就是用來標明同步重新整理的資料型別,我們用它來方便管理。比如學生和教師資料型別,繼承這個類即可,可以不用包含什麼資訊,如下。

public class SyncedObject {

    // 學生資料型別標識類
    public static class Student extends SyncedObject {
    }

    // 教師資料型別標識類
    public static class Teacher extends SyncedObject {
    }
}

再看看重新整理監聽的管理類FragmentSyncManager,它包含了對同步監聽的登記、登出(刪除)、執行三個方法。

public class FragmentSyncManager {

    // 存入【重新整理型別】->【所有介面的同步回撥集合】,
    // 如【學生資訊型別】->【A介面的同步回撥, B介面的同步回撥】
    // 如【教師資訊型別】->【B介面的同步回撥, C介面的同步回撥】
    private Map<Class, List<OnRefreshDataListener>> mOnUIThreadSyncedMap = new HashMap<>();

    private static FragmentSyncManager sFragmentSyncManager = new FragmentSyncManager();

    public static FragmentSyncManager getInstance() {
        return sFragmentSyncManager;
    }

    // 註冊一個回撥
    public void register(OnRefreshDataListener onRefreshDataListener) {
        if (onRefreshDataListener != null) {

            // 得到要同步的資料型別,隨後根據型別把本次註冊的同步監聽新增到對應的集合
            Class syncedObjectClass = onRefreshDataListener.getSyncedObjectClass();

            List<OnRefreshDataListener> onRefreshDataListeners = mOnUIThreadSyncedMap.get(syncedObjectClass);
            if (onRefreshDataListeners == null) {
                onRefreshDataListeners = new ArrayList<>();
            }
            onRefreshDataListeners.add(onRefreshDataListener);
            mOnUIThreadSyncedMap.put(syncedObjectClass, onRefreshDataListeners);
        }
    }

    // 刪除掉一個監聽,這個方法在介面銷燬時執行。
    // 原因很簡單,介面銷燬掉了,不再需要對這個介面上的資料進行同步重新整理了。
    public void deregister(OnRefreshDataListener onRefreshDataListener) {
        mOnUIThreadSyncedMap.get(onRefreshDataListener.getSyncedObjectClass()).remove(onRefreshDataListener);
    }

    /**
     * 傳入特定的資料型別,執行這個資料型別對應的所有同步監聽
     *
     * @param syncedObject 要同步的資料型別。
     */
    public void synced(SyncedObject syncedObject) {
        if (syncedObject != null && mOnUIThreadSyncedMap.containsKey(syncedObject.getClass())) {
            List<OnRefreshDataListener> onRefreshDataListeners = mOnUIThreadSyncedMap.get(syncedObject.getClass());
            for (int index = 0; index < onRefreshDataListeners.size(); index++) {
                OnRefreshDataListener onRefreshUIListener = onRefreshDataListeners.get(index);
                if (onRefreshUIListener != null) {
                    
                    // 執行同步監聽的onRefresh回撥方法
                    onRefreshUIListener.onRefresh(syncedObject);
                    
                }
            }
        }
    }
}

到這裡,重新整理監聽的定義和管理便完成了。接下來談談封裝方法。

2. Activity和Fragment的封裝

我們再回顧一下這個示意圖。

架構的大致示意圖

可以得到:每個Fragment對同步監聽有三種操作,為在登記、登出、執行;同時,一個Actvity裡會包含多個Fragment。

我們本可以把這三類操作放在Fragment的封裝基類,但為了減少建立Fragment的執行程式碼的記憶體佔用,我們把同步監聽的操作放到BaseActivity中,如下。

public abstract class BaseActivity extends AppCompatActivity implements OnFragmentSyncedListener {

    @Override
    public void registerListener(OnRefreshDataListener onRefreshUIListener) {
        FragmentSyncManager.getInstance().register(onRefreshUIListener);  // 登記監聽
    }

    @Override
    public void unRegisterListener(OnRefreshDataListener onRefreshUIListener) {
        FragmentSyncManager.getInstance().deregister(onRefreshUIListener);  // 登出監聽
    }

    @Override
    public void onSynced(SyncedObject syncedObject) {
        FragmentSyncManager.getInstance().synced(syncedObject);  // 執行同步
    }
}

顯然,這三個方法是需要在Fragment中被呼叫的。Fragment裡用getActivity()方法可以得到所在的activity,那如何讓Fragment呼叫得到BaseActivity裡的方法呢?

在上面程式碼中,我們讓BaseActivity實現介面OnFragmentSyncedListener(這個介面包含了增、刪、執行那三個方法),隨後在BaseFragment裡把activity轉為這個介面的例項,就可以實現呼叫BaseActivity這三個方法了,如下。

Activity activity = (Activity) context;
mOnFragmentSyncedListener = activity instanceof OnFragmentSyncedListener ? (OnFragmentSyncedListener) activity : null;

我們看看BaseFragment的總實現。

public abstract class BaseFragment extends Fragment {

    // 通過本Fragment對應的Activity轉換後得到的OnFragmentSyncedListener例項
    private OnFragmentSyncedListener mOnFragmentSyncedListener;

    // 儲存本Fragment登記過的所有同步監聽,這個欄位用於在Fragment銷燬時登出掉所有的同步監聽
    private List<OnRefreshDataListener> onRefreshDataListeners = new ArrayList<>();

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        Activity activity = (Activity) context;
        mOnFragmentSyncedListener = activity instanceof OnFragmentSyncedListener ? (OnFragmentSyncedListener) activity : null;
    }

    // 登記監聽
    protected void registerRefreshDataListener(OnRefreshDataListener onRefreshDataListener) {
        if (mOnFragmentSyncedListener != null) {
            mOnFragmentSyncedListener.registerListener(onRefreshDataListener);  // 這個方法就是Activity實現的registerListener方法
            onRefreshDataListeners.add(onRefreshDataListener);
        }
    }

    // 同步監聽
    protected void syncedRefreshDataListener(SyncedObject syncedObject) {
        if (mOnFragmentSyncedListener != null) {
            mOnFragmentSyncedListener.onSynced(syncedObject);  // 這個方法就是Activity實現的onSynced方法
        }
    }

    // 登出所有監聽
    private void unRegisterRefreshDataListeners(List<OnRefreshDataListener> onRefreshDataListeners) {
        if (mOnFragmentSyncedListener != null) {
            for (int index = 0; index < onRefreshDataListeners.size(); index++) {
                if (onRefreshDataListeners.get(index) != null) {
                    mOnFragmentSyncedListener.unRegisterListener(onRefreshDataListeners.get(index));  // 這個方法就是Activity實現的unRegisterListener方法
                }
            }
        }
    }

    protected void unRegisterALLRefreshDataListeners() {
        unRegisterRefreshDataListeners(onRefreshDataListeners);
    }

    // Fragment銷燬時,登出掉所有的同步監聽
    @Override
    public final void onDestroy() {
        unRegisterALLRefreshDataListeners();
        super.onDestroy();
    }
}

之後,每個BaseActivity子類中的BaseFragment子類就可以使用資料同步的功能了。

如Fragment A中登記了同步監聽:

registerRefreshDataListener(new OnRefreshDataListener<SyncedObject.Student>() {
    @Override
    public void onRefresh(SyncedObject.Student o) {
        refreshStudentData();  // 從資料來源得到學生資料並顯示在介面上,執行全域性重新整理時,這個方法將會被呼叫
    }
});

Fragment A 顯示了學生和老師的資訊

在Fragment B 中也顯示了學生的資訊,當我們點選按鈕【Update Student】更新學生資訊後,Fragment A 介面上的學生資訊也會改變。

Fragment B 也顯示了學生的資訊

按鈕【Update Student】的響應程式碼如下。

view.findViewById(R.id.btn_update_student).setOnClickListener(v -> {

    // 改變業務資料。實際應用中,可能是通過呼叫網路請求去更新業務資訊,呼叫成功後再更新記憶體資料
    StudentRepository.getInstance().getStudent().setNum("1234567890");

    showStudentData();  // 顯示介面學生的資訊

    // 針對學生資料型別,執行全域性重新整理操作
    syncedRefreshDataListener(new SyncedObject.Student());  // 這個方法是BaseFragment裡的同步方法

});

上面的程式碼中,當執行

syncedRefreshDataListener(new SyncedObject.Student()); 

後,如果Fragment A 此時還沒有銷燬的話,Fragment A 中的

void onRefresh(SyncedObject.Student o)

方法將會被執行(因為它是學生資料型別的同步監聽),即實現了重新整理介面。

現在安卓官方出了一個叫LiveData的元件,也同樣是基本記憶體快取資料實現了全域性重新整理機制的,還加入了Fragment和Activity生命週期的控制。它的實現方式和設計原理和本文提到的內容大致相同,但更加的傻瓜化和易用,朋友們可以自行搜尋檢視。