問(wèn)題背景
最近新版本發(fā)布后,出現(xiàn)了一個(gè)偶現(xiàn)的crash并且迅速增加為Top1,這里對(duì)該問(wèn)題做一個(gè)分析。
報(bào)錯(cuò)內(nèi)容如下:
NSException -[UITabBarController setSelectedViewController:] only a view controller in the tab bar controller's list of view controllers can be selected.
crash堆棧如下:

問(wèn)題分析
APM分析
首先是查看APM提供的信息,沒有系統(tǒng)聚集、沒有機(jī)型聚集,是版本新增問(wèn)題都是隱私界面(也就是新用戶冷啟場(chǎng)景)。

該問(wèn)題在灰度有出現(xiàn)過(guò),一位同事在排查過(guò)程中,發(fā)現(xiàn)另外一個(gè)類似問(wèn)題是在UITabBarController的 _viewControllerForTabBarItem:方法出現(xiàn)異常,這個(gè)問(wèn)題量級(jí)并不大,場(chǎng)景類似但是沒有特別信息幫助定位。

多維分析
由于crash出現(xiàn)在系統(tǒng)的UITabBarController類,無(wú)法調(diào)試獲取更多信息,逆向排查周期太長(zhǎng)。這里可以通過(guò)Slardar的信息,結(jié)合日志和業(yè)務(wù)場(chǎng)景逐步縮小排查范圍。
首先通過(guò)crash場(chǎng)景,我們猜測(cè)是在用戶新用戶冷啟才會(huì)遇到,這里通過(guò)回?fù)迫罩竞蚦rash的pv/uv相比可以確定;
其次通過(guò)排查新用戶冷啟場(chǎng)景的特有邏輯,關(guān)注點(diǎn)放在新版本相關(guān)的代碼和實(shí)驗(yàn)改動(dòng),發(fā)現(xiàn)在底tab在新用戶冷啟場(chǎng)景的底tab刷新邏輯有較大可疑。
結(jié)合crash信息only a view controller in the tab bar controller's list of view controllers can be selected以及crash堆棧里有viewWillAppear時(shí)機(jī),合理猜測(cè)一個(gè)場(chǎng)景:是否tab切換時(shí),導(dǎo)致某個(gè)vc不在tabbar的子vc里面。比如說(shuō),沒有某個(gè)tab但是又指定跳到該vc,類似self.tabbarVC setSelectedViewController:self.xxxVC;又或者,某個(gè)子vc不在self.viewConrollers里面,但是又要跳轉(zhuǎn)到該vc。
通過(guò)業(yè)務(wù)代碼排查,業(yè)務(wù)并無(wú)直接設(shè)置setSelectedViewController的操作;在排查過(guò)程中發(fā)現(xiàn)只有setSelectedIndex的操作,從堆棧上來(lái)看,如果是setSelectedIndex觸發(fā)crash,堆棧上應(yīng)該會(huì)有這個(gè)方法。
于是重點(diǎn)排查子vc不存在的情況,在查看新用戶切換tab的邏輯時(shí),發(fā)現(xiàn)了有一個(gè)vc復(fù)用的邏輯,舊tabbarVC的vc會(huì)被復(fù)用到新的tabbarVC,結(jié)合ViewController只能有一個(gè)parentVC的限制,從邏輯上分析是有可能出現(xiàn)堆棧所描述的場(chǎng)景。
結(jié)合這個(gè)猜測(cè),當(dāng)vc被復(fù)用到新的tabbarVC時(shí),加了一段代碼讓新的tabbarVC不添加到window,從而舊的tabbar繼續(xù)觸發(fā)viewWillAppear,問(wèn)題可以復(fù)現(xiàn)。
反向分析
當(dāng)問(wèn)題可以穩(wěn)定復(fù)現(xiàn)后,就可以進(jìn)一步分析邏輯上的缺陷。
首先是vc的復(fù)用邏輯分析:
App在啟動(dòng)時(shí)就要初始化tabbarVC,并且在后續(xù)會(huì)刷新底tab的數(shù)量。由于我們使用了某個(gè)tabbarVC的組件,組件并不支持動(dòng)態(tài)新增底tab,這里采用的是重新創(chuàng)建tabbarVC的方式。
而我們的vc復(fù)用邏輯就是將vc從舊的tabbarVC移到新的tabbarVC。
這里寫了一個(gè)復(fù)用的模擬代碼:
- (void)testAnotherTabbarVC {
UITabBarController *anotherTabbarVC = [UITabBarController new];
[anotherTabbarVC addChildViewController:self.tabVC.viewControllers.firstObject];
}
復(fù)用邏輯比較簡(jiǎn)單清晰,但是UIKit有一個(gè)限制:每個(gè)vc只能有一個(gè)parentVC。當(dāng)我們給新tabbarVC設(shè)置子vc,其中復(fù)用vc已經(jīng)有parentVC,此時(shí)因?yàn)閺?fù)用到新的tabbarVC,parentVC也會(huì)從舊的tabbarVC變成新的tabbarVC。
當(dāng)舊的tabbarVC觸發(fā)viewWillAppear的時(shí)候,復(fù)用vc的parentVC已經(jīng)變成新的tabbarVC(截圖為nil是因?yàn)樾碌膖abbarVC被釋放了),但是沒被復(fù)用的另外一個(gè)vc的parentVC仍然是舊tabbarVC。
此時(shí)出現(xiàn)了錯(cuò)誤:
only a view controller in the tab bar controller's list of view controllers can be selected

問(wèn)題解決
方案1:在viewWillAppear之前,不觸發(fā)reloadTab,也就是等待展示之后再把舊的tabbarVC替換為新的tabbarVC;(這也是之前采用的方案)
方案2:在設(shè)置新的tabbarVC的viewController屬性時(shí),將復(fù)用vc從舊的tabbarVC的viewController移除;(這是UIKit的默認(rèn)做法,但是需要修改tabbarVC的組件)
方案3:不復(fù)用vc,只復(fù)用數(shù)據(jù)源;(需要修改復(fù)用方案)
代碼地址
為了驗(yàn)證分析沒有出錯(cuò),特意寫了demo,問(wèn)題可以復(fù)現(xiàn),github地址。