Fragment全解析系列
文/YoKey(簡書作者)
原文鏈接:http://www.jianshu.com/p/d9143a92ad94
著作權歸作者所有,轉載請聯系作者獲得授權,并標注“簡書作者”。
Fragment系列文章:
1、Fragment全解析系列(一):那些年踩過的坑
2、Fragment全解析系列(二):正確的使用姿勢
3、Fragment之我的解決方案:Fragmentation
本篇主要介紹一些最常見的Fragment的坑以及官方Fragment庫的那些自身的BUG,并給出解決方案;這些BUG在你深度使用時會遇到,比如Fragment嵌套時或者單Activity+多Fragment架構時遇到的坑。
Fragment是可以讓你的app縱享絲滑的設計,如果你的app想在現在基礎上性能大幅度提高,并且占用內存降低,同樣的界面Activity占用內存比Fragment要多,響應速度Fragment比Activty在中低端手機上快了很多,甚至能達到好幾倍!如果你的app當前或以后有移植平板等平臺時,可以讓你節(jié)省大量時間和精力。
簡陋的目錄
1、getActivity()空指針
2、異常:Can not perform this action after onSaveInstanceState
3、Fragment重疊異常—–正確使用hide、show的姿勢
4、Fragment嵌套的那些坑
5、未必靠譜的出棧方法remove()
6、多個Fragment同時出棧的深坑BUG
7、深坑 Fragment轉場動畫
開始之前
最新版知乎,單Activity多Fragment的架構,響應可以說非常“絲滑”,非要說缺點的話,就是沒有轉場動畫,并且轉場會有類似閃屏現象。我猜測可能和Fragment轉場動畫的一些BUG有關。(這系列的最后一篇文章我會給出我的解決方案,可以自定義轉場動畫,并能在各種特殊情況下正常運行。)
但是!Fragment相比較Activity要難用很多,在多Fragment以及嵌套Fragment的情況下更是如此。
更重要的是Fragment的坑真的太多了,看Square公司的這篇文章吧,Square:從今天開始拋棄Fragment吧!
當然,不能說不再用Fragment,Fragment的這些坑都是有解決辦法的,官方也在逐步修復一些BUG。
下面羅列一些,有常見的,也有極度隱蔽的一些坑,也是我在用單Activity多Fragment時遇到的坑,可能有更多坑可以挖掘…
在這之前為了方便后面文章的介紹,先規(guī)定一個“術語”,安卓app有一種特殊情況,就是 app運行在后臺的時候,系統資源緊張的時候導致把app的資源全部回收(殺死app的進程),這時把app再從后臺返回到前臺時,app會重啟。這種情況下文簡稱為:“內存重啟”。(屏幕旋轉等配置變化也會造成當前Activity重啟,本質與“內存重啟”類似)
在系統要把app回收之前,系統會把Activity的狀態(tài)保存下來,Activity的FragmentManager負責把Activity中的Fragment保存起來。在“內存重啟”后,Activity的恢復是從棧頂逐步恢復,Fragment會在宿主Activity的onCreate方法調用后緊接著恢復(從onAttach生命周期開始)。
getActivity()空指針
可能你遇到過getActivity()返回null,或者平時運行完好的代碼,在“內存重啟”之后,調用getActivity()的地方卻返回null,報了空指針異常。
大多數情況下的原因:你在調用了getActivity()時,當前的Fragment已經onDetach()了宿主Activity。
比如:你在pop了Fragment之后,該Fragment的異步任務仍然在執(zhí)行,并且在執(zhí)行完成后調用了getActivity()方法,這樣就會空指針。
解決辦法:
更”安全”的方法:(對于Fragment已經onDetach這種情況,我們應該避免在這之后再去調用宿主Activity對象,比如取消這些異步任務,但我們的團隊可能會有粗心大意的情況,所以下面給出的這個方案會保證安全)
在Fragment基類里設置一個Activity mActivity的全局變量,在onAttach(Activity activity)里賦值,使用mActivity代替getActivity(),保證Fragment即使在onDetach后,仍持有Activity的引用(有引起內存泄露的風險,但是相比空指針閃退,這種做法“安全”些),即:
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; }異常:Can not perform this action after onSaveInstanceState
有很多小伙伴遇到這個異常,這個異常產生的原因是:
在你離開當前Activity等情況下,系統會調用onSaveInstanceState()幫你保存當前Activity的狀態(tài)、數據等,直到再回到該Activity之前(onResume()之前),你使用commit()提交了Fragment事務,就會拋出該異常!
解決方法2個:
1、(不推薦)該事務使用commitAllowingStateLoss()方法提交,但是有可能導致該次提交無效!(在此次離開時恰巧Activity被強殺時)
2、(推薦)在重新回到該Activity的時候(比如onReume里),再執(zhí)行該事務!
Fragment重疊異常—–正確使用hide、show的姿勢
如果你add()了幾個Fragment,使用show()、hide()方法控制,比如微信、QQ的底部tab等情景,如果你什么都不做的話,在“內存重啟”后回到前臺,app的這幾個Fragment界面會重疊。
原因是FragmentManager幫我們管理Fragment,當發(fā)生“內存重啟”,他會從棧底向棧頂的順序一次性恢復Fragment;
但是因為沒有保存Fragment的mHidden屬性,默認為false,即show狀態(tài),所以所有Fragment都是以show的形式恢復,我們看到了界面重疊。
(如果是replace,恢復形式和Activity一致,只有當你pop之后上一個Fragment才開始重新恢復,所有使用replace不會造成重疊現象)
還有一種場景,add和replace都有可能造成重疊: 在onCreate中加載Fragment,并且沒有判斷saveInstanceState==null,導致重復加載了同一個Fragment導致重疊。(PS:replace情況下,如果沒有加入回退棧,則不判斷也不會造成重疊,但建議還是統一判斷下)
@Override protected void onCreate(@Nullable Bundle savedInstanceState) { // 在頁面重啟時,Fragment會被保存恢復,而此時再加載Fragment會重復加載,導致重疊 ;if(saveInstanceState == null){// 正常情況下去 加載根Fragment } }詳細原因:從源碼角度分析,為什么會發(fā)生Fragment重疊?
這里給出3個解決方案:
1、是大家比較熟悉的 findFragmentByTag:
即在add()或者replace()時綁定一個tag,一般我們是用fragment的類名作為tag,然后在發(fā)生“內存重啟”時,通過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/下標,進行恢復。
2、使用getSupportFragmentManager().getFragments()恢復
通過getFragments()可以獲取到當前FragmentManager管理的棧內所有Fragment。
標準寫法如下:
@Override protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity);TargetFragment targetFragment;HideFragment hideFragment;if (savedInstanceState != null) { // “內存重啟”時調用List<Fragment> fragmentList = getSupportFragmentManager().getFragments();for (Fragment fragment : fragmentList) {if(fragment instanceof TartgetFragment){targetFragment = (TargetFragment)fragment; }else if(fragment instanceof HideFragment){hideFragment = (HideFragment)fragment;}}// 解決重疊問題getFragmentManager().beginTransaction().show(targetFragment).hide(hideFragment).commit();}else{ // 正常時targetFragment = TargetFragment.newInstance();hideFragment = HideFragment.newInstance();// 這里add時,tag可傳可不傳getFragmentManager().beginTransaction().add(R.id.container).add(R.id,container,hideFragment).hide(hideFragment).commit();} }從代碼看起來,這種方式比較復雜,但是這種方式在一些場景下比第一種方式更加簡便有效。
我會在下一篇中介紹在不同場景下如果選擇,何時用findFragmentByTag(),何時用getFragments()恢復。
順便一提,有些小伙伴會用一種并不合適的方法恢復Fragment,雖然效果也能達到,但并不恰當。即:
// 保存 @Override protected void onSaveInstanceState(Bundle outState) {super.onSaveInstanceState(outState);getSupportFragmentManager().putFragment(outState, KEY, targetFragment); } // 恢復 @Override protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_scrolling);if (savedInstanceState != null) {Fragment targetFragment = getSupportFragmentManager().getFragment(savedInstanceState, KEY);} }如果僅僅為了找回棧內的Fragment,使用putFragment(bundle, key, fragment)保存fragment,是完全沒有必要的;因為FragmentManager在任何情況都會幫你存儲Fragment,你要做的僅僅是在“內存重啟”后,找回這些Fragment即可。
。
3、我的解決方案,9行代碼解決所有情況的Fragment重疊:傳送門
Fragment嵌套的那些坑
其實一些小伙伴遇到的很多嵌套的坑,大部分都是由于對嵌套的棧視圖產生混亂,只要理清棧視圖關系,做好恢復相關工作以及正確選擇是使用getFragmentManager()還是getChildFragmentManager()就可以避免這些問題。
這部分內容是我們感覺Fragment非常難用的一個點,我會在下一篇中,詳細介紹使用Fragment嵌套的一些技巧,以及如何清晰分析各個層級的棧視圖。
附:startActivityForResult接收返回問題
在support 23.2.0以下的支持庫中,對于在嵌套子Fragment的startActivityForResult (),會發(fā)現無論如何都不能在onActivityResult()中接收到返回值,只有最頂層的父Fragment才能接收到,這是一個support v4庫的一個BUG,不過在前兩天發(fā)布的support 23.2.0庫中,已經修復了該問題,嵌套的子Fragment也能正常接收到返回數據了!
未必靠譜的出棧方法remove()
如果你想讓某一個Fragment出棧,使用remove()在加入回退棧時并不靠譜。
如果你在add的同時將Fragment加入回退棧:addToBackStack(name)的情況下,它并不能真正將Fragment從棧內移除,如果你在2秒后(確保Fragment事務已經完成)打印getSupportFragmentManager().getFragments(),會發(fā)現該Fragment依然存在,并且依然可以返回到被remove的Fragment,而且是空白頁面。
如果你沒有將Fragment加入回退棧,remove方法可以正常出棧。
如果你加入了回退棧,popBackStack()系列方法才能真正出棧,這也就引入下一個深坑,popBackStack(String tag,int flags)等系列方法的BUG。
多個Fragment同時出棧的深坑BUG
在Fragment庫中如下4個方法是有BUG的:
1、popBackStack(String tag,int flags)
2、popBackStack(int id,int flags)
3、popBackStackImmediate(String tag,int flags)
4、popBackStackImmediate(int id,int flags)
上面4個方法作用是,出棧到tag/id的fragment,即一次多個Fragment被出棧。
1、FragmentManager棧中管理fragment下標位置的數組ArrayList 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。
發(fā)現這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(){@Overridepublic void run() {FragmentTransactionBugFixHack.reorderIndices(fragmentManager));} });2、popBackStack的坑
popBackStack和popBackStackImmediate的區(qū)別在于前者是加入到主線隊列的末尾,等其它任務完成后才開始出棧,后者是立刻出棧。
如果你popBackStack多個Fragment后,緊接著beginTransaction() add新的一個Fragment,接著發(fā)生了“內存重啟”后,你再執(zhí)行popBackStack(),app就會Crash,解決方案是postDelay出棧動畫時間再執(zhí)行其它事務,但是根據我的觀察不是很穩(wěn)定。
我的建議是:如果你想出棧多個Fragment,你應盡量使用popBackStackImmediate(tag/id),而不是popBackStack(tag/id),如果你想在出棧后,立刻beginTransaction()開始一項事務,你應該把事務的代碼post/postDelay到主線程的消息隊列里,下一篇有詳細描述。
深坑 Fragment轉場動畫
如果你的Fragment沒有轉場動畫,或者使用setCustomAnimations(enter, exit)的話,那么上面的那些坑解決后,你可以愉快的玩耍了。
getFragmentManager().beginTransaction()
.setCustomAnimations(enter, exit)
// 如果你有通過tag/id同時出棧多個Fragment的情況時,
// 請謹慎使用.setCustomAnimations(enter, exit, popEnter, popExit)
// 因為在出棧多Fragment時,伴隨出棧動畫,會在某些情況下發(fā)生異常
// 你需要搭配Fragment的onCreateAnimation()臨時取消出棧動畫,或者延遲一個動畫時間再執(zhí)行一次上面提到的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) {
// 此處設置動畫
}
但是用代價也是有的,你需要解決出棧動畫帶來的幾個坑。
1、pop多個Fragment時轉場動畫 帶來的問題
在使用 pop(tag/id)出棧多個Fragment的這種情況下,將轉場動畫臨時取消或者延遲一個動畫的時間再去執(zhí)行其他事務;
原因在于這種情景下,如果發(fā)生“內存重啟”后,因為Fragment轉場動畫沒結束時再執(zhí)行其他方法,會導致Fragment狀態(tài)不會被FragmentManager正常保存下來。
2、進入新的Fragment并立刻關閉當前Fragment 時的一些問題
(1)如果你想從當前Fragment進入一個新的Fragment,并且同時要關閉當前Fragment。由于數據結構是棧,所以正確做法是先pop,再add,但是轉場動畫會有覆蓋的不正常現象,你需要特殊處理,不然會閃屏!
(2)Fragment的根布局要設置android:clickable = true,原因是在pop后又立刻add新的Fragment時,在轉場動畫過程中,如果你的手速太快,在動畫結束前你多點擊了一下,上一個Fragment的可點擊區(qū)域可能會在下一個Fragment上依然可用。
Tip:
如果你遇到Fragment的mNextAnim空指針的異常(通常是在你的Fragment被重啟的情況下),那么你首先需要檢查是否操作的Fragment是否為null;其次在你的Fragment轉場動畫還沒結束時,你是否就執(zhí)行了其他事務等方法;解決思路就是延遲一個動畫時間再執(zhí)行事務,或者臨時將該Fragment設為無動畫
總結
看了上面的介紹,你可能會覺得Fragment有點可怕。
但是我想說,如果你只是淺度使用,比如一個Activity容器包含列表Fragment+詳情Fragment這種簡單情景下,不涉及到popBackStack/Immediate(tag/id)這些的方法,還是比較輕松使用的,出現的問題,網上都可以找到解決方案。
但是如果你的Fragment邏輯比較復雜,有特殊需求,或者你的app架構是僅有一個Activity + 多個Fragment,上面說的這些坑,你都應該全部解決。
在下一篇中,介紹了一些非常實用的使用技巧,包括如何解決Fragment嵌套、各種環(huán)境、組件下Fragment的使用等技巧,推薦閱讀!
還有一些比較隱蔽的問題,不影響app的正常運行,僅僅是一些顯示的BUG,并沒有在上面介紹,在本系列的最后一篇,我給出了我的解決方案,一個我封裝的Fragmentation庫,解決了所有動畫問題,非常適合單Activity+多Fragment 或者 多模塊Activity+多Fragment的架構。有興趣的可以看看 :)
總結
以上是生活随笔為你收集整理的Fragment全解析系列的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android Fragment中嵌套F
- 下一篇: 使用ViewPager加载页面出现空白