轉(zhuǎn)自 安卓應(yīng)用頻道
本篇主要介紹一些最常見(jiàn)的Fragment的坑以及官方Fragment庫(kù)的那些自身的BUG,這些BUG在你深度使用時(shí)會(huì)遇到,比如Fragment嵌套時(shí)或者單Activity+多Fragment架構(gòu)時(shí)遇到的坑。
Fragment是可以讓你的app縱享絲滑的設(shè)計(jì),如果你的app想在現(xiàn)在基礎(chǔ)上性能大幅度提高,并且占用內(nèi)存降低,同樣的界面Activity占用內(nèi)存比Fragment要多,響應(yīng)速度Fragment比Activty在中低端手機(jī)上快了很多,甚至能達(dá)到好幾倍!如果你的app當(dāng)前或以后有移植平板等平臺(tái)時(shí),可以讓你節(jié)省大量時(shí)間和精力。
簡(jiǎn)陋的目錄
1、getActivity()空指針
2、Fragment重疊異?!C正確使用hide、show的姿勢(shì)
3、Fragment嵌套的那些坑
4、不靠譜的出棧方法remove()
5、多個(gè)Fragment同時(shí)出棧的那些深坑BUG
6、超級(jí)深坑 Fragment轉(zhuǎn)場(chǎng)動(dòng)畫
開(kāi)始之前
最新版知乎,單Activity多Fragment的架構(gòu),響應(yīng)可以說(shuō)非常“絲滑”,非要說(shuō)缺點(diǎn)的話,就是沒(méi)有轉(zhuǎn)場(chǎng)動(dòng)畫,并且轉(zhuǎn)場(chǎng)會(huì)有類似閃屏現(xiàn)象。我猜測(cè)可能和Fragment轉(zhuǎn)場(chǎng)動(dòng)畫的一些BUG有關(guān)。(這系列的最后一篇文章我會(huì)給出我的解決方案,可以自定義轉(zhuǎn)場(chǎng)動(dòng)畫,并能在各種特殊情況下正常運(yùn)行。)
但是!Fragment相比較Activity要難用很多,在多Fragment以及嵌套Fragment的情況下更是如此。
更重要的是Fragment的坑真的太多了,看Square公司的這篇文章吧,Square:從今天開(kāi)始拋棄Fragment吧!
當(dāng)然,不能說(shuō)不再用Fragment,F(xiàn)ragment的這些坑都是有解決辦法的,官方也在逐步修復(fù)一些BUG。
下面羅列一些,有常見(jiàn)的,也有極度隱蔽的一些坑,也是我在用單Activity多Fragment時(shí)遇到的坑,可能有更多坑可以挖掘…
在這之前為了方便后面文章的介紹,先規(guī)定一個(gè)“術(shù)語(yǔ)”,安卓app有一種特殊情況,就是 app運(yùn)行在后臺(tái)的時(shí)候,系統(tǒng)資源緊張的時(shí)候?qū)е掳補(bǔ)pp的資源全部回收(殺死app的進(jìn)程),這時(shí)把a(bǔ)pp再?gòu)暮笈_(tái)返回到前臺(tái)時(shí),app會(huì)重啟。這種情況下文簡(jiǎn)稱為:“內(nèi)存重啟”。
在系統(tǒng)要把a(bǔ)pp回收之前,系統(tǒng)會(huì)把Activity的狀態(tài)保存下來(lái),Activity的FragmentManager負(fù)責(zé)把Activity中的Fragment保存起來(lái)。在“內(nèi)存重啟”后,Activity的恢復(fù)是從棧頂逐步恢復(fù),F(xiàn)ragment會(huì)在宿主Activity的onCreate方法調(diào)用后緊接著恢復(fù)(從onAttach生命周期開(kāi)始)。
getActivity()空指針
可能你遇到過(guò)getActivity()返回null,或者平時(shí)運(yùn)行完好的代碼,在“內(nèi)存重啟”之后,調(diào)用getActivity()的地方卻返回null,報(bào)了空指針異常。
大多數(shù)情況下的原因:你在調(diào)用了getActivity()時(shí),當(dāng)前的Fragment已經(jīng)onDetach()了宿主Activity。
比如:你在pop了Fragment之后,該Fragment的異步任務(wù)仍然在執(zhí)行,并且在執(zhí)行完成后調(diào)用了getActivity()方法,這樣就會(huì)空指針。
解決辦法:
更安全的方法
在Fragment基類里設(shè)置一個(gè)Activity mActivity的全局變量,在onAttach(Activity activity)里賦值,使用mActivity代替getActivity(),保證Fragment即使在onDetach后,仍持有Activity的引用(有引起內(nèi)存泄露的風(fēng)險(xiǎn),但是相比閃退,這種做法更“好”些),即:
protected Activity mActivity;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
this.mActivity = activity;
}
/**
- 如果你用了support 23的庫(kù),上面的方法會(huì)提示過(guò)時(shí),有強(qiáng)迫癥的小伙伴,可以用下面的方法代替
*/
@Override
public void onAttach(Context context) {
super.onAttach(context);
this.mActivity = (Activity)context;
}
其實(shí)我們應(yīng)該盡量避免在Fragment已經(jīng)onDetach后,再去使用其宿主Activity對(duì)象,這樣才是最安全的辦法。
Fragment重疊異常—–正確使用hide、show的姿勢(shì)
如果你add()了幾個(gè)Fragment,使用show()、hide()方法控制,比如微信、QQ的底部tab等情景,如果你什么都不做的話,在“內(nèi)存重啟”后回到前臺(tái),app的這幾個(gè)Fragment界面會(huì)重疊。
原因是FragmentManager幫我們管理Fragment,每當(dāng)我們離開(kāi)該Activity,F(xiàn)ragmentManager都會(huì)保存它的Fragments,當(dāng)發(fā)生“內(nèi)存重啟”,他會(huì)從棧底向棧頂?shù)捻樞蚧謴?fù)Fragments,并且全都都是以show()的方式,所以我們看到了界面重疊。(如果是replace,恢復(fù)順序和Activity一致:棧頂先恢復(fù),當(dāng)pop返回上一個(gè)Fragment時(shí),再恢復(fù)這個(gè)Fragment)
這里給出2個(gè)解決方案:(為方便描述,以下皆不考慮Fragment嵌套的情況)
1、是大家比較熟悉的 findFragmentByTag:
即在add()或者replace()時(shí)綁定一個(gè)tag,一般我們是用fragment的類名作為tag,然后在發(fā)生“內(nèi)存重啟”時(shí),通過(guò)findFragmentByTag找到對(duì)應(yīng)的Fragment,并hide()需要隱藏的fragment。
下面是個(gè)標(biāo)準(zhǔn)恢復(fù)寫法:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity);
TargetFragment targetFragment;
HideFragment hideFragment;
if (savedInstanceState != null) { // “內(nèi)存重啟”時(shí)調(diào)用
targetFragment = getSupportFragmentManager().findFragmentByTag(targetFragment.getClass().getName);
hideFragment = getSupportFragmentManager().findFragmentByTag(hideFragment.getClass().getName);
// 解決重疊問(wèn)題
getFragmentManager().beginTransaction()
.show(targetFragment)
.hide(hideFragment)
.commit();
}else{ // 正常時(shí)
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();
}
}
如果你想恢復(fù)到用戶離開(kāi)時(shí)的那個(gè)Fragment的界面,你還需要在onSaveInstanceState(Bundle outState)里保存離開(kāi)時(shí)的那個(gè)見(jiàn)面的tag或下標(biāo),在onCreate“內(nèi)存重啟”代碼塊中,取出tag/下標(biāo),進(jìn)行恢復(fù)。
2、使用getSupportFragmentManager().getFragments()恢復(fù)
通過(guò)getFragments()可以獲取到當(dāng)前FragmentManager管理的棧內(nèi)所有Fragment。
標(biāo)準(zhǔn)寫法如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity);
TargetFragment targetFragment;
HideFragment hideFragment;
if (savedInstanceState != null) { // “內(nèi)存重啟”時(shí)調(diào)用
List fragmentList = getSupportFragmentManager().getFragments();
for (Fragment fragment : fragmentList) {
if(fragment instanceof TartgetFragment){
targetFragment = (TargetFragment)fragment;
}else if(fragment instanceof HideFragment){
hideFragment = (HideFragment)fragment;
}
}
// 解決重疊問(wèn)題
getFragmentManager().beginTransaction()
.show(targetFragment)
.hide(hideFragment)
.commit();
}else{ // 正常時(shí)
targetFragment = TargetFragment.newInstance();
hideFragment = HideFragment.newInstance();
// 這里add時(shí),tag可傳可不傳
getFragmentManager().beginTransaction()
.add(R.id.container)
.add(R.id,container,hideFragment)
.hide(hideFragment)
.commit();
}
}
從代碼看起來(lái),這種方式比較復(fù)雜,但是這種方式在一些場(chǎng)景下比第一種方式更加簡(jiǎn)便有效。
我會(huì)在下一篇中介紹在不同場(chǎng)景下如果選擇,何時(shí)用findFragmentByTag(),何時(shí)用getFragments()恢復(fù)。
順便一提,有些小伙伴會(huì)用一種并不合適的方法恢復(fù)Fragment,雖然效果也能達(dá)到,但并不恰當(dāng)。即:
// 保存
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
getSupportFragmentManager().putFragment(outState, KEY, targetFragment);
}
// 恢復(fù)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scrolling);
if (savedInstanceState != null) {
Fragment targetFragment = getSupportFragmentManager().getFragment(savedInstanceState, KEY);
}
}
如果僅僅為了找回棧內(nèi)的Fragment,使用putFragment(bundle, key, fragment)保存fragment,是完全沒(méi)有必要的;因?yàn)镕ragmentManager在任何情況都會(huì)幫你存儲(chǔ)Fragment,你要做的僅僅是在“內(nèi)存重啟”后,找回這些Fragment即可。
Fragment嵌套的那些坑
其實(shí)一些小伙伴遇到的很多嵌套的坑,大部分都是由于對(duì)嵌套的棧視圖產(chǎn)生混亂,只要理清棧視圖關(guān)系,做好恢復(fù)相關(guān)工作以及正確選擇是使用getFragmentManager()還是getChildFragmentManager()就可以避免這些問(wèn)題。
這部分內(nèi)容是我們感覺(jué)Fragment非常難用的一個(gè)點(diǎn),我會(huì)在下一篇中,詳細(xì)介紹使用Fragment嵌套的一些技巧,以及如何清晰分析各個(gè)層級(jí)的棧視圖。
附:startActivityForResult接收返回問(wèn)題
在support 23.2.0以下的支持庫(kù)中,對(duì)于在嵌套子Fragment的startActivityForResult (),會(huì)發(fā)現(xiàn)無(wú)論如何都不能在onActivityResult()中接收到返回值,只有最頂層的父Fragment才能接收到,這是一個(gè)support v4庫(kù)的一個(gè)BUG,不過(guò)在前兩天發(fā)布的support 23.2.0庫(kù)中,已經(jīng)修復(fù)了該問(wèn)題,嵌套的子Fragment也能正常接收到返回?cái)?shù)據(jù)了!
不靠譜的出棧方法remove()
如果你想讓某一個(gè)Fragment出棧,使用remove()并不靠譜。它并不能真正將Fragment從棧內(nèi)移除,如果你在2秒后(確保Fragment事務(wù)已經(jīng)完成)打印getSupportFragmentManager().getFragments(),會(huì)發(fā)現(xiàn)該Fragment依然存在。并且依然可以返回到被remove的Fragment,而且是空白頁(yè)面。
popBackStack()系列方法才能真正出棧,這也就引入下一個(gè)深坑,popBackStack(String tag,int flags)等系列方法的BUG。
多個(gè)Fragment同時(shí)出棧的那些深坑BUG
在Fragment庫(kù)中如下4個(gè)方法是有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個(gè)方法作用是,出棧到tag/id的fragment,即一次多個(gè)Fragment被出棧。
1、FragmentManager棧中管理fragment下標(biāo)位置的數(shù)組ArrayList mAvailIndeices的BUG
下面的方法FragmentManagerImpl類方法,產(chǎn)生BUG的罪魁禍?zhǔn)资枪芾鞦ragment棧下標(biāo)的mAvailIndeices屬性:
void makeActive(Fragment f) {
if (f.mIndex >= 0) {
return;
}
if (mAvailIndices == null || mAvailIndices.size() ();
}
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);
}
上面代碼最終導(dǎo)致了棧內(nèi)順序不正確的問(wèn)題,如下圖:
上面的這個(gè)情況,會(huì)一次異常,一次正常。帶來(lái)的問(wèn)題就是“內(nèi)存重啟”后,各種異常甚至Crash。
我發(fā)現(xiàn)這BUG的時(shí)候,我也懵比了,幸好,stackoverflow上有大神給出了解決方案!hack FragmentManagerImpl的mAvailIndices,對(duì)其進(jìn)行一次Collections.reverseOrder()降序排序,保證棧內(nèi)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());
}
}
}
使用方法就是通過(guò)popBackStackImmediate(tag/id)多個(gè)Fragment后,調(diào)用
hanler.post(new Runnable(){
@Override
public void run() {
FragmentTransactionBugFixHack.reorderIndices(fragmentManager));
}
});
2、popBackStack的BUG
popBackStack和popBackStackImmediate的區(qū)別在于前者是加入到主線隊(duì)列的末尾,等其它任務(wù)完成后才開(kāi)始出棧,后者是立刻出棧。
但是?。。≡谖沂褂玫倪^(guò)程中我發(fā)現(xiàn),popBackStack(tag/id)這2個(gè)方法是有BUG的!
先不說(shuō)有轉(zhuǎn)場(chǎng)動(dòng)畫情況下的各種深坑BUG…
如果你popBackStack多個(gè)Fragment后,緊接著beginTransaction() add新的一個(gè)Fragment,接著發(fā)生了“內(nèi)存重啟”后,你再執(zhí)行popBackStack(),app就會(huì)Crash。
所以,如果你想出棧多個(gè)Fragment,你應(yīng)盡量使用popBackStackImmediate(tag/id),而不是popBackStack(tag/id),如果你想在出棧后,立刻beginTransaction()開(kāi)始一項(xiàng)事務(wù),你應(yīng)該把事務(wù)的代碼post到主線程的消息隊(duì)列里,下一篇有詳細(xì)描述。
超級(jí)深坑 Fragment轉(zhuǎn)場(chǎng)動(dòng)畫
如果你的Fragment沒(méi)有轉(zhuǎn)場(chǎng)動(dòng)畫,或者使用setCustomAnimations(enter, exit)的話,那么上面的那些坑解決后,你可以愉快的玩耍了。
getFragmentManager().beginTransaction()
.setCustomAnimations(enter, exit)
// 如果你有通過(guò)tag/id同時(shí)出棧多個(gè)Fragment的情況時(shí),
// 請(qǐng)謹(jǐn)慎使用.setCustomAnimations(enter, exit, popEnter, popExit)
// 因?yàn)樵诔鰲6郌ragment時(shí),伴隨出棧動(dòng)畫,會(huì)在某些情況下發(fā)生異常
// 你還需要搭配Fragment的onCreateAnimation()臨時(shí)取消出棧動(dòng)畫
總結(jié)起來(lái)就是Fragment沒(méi)有出棧動(dòng)畫的話,可以避免很多坑。
如果想讓出棧動(dòng)畫運(yùn)作正常的話,需要使用Fragment的onCreateAnimation中控制動(dòng)畫。
@Override
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
// 此處設(shè)置動(dòng)畫
}
但是用代價(jià)也是有的,你需要解決出棧動(dòng)畫帶來(lái)的幾個(gè)坑。
1、pop多個(gè)Fragment時(shí)轉(zhuǎn)場(chǎng)動(dòng)畫 帶來(lái)的問(wèn)題
在使用 pop(tag/id)出棧多個(gè)Fragment的這種情況下,務(wù)必不能設(shè)定轉(zhuǎn)場(chǎng)動(dòng)畫;
原因在于這種情景下,如果發(fā)生“內(nèi)存重啟”后,F(xiàn)ragment并不會(huì)被FragmentManager正常保存下來(lái)。
2、進(jìn)入新的Fragment并立刻關(guān)閉當(dāng)前Fragment 時(shí)的一些問(wèn)題
(1)如果你想從當(dāng)前Fragment進(jìn)入一個(gè)新的Fragment,并且同時(shí)要關(guān)閉當(dāng)前Fragment。由于數(shù)據(jù)結(jié)構(gòu)是棧,所以正確做法是先pop,再add,但是轉(zhuǎn)場(chǎng)動(dòng)畫會(huì)有覆蓋的不正?,F(xiàn)象,你需要特殊處理,不然會(huì)閃屏!
(2)Fragment的根布局要設(shè)置android:clickable = true,原因是在pop后又立刻add新的Fragment時(shí),在轉(zhuǎn)場(chǎng)動(dòng)畫過(guò)程中,如果你的手速太快,在動(dòng)畫結(jié)束前你多點(diǎn)擊了一下,上一個(gè)Fragment的可點(diǎn)擊區(qū)域可能會(huì)在下一個(gè)Fragment上依然可用。
總結(jié)
看了上面的介紹,你可能會(huì)覺(jué)得Fragment有點(diǎn)可怕。
但是我想說(shuō),如果你只是淺度使用,比如一個(gè)Activity容器包含列表Fragment+詳情Fragment這種簡(jiǎn)單情景下,不涉及到popBackStack/Immediate(tag/id)這些的方法,還是比較輕松使用的,出現(xiàn)的問(wèn)題,網(wǎng)上都可以找到解決方案。
但是如果你的Fragment邏輯比較復(fù)雜,有特殊需求,或者你的app架構(gòu)是僅有一個(gè)Activity + 多個(gè)Fragment,上面說(shuō)的這些坑,你都應(yīng)該全部解決。
在下一篇中,介紹了一些非常實(shí)用的使用技巧,包括如何解決Fragment嵌套、各種環(huán)境、組件下Fragment的使用等技巧,推薦閱讀!
還有一些比較隱蔽的問(wèn)題,不影響app的正常運(yùn)行,僅僅是一些顯示的BUG,并沒(méi)有在上面介紹,在本系列的最后一篇,我給出了我的解決方案,一個(gè)我封裝的Fragmentation庫(kù),解決了所有動(dòng)畫問(wèn)題,非常適合單Activity+多Fragment 或者 多模塊Activity+多Fragment的架構(gòu)。有興趣的可以看看 :)-