野指針是指指向一個已刪除的對象或未申請訪問受限內(nèi)存區(qū)域的指針摊鸡。本文說的Obj-C野指針先改,說的是Obj-C對象釋放之后指針未置空,導(dǎo)致的野指針(Obj-C里面一般不會出現(xiàn)未初始化對象的常識性錯誤)际插。
既然是訪問已經(jīng)釋放的對象為什么不是必現(xiàn)Crash呢哥艇?
因為dealloc執(zhí)行后只是告訴系統(tǒng),這片內(nèi)存我不用了胁黑,而系統(tǒng)并沒有就讓這片內(nèi)存不能訪問废封。
現(xiàn)實大概是下面幾種可能的情況:
1.對象釋放后內(nèi)存沒被改動過,原來的內(nèi)存保存完好丧蘸,可能不Crash或者出現(xiàn)邏輯錯誤(隨機(jī)Crash)漂洋。
2.對象釋放后內(nèi)存沒被改動過,但是它自己析構(gòu)的時候已經(jīng)刪掉某些必要的東西力喷,可能不Crash刽漂、Crash在訪問依賴的對象比如類成員上、出現(xiàn)邏輯錯誤(隨機(jī)Crash)冗懦。
3.對象釋放后內(nèi)存被改動過爽冕,寫上了不可訪問的數(shù)據(jù),直接就出錯了很可能Crash在objc_msgSend上面(必現(xiàn)Crash披蕉,常見)颈畸。
4.對象釋放后內(nèi)存被改動過,寫上了可以訪問的數(shù)據(jù)没讲,可能不Crash眯娱、出現(xiàn)邏輯錯誤、間接訪問到不可訪問的數(shù)據(jù)(隨機(jī)Crash)爬凑。
5.對象釋放后內(nèi)存被改動過徙缴,寫上了可以訪問的數(shù)據(jù),但是再次訪問的時候執(zhí)行的代碼把別的數(shù)據(jù)寫壞了,遇到這種Crash只能哭了(隨機(jī)Crash于样,難度大疏叨,概率低)!穿剖!
6.對象釋放后再次release(幾乎是必現(xiàn)Crash蚤蔓,但也有例外,很常見)糊余。
仔細(xì)看看上面的關(guān)鍵路徑只有出現(xiàn)被隨機(jī)填入的數(shù)據(jù)是不可訪問的時候才會必現(xiàn)Crash秀又。
所以把這一隨機(jī)的過程變成不隨機(jī)的過程。對象釋放后在內(nèi)存上填上不可訪問的數(shù)據(jù)贬芥,其實這種技術(shù)其實一直都有吐辙,xcode的Enable Scribble就是這個作用。
但是有個問題:這個方法不能放在測試那邊用蘸劈!因為總不能讓測試裝了xcode來測試吧昏苏?
于是我們自己動手實現(xiàn)一個,這個過程中我們要解決幾個問題:
1.怎么在內(nèi)存釋放后填上不可訪問的數(shù)據(jù)昵时?
內(nèi)存釋放很可能不在我們的代碼中捷雕。為此我們需要hook對象釋放的接口,內(nèi)存時候之后馬上執(zhí)行我們的破壞工作壹甥。
2.我們要重寫對象釋放的接口救巷,重寫哪個呢?
NSObject的dealloc句柠、runtime的 object_dispose浦译,C的free應(yīng)該都是可以,但是各有優(yōu)點溯职,我選擇的是覆蓋面最廣的free精盅,free是C的函數(shù),重寫了它之后還可以順帶解決一部分C的野指針問題谜酒。
3.怎么重寫叹俏?
重寫C的接口場景的有兩種:
a.替換系統(tǒng)動態(tài)庫
b.hook
替換動態(tài)庫太麻煩,還不知道行不行得通僻族;hook我們就找現(xiàn)成的fishhook粘驰,github里面找的,但現(xiàn)成的代碼需要防止代碼沖突述么。
4.填充的不可訪問的數(shù)據(jù)的長度怎么確定蝌数?
獲取內(nèi)存長度的接口不在標(biāo)準(zhǔn)庫中,好在在Mac和iOS中可以用malloc_size就可以度秘。
5.填什么顶伞? ? ? ? ? ? ??和xcode一樣,填0x55。
上hook后的free代碼:
[size=0.85em]void safe_free(void* p){? ? size_t memSiziee=malloc_size(p);? ? memset(p, 0x55, memSiziee);? ? orig_free(p);? ? return;}
測試一下唆貌,出現(xiàn)了和Enable Scribble一樣的Crash滑潘!
以上就是一種在內(nèi)存釋放后填充0x55使野指針后數(shù)據(jù)不能訪問,從而使某些野指針從不必現(xiàn)Crash變成了必現(xiàn);
其實這就是上一篇文中留下了幾個問題之一锨咙,如果我們填充0x55后內(nèi)存又被別的內(nèi)存覆蓋了众羡,最終還是會出現(xiàn)隨機(jī)Crash。而在真實環(huán)境中蓖租,這種情況是非常常見的。
我們再梳理一下這個過程:
1.我們在即將要釋放的填了0x55羊壹,之后調(diào)用了free真正釋放蓖宦,內(nèi)存被系統(tǒng)回收。
2.這個時候系統(tǒng)隨時可能把這片內(nèi)存給別的代碼使用油猫,也就是說我們的0x55被再次寫上隨機(jī)的數(shù)據(jù)(在這里再強(qiáng)調(diào)一下稠茂,訪問野指針是不會Crash的,只有野指針指向的地址被寫上了有問題的數(shù)據(jù)才會引發(fā)Crash)情妖。
3.假如釋放的內(nèi)存上又填上了另一個對象的指針睬关,而那個對象也有同樣的一個方法,那很可能只是邏輯上有問題毡证,并不會直接Crash电爹,甚至悄無聲息地像什么事情都沒發(fā)生一樣。(這個地方可能會發(fā)生多種情況料睛,可以參考之上一篇文章中的圖)
沒有發(fā)生Crash可不是好事丐箩,因為這種情況如果后續(xù)再Crash,問題就非常難查恤煞,因為你看到的Crash棧很可能和出錯的代碼完全沒有關(guān)聯(lián)屎勘。既然這個問題這么棘手,最好還是和之前一樣居扒,讓這個Crash提前暴露概漱。
首先,我們要解決的問題就是怎么讓系統(tǒng)不再往這片釋放的內(nèi)存上亂放東西喜喂。
要控制底層內(nèi)存管理機(jī)制讓它不使用這些內(nèi)存可能很困難瓤摧。但是,我們變通一下夜惭,簡單粗暴地姻灶,我們干脆就不釋放這片內(nèi)存了。也就是當(dāng)free被調(diào)用的時候我們不真的調(diào)用free诈茧,而是自己保留著內(nèi)存产喉,這樣系統(tǒng)不知道這片內(nèi)存已經(jīng)不需要用了,自然就不會被再次寫上別的數(shù)據(jù)
為了防止系統(tǒng)內(nèi)存過快耗盡,還需要額外多做幾件事:
1.自己保留的內(nèi)存大于一定值的時候就釋放一部分曾沈,防止被系統(tǒng)殺死这嚣。
2.系統(tǒng)內(nèi)存警告的時候,也要釋放一部分內(nèi)存塞俱。
在safe_free以及它調(diào)用的函數(shù)里面盡量不要再用帶鎖的函數(shù)姐帚,不然很容易導(dǎo)致死鎖。
加上這個代碼之后APP的內(nèi)存占用會增大不少障涯,拿過來測試可以罐旗,但萬萬不能放在正式的發(fā)布版本中。
關(guān)于性能問題唯蝶,我的機(jī)器是iPhone5九秀,跑在App里面運行,還算流暢(不同App性能可能會有些不同)粘我。
可能由于鎖的存在鼓蜒,會使cpu線程切換變得頻繁,這樣多線程的問題Crash率也可能會提升(最近遇到一個多線程引起的Crash很難重現(xiàn)征字,但我加了這個代碼后就變成了必現(xiàn)Crash)