我給 iOS 系統(tǒng)打了個(gè)補(bǔ)丁——修復(fù) iOS 16 系統(tǒng)鍵盤(pán)重大 Crash


?????♀??編者按:本文作者是螞蟻集團(tuán)客戶端工程師巴樂(lè),通過(guò)逆向分析發(fā)現(xiàn)了 iOS 16 系統(tǒng)鍵盤(pán)存在重大 Bug,可能導(dǎo)致使用到鍵盤(pán)的業(yè)務(wù)場(chǎng)景出現(xiàn)嚴(yán)重 Crash。在支付寶 App 近期版本 10.5.16.6000 上,巴樂(lè)用匯編重新實(shí)現(xiàn)了一套 iOS 16 系統(tǒng)鍵盤(pán) tryLock 方法后,問(wèn)題得到完全修復(fù),該版本上的對(duì)應(yīng) Crash 已降到 0。本文記錄了該問(wèn)題解決的完整過(guò)程,包括問(wèn)題發(fā)現(xiàn)、分析、修復(fù)以及驗(yàn)證,歡迎查閱與交流~
背景
在螞蟻集團(tuán)內(nèi)部,支付寶技術(shù)部及螞蟻終端技術(shù)委員會(huì)聯(lián)合發(fā)起了“技術(shù)挑戰(zhàn)英雄榜”活動(dòng),通過(guò)張榜一系列技術(shù)難題,尋找那些富有激情、敢于挑戰(zhàn)的同學(xué),揭榜解題,攻克頑疾!
在難題榜中,有螞蟻內(nèi)部同學(xué)張榜反饋了 iOS 支付寶 App Top 1 的 iOS 16 鍵盤(pán) Crash(下文可簡(jiǎn)稱(chēng)“鍵盤(pán) Crash“),即下圖 1 的 issue 1。該 Crash 量級(jí)大且持續(xù)時(shí)間長(zhǎng),線下不好復(fù)現(xiàn)又不好排查,對(duì)線上業(yè)務(wù)影響很大,急需攻堅(jiān)。
本人基于對(duì)客戶端運(yùn)行時(shí)技術(shù)的濃厚興趣,揭榜領(lǐng)題,挑戰(zhàn)解決該 Crash。

原始信息
Crash 信息
Crash 日志關(guān)鍵信息如下:
提取 Crash 關(guān)鍵信息(后續(xù)分析基于該信息):
摘要信息:iPhone 12 Pro Max(Hardware Mode: iPhone13 4)、iOS 16.6、支付寶App 10.5.0.6000 版本、Crash 直接原因是讀內(nèi)存地址
0x2ab3106e0
異常(一般讀內(nèi)存報(bào)錯(cuò)為SEGV_MAPERR
,寫(xiě)內(nèi)存報(bào)錯(cuò)為EXC_BAD_ACCESS
)Crash 關(guān)鍵函數(shù):
0x00000001a5183a7c
?_objc_retain
、0x00000001aed4d4d4
?-[UIKeyboardTaskQueue performDeferredTaskIfIdle]
、0x00000001ae533148 -[UIKeyboardTaskQueue continueExecutionOnMainThread]
Thread State
:通用寄存器和浮點(diǎn)寄存器快照,用于查看運(yùn)行時(shí)變量值及更深入的邏輯推測(cè);Binary Images
:各 Image (運(yùn)行時(shí)可執(zhí)行指令的文件)二進(jìn)制布局在內(nèi)存起始位置及結(jié)束地址,起始位置可做基準(zhǔn),可用于計(jì)算 Crash 時(shí)的某指令地址相對(duì)于所屬 Image 起始地址的偏移。
量級(jí)及分布
鍵盤(pán) Crash 日 PV 一直處于大幾百次,持續(xù)至少半年多,從操作系統(tǒng)版本分布來(lái)看僅在?iOS 16?上出現(xiàn)(覆蓋所有機(jī)型)。


信息小結(jié)
從 Crash 日志棧頂?shù)?objc_retain?
函數(shù)關(guān)鍵字和量級(jí)分布情況來(lái)看,該 Crash 很可能是由 iOS 16 系統(tǒng)鍵盤(pán)控件的內(nèi)存管理異常導(dǎo)致。
分析推演
下文分析推演涉及的知識(shí)點(diǎn)或技能:
使用軟件:Sublime Text、Xcode 及自帶的
lldb
命令,包括b
、c
、bt
、frame select
、di
、image list
、p/x
、po
、x/1b
;匯編能力:Arm64 寄存器說(shuō)明?[1]?、Arm64 匯編指令集說(shuō)明?[2]?;
腳本工具:
otool
、自研腳本fetch_class_text_from_all.sh
;關(guān)鍵類(lèi):
UIKeyboardTaskQueue
?鍵盤(pán)核心類(lèi)、NSConditionLock
條件狀態(tài)鎖(具體使用見(jiàn)官方文檔?[3]?);依賴(lài)模塊:螞蟻?zhàn)匝械?code>DebugKit.framework(后續(xù)考慮對(duì)外輸出)調(diào)試模塊。
一、看現(xiàn)場(chǎng),從 Crash 點(diǎn)開(kāi)始
——計(jì)算 Crash 函數(shù)的偏移?
因 iOS 運(yùn)行時(shí)加載到內(nèi)存的 Image 的起始地址是動(dòng)態(tài)的(對(duì)應(yīng)?Binary Images
?列表中的起始地址),但某指令地址與所屬 Image 的起始地址的偏移是固定的,所以可根據(jù)該偏移來(lái)查看 Crash 時(shí)是哪條指令。
0x00000001a5183a7c _objc_retain
所屬的libobjc.A.dylib
的起始地址是0x00000001a5180000
,所以相對(duì)偏移 =?0x00000001a5183a7c
?-?0x00000001a5180000
?=?0x3a7c
0x00000001aed4d4d4 -[UIKeyboardTaskQueue performDeferredTaskIfIdle]
所屬的UIKitCore
的起始地址是0x00000001ae166000
,所以相對(duì)偏移 =0x00000001aed4d4d4
?-?0x00000001ae166000
?=?0xbe74d4
二、模擬現(xiàn)場(chǎng),尋找蛛絲馬跡
—— Xcode 設(shè)置斷點(diǎn)模擬現(xiàn)場(chǎng)
為模擬與 Crash 時(shí)一樣的現(xiàn)場(chǎng),需找一臺(tái)與 Crash 日志中一致的設(shè)備,即 iOS 16.6 的iPhone 12 Pro Max(Hardware Mode: iPhone13 4),只有這樣在下文中斷點(diǎn)時(shí)的函數(shù)棧以及各函數(shù)偏移對(duì)應(yīng)的指令才能與 Crash 日志中的完全對(duì)上。
將找到的設(shè)備與 Mac 連接并用 Xcode 啟動(dòng) App(可用下文附件中 Demo 關(guān)鍵代碼調(diào)試)。
從上述計(jì)算出的關(guān)鍵函數(shù)的偏移加上所屬 Image 的起始地址,模擬出 Crash 時(shí)運(yùn)行的函數(shù)棧,具體操作如下圖 4。

從圖 4 的第 11 步可知 Crash 的直接原因是objc_retain
的對(duì)象野指針了,導(dǎo)致讀取內(nèi)存異常而觸發(fā) Crash。

從圖 5 可知兩點(diǎn):
先后調(diào)用關(guān)系是
-[UIKeyboardTaskQueue performDeferredTaskIfIdle]
?->?-[UIKeyboardTaskQueue promoteDeferredTaskIfIdle]
(該函數(shù)在 Crash 函數(shù)棧中未出現(xiàn),所以只有模擬現(xiàn)場(chǎng)才能發(fā)現(xiàn))->?objc_retain
UIKeyboardTaskQueue
類(lèi)有個(gè)NSMutableArray
類(lèi)型的成員變量持有UIKeyboardTaskEntry
對(duì)象(從圖 5 中第 8 步的輸出得出),而 Crash 的直接原因就是獲取該數(shù)組index = 0
的UIKeyboardTaskEntry
對(duì)象后,執(zhí)行objc_retain
該對(duì)象 Crash ,所以異常的原因需要從對(duì)該數(shù)組的讀寫(xiě)排查。
小結(jié):UIKeyboardTaskQueue
類(lèi)的NSMutableArray
類(lèi)型的成員變量是關(guān)鍵數(shù)組(在實(shí)例對(duì)象偏移?0x20?
的位置),懷疑是多線程讀寫(xiě)該數(shù)組導(dǎo)致的。那么該成員變量名是啥,UIKeyboardTaskQueue
類(lèi)又是如何保證安全使用該數(shù)組的呢?
三、全面排查,收集更多信息
—— 獲取?UIKeyboardTaskQueue?
類(lèi)的全部信息?
借助螞蟻?zhàn)匝械?DebugKit.framework?
調(diào)試模塊可在運(yùn)行時(shí)導(dǎo)出?UIKeyboardTaskQueue?
類(lèi)所有的實(shí)例方法、類(lèi)方法、property?
和?ivars?
成員變量。

從圖 6 可知兩點(diǎn):
UIKeyboardTaskQueue?
的成員變量??_deferredTasks?
的類(lèi)型是?NSMutableArray?
(在實(shí)例對(duì)象起始地址偏移?0x20
?
的位置,從圖 6 中第 6 點(diǎn)可知)就是上述提到關(guān)鍵數(shù)組。野指針一般是有多線程讀寫(xiě)對(duì)象導(dǎo)致的,對(duì)_deferredTasks?
數(shù)組讀寫(xiě)時(shí)應(yīng)該是有鎖來(lái)控制的,該類(lèi)中類(lèi)型為?NSConditionLock?
的成員變量?_lock
(在實(shí)例對(duì)象偏移?0x10
?
的位置,從圖 6 中第 5 點(diǎn)可知)與?_deferredTasks?
是啥關(guān)系?發(fā)現(xiàn)該類(lèi)的?
property?
列表只有?executionContext?
和?activeOriginator
,不包含?deferredTasks?
和?lock
,所以對(duì)?_deferredTasks?
和?_lock
(類(lèi)的成員變量名一般是在?property?
名前多加前綴“_”)的所有讀寫(xiě)全在該類(lèi)中,不存在其他類(lèi)直接引用,也就是?Crash 相關(guān)的全部邏輯都在?UIKeyboardTaskQueue
?
類(lèi)中,所以破案的邊界也劃清楚了,圈定范圍。將?UIKeyboardTaskQueue?
類(lèi)的所有方法的匯編都導(dǎo)出來(lái)查看。

圖 7 中第 2 步涉及的?fetch_class_text_from_all.sh?
見(jiàn)下文附件中腳本源碼。?
小結(jié):通過(guò)分析圈定排查范圍在?UIKeyboardTaskQueue?
類(lèi)內(nèi),借助腳本可一鍵導(dǎo)出其所有方法的匯編,為進(jìn)一步研究?_deferredTasks?
和?_lock?
的關(guān)系做基礎(chǔ)。
四、理清關(guān)系,找到突破口
—— 研究?_deferredTasks?
和?_lock?
關(guān)系?
理清以下重要的兩個(gè)關(guān)系:
_deferredTasks?
角度:UIKeyboardTaskQueue?
類(lèi)對(duì)?_deferredTasks?
的多線程讀寫(xiě)是如何保證安全的,哪些方法有用到,與?_lock?
又是什么關(guān)系?_lock?
角度:UIKeyboardTaskQueue?
類(lèi)對(duì)?_lock?
又是如何使用的,哪些方法有用到,加鎖和解鎖是否配對(duì)?
deferredTasks 角度
圖 7 第 2 步導(dǎo)出的UIKeyboardTaskQueue
的所有方法實(shí)現(xiàn)都是匯編的,為理清對(duì)_deferredTasks
對(duì)象的所有讀寫(xiě)有哪些指令,分別在哪些方法中(UIKeyboardTaskQueue
實(shí)例對(duì)象偏移?0x20
?
的位置,該地址下存儲(chǔ)的 8 字節(jié)地址才是_deferredTasks
對(duì)象),需要在文件中全文搜索正則表達(dá)式x.{1,2}, #0x20
篩選出所有引用_deferredTasks
的指令以及所屬方法,操作如下圖 8(Sublime Text)。

在匯編層面,面向?qū)ο笳Z(yǔ)言中方法的第一個(gè)入?yún)⑹?code>self(C++ 稱(chēng)this
,Objective-C 稱(chēng)self
),存放在x0
寄存器上,所以?xún)H篩選出偏移是從方法入?yún)r(shí)的x0
或x0
備份(如mov x19, x0
,x19
就是備份了x0
的值)開(kāi)始的,最后整理出所有UIKeyboardTaskQueue
對(duì)_deferredTasks
有引用并讀寫(xiě)的指令及所屬方法,如下。
注:
一般面向過(guò)程語(yǔ)言的代碼塊稱(chēng)為函數(shù),而面向?qū)ο笳Z(yǔ)言的代碼塊稱(chēng)為方法,為避免文章的混用造成困擾,這里特別說(shuō)明。
下列部分的“讀”或“寫(xiě)”是指獲取到
_deferredTasks
對(duì)象后,對(duì)該對(duì)象是讀操作還是寫(xiě)操作。
讀_deferredTasks
的方法有 6 個(gè):
-[UIKeyboardTaskQueue isEmpty]
-[UIKeyboardTaskQueue finishExecution]
-[UIKeyboardTaskQueue promoteDeferredTaskIfIdle]
-[UIKeyboardTaskQueue continueExecutionOnMainThread]
-[UIKeyboardTaskQueue waitUntilAllTasksAreFinished]
-[UIKeyboardTaskQueue init]
寫(xiě)_deferredTasks的方法有 4 個(gè):
-[UIKeyboardTaskQueue promoteDeferredTaskIfIdle]
-[UIKeyboardTaskQueue addDeferredTask:]
-[UIKeyboardTaskQueue init]
-[UIKeyboardTaskQueue .cxx_destruct]
_lock 角度
在文件中全文搜索正則表達(dá)式x.{1,2}, #0x10
篩選出所有引用_lock
的指令以及所屬方法,操作類(lèi)似上述的_deferredTasks
;從上可知,UIKeyboardTaskQueue
類(lèi)對(duì)_lock
的使用封裝成 4 個(gè)方法(忽略init
創(chuàng)建和.cxx_destruct
銷(xiāo)毀的兩個(gè)方法,該兩方法不會(huì)有并發(fā)問(wèn)題),也就是方法使用_lock
必定會(huì)調(diào)用這 4 個(gè)方法。
解鎖方法有 1 個(gè):
-[UIKeyboardTaskQueue unlock]
加鎖方法有 3 個(gè):
-[UIKeyboardTaskQueue lock]
-[UIKeyboardTaskQueue lockWhenReadyForMainThread]
-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]
串聯(lián)關(guān)系,發(fā)現(xiàn) Bug
串聯(lián)上述_deferredTasks
和_lock
兩個(gè)角度的方法調(diào)用(忽略init
創(chuàng)建和.cxx_destruct
銷(xiāo)毀的兩個(gè)方法),從原匯編的關(guān)鍵方法中列出簡(jiǎn)版的關(guān)系描述,如下圖 9。

為方便理清鎖的對(duì)應(yīng)關(guān)系,圖 9 中用紅色表示加鎖,綠色表示解鎖,從中可知:
對(duì)
_deferredTasks
的關(guān)鍵讀寫(xiě)的方法內(nèi)是有 1 個(gè)加鎖和 1 個(gè)解鎖對(duì)應(yīng)的,預(yù)期是多線程下保護(hù)讀寫(xiě)的安全性;即使不讀寫(xiě)
_deferredTasks
的方法內(nèi)上也是有 1 個(gè)加鎖和 1 個(gè)解鎖對(duì)應(yīng)的,用于多線程下保護(hù)其他成員變量的讀寫(xiě)安全性;發(fā)現(xiàn)問(wèn)題,有 Bug:
-[UIKeyboardTaskQueue continueExecutionOnMainThread]
方法內(nèi)的0000000189466ff8 bl _objc_msgSend$tryLockWhenReadyForMainThread
這行指令執(zhí)行是返回BOOL
類(lèi)型的,即加鎖成功為YES
,加鎖失敗為NO
。(參看圖 6 中-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]
的方法簽名為typeEncoding=B16@0:8
,即返回為BOOL
類(lèi)型);如該行指令嘗試加鎖但失敗了,不會(huì)直接return
,還會(huì)繼續(xù)執(zhí)行紅色框內(nèi)的指令并做解鎖操作,會(huì)導(dǎo)致多線程下UIKeyboardTaskQueue
類(lèi)的加鎖和解鎖的功能不配對(duì),也就存在鎖失效的情況。
小結(jié):-[UIKeyboardTaskQueue continueExecutionOnMainThread]
方法內(nèi)有 Bug,導(dǎo)致存在鎖失效的情況,猜測(cè)在多線程下并發(fā)讀寫(xiě)_deferredTasks
時(shí)就會(huì)偶現(xiàn) Crash。
五、重新推演,確定根因
推演圖

按時(shí)間軸重新推演鍵盤(pán) Crash 過(guò)程:
T0:
Thread A
加鎖成功后執(zhí)行指令bl _objc_msgSend$addObject:
添加對(duì)象A
到數(shù)組_deferredTasks
。同時(shí),因?yàn)?code>Main Thread執(zhí)行指令bl _objc_msgSend$tryLockWhenReadyForMainThread
失敗后繼續(xù)執(zhí)行指令bl _objc_msgSend$unlock
,使得Thread B
也加鎖成功后執(zhí)行指令bl _objc_msgSend$addObject:
添加對(duì)象B
到數(shù)組_deferredTasks
,導(dǎo)致出現(xiàn)多線程同時(shí)寫(xiě)入數(shù)組_deferredTasks
的異常情況。T1:
Thread A
解鎖后,Main Thread
在-[UIKeyboardTaskQueue performDeferredTaskIfIdle]
方法內(nèi)加鎖成功后,在-[UIKeyboardTaskQueue promoteDeferredTaskIfIdle]
方法內(nèi)執(zhí)行指令bl _objc_msgSend$objectAtIndex:
后獲取數(shù)組inde = 0
的對(duì)象地址時(shí),因多線程寫(xiě)入導(dǎo)致該對(duì)象地址被異常破壞而出現(xiàn)野指針(野指針存入x0
寄存器)。T2:
Main Thread
繼續(xù)執(zhí)行下一條指令bl _objc_claimAutoreleasedReturnValue
會(huì)間接觸發(fā)了_objc_retain
并透?jìng)?code>x0寄存器的值,最終在該函數(shù)內(nèi)執(zhí)行指令ldr x17, [x17, #0x20]
時(shí)?Crash?了。
注:不同語(yǔ)言的編譯器對(duì)應(yīng)的符號(hào)名的生成規(guī)則是不同的,C 語(yǔ)言只是在原函數(shù)名前加一個(gè)前綴“_”,如objc_retain(A)
,編譯后符號(hào)名是_objc_retain
,而 C++ 語(yǔ)言會(huì)根據(jù)方法名加上參數(shù)名生成的符號(hào)名,如__ZNSt3__16vectorIdNS_9allocatorIdEEEixB6v15006Em
。
模擬 Crash
按推演的邏輯用本地 Xcode 重新起個(gè) Demo 驗(yàn)證下(可用下文附件中 Demo 關(guān)鍵代碼),通過(guò)調(diào)用[self test_crash]
可模擬出 tryLock 失敗時(shí)導(dǎo)致的 Crash(如調(diào)用[self test_ok]
就不會(huì)出現(xiàn) Crash),現(xiàn)場(chǎng)如下。

從 Xcode 的 Console 控制臺(tái)的日志中可以看到出現(xiàn)多線程并發(fā)添加到_deferredTasks
數(shù)組的情況,在后續(xù)removeEntry_crash
方法內(nèi)出現(xiàn)了objc_retain
野指針對(duì)象導(dǎo)致的 Crash,與上述推演的邏輯相符。
對(duì)比不同 iOS 版本

通過(guò)對(duì)比發(fā)現(xiàn)僅 iOS 16 上有問(wèn)題,iOS 15 或 iOS 17 上 tryLock 失敗后都會(huì)立即return
的,也就是為什么 Crash 僅出現(xiàn)在 iOS 16 的原因。從中我們可以看出在 iOS 17 上蘋(píng)果技術(shù)同學(xué)也發(fā)現(xiàn)了該 Bug 并做了修復(fù)。
給蘋(píng)果反饋 Bug
該問(wèn)題已提交至蘋(píng)果“反饋助理”(圖 13),但截至目前未得到其官方的 iOS 16 上的解決方案。

六、總結(jié)根因
通過(guò)上述分析推演,iOS 16?鍵盤(pán)?Crash 的根因已查明,即-[UIKeyboardTaskQueue continueExecutionOnMainThread]
方法內(nèi)執(zhí)行-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]
嘗試加鎖失敗后,不return
繼續(xù)向下執(zhí)行讀寫(xiě)不安全內(nèi)存以及解鎖,導(dǎo)致存在鎖失效的情況,使得UIKeyboardTaskQueue
成員變量_deferredTasks
數(shù)組在多線程下出現(xiàn)并發(fā)添加UIKeyboardTaskEntry
實(shí)例而引起野指針,導(dǎo)致最終 Crash。
注:該根因除了導(dǎo)致數(shù)組讀寫(xiě)異常而 Crash,也可能導(dǎo)致其他變量的狀態(tài)不一致性,只是不一定表現(xiàn)為 Crash 而已,建議用本文方案修復(fù)。
解決方案(App 內(nèi)置補(bǔ)丁源碼)
明確根因后,解決方案就比較明確了,寫(xiě)一個(gè) App 內(nèi)置補(bǔ)丁代碼使得-[UIKeyboardTaskQueue continueExecutionOnMainThread]
方法內(nèi)執(zhí)行-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]
嘗試加鎖失敗后,正常return
即可。補(bǔ)丁方案有兩個(gè):
重寫(xiě)
-[UIKeyboardTaskQueue continueExecutionOnMainThread]
方法。在原匯編基礎(chǔ)上新增一條指令,即在bl _objc_msgSend$tryLockWhenReadyForMainThread
后添加一條匯編指令cbz w0, return_label
(return_label
對(duì)應(yīng)源碼return
對(duì)應(yīng)的匯編指令地址),如失敗則return
。但該方案涉及的原匯編指令較多,有 95 條匯編指令(見(jiàn)下文附件中 iOS 系統(tǒng)匯編),容易踩坑。重寫(xiě)
-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]
方法。在該方法內(nèi)如加鎖失敗則模擬兩次return
,回到-[UIKeyboardTaskQueue continueExecutionOnMainThread]
的上一個(gè)函數(shù)棧,改造的匯編指令較少,安全性較好,也確認(rèn)了除-[UIKeyboardTaskQueue continueExecutionOnMainThread]
調(diào)用外,無(wú)其他方法調(diào)用-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]
。
最終,支付寶 App 基于穩(wěn)定性的考慮,采用第 2 種補(bǔ)丁方案修復(fù)鍵盤(pán) Crash。
補(bǔ)丁原理

在-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]
實(shí)現(xiàn)以下邏輯:
如加鎖成功,則
return
?1 次,返回到-[UIKeyboardTaskQueue continueExecutionOnMainThread]
方法的下一條指令繼續(xù)執(zhí)行;如加鎖失敗,則模擬
return
?2 次,返回到-[UIKeyboardTaskQueue continueExecutionOnMainThread]
的函數(shù)棧的上一層函數(shù)的地址繼續(xù)執(zhí)行,也就是模擬了從-[UIKeyboardTaskQueue continueExecutionOnMainThread]
中執(zhí)行return
操作。
源碼return
語(yǔ)句,對(duì)應(yīng)匯編的 4 步:
恢復(fù)
fp
和lr
寄存器。fp
(也稱(chēng)x29
)記錄當(dāng)前幀的內(nèi)存地址,lr
(也稱(chēng)x30
)記錄從當(dāng)前函數(shù)返回時(shí)跳轉(zhuǎn)到哪個(gè)地址繼續(xù)執(zhí)行。運(yùn)行時(shí)就是通過(guò)fp
和lr
寄存器,輸出線程的函數(shù)棧的。如 Crash 函數(shù)棧,或從lldb
的bt
輸出的函數(shù)棧;恢復(fù)
callee-saved
寄存器。即x19-x28
的寄存器,try-catch
的實(shí)現(xiàn)就涉及該類(lèi)寄存器,一般按需執(zhí)行;恢復(fù)
sp
寄存器。sp
記錄當(dāng)前幀的棧頂?shù)刂?,,?dāng)前函數(shù)的局部變量所在的內(nèi)存地址就在(fp
,?sp
]之間;執(zhí)行
ret
指令。執(zhí)行ret
指令后,pc
就指向lr
寄存器的值,然后繼續(xù)執(zhí)行;
本文補(bǔ)丁方案的原理中,tryLock 失敗時(shí)就是通過(guò):恢復(fù)fp
和lr
寄存器 + 恢復(fù)callee-saved
寄存器 + 恢復(fù)sp
寄存器 + 再次恢復(fù)fp
和lr
寄存器 + 再次恢復(fù)callee-saved
寄存器 + 再次恢復(fù)sp
寄存器 +?ret
指令?來(lái)模擬在-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]
方法內(nèi)return
?2 次直接返回到-[UIKeyboardTaskQueue continueExecutionOnMainThread]
的函數(shù)棧的上一層函數(shù)的。
補(bǔ)丁實(shí)現(xiàn)
有兩部分組成:
重寫(xiě)方法:對(duì)應(yīng) fix_UIKeyboardTaskQueue.S 文件;
Hook 入口:對(duì)應(yīng) fix_UIKeyboardTaskQueue.m 文件;
重寫(xiě)方法
重寫(xiě)-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]
方法實(shí)現(xiàn),對(duì)應(yīng)下文附件中補(bǔ)丁源碼的 fix_UIKeyboardTaskQueue.S 文件。

Hook 入口
借助+ (void)load
方法在 App 啟動(dòng)時(shí)執(zhí)行的特點(diǎn)實(shí)現(xiàn)對(duì)-[UIKeyboardTaskQueue tryLockWhenReadyForMainThread]
方法的 Hook,僅在 iOS 16 的 Arm64 架構(gòu)上生效,對(duì)應(yīng)下文附件中補(bǔ)丁源碼的 fix_UIKeyboardTaskQueue.m 文件。

方案效果
于 2023.8.25 在支付寶 App 近期版本 10.5.16.6000 上全量開(kāi)啟解決方案的開(kāi)關(guān)后,該版本上的?Crash 日 PV 已經(jīng)降到 0 了。

同時(shí),支付寶 App 的全量版本(包括所有歷史版本)的鍵盤(pán)?Crash 日 PV?下降了近 90%,隨著更多用戶升級(jí)到支付寶 App 最新版本,預(yù)計(jì)會(huì)降到個(gè)位數(shù)。

最終該方案由驗(yàn)收人確認(rèn)有效,鍵盤(pán) Crash 已解決,揭榜挑戰(zhàn)成功,附上一張?zhí)魬?zhàn)成功捷報(bào)圖收個(gè)尾。

附件
1、補(bǔ)丁源碼
補(bǔ)丁源碼包括兩部分:fix_UIKeyboardTaskQueue.S 和 fix_UIKeyboardTaskQueue.m。使用時(shí)將該兩文件直接內(nèi)置在 App 中即可,也可在 App 啟動(dòng)時(shí)加開(kāi)關(guān)控制 Hook 入口的時(shí)機(jī)。
2、Demo 關(guān)鍵源碼
3、腳本源碼
4、iOS 系統(tǒng)匯編(關(guān)鍵方法)
將 iOS 16.6 的 iPhone 12 Pro Max(Hardware Mode: iPhone13 4)設(shè)備連接到 Xcode 后,按如下操作可獲取到 UIKeyboardTaskQueue 類(lèi)的實(shí)現(xiàn)匯編,即UIKitCore_20G75_arm64e_TEXT.txt 文件。
???? 相關(guān)鏈接
[1] Arm64 寄存器說(shuō)明:https://developer.arm.com/documentation/den0024/a/The-ABI-for-ARM-64-bit-Architecture/Register-use-in-the-AArch64-Procedure-Call-Standard/Parameters-in-general-purpose-registers
[2] Arm64 匯編指令集說(shuō)明:https://documentation-service.arm.com/static/6023d5512cb3723f20208db2
[3] NSConditionLock 條件狀態(tài)鎖:https://developer.apple.com/documentation/foundation/nsconditionlock/