【iOS內(nèi)功】ARM匯編實戰(zhàn),解析iOS14 UICollectionView死循環(huán)問題

image.png

【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分析模型

【iOS內(nèi)功】深入解析Crash調(diào)用棧的內(nèi)存布局

【iOS內(nèi)功】ARM黑魔法—棧楨的入棧和出棧

【iOS內(nèi)功】使用Hopper定位疑難問題

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ù)制代碼
image.png

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指令

image.png

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

注1:tbz指令
復(fù)制代碼
image.png

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

image.png

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

頁面B指令

image.png

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。

image.png

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

image.png

進(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有源碼,直接分析源碼的邏輯。

image.png
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)用鏈路。

image.png
  • =>"-[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…

收錄:原文地址

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

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

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