Fragment系列文章:
一、Fragment全解析系列(一):那些年踩過的坑
二、Fragment全解析系列(二):正確的使用姿式
三、Fragment之個人解決方案:Fragmentationhtml
本篇主要介紹一些最多見的Fragment的坑以及官方Fragment庫的那些自身的BUG,並給出解決方案;這些BUG在你深度使用時會遇到,好比Fragment嵌套時或者單Activity+多Fragment架構時遇到的坑。android
Fragment是可讓你的app縱享絲滑的設計,若是你的app想在如今基礎上性能大幅度提升,而且佔用內存下降,一樣的界面Activity佔用內存比Fragment要多,響應速度Fragment比Activty在中低端手機上快了不少,甚至能達到好幾倍!若是你的app當前或之後有移植平板等平臺時,可讓你節省大量時間和精力。git
簡陋的目錄
一、getActivity()空指針
二、異常:Can not perform this action after onSaveInstanceState
三、Fragment重疊異常-----正確使用hide、show的姿式
四、Fragment嵌套的那些坑
五、未必靠譜的出棧方法remove()
六、多個Fragment同時出棧的深坑BUG
七、深坑 Fragment轉場動畫github
最新版知乎,單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()返回null,或者平時運行無缺的代碼,在「內存重啓」以後,調用getActivity()的地方卻返回null,報了空指針異常。app
大多數狀況下的緣由:你在調用了getActivity()時,當前的Fragment已經onDetach()
了宿主Activity。
好比:你在pop了Fragment以後,該Fragment的異步任務仍然在執行,而且在執行完成後調用了getActivity()方法,這樣就會空指針。
解決辦法:
更"安全"的方法:(對於Fragment已經onDetach這種狀況,咱們應該避免在這以後再去調用宿主Activity對象,好比取消這些異步任務,但咱們的團隊可能會有粗枝大葉的狀況,因此下面給出的這個方案會保證安全)
在Fragment基類裏設置一個Activity mActivity的全局變量,在onAttach(Activity activity)
裏賦值,使用mActivity代替getActivity()
,保證Fragment即便在onDetach
後,仍持有Activity的引用(有引發內存泄露的風險,可是異步任務沒中止的狀況下,自己就可能已內存泄漏,相比Crash,這種作法「安全」些),即:
protected Activity mActivity; @Override public void onAttach(Activity activity) { super.onAttach(activity); this.mActivity = activity; } /** * 若是你用了support 23的庫,上面的方法會提示過期,有強迫症的小夥伴,能夠用下面的方法代替 */ @Override public void onAttach(Context context) { super.onAttach(context); this.mActivity = (Activity)context; }
有不少小夥伴遇到這個異常,這個異常產生的緣由是:
在你離開當前Activity等狀況下,系統會調用onSaveInstanceState()
幫你保存當前Activity的狀態、數據等,直到再回到該Activity以前(onResume()
以前),你執行Fragment事務,就會拋出該異常!(通常是其餘Activity的回調讓當前頁面執行事務的狀況,會引起該問題)
commitAllowingStateLoss()
方法提交,可是有可能致使該次提交無效!(宿主Activity被強殺時)對於
popBackStack()
沒有對應的popBackStackAllowingStateLoss()
方法,因此能夠在下次可見時提交事務,參考2
onActivityForResult()
/onNewIntent()
,能夠作到事務的完整性,不會丟失事務一個簡單的示例代碼 :
// ReceiverActivity 或 其子Fragment: void start(){ startActivityForResult(new Intent(this, SenderActivity.class), 100); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == 100 && resultCode == 100) { // 執行Fragment事務 } } // SenderActivity 或 其子Fragment: void do() { // 操做ReceiverActivity(或其子Fragment)執行事務 setResult(100); finish(); }
在類onCreate()
的方法加載Fragment,而且沒有判斷saveInstanceState==null
或if(findFragmentByTag(mFragmentTag) == null)
,致使重複加載了同一個Fragment致使重疊。(PS:replace
狀況下,若是沒有加入回退棧,則不判斷也不會形成重疊,但建議仍是統一判斷下)
@Override protected void onCreate(@Nullable Bundle savedInstanceState) { // 在頁面重啓時,Fragment會被保存恢復,而此時再加載Fragment會重複加載,致使重疊 ; if(saveInstanceState == null){ // 或者 if(findFragmentByTag(mFragmentTag) == null) // 正常狀況下去 加載根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。
下面是個標準恢復寫法:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity); TargetFragment targetFragment; HideFragment hideFragment; if (savedInstanceState != null) { // 「內存重啓」時調用 targetFragment = getSupportFragmentManager().findFragmentByTag(TargetFragment.class.getName); hideFragment = getSupportFragmentManager().findFragmentByTag(HideFragment.class.getName); // 解決重疊問題 getFragmentManager().beginTransaction() .show(targetFragment) .hide(hideFragment) .commit(); }else{ // 正常時 targetFragment = TargetFragment.newInstance(); hideFragment = HideFragment.newInstance(); getFragmentManager().beginTransaction() .add(R.id.container, targetFragment, targetFragment.getClass().getName()) .add(R.id,container,hideFragment,hideFragment.getClass().getName()) .hide(hideFragment) .commit(); } }
若是你想恢復到用戶離開時的那個Fragment的界面,你還須要在onSaveInstanceState(Bundle outState)
裏保存離開時的那個可見的tag或下標,在onCreate
「內存重啓」代碼塊中,取出tag/下標,進行恢復。
** 二、個人解決方案,9行代碼解決全部狀況的Fragment重疊:傳送門**
其實一些小夥伴遇到的不少嵌套的坑,大部分都是因爲對嵌套的棧視圖產生混亂,只要理清棧視圖關係,作好恢復相關工做以及正確選擇是使用getFragmentManager()
仍是getChildFragmentManager()
就能夠避免這些問題。
這部份內容是咱們感受Fragment很是難用的一個點,我會在下一篇中,詳細介紹使用Fragment嵌套的一些技巧,以及如何清晰分析各個層級的棧視圖。
附:startActivityForResult接收返回問題
在support 23.2.0如下的支持庫中,對於在嵌套子Fragment的startActivityForResult ()
,會發現不管如何都不能在onActivityResult()
中接收到返回值,只有最頂層的父Fragment才能接收到,這是一個support v4庫的一個BUG,不過在前兩天發佈的support 23.2.0庫中,已經修復了該問題,嵌套的子Fragment也能正常接收到返回數據了!
若是你想讓某一個Fragment出棧,使用remove()
在加入回退棧時並不靠譜。
若是你在add的同時將Fragment加入回退棧:addToBackStack(name)的狀況下,它並不能真正將Fragment從棧內移除,若是你在2秒後(確保Fragment事務已經完成)打印getSupportFragmentManager().getFragments()
,會發現該Fragment依然存在,而且依然能夠返回到被remove的Fragment,並且是空白頁面。
若是你沒有將Fragment加入回退棧,remove方法能夠正常出棧。
若是你加入了回退棧,popBackStack()
系列方法才能真正出棧,這也就引入下一個深坑,popBackStack(String tag,int flags)
等系列方法的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<Integer> mAvailIndeices的BUG
下面的方法FragmentManagerImpl類方法,產生BUG的罪魁禍首是管理Fragment棧下標的mAvailIndeices
屬性:
void makeActive(Fragment f) { if (f.mIndex >= 0) { return; } if (mAvailIndices == null || mAvailIndices.size() <= 0) { if (mActive == null) { mActive = new ArrayList<Fragment>(); } f.setIndex(mActive.size(), mParent); mActive.add(f); } else { f.setIndex(mAvailIndices.remove(mAvailIndices.size()-1), mParent); mActive.set(f.mIndex, f); } if (DEBUG) Log.v(TAG, "Allocated fragment index " + f); }
上面代碼最終致使了棧內順序不正確的問題,以下圖:
上面的這個狀況,會一次異常,一次正常。帶來的問題就是「內存重啓」後,各類異常甚至Crash。
發現這BUG的時候,我一臉懵比,幸虧,stackoverflow上有大神給出了解決方案!hack FragmentManagerImpl
的mAvailIndices
,對其進行一次Collections.reverseOrder()
降序排序,保證棧內Fragment的index的正確。
public class FragmentTransactionBugFixHack { public static void reorderIndices(FragmentManager fragmentManager) { if (!(fragmentManager instanceof FragmentManagerImpl)) return; FragmentManagerImpl fragmentManagerImpl = (FragmentManagerImpl) fragmentManager; if (fragmentManagerImpl.mAvailIndices != null && fragmentManagerImpl.mAvailIndices.size() > 1) { Collections.sort(fragmentManagerImpl.mAvailIndices, Collections.reverseOrder()); } } }
使用方法就是經過popBackStackImmediate(tag/id)
多個Fragment後,調用
hanler.post(new Runnable(){ @Override public void run() { FragmentTransactionBugFixHack.reorderIndices(fragmentManager)); } });
二、popBackStack的坑
popBackStack
和popBackStackImmediate
的區別在於前者是加入到主線隊列的末尾,等其它任務完成後纔開始出棧,後者是隊列內的任務當即執行,再將出棧任務放到隊列尾(能夠理解爲當即出棧)。
若是你popBackStack
多個Fragment後,緊接着beginTransaction()
add新的一個Fragment,接着發生了「內存重啓」後,你再執行popBackStack()
,app就會Crash,解決方案是postDelay出棧動畫時間再執行其它事務,可是根據個人觀察不是很穩定。
個人建議是:若是你想出棧多個Fragment,你應儘可能使用popBackStackImmediate(tag/id)
,而不是popBackStack(tag/id)
,若是你想在出棧後,馬上beginTransaction()
開始一項事務,你應該把事務的代碼post/postDelay到主線程的消息隊列裏,下一篇有詳細描述。
若是你的Fragment沒有轉場動畫,或者使用setCustomAnimations(enter, exit)
的話,那麼上面的那些坑解決後,你能夠愉快的玩耍了。
getFragmentManager().beginTransaction() .setCustomAnimations(enter, exit) // 若是你有經過tag/id同時出棧多個Fragment的狀況時, // 請謹慎使用.setCustomAnimations(enter, exit, popEnter, popExit) // 在support-25.4.0以前出棧多Fragment時,伴隨出棧動畫,會在某些狀況下發生異常 // 你須要搭配Fragment的onCreateAnimation()臨時取消出棧動畫,或者延遲一個動畫時間再執行一次上面提到的Hack方法,排序
(注意:若是你想給下一個Fragment設置進棧動畫和出棧動畫,.setCustomAnimations(enter, exit)只能設置進棧動畫,第二個參數並非設置出棧動畫;
請使用.setCustomAnimations(enter, exit, popEnter, popExit),這個方法的第1個參數對應進棧動畫,第4個參數對應出棧動畫,因此是.setCustomAnimations(進棧動畫, exit, popEnter, 出棧動畫))
總結起來就是Fragment沒有出棧動畫的話,能夠避免不少坑。
若是想讓出棧動畫運做正常的話,須要使用Fragment的onCreateAnimation
中控制動畫。
@Override public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) { // 此處設置動畫 }
可是用代價也是有的,你須要解決出棧動畫帶來的幾個坑。
一、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的架構。有興趣的能夠看看 :)
做者:YoKey 連接:https://www.jianshu.com/p/d9143a92ad94 來源:簡書 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。