Fragment進(jìn)階-commit使用細(xì)節(jié)及源碼分析

1. 每個(gè)事務(wù)(FragmentTranscation)只能被commit一次

承接Fragment進(jìn)階 - 基本用法中“Fragment動(dòng)態(tài)加載”的事例,如果界面里有多個(gè)Fragment需要提交,而且我不想一次性全部提交,而是分幾次提交...(事例代碼如下)

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getSupportFragmentManager().beginTransaction().add(R.id.fl_main, new ContentFragment(), null).commit();
        getSupportFragmentManager().beginTransaction().add(R.id.fl_main, new ContentFragment(), null).commit();
        getSupportFragmentManager().beginTransaction().add(R.id.fl_main, new ContentFragment(), null).commit();
    }
}

運(yùn)行程序,沒(méi)有問(wèn)題程序正常,然后看下此時(shí)的視圖結(jié)構(gòu):

add.png

我們add了3次,我們的Activty里有了3個(gè)Fragment的視圖(這也是小編踩坑之后才成功的)。

在最初進(jìn)行編寫(xiě)代碼的時(shí)候,小編遇到一個(gè)坑,最初的代碼是這樣寫(xiě)的:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        transaction.add(R.id.fl_main, new ContentFragment(), null).commit();
        transaction.add(R.id.fl_main, new ContentFragment(), null).commit();
        transaction.add(R.id.fl_main, new ContentFragment(), null).commit();
    }
}

我們把事務(wù)對(duì)象提取了出來(lái)(相信這是有潔癖的程序員的愛(ài)好),進(jìn)行了多次提交,運(yùn)行一下結(jié)果程序崩潰了(崩潰日志如下)...

 Caused by: java.lang.IllegalStateException: commit already called

日志告訴我們commit已經(jīng)被調(diào)用了,看來(lái)每個(gè)事務(wù)對(duì)象只能commit一次...

但是之前的寫(xiě)法是沒(méi)問(wèn)題的,由此可以推測(cè):每次調(diào)用getSupportFragmentManager().beginTransaction()獲取的都是一個(gè)新的實(shí)例。

查看源碼,證實(shí)了我們的推測(cè)...

fragment_transation.png

原來(lái),我們開(kāi)啟的每一個(gè)事務(wù)都是一個(gè)回退棧記錄(BackStackRecord是FragmentTransaction的一個(gè)具體實(shí)現(xiàn)類(lèi))

接下來(lái),我們研究下BackStackRecord,看看commit異常是怎么拋出的。

commit_error.png

BackStackRecord的commit操作會(huì)調(diào)用一個(gè)commitInternal方法。

mCommitted記錄了提交狀態(tài),我們的第1次提交mCommitted被置為true,第2次提交就拋異常了(整個(gè)源碼中對(duì)mCommitted進(jìn)行賦值的地方僅此1處(628行))。

結(jié)論:每個(gè)事務(wù)對(duì)象只能被commit一次。


2. Activity執(zhí)行完onSaveInstanceState()方法后不能再執(zhí)行commit()方法

小編想手動(dòng)控制Fragment的添加顯示,在Activity被銷(xiāo)毀的時(shí)候?qū)ragment移除掉(代碼如下)

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getSupportFragmentManager().beginTransaction().add(R.id.fl_main, new ContentFragment(), "ContentFragment").commit();
    }

    @Override
    protected void onDestroy() {
        getSupportFragmentManager().beginTransaction().remove(getSupportFragmentManager().findFragmentByTag("ContentFragment")).commit();
        super.onDestroy();
    }
}

運(yùn)行下程序,發(fā)現(xiàn)退出時(shí)程序崩潰了...我們得到如下崩潰日志:

Caused by: java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState

異常日志告訴我們:不能在onSaveInstanceState()方法被執(zhí)行之后調(diào)用commit()方法。

那么我們就提到onSaveInstanceState()之前調(diào)用,代碼如下:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getSupportFragmentManager().beginTransaction().add(R.id.fl_main, new ContentFragment(), "ContentFragment").commit();
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        getSupportFragmentManager().beginTransaction().remove(getSupportFragmentManager().findFragmentByTag("ContentFragment")).commit();
        super.onSaveInstanceState(outState);
    }
}

運(yùn)行下程序,發(fā)現(xiàn)退出程序不蹦了...但又有一個(gè)新問(wèn)題,手機(jī)滅屏?xí)r也會(huì)調(diào)用onSaveInstanceState方法,在打開(kāi)界面發(fā)現(xiàn)界面變成空白了...這種方法(手動(dòng)移除Fragment)顯然不行,這點(diǎn)需要注意。

3. commitAllowingStateLoss()方法

FragmentTranscation 給我們提供了另外一個(gè)方法 commitAllowingStateLoss(),從名字我們也能看出這個(gè)方法的作用:允許狀態(tài)丟失的提交。

看下官方文檔的解釋?zhuān)覀兙湍苊靼自蛄?

FragmentTransaction API commitAllowingStateLoss() (需要翻墻)

commit.png

原來(lái),在Activity的狀態(tài)被保存之后的提交操作是沒(méi)有被Activity所記錄的,恢復(fù)時(shí)也就沒(méi)辦法恢復(fù)這些提交操作,所以官方文檔稱(chēng)這個(gè)方法是一個(gè)危險(xiǎn)的操作。如果這些提交操作不是很重要,丟不丟失無(wú)所謂的話你就可以使用commitAllowingStateLoss()這個(gè)方法了。

我們調(diào)整下代碼,用commitAllowingStateLoss()代替commit()方法(如下):

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getSupportFragmentManager().beginTransaction().add(R.id.fl_main, new ContentFragment(), "ContentFragment").commit();
    }

    @Override
    protected void onDestroy() {
        getSupportFragmentManager().beginTransaction().remove(getSupportFragmentManager().findFragmentByTag("ContentFragment")).commitAllowingStateLoss();
        super.onDestroy();
    }
}

運(yùn)行下代碼,現(xiàn)在看起來(lái)沒(méi)問(wèn)題了...但我們知道如果出現(xiàn)了Activity異常銷(xiāo)毀重啟的情況,我們的移除操作在恢復(fù)時(shí)就丟失了,我們的Fragment將會(huì)出現(xiàn)重復(fù)疊加的問(wèn)題。

之前小編在Fragment進(jìn)階 - FragmentTransaction詳解最開(kāi)始的時(shí)候闡述過(guò)這個(gè)問(wèn)題,給出過(guò)最佳的解決辦法,就不再重復(fù)說(shuō)明了。

最后,我們從源碼上看下commit()commitAllowingStateLoss()有什么區(qū)別。

下面為BackStackRecord(FragmentTransaction的具體實(shí)現(xiàn)類(lèi))的部分源碼。

final class BackStackRecord extends FragmentTransaction implements
        FragmentManager.BackStackEntry, Runnable {

        ......

        public int commit() {
            return commitInternal(false);
        }

        public int commitAllowingStateLoss() {
            return commitInternal(true);
        }
    
        int commitInternal(boolean allowStateLoss) {
            ......
            mManager.enqueueAction(this, allowStateLoss);
            ......
        }
        ......    
}

我們看到commit()commitAllowingStateLoss()都調(diào)用了commitInternal(boolean allowStateLoss)這個(gè)方法只不過(guò)傳入?yún)?shù)不同而已(commit()傳入的false,commitAllowingStateLoss()傳入的true),接下來(lái)會(huì)調(diào)用FragmentManagerImpl(FragmentManager的具體實(shí)現(xiàn)類(lèi))的enqueueAction方法。

final class FragmentManagerImpl extends FragmentManager implements LayoutInflaterFactory {
    ......
    public void enqueueAction(Runnable action, boolean allowStateLoss) {
        if (!allowStateLoss) {
            checkStateLoss();
        }
        ......
    }
    .....

}

我們看到,如果commitAllowingStateLoss()傳的是true所以忽略掉了檢查,commit()傳的是false,所以進(jìn)行了檢查。

最后我們看下檢查的方法checkStateLoss():

final class FragmentManagerImpl extends FragmentManager implements LayoutInflaterFactory {
    ......
    private void checkStateLoss() {
        if (mStateSaved) {
            throw new IllegalStateException(
                    "Can not perform this action after onSaveInstanceState");
        }
        ......
    }
    ......
}

看來(lái)我們之前的異常就是在這拋出的,也算是找到根源了。

4. commit()方法被調(diào)用時(shí)并不會(huì)立即執(zhí)行

舉個(gè)簡(jiǎn)單的例子:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getSupportFragmentManager().beginTransaction().add(R.id.fl_main, new ContentFragment(), null).commit();
        Log.e("TAG", "MainActivity onCreated");
    }

    @Override
    public void onAttachFragment(Fragment fragment) {
        super.onAttachFragment(fragment);
        Log.e("TAG", "MainActivity onAttachFragmented");
    }
}

onAttachFragment(Fragment fragment)方法,會(huì)在添加進(jìn)來(lái)的Fragment執(zhí)行完onAttach方法后被回調(diào)。

我們得到如下的運(yùn)行日志:

10-17 17:26:51.076 9035-9035/com.sina.example.fragmentdemo E/TAG: MainActivity onCreated
10-17 17:26:51.078 9035-9035/com.sina.example.fragmentdemo E/TAG: Fragment onAttach()
10-17 17:26:51.078 9035-9035/com.sina.example.fragmentdemo E/TAG: MainActivity onAttachFragmented

我們看到commit()被調(diào)用后Fragment的生命周期沒(méi)有立即開(kāi)始,而是放在了onCreate(Bundle savedInstanceState)執(zhí)行完成之后的某個(gè)時(shí)間。

(具體開(kāi)始執(zhí)行的時(shí)間點(diǎn):在源碼中該任務(wù)是被放在一個(gè)mHost.getHandler().post(mExecCommit)方法的參數(shù)里,也就是Handler初始化完畢,開(kāi)始發(fā)送消息的時(shí)候,我們提交的任務(wù)將會(huì)被真正執(zhí)行)

如果不想等待Handler初始化完畢,想要立即執(zhí)行commit的操作可以使用FragmentManager里提供的executePendingTransactions()方法,這個(gè)方法將會(huì)將你所有提交的操作一并執(zhí)行,而且是立即執(zhí)行。

我們更改代碼,添加上這個(gè)方法(如下所示)。

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getSupportFragmentManager().beginTransaction().add(R.id.fl_main, new ContentFragment(), null).commit();
        getSupportFragmentManager().executePendingTransactions();
        Log.e("TAG", "MainActivity onCreated");
    }

    @Override
    public void onAttachFragment(Fragment fragment) {
        super.onAttachFragment(fragment);
        Log.e("TAG", "MainActivity onAttachFragmented");
    }
}

運(yùn)行程序,打印下日志:

10-17 17:51:37.626 22400-22400/com.sina.example.fragmentdemo E/TAG: Fragment onAttach()
10-17 17:51:37.626 22400-22400/com.sina.example.fragmentdemo E/TAG: MainActivity onAttachFragmented
10-17 17:51:37.627 22400-22400/com.sina.example.fragmentdemo E/TAG: MainActivity onCreated

我們看到在"MainActivity onCreated"日志還未被打印之前,我們提交的Fragment的生命周期已經(jīng)開(kāi)始了。

5. commitNow()commitNowAllowingStateLoss()

在最新的API_24文檔里FragmentTranslation里添加了兩個(gè)方法:

  • commitNow()

  • commitNowAllowingStateLoss()

調(diào)用這兩個(gè)方法類(lèi)似于先執(zhí)行commit()/commitAllowingStateLoss()然后執(zhí)行executePendingTransactions()方法。但也有區(qū)別。

區(qū)別一:不支持添加到回退棧的操作(源碼如下)

final class BackStackRecord extends FragmentTransaction implements
            FragmentManager.BackStackEntry, Runnable {
    ......

    @Override
    public void commitNow() {
        disallowAddToBackStack();
        mManager.execSingleAction(this, false);
    }

    @Override
    public void commitNowAllowingStateLoss() {
        disallowAddToBackStack();
        mManager.execSingleAction(this, true);
    }
    ......
}

如果還調(diào)用addToBackStack(String name)方法會(huì)報(bào)一個(gè)IllegalStateException異常,告訴你不能向回退棧中添加FragmentTransaction。

區(qū)別二:源碼沒(méi)有再使用Handler,而是直接執(zhí)行(源碼如下)

final class FragmentManagerImpl extends FragmentManager implements LayoutInflaterFactory {

    ......
    public void execSingleAction(Runnable action, boolean allowStateLoss) {

        ......
        action.run();
        ......

    }
    ......
}

BackStackRecord實(shí)現(xiàn)了Runnable接口重寫(xiě)了run方法,action.run()會(huì)直接執(zhí)行BackStackRecord的run方法。

補(bǔ)充:

官方更推薦使用commitNow()commitNowAllowingStateLoss()來(lái)代替先執(zhí)行commit()/commitAllowingStateLoss()然后執(zhí)行executePendingTransactions()這種方式,因?yàn)楹笳邥?huì)有不可預(yù)料的副作用。

總結(jié):

  1. 如果你需要同步提交Fragment并且無(wú)需添加到回退棧中,則使用commitNow()。Support庫(kù)中在 FragmentPagerAdapter中使用這個(gè)函數(shù),來(lái)確保更新Adapter的時(shí)候頁(yè)面被正確的添加和刪除。一般來(lái)說(shuō),只要不添加到回退棧中,都可以使用這個(gè)函數(shù)來(lái)提交。

  2. 如果執(zhí)行的提交不需要是同步的,或者需要將提交都添加到回退棧中,那么就使用commit()

  3. 如果你需要把多次提交的操作在同一個(gè)時(shí)間點(diǎn)一起執(zhí)行,則使用 executePendingTransactions()

  4. 如果你需要在Activity執(zhí)行完onSaveInstanceState()之后還要進(jìn)行提交,而且不關(guān)心恢復(fù)時(shí)是否會(huì)丟失此次提交,那么可以使用commitAllowingStateLoss()commitNowAllowingStateLoss()

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容