前言
本文轉載自 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==null
或if(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
下面的方法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上有大神給出瞭解決方案!hackFragmentManagerImpl
的mAvailIndices
,對其進行一次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的坑 popBackStack
和popBackStackImmediate
的區別在於前者是加入到主線隊列的末尾,等其它任務完成後纔開始出棧,後者是隊列內的任務當即執行,再將出棧任務放到隊列尾(能夠理解爲當即出棧)。
若是你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 面試必備 - 系統、App、Activity 啓動過程

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