單Activity+多Fragment以及多模塊Activity+多Fragment"的設計模式編寫的架構

Rocket

採用"單Activity+多Fragment"以及"多模塊Activity+多Fragment"的設計模式編寫的架構。一個非常輕量級又十分強大的Fragment管理框架。

轉場動畫 路由棧視圖 動態權限
轉場動畫 路由棧視圖 動態權限
狀態欄 崩潰處理 日誌
狀態欄 崩潰處理 日誌管理

一、特性

  • 無任何第三方依賴,純原生編寫,無需擔心因爲版本迭代導致的維護問題

  • 採用線性路由棧,自行管理回退以及內存回收,提供懸浮球實時查看Fragment的棧視圖,降低開發難度

  • 頁面切換以及頁面通訊採用原生的commit以及setArguments,性能開銷極小,頁面切換流暢無卡頓

  • 支持自定義Fragment轉場動畫

  • 集成動態權限管理,包含必須權限(不允許不走生命週期)以及可選權限

  • 集成狀態欄管理,可適配狀態欄各種場景,包括沉浸式風格

  • 集成日誌管理,記錄崩潰日誌、內部日誌以及外部日誌,可設置日誌有效期,超時自動清理

  • 提供更加人性化的崩潰捕獲頁面,精準定位根源bug文件以及行數,降低查bug難度

  • 提供View註解,僅僅300行代碼支持了各種View注入和事件綁定

  • 提供百分比佈局,支持PercentRelativeLayout、PercentLinearLayout和PercentFrameLayout

二、Github地址

三、分享設計Rockt架構的思路

1、爲什麼要設計這個架構

Activity是一個非常重量級的設計!Activity的創建並不能由開發者自己控制,它是通過多進程遠程調用並最終通過放射的方式創建的。在此期間,AMS需要做大量的工作,以至於Activity的啓動過程極其緩慢。同時,Activity切換的開銷也非常重量級,很容易造成卡頓,用戶體驗不好。另外,在寬屏設備上,如果需要多屏互動時,Activity的侷限性也就表現了出來。爲此Android團隊在Android 3.0的時候引入了Fragment。Fragment的出現解決了上面兩個問題。根據詞海的翻譯可以譯爲:碎片、片段,可以很好解決Activity間的切換不流暢。因爲是輕量切換,性能更好,更加靈活。

2、如何設計Fragment的路由棧

一個好用的框架必然有一個好用的路由管理、堆棧管理的機制。Rocket的路由棧設計原則爲:一個Activity對應一個線性的路由棧,所有的頁面回退、內存回收以及Fragemnt生命週期管理都是內部完成,開發者只要專注跳轉目標即可,並提供可視化的棧視圖,極大簡化開發難度。那麼如何保證Fragment路由棧是線性的呢?只要能夠做到所有的頁面切換隻有一個transfer的入口,路由棧的出棧入棧都在這裏管理即可。

3、如何處理頁面切換、頁面通信以及內存回收

Fragment頁面跳轉建議用commit在隊列中提交,不建議用commitNowAllowingStateLoss等在線程中提交事務。一旦有快速切換頁面邏輯,線程中提交事務很容易出現因爲上個事務沒消耗完畢導致崩潰。
頁面通訊有很多方案,包括handle、廣播、接口回調、Eventbus等等,都不是最優方案。Fragment提供的setArguments方法非常的輕量級,可以完美實現頁面通訊。Rocket採用的就是原生的setArguments方案。
雖然Fragment相對Activity內存開銷小了很多,但是如果大量Fragment創建沒有及時回收的話會造成Activity內存臃腫。Rocket儘可能優的清理無用的Fragment,及時回收,消除應用卡頓。

4、如何處理轉場動畫與的Fragment生命週期

當Fragment中有setCustomAnimations轉場動畫的時候,做頁面切換、頁面通訊、內存管理等與生命週期相關的就多了很多的坑。Fragment提供的onCreateAnimation方法,不管有無動畫都會走這個方法,並且提供完整的動畫生命週期與動畫詳情。Rocket中充分利用這一點,
很好的規避了轉場動畫導致的各類難題。同時,Rocket提供設置轉場動畫入口給開發者,讓開發者隨心所欲定製自己的轉場動畫。

5、如何實現沉浸式狀態欄與正常狀態欄的無縫切換

狀態欄有兩種形態,顯示以及隱藏。隱藏的時候整個頁面向上頂滿屏幕,帶來很嚴重的突兀感。狀態欄依附的是window窗體,在Rocket框架中,因爲我們的頁面單位是Fragment,也就是說只要一個頁面切換狀態欄都會導致整個窗體一起變化。
Rocket提供一個方法,讓用戶自定義的標題欄可以向上或者向下偏移一個狀態欄高度,在頁面切換前後動態控制,避免突兀。對於沉浸風格的實現,Rocket在隱藏狀態欄的情況下會動態創建一個浮在表面的新狀態欄,用戶可以控制顏色與透明度,達到沉浸效果。

6、如何優雅處理動態權限申請與處理

我們知道,Fragment一般依賴於Activity存活,並且生命週期跟Activity差不多,因此,我們進行權限申請的時候,可以利用透明的Fragment進行申請,在裏面處理完之後,再進行相應的回調。
第一步:當我們申請權限申請的時候,先查找我們當前Activity是否存在代理fragment,不存在,進行添加,並使用代理Fragment進行申請權限
第二步:在代理 Fragment 的 onRequestPermissionsResult 方法進行相應的處理,判斷是否授權成功
第三步:進行相應的回調。這些繁瑣的步驟封裝在空白的Fragment中,降低耦合,方便維護。

7、如何更好的進行Fragment的事務提交

Fragment的事務提交主要涉及的函數有:commit()、commitNow()、commitAllowingStateLoss()、commitNowAllowingStateLoss()以及executePendingTransactions()。下面進行比較:
1、使用commit()的時候, 一旦調用, 這個commit並不是立即執行的, 它會被髮送到主線程的任務隊列當中去, 當主線程準備好執行它的時候執行。但是有時候你希望你的操作是立即執行的, 之前的開發者會在commit()調用之後加上 executePendingTransactions()來保證立即執行, 即變異步爲同步。
2、support library從v24.0.0開始提供了 commitNow()方法, 之前用executePendingTransactions()會將所有pending在隊列中還有你新提交的transactions都執行了, 而commitNow()將只會執行你當前要提交的transaction. 所以commitNow()避免你會不小心執行了那些你可能並不想執行的transactions。
3、如果你調用的是commitAllowingStateLoss()與commitNowAllowingStateLoss(),並且是在onSaveInstanceState()之後, 就不會拋出IllegalStateException,允許你丟失狀態,通常你不應該使用這個函數。
在Rocket的架構中,路由棧是串行的,主張採用commit()提交事務,保證每條事務在隊列中依次執行,不爭不搶,有條不紊。

四、踩坑經驗之旅

1、Can not perform this action after onSaveInstanceState

onSaveInstanceState方法是在該Activity即將被銷燬前調用,來保存Activity數據的,如果在保存完畢狀態後 再給它添加或者隱藏Fragment就會出錯。

解決方案

  • 方案一:把commit()方法替換成 commitAllowingStateLoss(),不採用。
  • 方案二:在Activity 回收時 onSaveInstanceState 中不緩存Fragment ,在OnCreate 中移除緩存相應Fragment數據,採用。
private static final String BUNDLE_SURPOTR_FRAGMENTS_KEY = "android:support:fragments";
private static final String BUNDLE_FRAGMENTS_KEY = "android:fragments";

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
        if (outState != null) {
            //重建時清除系統緩存的fragment的狀態
        	outState.remove(BUNDLE_SURPOTR_FRAGMENTS_KEY);
        	outState.remove(BUNDLE_FRAGMENTS_KEY);
        }
}

@Override
protected void onCreate(Bundle savedInstanceState) {
    if (savedInstanceState != null) {
        //重建時清除系統緩存的fragment的狀態
        savedInstanceState.remove(BUNDLE_FRAGMENTS_KEY);
        savedInstanceState.remove(BUNDLE_SURPOTR_FRAGMENTS_KEY);
    }
    super.onCreate(savedInstanceState);
}

2、FragmentManager is already executing transactions

這個問題是由於在執行Fragment事務提交的時候,commitNow以及commitNowAllowingStateLoss表示立刻執行事務提交,這個時候Activity 中的 FragmentManager 的第一次任務還沒有執行完畢,其他的操作又導致它需要進行第二次任務,所以發生錯誤。

private void ensureExecReady(boolean allowStateLoss) {
    if (mExecutingActions) {
        // 這正是堆棧日誌中拋出的異常信息
        throw new IllegalStateException("FragmentManager is already executing transactions");
    }
}

解決方案

  • 等待第一個任務執行完畢後再執行第二個任務
new Handler().post(() -> {
    //事務的提交            
});
  • 採用commit()在隊列中提交事務
//commit能保證在消息隊列中提交事務,保證上個事務處理完畢纔會執行
fragmentTransaction.commit()

3、Fragment xxx not attached to a context.

當一個Fragment已經從Activity中remove掉的時候,執行getString()、getResources()等,就會拋出這個異常。這種情況經常出現在異步回調的場景,比如一個網絡請求比較耗時,在返回之前fragment已經銷燬,當嘗試讀取資源就會出現。

//異步獲取數據,並提示
new Thread(() -> {
    try {
        Thread.sleep(5000);
        toast(getString(R.string.app_name));
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}).start();
//返回上個頁面並且銷燬
back();

解決方案

  • 方案一:直接使用Activity的getString()與getResources()獲取資源。
  • 方案二:利用Fragment的isAdd()判斷是否被移除在獲取資源。
/**
 * 防止在Fragment銷燬的時候調用資源導致崩潰
 *
 * @return Resources
 */
public Resources getRoResources() {
    if(isAdded()){
        return requireContext().getResources();
    }else{
        return activity.getResources();
    }
}

4、fragmentTransaction.add(fragment).commit() 沒有立即生效

上文已經提到過,commit()會在隊列中依次提交事務。當你提交成功並且利用fm.findFragmentByTag(targetTag)去尋找它的時候發現並不存在。但是他確實已經在隊列中排隊提交了。當你嘗試延時幾十毫秒去讀的時候發現有了。
這會導致一個邏輯問題,比如你做頁面切換的時候,當兩次切換的時差小於單個事務提交,就會導致一個Activity中存在多個相同tag的Fragment,這不是我們想要看到的。

//當快速執行兩次頁面跳轉,理論上應該只會添加一個,但實際上兩個都會被添加
toFrag(Frag_rocket_permission.class);
toFrag(Frag_rocket_permission.class);
/**
 * 頁面跳轉
 * @param targetClass 已經註冊的Fragment
 */
public void toFrag(Class targetClass) {
    FragmentManager fm = activity.getSupportFragmentManager();
    FragmentTransaction ft = fm.beginTransaction();
    String targetTag = targetClass.getSimpleName();
    RoFragment targetFragment = (RoFragment) fm.findFragmentByTag(targetTag);//找出目標Fragment
    if (targetFragment == null) {//目標不存在纔會添加,保證單一性
        targetFragment = (RoFragment) targetClass.newInstance();
        ft.add(containid, targetFragment, targetTag);//添加
    }
    ft.commit();
}

解決方案

  • 上個頁面跳轉的時候加鎖,直到頁面跳轉結束才釋放鎖,允許下個跳轉,過快的跳轉丟棄
if(!toFragEnable){//太快跳轉鎖住丟棄
    return;
}
boolean result = toFrag();
//跳轉成功先鎖住
if(result){
    toFragEnable = false;
}
@Override
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
    //Fragment show and hide 都會執行這個回調,用來處理頁面跳轉邏輯
    //成功跳轉纔開鎖,允許下次跳轉
    toFragEnable = true;
}

5、請不要在Fragment轉場動畫沒結束之前允許用戶操作

你的Fragment轉場動畫還沒結束時,如果執行了其他事務等方法,可能會導致棧內順序錯亂,同時會增加頁面狀態的複雜度與不可控性。

解決方案

  • 動畫結束再執行事務(加鎖),或者臨時將該Fragment設爲無動畫
@Override
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
 	if(nextAnim > 0){//有轉場動畫的情況
        Animation anim = AnimationUtils.loadAnimation(getActivity(), nextAnim);
        anim.setAnimationListener(new Animation.AnimationListener() {
            public void onAnimationStart(Animation animation) {
                isAnimationEnd = false;//動畫開始
            }
            public void onAnimationRepeat(Animation animation) {}
            public void onAnimationEnd(Animation animation) {
                isAnimationEnd = true;//動畫結束了
            }
        });
        return anim;
    }
    return null;
}

6、getActivity() = null導致Crash

可能你遇到過getActivity() = null,或者平時運行完好的代碼,在“內存重啓”之後,調用getActivity()的地方卻返回null,報了空指針異常。大多數情況下的原因:你在調用了getActivity()時,當前的Fragment已經onDetach()了宿主Activity。

解決方案

  • 在onAttach(Activity activity)裏將Activity全局保存下來
//緩存activity
public RoActivity activity;
@Override
public void onAttach(Context context) {
    super.onAttach(context);
    this.activity = (RoActivity) context;
}

關於我

五、持續更新中…

發佈了35 篇原創文章 · 獲贊 7 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章