
【iOS內(nèi)功】ARM匯編實戰(zhàn),解析iOS14 UICollectionView死循環(huán)問題
背景
9月初iOS14正式發(fā)布,線上版本新冒出許多Crash。有一個Crash,UICollectionView刷新邏輯死循環(huán),卡死了主線程。
陽差陽錯,中美兩個程序員的“誤會”造成了這個Crash。
App有一個頁面,自定義了一個XXCollectionView。XXCollectionView嵌套在Cell里,寫代碼的人偷懶,把delegate設(shè)置成自己。Apple工程師也不講武德,把協(xié)議(UICollectionViewDelegate)沒聲明的方法轉(zhuǎn)發(fā)給delegate執(zhí)行,一點契約精神都沒有。
Apple框架大多不開源,內(nèi)部有奇怪的邏輯,我們只能通過讀匯編指令去分析。剛開始分析源碼時,奇怪的邏輯讓我懷疑人生,最后通過調(diào)試匯編指令,才理清楚具體的原因。
這個案例挺經(jīng)典的,分析過程中,用到了oc、lldb調(diào)試、arm等常用知識,也用到了歸納法、邏輯推理、逆向思維、抽象思維等思維方法。
我詳細(xì)總結(jié)了分析過程,和大家交流和探討。
iOS內(nèi)功系列文章
Crash分析的基礎(chǔ)知識了解不多的,可以參考原來寫的一些文章
【iOS內(nèi)功】深入解析Crash調(diào)用棧的內(nèi)存布局
1.0、Crash Log特征分析
1.1、環(huán)境特征
crash都發(fā)生在iOS14,推斷是iOS14系統(tǒng)庫邏輯改動引發(fā)的Crash。
1.2、調(diào)用堆棧
堆棧里有UICollectionview和CPXXProxy,接下來可以找一下,哪些頁面同時用到這兩個類,
swipe點擊事件
...
-[UICollectionView _diffableDataSourceImpl]
...
_CF_forwarding_prep_0
...
-[CPXXProxy forwardInvocation]
...
-[UICollectionView _diffableDataSourceImpl] (開始循環(huán))
...
_CF_forwarding_prep_0
...
-[CPXXProxy forwardInvocation]
復(fù)制代碼

1.3、場景復(fù)現(xiàn)
符合條件的頁面很多,試了幾個都復(fù)現(xiàn)不了。后面發(fā)現(xiàn)有一個頁面A,abort日志里有很多swipe事件,頁面也用了UICollectionview。點擊頁面A的各種區(qū)域,最后在點擊空白處卡死了,并且復(fù)現(xiàn)了同樣的堆棧。
2.0、源碼調(diào)試分析
為了簡潔,下面的術(shù)語用縮略詞表示
diffxx代表_diffableDataSourceImpl
CPXXProxy代表_CPCollectionViewFlowLayoutProxy
復(fù)制代碼
2.1、頁面A和頁面B有什么差別?
很多頁面雖然也用了UICollectionView和CPXXProxy,但不會出現(xiàn)這個crash。于是我找了其中一個頁面,下面稱為頁面B,從邏輯上推導(dǎo),頁面A和頁面B邏輯上一定有一個差異,這個差異最終造成頁面A的Crash。接下來,分析頁面A和B的差異。
差異1:頁面A里嵌套了UICollectionview
頁面A和頁面B有一個差異點,頁面A的UICollectionView有多種cell,其中一個cell里又嵌套了UICollectionView。cell里的XXCollectionview繼承自系統(tǒng)的UICollectionview。
差異2:頁面B不會調(diào)用觸發(fā)“-[CPXXProxy forwardInvocation]”
找到關(guān)鍵路徑的3個方法,添加符號斷點。模擬Crash的操作,分析頁面A和頁面B調(diào)用棧的差異.
-[UICollectionView _diffableDataSourceImpl]
_CF_forwarding_prep_0
-[CPXXProxy forwardInvocation]
復(fù)制代碼
調(diào)試發(fā)現(xiàn),頁面A會調(diào)用到“-[CPXXProxy forwardInvocation]”,而頁面B并不會。頁面A走到forwardInvocation,說明系統(tǒng)給CPXXProxy發(fā)送了未實現(xiàn)的方法,這個方法是“_diffableDataSourceImpl”。
2.2、_diffableDataSourceImpl方法是哪里定義的?
iOS13引入了DiffableDataSource,幫助UITableView和UICollection更方便地實現(xiàn)局部刷新。對外開發(fā)的類是NSDiffableDataSourceSnapshot,并沒有diffxx方法。
使用runtime的接口,導(dǎo)出UICollectionView所有的方法,發(fā)現(xiàn)里面包括diffxx,diffxx方法并沒有對外開放,是一個私有方法。
2.3、頁面A為什么會調(diào)用到"-[CPXXProxy _diffableDataSourceImpl]"
diffxx是UICollectionView自己的方法,為什么轉(zhuǎn)發(fā)給delegate,這是挺奇怪的邏輯。
CPXXProxy被設(shè)置為UICollectionView的delegate,它會接收到UICollectionViewDelegate協(xié)議聲明的方法,但UICollectionViewDelegate里并沒有diffxx方法,理論上不應(yīng)該觸發(fā)這個方法的調(diào)用。
"-[CPXXProxy _diffableDataSourceImpl]"是UIKit內(nèi)部邏輯觸發(fā)的,而UIkit的源碼沒有開源,所以接下來只能調(diào)試Arm匯編繼續(xù)分析。
3.0、匯編調(diào)試分析
我們應(yīng)該從哪個方法入手?梳理一下思路。
“頁面A為什么會出現(xiàn)異?!??觸發(fā)異常邏輯肯定有一個源點,在這個源點之前頁面A的邏輯也應(yīng)該是正常的。
頁面B作為正常的參照物,我們要找到它和頁面A出現(xiàn)邏輯分叉的地方。對比頁面A和頁面B的調(diào)用棧,它們最后一個相同的方法是“-[UICollectionView _diffableDataSourceImpl]”,邏輯分叉就在這個方法里,因此我們就從這個方法入手分析。
3.1、“-[UICollectionView _diffableDataSourceImpl]”哪行指令出現(xiàn)邏輯分叉?
頁面A指令

w0寄存器的值是1,沒有命中tbz指令的跳轉(zhuǎn),按順序繼續(xù)執(zhí)行下一行指令。一直執(zhí)行到bl 0x196d04850指令,跳轉(zhuǎn)到0x196d04850。
注1:tbz指令
復(fù)制代碼

bl 0x196d04820里面經(jīng)過幾次跳轉(zhuǎn),最后執(zhí)行objc_msgSend方法。根據(jù)寄存器的值,objc_msgSend里的target是“CPXXProxy”,selector就是是"diffxx"

CPXXProxy里并沒有實現(xiàn)diffxx函數(shù),進(jìn)行消息轉(zhuǎn)發(fā)_CF_forwarding_prep_0
頁面B指令

w0寄存器的值是0,命中tbz指令的跳轉(zhuǎn),跳轉(zhuǎn)到0x1991c12ac繼續(xù)執(zhí)行指令,后續(xù)也沒有調(diào)用到CPXXProxy的方法。
結(jié)論
頁面A和頁面B的分叉點在tbz指令。tbz是一個條件跳轉(zhuǎn),頁面A里tbz的測試值w0為1,頁面B的測試值為0,最后走到了不同的邏輯。
3.2、w0的差異,是哪里造成的?
我們要找的關(guān)鍵指令就是“tbz w0 #0x0”,下面分析哪里將w0的值改為1。

執(zhí)行"bl 0x197157750"指令前x0寄存器還是一個對象,執(zhí)行后x0寄存器就成了1,說明這個方法調(diào)用的返回值就是1,也就是true。

進(jìn)入"bl 0x197157750"調(diào)試,發(fā)現(xiàn)最終調(diào)用的方法是“CPXXXProxy respondsToSelector”,這個方法的返回值是true。也就是說,調(diào)用“-[CPXXXProxy respondsToSelector]”方法時,頁面A和頁面B結(jié)果不一樣。
3.3、為什么“-[CPXXXProxy respondsToSelector]”的返回值不一樣
CPXXXProxy有源碼,直接分析源碼的邏輯。

CPXXXProxy 簡介
CPXXXProxy對象里有一個target屬性,它是Cell里嵌套的XXCollectionView。
XXCollectionView的delegate和datasource設(shè)置為CPXXXProxy,collectionVie的回調(diào)方法先發(fā)給CPXXXProxy,CPXXXProxy接管了部分方法,自己不接管的回調(diào)方法,CPXXXProxy會再轉(zhuǎn)發(fā)給target自行處理。
復(fù)制代碼
根據(jù)截圖顯示,調(diào)用了“-[_target respondsToSelector:aSelector]”,運(yùn)行結(jié)果是true。說明頁面A的_target實現(xiàn)了“_diffableDataSourceImpl”方法,而頁面B的_target并沒有實現(xiàn)。
在頁面A,XXCollectionView嵌套在cell里,CPXXXProxy的target設(shè)置為XXCollectionView。而在頁面B,XXCollectionView沒有嵌套,CPXXXProxy的target設(shè)置為頁面B的頁面控制器,XXViewController。
“_diffableDataSourceImpl”本身就是UICollectionView的方法,而XXCollectionView繼承于UICollectionView,結(jié)果當(dāng)然是true。
4.0、總結(jié)
分析到這里,已經(jīng)豁然開朗,下圖是死循環(huán)的調(diào)用鏈路。

- =>"-[XXCollectionView _diffableDataSourceImpl"]"
- =>"-[CPXXXProxy respondsToSelector:@"_diffableDataSourceImpl"]"
- => "-[XXCollectionView respondsToSelector:@"_diffableDataSourceImpl"]" 結(jié)果是True
- =>"-[CPXXXProxy _diffableDataSourceImpl"]"
- =>"-[CPXXXProxy forwardInvocation"]"
- =>"-[XXCollectionView _diffableDataSourceImpl"]"
- =>...死循環(huán)
參考
注1:tbz指令
測試位為0發(fā)生跳轉(zhuǎn),imm指定目的寄存器的某一個位,『b5:b40』組成,0-63或者0-31,有b5決定。哪個目的寄存器由Rt指定,label是偏移地址。
TBNZ介紹 www.cnblogs.com/rongmouzhan…
收錄:原文地址