這里主要介紹一些對Fragment的深入理解趋艘。挑了一些個(gè)人認(rèn)為比較有價(jià)值的枢泰,大部分技術(shù)博客通常都會(huì)忽略的點(diǎn),列了出來铡溪,如果你對Fragment有什么其他疑惑漂辐,也可以在評論區(qū)留言。
Fragment究竟是什么呢棕硫?
Fragment簡單說可以認(rèn)為是一個(gè)帶有生命周期的View髓涯。閱讀過Fragment整個(gè)實(shí)現(xiàn)過程的,其實(shí)可以知道Fragment能顯示其實(shí)最終還是把要顯示的View add到ViewGroup中哈扮,它的生命周期回調(diào)纬纪,其實(shí)也全部來自Activity。然后又封裝了一些本來Activity才有的特性滑肉,比如過渡動(dòng)畫包各,返回棧等等。你甚至可以說Activity能做的大部分事情靶庙,F(xiàn)ragment都能做问畅,甚至市面有很多App是只有一個(gè)Activity或者少數(shù)幾個(gè)Activity,然后絕大部分界面全部用Fragment實(shí)現(xiàn)的六荒。比如:知乎护姆。
如何知道呢?你可以使用下面的命令來查看當(dāng)前手機(jī)顯示的Activity名稱掏击。
Linux/Unix/Mac
adb shell dumpsys activity | grep "mFocusedActivity"
Windows
adb shell dumpsys activity | findstr "mFocusedActivity"
- 為什么有App要這么做呢卵皂?
用過知乎的應(yīng)該會(huì)知道知乎的頁面跳轉(zhuǎn)邏輯很復(fù)雜。點(diǎn)開一個(gè)回答砚亭,然后點(diǎn)到回答的列表灯变,會(huì)發(fā)現(xiàn)回答列表也有底欄,同時(shí)底欄的操作邏輯跟主界面時(shí)候的操作邏輯完全一致钠惩。每個(gè)底欄都有自己獨(dú)立的返回棧柒凉。所以借助Fragment可以實(shí)現(xiàn)一些非常復(fù)雜的界面的跳轉(zhuǎn)邏輯。試想如果用Activity要實(shí)現(xiàn)知乎這種復(fù)雜的跳轉(zhuǎn)邏輯改有多復(fù)雜了篓跛。
其次膝捞,F(xiàn)ragment的消耗要比Activity小,F(xiàn)ragment最終被處理是FragmentTransaction.commit方法∈咭В看過整個(gè)Fragment實(shí)現(xiàn)過程的話鲤遥,會(huì)知道commit操作后最終會(huì)調(diào)用:
private void scheduleCommit() {
synchronized (this) {
boolean postponeReady =
mPostponedTransactions != null && !mPostponedTransactions.isEmpty();
boolean pendingReady = mPendingActions != null && mPendingActions.size() == 1;
if (postponeReady || pendingReady) {
mHost.getHandler().removeCallbacks(mExecCommit);
mHost.getHandler().post(mExecCommit);
}
}
}
前面對請求做了一系列處理后,最終通過scheduleCommit講請求以Handler消息形式重新提交林艘。直到Fragment中的View被Add進(jìn)父ViewGroup都不會(huì)涉及到跨進(jìn)程通信盖奈。但是Activity啟動(dòng)過程就不一樣了,startActivity最終會(huì)通過
ActivityManagerNative.getDefault().startActivity
向ActivityManagerService所在進(jìn)程發(fā)送一個(gè)跨進(jìn)程通信消息狐援,然后ActivityManagerService響應(yīng)消息回傳钢坦,App的ActivityThread接收到消息后打開Activity界面。整個(gè)過程是需要進(jìn)行跨進(jìn)程通信的啥酱,消耗當(dāng)然要比Fragment高了爹凹。
- 那是不是就沒有壞處了?
也不是镶殷,F(xiàn)ragment將失去Activity本身的很多特性禾酱,比如啟動(dòng)模式,不能直接通過ACTION啟動(dòng)绘趋。將失去Manifest中你能看到的Activity的特性颤陶。當(dāng)然如果你不關(guān)心這些東西,那完全用Fragment替換也無妨陷遮。
究竟應(yīng)該用 android.support.v4.app.Fragment還是android.app.Fragment滓走?
android.app.Fragment是Google在Android3.0(API11)的時(shí)候推出的。現(xiàn)在大部分AndroidApp應(yīng)該都已經(jīng)最低兼容到4.0(API14)拷呆。首先從Api上來說不存在問題闲坎,雖然android.support.v4.app.Fragment兼容到v4,但對于大多數(shù)App來說兼容到14就夠了茬斧。那從使用角度來看呢腰懂?
android.support.v4.app.Fragment supportFragment = new android.support.v4.app.Fragment();
android.support.v4.app.FragmentTransaction supportTransaction = getSupportFragmentManager().beginTransaction();
supportTransaction.add(supportFragment, "tag");
supportTransaction.commit();
android.app.Fragment fragment = new android.app.Fragment();
android.app.FragmentTransaction transaction = getFragmentManager().beginTransaction();
transaction.add(fragment, "tag");
transaction.commit();
使用上就是包路徑不一樣,然后一個(gè)是getSupportFragmentManager一個(gè)是getFragmentManager项秉。Api層面基本算是完全兼容了绣溜。還有一點(diǎn)小區(qū)別就是SupportFragmentManager會(huì)多出一些方法,比如getFragments(但這個(gè)方法現(xiàn)在已經(jīng)被標(biāo)記為@RestrictTo(LIBRARY_GROUP)不推薦使用了)拿到SupportFragmentManager持有的Fragment引用娄蔼。
另外就是android.app.Fragment是android.app.Activity原生支持怖喻。不需要額外引入lib庫。但android.support.v4.app.Fragment只能用在android.support.v4.app.FragmentActivity中岁诉,需要額外引入supportV4包锚沸。getSupportFragmentManager方法就來自FragmentActivity。但考慮到通常Activity大家都會(huì)選擇繼承AppCompatActivity或者FragmentActivity涕癣,所以通常也不會(huì)有太大問題哗蜈。
說了這么多,可以這么說從兩個(gè)Fragment Api基本一致,使用起來也基本沒太大區(qū)別距潘。
基本表現(xiàn)一致炼列,那怎么選?
還是推薦用android.support.v4.app.Fragment音比。
為什么俭尖?我們之前嘗試過使用原生Fragment,從API層面確實(shí)沒遇到太多麻煩洞翩。主要是在適配第三方庫的時(shí)候遇到了很多麻煩稽犁,因?yàn)榈谌綆旎救慷际沁x擇使用的v4Fragment。另外還有一個(gè)麻煩是v4是額外的包菱农,所以v4Fragment是可以升級的缭付,但Fragment就只能跟隨手機(jī)系統(tǒng)版本升級了。比如v4FragmentManager后面新添加的方法registerFragmentLifecycleCallbacks可以用來很方便的監(jiān)聽Fragment生命周期循未,但是用FragmentManager就沒有這個(gè)待遇了。使用v4Fragment就沒有那么多麻煩了秫舌。雖然v4要額外引入supportV4包的妖,但這個(gè)包對于大家做應(yīng)用開發(fā)的話,基本是必定要引入的足陨。所以這個(gè)引入成本是必須承受的嫂粟。
為什么DialogFragment能自動(dòng)恢復(fù)?
Google推薦我們使用DialogFragment墨缘。因?yàn)镈ialogFragment在Activity重建后依然可以自動(dòng)恢復(fù)星虹,但是Dialog就不可以。那問題來了镊讼,為什么DialogFragment就可以自動(dòng)恢復(fù)宽涌,但是Dialog就不可以?
首先Dialog不能恢復(fù)蝶棋,這個(gè)應(yīng)該很容易理解卸亮,因?yàn)锳ctivity都被重建了,當(dāng)然不會(huì)自動(dòng)恢復(fù)玩裙。除非你手動(dòng)保存Dialog狀態(tài)兼贸,然后在重建時(shí)候重新show出來。
那Fragment呢吃溅?在FragmentActivity源碼中是這么處理onSaveInstanceState的溶诞。
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
Parcelable p = mFragments.saveAllState();
if (p != null) {
outState.putParcelable(FRAGMENTS_TAG, p);
}
......
}
mFragments是個(gè)FragmentController。通過saveAllState拿到了所有Fragment的State决侈,在onSaveInstanceState的時(shí)候保存起來了螺垢。
然后我們在看下FragmentActivity的onCreate方法。
protected void onCreate(@Nullable Bundle savedInstanceState) {
mFragments.attachHost(null /*parent*/);
super.onCreate(savedInstanceState);
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
mFragments.restoreLoaderNonConfig(nc.loaders);
}
if (savedInstanceState != null) {
Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
mFragments.restoreAllState(p, nc != null ? nc.fragments : null);
// Check if there are any pending onActivityResult calls to descendent Fragments.
if (savedInstanceState.containsKey(NEXT_CANDIDATE_REQUEST_INDEX_TAG)) {
mNextCandidateRequestIndex =
savedInstanceState.getInt(NEXT_CANDIDATE_REQUEST_INDEX_TAG);
int[] requestCodes = savedInstanceState.getIntArray(ALLOCATED_REQUEST_INDICIES_TAG);
String[] fragmentWhos = savedInstanceState.getStringArray(REQUEST_FRAGMENT_WHO_TAG);
if (requestCodes == null || fragmentWhos == null ||
requestCodes.length != fragmentWhos.length) {
Log.w(TAG, "Invalid requestCode mapping in savedInstanceState.");
} else {
mPendingFragmentActivityResults = new SparseArrayCompat<>(requestCodes.length);
for (int i = 0; i < requestCodes.length; i++) {
mPendingFragmentActivityResults.put(requestCodes[i], fragmentWhos[i]);
}
}
}
}
......
}
在saveInstanceState不為null的時(shí)候,調(diào)用了restoreAllState來恢復(fù)Fragment甩苛。
public void restoreAllState(Parcelable state, FragmentManagerNonConfig nonConfig) {
mHost.mFragmentManager.restoreAllState(state, nonConfig);
}
繼續(xù)跟進(jìn)源碼可以看到具體的恢復(fù)過程蹂楣。
void restoreAllState(Parcelable state, FragmentManagerNonConfig nonConfig) {
......
Fragment f = fs.instantiate(mHost, mParent, childNonConfig);
......
}
代碼比較多,這里只貼了實(shí)際恢復(fù)的部分讯蒲,其他代碼邏輯也不復(fù)雜痊土,可以自行查看。FragmentState最終調(diào)用了Fragment.instantiate
public Fragment instantiate(FragmentHostCallback host, Fragment parent,
......
mInstance = Fragment.instantiate(context, mClassName, mArguments);
......
return mInstance;
}
最終通過class.newInstance創(chuàng)建出了Fragment實(shí)例墨林。
public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) {
try {
Class<?> clazz = sClassMap.get(fname);
if (clazz == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = context.getClassLoader().loadClass(fname);
sClassMap.put(fname, clazz);
}
Fragment f = (Fragment)clazz.newInstance();
if (args != null) {
args.setClassLoader(f.getClass().getClassLoader());
f.mArguments = args;
}
return f;
} catch (ClassNotFoundException e) {
throw new InstantiationException("Unable to instantiate fragment " + fname
+ ": make sure class name exists, is public, and has an"
+ " empty constructor that is public", e);
} catch (java.lang.InstantiationException e) {
throw new InstantiationException("Unable to instantiate fragment " + fname
+ ": make sure class name exists, is public, and has an"
+ " empty constructor that is public", e);
} catch (IllegalAccessException e) {
throw new InstantiationException("Unable to instantiate fragment " + fname
+ ": make sure class name exists, is public, and has an"
+ " empty constructor that is public", e);
}
}
Activity的實(shí)例化也是通過反射創(chuàng)建的赁酝。這也是為什么系統(tǒng)能創(chuàng)建和恢復(fù)出Activity/Fragment實(shí)例的原因⌒竦龋恢復(fù)后引用雖然不是同一個(gè)酌呆,但是狀態(tài)是一致,所以也就會(huì)明白搔耕,為什么Activity/Fragment一定要有一個(gè)無參構(gòu)造方法隙袁,對于參數(shù)必須通過onSaveInstanceState來保存和恢復(fù)了。
如何優(yōu)雅的初始化Fragment弃榨?
大家可能會(huì)看到下面的代碼菩收。通常下面代碼會(huì)在Activity的onCrate方法中初始化。
private SampleFragment mFragment;
private void init(){
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
mFragment = new SampleFragment();
transaction.add(R.id.fragment_content, mFragment);
transaction.commit();
}
有什么問題嗎鲸睛?粗看似乎沒什么問題娜饵。但實(shí)際這是一種很不好的寫法。你可以寫個(gè)Demo然后利用Android Studio的dump java heap(或者在SampleFragment的構(gòu)造方法中打印日志也可以)看下內(nèi)存中有幾個(gè)Fragment實(shí)例官辈。然后把屏幕旋轉(zhuǎn)下箱舞,觸發(fā)Activity的SaveInstanceState然后再看下內(nèi)存中有幾個(gè)實(shí)例。你會(huì)發(fā)現(xiàn)內(nèi)存中出現(xiàn)了兩個(gè)SampleFragment實(shí)例拳亿。
把上面代碼改造成下面這樣晴股,再看下旋轉(zhuǎn)屏幕后,有幾個(gè)SampleFragment實(shí)例风瘦。
private SampleFragment mFragment;
private void initFrom(){
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_content);
if (fragment == null) {
fragment = new SampleFragment();
}
mFragment = (SampleFragment) fragment;
transaction.add(R.id.fragment_content, mFragment);
transaction.commit();
}
上面代碼無論怎么跑SampleFragment永遠(yuǎn)都只有一個(gè)實(shí)例队魏。為什么?
好了万搔。說了這么多胡桨,可能會(huì)很多人會(huì)說,用自己手動(dòng)new Fragment的寫法并沒有遇到過問題瞬雹,怎么回事昧谊?主要是因?yàn)楝F(xiàn)在很多App都沒有適配橫屏模式,很多都鎖死只能豎屏酗捌,然后現(xiàn)在的Android手機(jī)普遍內(nèi)存都比較大呢诬,也較少的出現(xiàn)系統(tǒng)內(nèi)存不足涌哲,觸發(fā)onSaveInstanceState的情況。所以問題沒有發(fā)生并不是說這樣是對的尚镰。
另外阀圾,你會(huì)發(fā)現(xiàn)無論你用add還是replace至少加一個(gè)tag或者Rid。為什么狗唉?其實(shí)就是為了讓你重新找回Fragment實(shí)例引用的初烘。
為什么使用FragmentTransaction的add(Fragment fragment, String tag);方法Fragment不會(huì)顯示?
前面說到Fragment在add或者replace的時(shí)候一定需要指定tag和RId中的至少一個(gè)分俯。那問題來了肾筐,假如我調(diào)用add不指定RId的方法會(huì)怎么樣?
實(shí)際測試下會(huì)發(fā)現(xiàn)界面沒有任何變化缸剪,如果你打印Fragment的生命周期的話會(huì)發(fā)現(xiàn)Fragment的生命周期是正常的吗铐,但實(shí)際沒有顯示。我們來看下Fragment實(shí)際是如何處理View的顯示的杏节。詳細(xì)的Fragment處理過程比較復(fù)雜唬渗,回頭有空了會(huì)寫一篇詳細(xì)的文章介紹Fragment是如何顯示在Activity中的,這里先略過拢锹。直接在android.support.v4.app.FragmentManager類中搜索:"case Fragment.CREATED:"找到下面代碼
case Fragment.CREATED:
if (newState > Fragment.CREATED) {
if (DEBUG) Log.v(TAG, "moveto ACTIVITY_CREATED: " + f);
if (!f.mFromLayout) {
ViewGroup container = null;
if (f.mContainerId != 0) {
if (f.mContainerId == View.NO_ID) {
throwException(new IllegalArgumentException(
"Cannot create fragment "
+ f
+ " for a container view with no id"));
}
......
注意到if (f.mContainerId != 0) 這一行谣妻,假如mContainerId為0則container就null。然后
if (f.mView != null) {
......
if (container != null) {
container.addView(f.mView);
}
最終調(diào)用了container的addView方法讓View實(shí)際添加了ViewGroup中卒稳。所以到這里就清楚了假如addView沒有指定id,那么container就是null他巨,container是null那么即使onCreateView返回了真實(shí)有效的View也一樣沒用充坑,因?yàn)闆]有地方可以給它add,當(dāng)然也就不會(huì)顯示染突。
為什么DialogFragment也不需要指定id捻爷,但是DialogFragment就可以正常顯示?
DialogFragment大家通撤萜螅可能會(huì)有類似下面的代碼也榄。
new SampleDialogFragment().show(getSupportFragmentManager(), "dialog");
問題來了,我們確實(shí)沒有給DialogFragment指定id司志,那為什么DialogFragment還能正常顯示甜紫?跟進(jìn)show方法
public void show(FragmentManager manager, String tag) {
mDismissed = false;
mShownByMe = true;
FragmentTransaction ft = manager.beginTransaction();
ft.add(this, tag);
ft.commit();
}
DialogFragment在show的時(shí)候調(diào)用的是無Rid的add方法。不開心了骂远,憑什么DialogFragment能顯示囚霸?
先不急,我們把前面的SampleFragment繼承從Fragment改成DialogFragment激才,然后也調(diào)用無RId的add方法看看會(huì)怎么樣拓型。
實(shí)際測試后會(huì)發(fā)現(xiàn)繼承修改成DialogFragment后额嘿,SampleFragment也能顯示了。
問題來了劣挫,為什么呢册养?明明沒有指定ID,View被搞哪里去了压固?
弄清楚這個(gè)問題前球拦,我們先想下
- 怎么顯示一個(gè)View?
把View添加進(jìn)ViewGroup - 那ViewGroup從哪里來邓夕?
順著setContent方法一路找下去就會(huì)發(fā)現(xiàn)刘莹,Activity被顯示是因?yàn)閃indow,最終的根ViewGroup來自Window焚刚〉阃洌可以理解為有Window就可以顯示View。(一個(gè)Activity可以有多個(gè)Window矿咕,有興趣的可以搜下相關(guān)文檔抢肛,怎么創(chuàng)建View,怎么在Window中顯示View碳柱,這里不做詳細(xì)介紹)
到這里一下子就柳暗花明了捡絮,一個(gè)View能否顯示要看是否被添加進(jìn)Window里了。實(shí)際負(fù)責(zé)顯示的是Window莲镣。DialogFragment雖然沒有被add進(jìn)父ViewGroup福稳,只要它被add進(jìn)Window其實(shí)一樣可以顯示。那DialogFragment是不是這樣做了呢瑞侮?DialogFragment源碼其實(shí)并不多的圆,自己可以詳細(xì)一點(diǎn)點(diǎn)看一遍,這里只說重點(diǎn)部分半火。
public LayoutInflater getLayoutInflater(Bundle savedInstanceState) {
if (!mShowsDialog) {
return super.getLayoutInflater(savedInstanceState);
}
mDialog = onCreateDialog(savedInstanceState);
if (mDialog != null) {
setupDialog(mDialog, mStyle);
return (LayoutInflater) mDialog.getContext().getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
}
return (LayoutInflater) mHost.getContext().getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
}
這里L(fēng)ayoutInflater被復(fù)寫成了Dialog的LayoutInflater越妈。
@NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new Dialog(getActivity(), getTheme());
}
而且onCreateDialog方法默認(rèn)會(huì)創(chuàng)建一個(gè)Dialog,而且還加了NonNull的注解钮糖。
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (!mShowsDialog) {
return;
}
View view = getView();
if (view != null) {
if (view.getParent() != null) {
throw new IllegalStateException(
"DialogFragment can not be attached to a container view");
}
mDialog.setContentView(view);
}
final Activity activity = getActivity();
if (activity != null) {
mDialog.setOwnerActivity(activity);
}
mDialog.setCancelable(mCancelable);
mDialog.setOnCancelListener(this);
mDialog.setOnDismissListener(this);
if (savedInstanceState != null) {
Bundle dialogState = savedInstanceState.getBundle(SAVED_DIALOG_STATE_TAG);
if (dialogState != null) {
mDialog.onRestoreInstanceState(dialogState);
}
}
}
這里一下就完全清楚了梅掠,View被添加到Dialog里。你只要繼承了DialogFragment店归,那么雖然你add沒有指定RId阎抒,但是View會(huì)被set到Dialog里,所以最終顯示是在Dialog中娱节。也就是說DialogFragment實(shí)際顯示還是Dialog挠蛉,但是利用了Fragment的生命周期管理來實(shí)現(xiàn)一些比如重建之類的工作。說到這里是不是對Fragment的add無Rid方法有了一個(gè)更深入的理解肄满?