Fragment全解析系列(一):那些年踩過的坑

前言

本文轉載自 Yokey,分享了Fragment 那些年走過的坑,很是精彩,相信對你們有所幫助。java

Yokey的博客地址程序員

https://www.jianshu.com/u/6b372d09b617web

本篇主要介紹一些最多見的Fragment的坑以及官方Fragment庫的那些自身的BUG,並給出解決方案;這些BUG在你深度使用時會遇到,好比Fragment嵌套時或者單Activity+多Fragment架構時遇到的坑。
面試


Fragment是可讓你的app縱享絲滑的設計,若是你的app想在如今基礎上 性能大幅度提升 ,而且 佔用內存下降
,一樣的界面Activity佔用內存比Fragment要多,響應速度Fragment比Activty在中低端手機上快了不少,甚至能達到好幾倍!若是你的app當前或之後有
移植 平板等平臺時,可讓你節省大量時間和精力。數組


簡陋的目錄 
一、getActivity()空指針 
二、異常:Can not perform this action after onSaveInstanceState 
三、Fragment重疊異常-----正確使用hide、show的姿式 
四、Fragment嵌套的那些坑 
五、未必靠譜的出棧方法remove() 
六、多個Fragment同時出棧的深坑BUG 
七、深坑 Fragment轉場動畫安全


開始以前

最新版知乎,單Activity多Fragment的架構,響應能夠說很是「絲滑」,非要說缺點的話,就是沒有轉場動畫,而且轉場會有相似閃屏現象。我猜想可能和Fragment轉場動畫的一些BUG有關。(這系列的最後一篇文章我會給出個人解決方案,能夠自定義轉場動畫,並能在各類特殊狀況下正常運行。)微信

可是!Fragment相比較Activity要難用不少,在多Fragment以及嵌套Fragment的狀況下更是如此。 
更重要的是Fragment的坑真的太多了,看Square公司的這篇文章吧,Square:從今天開始拋棄Fragment吧!網絡

固然,不能說再也不用Fragment,Fragment的這些坑都是有解決辦法的,官方也在逐步修復一些BUG。 
下面羅列一些,有常見的,也有極度隱蔽的一些坑,也是我在用單Activity多Fragment時遇到的坑,可能有更多坑能夠挖掘…數據結構

在這以前爲了方便後面文章的介紹,先規定一個「術語」,安卓app有一種特殊狀況,就是
app運行在後臺的時候,系統資源緊張的時候致使把app的資源所有回收(殺死app的進程),這時把app再從後臺返回到前臺時,app會重啓。這種狀況下文簡稱爲:
「內存重啓」 。(屏幕旋轉等配置變化也會形成當前Activity重啓,本質與「內存重啓」相似)架構

在系統要把app回收以前,系統會把Activity的狀態保存下來,Activity的FragmentManager負責把Activity中的Fragment保存起來。在「內存重啓」後,Activity的恢復是從棧頂逐步恢復,Fragment會在宿主Activity的onCreate方法調用後緊接着恢復(從onAttach生命週期開始)。


getActivity()空指針

可能你遇到過getActivity()返回null,或者平時運行無缺的代碼,在「內存重啓」以後,調用getActivity()的地方卻返回null,報了空指針異常。

大多數狀況下的緣由:你在調用了getActivity()時,當前的Fragment已經onDetach()了宿主Activity。 
好比:你在pop了Fragment以後,該Fragment的異步任務仍然在執行,而且在執行完成後調用了getActivity()方法,這樣就會空指針。

解決辦法: 
更"安全"的方法
:(對於Fragment已經onDetach這種狀況,咱們應該避免在這以後再去調用宿主Activity對象,好比取消這些異步任務,但咱們的團隊可能會有粗枝大葉的狀況,因此下面給出的這個方案會保證安全)

在Fragment基類裏設置一個Activity mActivity的全局變量,在onAttach(Activity activity)裏賦值,使用mActivity代替getActivity(),保證Fragment即便在onDetach後,仍持有Activity的引用(有引發內存泄露的風險,可是異步任務沒中止的狀況下,自己就可能已內存泄漏,相比Crash,這種作法「安全」些),即:

 1protected Activity mActivity;
2@Override
3public void onAttach(Activity activity) {
4    super.onAttach(activity);
5    this.mActivity = activity;
6}
7
8/**
9*  若是你用了support 23的庫,上面的方法會提示過期,有強迫症的小夥伴,能夠用下面的方法代替
10*/

11@Override
12public void onAttach(Context context) {
13    super.onAttach(context);
14    this.mActivity = (Activity)context;
15}

異常:Can not perform this action after onSaveInstanceState

有不少小夥伴遇到這個異常,這個異常產生的緣由是:

在你離開當前Activity等狀況下,系統會調用onSaveInstanceState()幫你保存當前Activity的狀態、數據等,
直到再回到該Activity以前(onResume()以前),你執行Fragment事務,就會拋出該異常!
(通常是其餘Activity的回調讓當前頁面執行事務的狀況,會引起該問題)

解決方法:

  • 一、該事務使用`commitAllowingStateLoss()`方法提交,可是有可能致使該次提交無效!(宿主Activity被強殺時)

>
對於popBackStack()沒有對應的popBackStackAllowingStateLoss()方法,因此能夠在下次可見時提交事務,參考2

  • 二、利用`onActivityForResult()`/`onNewIntent()`,能夠作到事務的完整性,不會丟失事務

一個簡單的示例代碼 :

 1// ReceiverActivity 或 其子Fragment:
2void start(){
3   startActivityForResult(new Intent(this, SenderActivity.class), 100);
4}
5
6@Override
7protected void onActivityResult(int requestCode, int resultCode, Intent data) {
8     super.onActivityResult(requestCode, resultCode, data);
9     if (requestCode == 100 && resultCode == 100) {
10         // 執行Fragment事務
11     }
12 }
13
14// SenderActivity 或 其子Fragment:
15void do() // 操做ReceiverActivity(或其子Fragment)執行事務
16    setResult(100);
17    finish();
18}

Fragment重疊異常-----正確使用hide、show的姿式

在類onCreate()的方法加載Fragment,而且沒有判斷saveInstanceState==nullif(findFragmentByTag(mFragmentTag) ==null),致使重複加載了同一個Fragment致使重疊。(PS:replace狀況下,若是沒有加入回退棧,則不判斷也不會形成重疊,但建議仍是統一判斷下)

 1```
2@Override 
3protected void onCreate(@Nullable Bundle savedInstanceState) {
4// 在頁面重啓時,Fragment會被保存恢復,而此時再加載Fragment會重複加載,致使重疊 ;
5    if(saveInstanceState == null){
6    // 或者 if(findFragmentByTag(mFragmentTag) == null)
7       // 正常狀況下去 加載根Fragment 
8    } 
9}
10```

詳細緣由:從源碼角度分析,爲何會發生Fragment重疊?

若是你add()了幾個Fragment,使用show()、hide()方法控制,好比微信、QQ的底部tab等情景,若是你什麼都不作的話,在「內存重啓」後回到前臺,app的這幾個Fragment界面會重疊。

緣由是FragmentManager幫咱們管理Fragment,當發生「內存重啓」,他會從棧底向棧頂的順序一次性恢復Fragment;可是由於官方沒有保存Fragment的mHidden屬性,默認爲false,即show狀態,因此全部Fragment都是以show的形式恢復,咱們看到了界面重疊。(若是是replace,恢復形式和Activity一致,只有當你pop以後上一個Fragment纔開始從新恢復,全部使用replace不會形成重疊現象)

v4-24.0.0+ 開始,官方修復了上述
沒有保存mHidden的問題,因此若是你在使用24.0.0+的v4包,下面分析的2個解決方案能夠自行跳過…

這裏給出2個解決方案: 
一、是你們比較熟悉的findFragmentByTag

即在add()或者replace()時綁定一個tag,通常咱們是用fragment的類名做爲tag,而後在發生「內存重啓」時,經過findFragmentByTag找到對應的Fragment,並hide()須要隱藏的fragment。

下面是個標準恢復寫法:

 1@Override
2protected void onCreate(Bundle savedInstanceState
{
3    super.onCreate(savedInstanceState);
4    setContentView(R.layout.activity);
5
6    TargetFragment targetFragment;
7    HideFragment hideFragment;
8
9    if (savedInstanceState != null) {  // 「內存重啓」時調用
10        targetFragment = getSupportFragmentManager().findFragmentByTag(TargetFragment.class.getName);
11        hideFragment = getSupportFragmentManager().findFragmentByTag(HideFragment.class.getName);
12        // 解決重疊問題
13        getFragmentManager().beginTransaction()
14                .show(targetFragment)
15                .hide(hideFragment)
16                .commit();
17    }else{  // 正常時
18        targetFragment = TargetFragment.newInstance();
19        hideFragment = HideFragment.newInstance();
20
21        getFragmentManager().beginTransaction()
22                .add(R.id.container, targetFragment, targetFragment.getClass().getName())
23                .add(R.id,container,hideFragment,hideFragment.getClass().getName())
24                .hide(hideFragment)
25                .commit();
26    }
27}

若是你想恢復到用戶離開時的那個Fragment的界面,你還須要在onSaveInstanceState(Bundle outState)裏保存離開時的那個可見的tag或下標,在onCreate「內存重啓」代碼塊中,取出tag/下標,進行恢復。

**
二、個人解決方案,9行代碼解決全部狀況的Fragment重疊:傳送門**


Fragment嵌套的那些坑

其實一些小夥伴遇到的不少嵌套的坑,大部分都是因爲對嵌套的棧視圖產生混亂,只要理清棧視圖關係,作好恢復相關工做以及正確選擇是使用getFragmentManager()仍是getChildFragmentManager()就能夠避免這些問題。

這部份內容是咱們感受Fragment很是難用的一個點,我會在下一篇中,詳細介紹使用Fragment嵌套的一些技巧,以及如何清晰分析各個層級的棧視圖。

附:startActivityForResult接收返回問題 
在support 23.2.0如下的支持庫中,對於在嵌套子Fragment的startActivityForResult (),會發現不管如何都不能在onActivityResult()中接收到返回值,只有最頂層的父Fragment才能接收到,這是一個support
v4庫的一個BUG,不過在前兩天發佈的support 23.2.0庫中,已經修復了該問題,嵌套的子Fragment也能正常接收到返回數據了!


未必靠譜的出棧方法remove()

若是你想讓某一個Fragment出棧,使用remove()在加入回退棧時並不靠譜。

若是你在add的同時將Fragment加入回退棧:addToBackStack(name)的狀況下,它並不能真正將Fragment從棧內移除,若是你在2秒後(確保Fragment事務已經完成)打印getSupportFragmentManager().getFragments(),會發現該Fragment依然存在,而且依然能夠返回到被remove的Fragment,並且是空白頁面。

若是你沒有將Fragment加入回退棧,remove方法能夠正常出棧。

若是你加入了回退棧,popBackStack()系列方法才能真正出棧,這也就引入下一個深坑,popBackStack(String tag,int flags)等系列方法的BUG。


多個Fragment同時出棧的深坑BUG

6月17日更新:在support-25.4.0版本,google意識到下面的問題,並修復了。若是你使用25.4.0及以上版本,下面的方法不要再使用,google移除了mAvailIndices屬性

在Fragment庫中以下4個方法是可能產生BUG的:

一、popBackStack(String tag,int flags) 
二、popBackStack(int id,int flags) 
三、popBackStackImmediate(String tag,int flags) 
四、popBackStackImmediate(int id,int flags)

上面4個方法做用是,出棧到tag/id的fragment,即一次多個Fragment被出棧。

一、FragmentManager棧中管理fragment下標位置的數組ArrayList  mAvailIndeices的BUG

下面的方法FragmentManagerImpl類方法,產生BUG的罪魁禍首是管理Fragment棧下標的mAvailIndeices屬性:

 1void makeActive(Fragment f{
2      if (f.mIndex >= 0) {
3         return;
4      } 
5      if (mAvailIndices == null || mAvailIndices.size() <= 0) {
6           if (mActive == null) {
7              mActive = new ArrayList<Fragment>();
8           } 
9           f.setIndex(mActive.size(), mParent); 
10           mActive.add(f);
11       } else {
12           f.setIndex(mAvailIndices.remove(mAvailIndices.size()-1), mParent);
13           mActive.set(f.mIndex, f);
14       } 
15      if (DEBUG) Log.v(TAG, "Allocated fragment index " + f);
16 }

上面代碼最終致使了棧內順序不正確的問題,以下圖:

上面的這個狀況,會一次異常,一次正常。帶來的問題就是「內存重啓」後,各類異常甚至Crash。

發現這BUG的時候,我一臉懵比,幸虧,stackoverflow上有大神給出瞭解決方案!hack
FragmentManagerImplmAvailIndices,對其進行一次Collections.reverseOrder()降序排序,保證棧內Fragment的index的正確。

 1public class FragmentTransactionBugFixHack {
2
3  public static void reorderIndices(FragmentManager fragmentManager) {
4    if (!(fragmentManager instanceof FragmentManagerImpl))
5      return;
6    FragmentManagerImpl fragmentManagerImpl = (FragmentManagerImpl) fragmentManager;
7    if (fragmentManagerImpl.mAvailIndices != null && fragmentManagerImpl.mAvailIndices.size() > 1) {
8      Collections.sort(fragmentManagerImpl.mAvailIndices, Collections.reverseOrder());
9    }
10  }
11}

使用方法就是經過popBackStackImmediate(tag/id)多個Fragment後,調用

1hanler.post(new Runnable(){
2    @Override
3     public void run() {
4         FragmentTransactionBugFixHack.reorderIndices(fragmentManager));
5     }
6});

二、popBackStack的坑 
popBackStackpopBackStackImmediate的區別在於前者是加入到主線隊列的末尾,等其它任務完成後纔開始出棧,後者是隊列內的任務當即執行,再將出棧任務放到隊列尾(能夠理解爲當即出棧)。

若是你popBackStack多個Fragment後,緊接着beginTransaction()
add新的一個Fragment,接着發生了「內存重啓」後,你再執行popBackStack(),app就會Crash,解決方案是postDelay出棧動畫時間再執行其它事務,可是根據個人觀察不是很穩定。 
個人建議是:若是你想出棧多個Fragment,你應儘可能使用popBackStackImmediate(tag/id),而不是popBackStack(tag/id),若是你想在出棧後,馬上beginTransaction()開始一項事務,你應該把事務的代碼post/postDelay到主線程的消息隊列裏,下一篇有詳細描述。


深坑 Fragment轉場動畫(僅分析v4包下的Fragment)

若是你的Fragment沒有轉場動畫,或者使用setCustomAnimations(enter, exit)的話,那麼上面的那些坑解決後,你能夠愉快的玩耍了。

1getFragmentManager().beginTransaction()
2         .setCustomAnimations(enter, exit)
3        // 若是你有經過tag/id同時出棧多個Fragment的狀況時,
4        // 請謹慎使用.setCustomAnimations(enter, exit, popEnter, popExit)  
5        // 在support-25.4.0以前出棧多Fragment時,伴隨出棧動畫,會在某些狀況下發生異常
6        // 你須要搭配Fragment的onCreateAnimation()臨時取消出棧動畫,或者延遲一個動畫時間再執行一次上面提到的Hack方法,排序

(注意:若是你想給下一個Fragment設置進棧動畫和出棧動畫,.setCustomAnimations(enter, exit)只能設置進棧動畫,第二個參數並非設置出棧動畫;請使用.setCustomAnimations(enter, exit, popEnter, popExit),這個方法的第1個參數對應進棧動畫,第4個參數對應出棧動畫,因此是.setCustomAnimations(進棧動畫, exit, popEnter, 出棧動畫))

總結起來就是Fragment沒有出棧動畫的話,能夠避免不少坑。 
若是想讓出棧動畫運做正常的話,須要使用Fragment的onCreateAnimation中控制動畫。

1@Override
2public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
3    // 此處設置動畫
4}

可是用代價也是有的,你須要解決出棧動畫帶來的幾個坑。

一、pop多個Fragment時轉場動畫 帶來的問題

6月17日更新:在support-25.4.0版本,google意識到下面動畫引發的問題,並修復了。

在使用 pop(tag/id)出棧多個Fragment的這種狀況下,將轉場動畫臨時取消或者延遲一個動畫的時間再去執行其餘事務;

緣由在於這種情景下,可能會致使棧內順序錯亂(上文有提到),同時若是發生「內存重啓」後,由於Fragment轉場動畫沒結束時再執行其餘方法,會致使Fragment狀態不會被FragmentManager正常保存下來。

二、進入新的Fragment並馬上關閉當前Fragment 時的一些問題 
(1)若是你想從當前Fragment進入一個新的Fragment,而且同時要關閉當前Fragment。因爲數據結構是棧,因此正確作法是先pop,再add,可是轉場動畫會有覆蓋的不正常現象,你須要特殊處理,否則會閃屏!

Tip: 
若是你遇到Fragment的mNextAnim空指針的異常(一般是在你的Fragment被重啓的狀況下),那麼你首先須要檢查是否操做的Fragment是否爲null;其次在你的Fragment轉場動畫還沒結束時,你是否就執行了其餘事務等方法;解決思路就是延遲一個動畫時間再執行事務,或者臨時將該Fragment設爲無動畫

總結

看了上面的介紹,你可能會以爲Fragment有點可怕。

可是我想說,若是你只是淺度使用,好比一個Activity容器包含列表Fragment+詳情Fragment這種簡單情景下,不涉及到popBackStack/Immediate(tag/id)這些的方法,仍是比較輕鬆使用的,出現的問題,網上均可以找到解決方案。

可是若是你的Fragment邏輯比較複雜,有特殊需求,或者你的app架構是僅有一個Activity +
多個Fragment,上面說的這些坑,你都應該所有解決。

在下一篇中,介紹了一些很是實用的使用技巧,包括如何解決Fragment嵌套、各類環境、組件下Fragment的使用等技巧,推薦閱讀!

還有一些比較隱蔽的問題,不影響app的正常運行,僅僅是一些顯示的BUG,並無在上面介紹,在本系列的最後一篇,我給出了個人解決方案,一個我封裝的Fragmentation庫,解決了全部動畫問題,很是適合
單Activity+多Fragment 或者 多模塊Activity+多Fragment 的架構。有興趣的能夠看看 :)

推薦閱讀

Android 面試必備 - http 與 https 協議

Android 面試必備 - 計算機網絡基本知識(TCP,UDP,Http,https)

Android 面試必備 - 線程

Android 面試必備 - JVM 及 類加載機制

Android 面試必備 - 系統、App、Activity 啓動過程

致剛入職場的你 - 程序員的成長筆記

幹起來,你就超過了 50% 的人

一個程序員的五年總結,給你不同的角度


stormjun94

掃一掃,歡迎關注個人公衆號 stormjun94。若是你有好的文章,也歡迎你的投稿。


本文分享自微信公衆號 - 徐公碼字(stormjun94)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索