在大多數(shù)iOS應(yīng)用開(kāi)發(fā)過(guò)程中, 循環(huán)引用一直都是最常見(jiàn)的iOS開(kāi)發(fā)問(wèn)題之一踢涌。通常情況下, 最常見(jiàn)的循環(huán)引用問(wèn)題就是在Block回調(diào)中的self指針的不當(dāng)使用了。在這種最常見(jiàn)的場(chǎng)景中, 對(duì)象self本身使用的Block被self對(duì)象本身持有(臨時(shí)變量block是不會(huì)形成循環(huán)引用的), 而在Block回調(diào)中又使用了self導(dǎo)致形成了一個(gè)循環(huán)引用。
self.myBlock = ^{
[self doSomething];
};
+-----------+ +-----------+
| self | ref | Block |
---> | | --------> | |
| retain 2 | <-------- | retain 1 |
| | ref | |
+-----------+ +-----------+
針對(duì)上述最常見(jiàn)的循環(huán)引用的場(chǎng)景, 在市面上最常用的幾款循環(huán)引用檢查工具都不能特別有效的檢測(cè)出是否真正的產(chǎn)生了循環(huán)引用。筆者針對(duì)這種場(chǎng)景突然腦洞大開(kāi), 設(shè)想是否能夠通過(guò)文本掃描的方式提前知道哪些block使用場(chǎng)景可能會(huì)產(chǎn)生潛在的循環(huán)引用問(wèn)題。為了支持自己的大腦洞想法, 筆者嘗試用python書(shū)寫(xiě)一個(gè)文本掃描器來(lái)提前預(yù)知潛在Self Retain Block循環(huán)引用風(fēng)險(xiǎn)的代碼旧巾。哈哈, 至此, 一個(gè)無(wú)聊又奇怪的檢測(cè)Block的小工具RiskBlockScanner就誕生了。
想法起源
筆者這個(gè)奇怪的Block想法的起源是因?yàn)楣P者在工作中參與過(guò)的幾個(gè)大型iOS項(xiàng)目(大于30人協(xié)作開(kāi)發(fā))中都涉及到很多使用Block的場(chǎng)景, 而且經(jīng)常會(huì)發(fā)現(xiàn)代碼中有些潛在或者確定的Block引用風(fēng)險(xiǎn), 并且這些引用的風(fēng)險(xiǎn)多數(shù)和self
指針和Block
的組合使用相關(guān)聯(lián)忍些。同時(shí), XCode自帶的Warning只能報(bào)出比較簡(jiǎn)單的循環(huán)引用警告, 并且通過(guò)XCode自帶的靜態(tài)分析有時(shí)候也難以發(fā)現(xiàn)循環(huán)引用的出處鲁猩。
雖然這個(gè)奇怪的想法是突然萌生的, 但是刺激因素有如下:
- 上百萬(wàn)行的代碼量太多, 無(wú)法人肉檢查(最主要用途)
- ReactiveCocoa使用中大量涉及Self Retain Cycle的場(chǎng)景
- 大型項(xiàng)目開(kāi)發(fā)參差不齊&參與人數(shù)過(guò)多, 低級(jí)錯(cuò)誤無(wú)法100%避免
- 循環(huán)引用排查困難
- XCode自帶循環(huán)引用排查功能不太完善
實(shí)現(xiàn)方案
有奇怪的想法第一件事情當(dāng)然是驗(yàn)證想法的可行性。在不追求性價(jià)比的前提下, 驗(yàn)證想法可行性最愚蠢也是最有效的方法就是把想法實(shí)現(xiàn)出來(lái)罢坝,那么RiskBlockScanner就這么誕生了廓握。
市面上大多數(shù)的Block循環(huán)檢查基本都是要編譯鏈接的(例如XCode自帶的Anaylse), 有些甚至是在操作過(guò)程中分析內(nèi)存是否釋放(例如facebook開(kāi)源的FBRetainCycleDetector), 很少有檢測(cè)工具針對(duì)編寫(xiě)代碼過(guò)程中快速發(fā)現(xiàn)風(fēng)險(xiǎn)的方面進(jìn)行設(shè)計(jì)。
我的這個(gè)奇怪的思想是通過(guò)正則表達(dá)式去匹配發(fā)現(xiàn)所有的Block使用起始行嘁酿、該Block所在的方法起始&結(jié)束行隙券。
RiskBlockScanner的大致思路如下:
- 掃描目錄提取代碼文件(.m格式)
- 提取每一行代碼行
- 根據(jù)每一行代碼行去根據(jù)正則表達(dá)式匹配出方法起始行(func line)、block起始行(block line)闹司、潛在多行block起始行(potential block line)娱仔、block結(jié)束行和weak執(zhí)行行(weak line)
- 根據(jù)"}"和";"組合判斷和潛在多行block起始行過(guò)濾不是block的行, 保留真正block行
- 過(guò)濾固定匹配關(guān)鍵字的block行(例如masonry等常見(jiàn)不持有block的場(chǎng)景)
- 判斷weak執(zhí)行行(weak line)是否包含在方法起始行(func line)和block起始行(block line)之間
- 根據(jù)第7步的判斷結(jié)果來(lái)決定是否該Block存在循環(huán)引用風(fēng)險(xiǎn)
上圖表述的步驟均是根據(jù)存儲(chǔ)數(shù)據(jù)來(lái)維護(hù)的, 具體的邏輯實(shí)現(xiàn)因?yàn)楸容^簡(jiǎn)單本文就不贅述了。如果大家有感興趣的話請(qǐng)自行訪問(wèn)Github地址進(jìn)行代碼閱讀~
通過(guò)RiskBlockScanner可以掃描出大量的Block Self-Retain的風(fēng)險(xiǎn)代碼行, 使用方法相對(duì)簡(jiǎn)單.(PS: 對(duì)代碼量比較小或者不怎么使用Block的工程幾乎沒(méi)有作用)
下圖為簡(jiǎn)單的使用示例:
優(yōu)化設(shè)想
- 通過(guò)不斷的積累實(shí)驗(yàn), 過(guò)濾更多的白名單方法
- 風(fēng)險(xiǎn)檢測(cè)包含但不限于self的檢測(cè)
- 開(kāi)發(fā)一個(gè)XCode插件工具(看筆者是否夠勤奮)
- 嘗試支持檢查Swift語(yǔ)法
總結(jié)
RiskBlockScanner是一個(gè)檢測(cè)簡(jiǎn)單的Block循環(huán)引用風(fēng)險(xiǎn)的靈感實(shí)現(xiàn)游桩。鑒于該工具是基于文本掃描實(shí)現(xiàn)的, 并且沒(méi)有檢測(cè)Block是否被使用對(duì)象真正持有(不持有則不產(chǎn)生循環(huán)引用), 所以它并不能真正意義上的檢查出Block循環(huán)引用, 只能檢查出所有潛在的風(fēng)險(xiǎn)點(diǎn)方便開(kāi)發(fā)者排查定位牲迫。
該工具純屬筆者好奇實(shí)現(xiàn), 可能有不少童鞋會(huì)覺(jué)得這個(gè)很無(wú)聊, 其實(shí)筆者覺(jué)得這個(gè)實(shí)際意義也不是特別大哈, 但是有總比沒(méi)有好。至少該工具可以在開(kāi)發(fā)者編寫(xiě)代碼過(guò)程中快速分析定位block引用風(fēng)險(xiǎn)的代碼位置, 能夠幫助開(kāi)發(fā)者高效的定位Block使用風(fēng)險(xiǎn)借卧。
PS: 筆者水平有限, 如果文章有錯(cuò)誤之處, 請(qǐng)大家一定要指出, 防止誤導(dǎo)大家哈~~