XCode 8有非常多的更新,其中的 memory graph 對(duì)于內(nèi)存分析非常有用,十分強(qiáng)大,可以方便的查看對(duì)象引用關(guān)系以及偵測(cè)內(nèi)存泄漏潘明,近期在使用memory graph進(jìn)行調(diào)試的過(guò)程中發(fā)現(xiàn)了一些奇怪的現(xiàn)象,這里使用一個(gè)簡(jiǎn)單的demo工程來(lái)說(shuō)明這個(gè)內(nèi)存泄漏的原因和解決方法秕噪。
@0 內(nèi)存孤島
如下圖是在調(diào)試過(guò)程中發(fā)現(xiàn)的一處內(nèi)存泄漏:從圖上看钳降,這里被檢測(cè)到有內(nèi)存泄漏,但是這個(gè)對(duì)象沒(méi)有被任何其它對(duì)象引用腌巾,也沒(méi)有引用其它對(duì)象遂填,一般在A(yíng)RC代碼里面常見(jiàn)的內(nèi)存泄漏一般都是由于retain cycle引起的,這里看起來(lái)沒(méi)有任何循環(huán)持有澈蝙,那到底是怎么回事兒呢吓坚,從右邊的backtrace可以看到這個(gè)對(duì)象創(chuàng)建時(shí)候的調(diào)用堆棧(如果你看不到的話(huà),需要在scheme的Diagnostics下面打開(kāi)Malloc Stack的開(kāi)關(guān))灯荧,從調(diào)用棧上可以輕松找到這個(gè)對(duì)象創(chuàng)建的代碼礁击。
看起來(lái)這里應(yīng)該很容易解決了,畢竟代碼已經(jīng)找到了《吡可實(shí)際上不是那么回事兒链烈,下面來(lái)看一下這里的代碼:
class DerrivedView : LKSuperView {
let config = Config() // <<<--- 報(bào)告就是這里創(chuàng)建的對(duì)象沒(méi)有釋放
override init() {
super.init()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = config.color
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// 在某個(gè)地方用到了這個(gè)view
{
let derrivedView = DerrivedView()
}
@1 代碼追查
既然代碼已經(jīng)定位到了,為什么還不能解決呢更耻,不過(guò)這一句的語(yǔ)法實(shí)在是簡(jiǎn)單到不能再簡(jiǎn)單了测垛,完全無(wú)法修改捏膨。仔細(xì)檢查代碼秧均,沒(méi)有其它的地方引用這個(gè)對(duì)象,完全不會(huì)存在循環(huán)引用的問(wèn)題号涯,難道是swift最后沒(méi)有釋放這個(gè)對(duì)象目胡,這里來(lái)看看LKSuperView有沒(méi)有問(wèn)題:
@interface LKSuperView : UIView
- (instancetype)init;
//...
@end
@implementation LKSuperView
- (instancetype)init {
return [self initWithFrame:CGRectMake(0, 0, 100, 100)];
}
@end
這里是一個(gè)OC類(lèi),重寫(xiě)了init方法链快,給了一個(gè)默認(rèn)的frame誉己,看起來(lái)也沒(méi)什么問(wèn)題。
會(huì)不會(huì)是誤報(bào)呢域蜗,給Config類(lèi)加一些log看看
class Config {
let color : UIColor
init() {
print(#function)
color = UIColor.red
}
deinit {
print(#function)
}
}
//output
//init()
//init()
//deinit
從log輸出看確實(shí)調(diào)用了兩次構(gòu)造和一次析構(gòu)巨双,但是DerrivedView也只有一個(gè)對(duì)象,為什么會(huì)調(diào)用兩次Config的init呢霉祸,打個(gè)斷點(diǎn)看一下筑累,好像有些新發(fā)現(xiàn):
從兩次斷點(diǎn)來(lái)看,一次來(lái)自于DerrivedView::init丝蹭,一次來(lái)自于DerrivedView::initWithFrame慢宗,回頭看看LKSuperView::init,確實(shí)調(diào)用了initWithFrame奔穿,最后又回到了DerrivedView::initWithFrame镜沽,兩次調(diào)用在OC里面看起來(lái)是比較正常的寫(xiě)法了,在swift里面難道會(huì)初始化兩遍成員變量嗎贱田,是不是這樣的呢缅茉,接下來(lái)打開(kāi)調(diào)試時(shí)顯示匯編代碼的功能,一看究竟
果然男摧,在init和initWithFrame的匯編代碼里前面都看到同樣的匯編代碼蔬墩,天書(shū)一樣的匯編可以先忽略,因?yàn)榉磪R編代碼的注釋也很強(qiáng)大彩倚,看看框起來(lái)的代碼注釋?zhuān)谝恍惺钦{(diào)用Config::init創(chuàng)建一個(gè)Config對(duì)象筹我,第二行就是把生成的Config對(duì)象地址直接賦值到DerrivedView.config,這里不存在getter和setter帆离,也不會(huì)考慮這里是否被初始化過(guò)蔬蕊,最終直接覆蓋了第一次初始化的對(duì)象,第一次創(chuàng)建的對(duì)象變成了一具尸體,放逐到無(wú)盡的深淵岸夯。
@2 知識(shí)回顧
Swift對(duì)象構(gòu)造過(guò)程
Swift構(gòu)造函數(shù)有兩種:指定構(gòu)造函數(shù)和便利構(gòu)造函數(shù)麻献,這里不詳細(xì)描述,可以參考官方文檔猜扮,簡(jiǎn)單來(lái)說(shuō)勉吻,便利構(gòu)造函數(shù)必須調(diào)用本類(lèi)的其它構(gòu)造函數(shù),指定構(gòu)造函數(shù)必須調(diào)用父類(lèi)的指定構(gòu)造函數(shù)旅赢,便利構(gòu)造函數(shù)最終必須要調(diào)用到一個(gè)指定構(gòu)造函數(shù)齿桃,如圖:
實(shí)際上還有一個(gè)非常重要的區(qū)別,指定構(gòu)造函數(shù)都有隱式的進(jìn)行成員變量的初始化煮盼,而便利構(gòu)造函數(shù)沒(méi)有短纵,這也是為什么便利構(gòu)造函數(shù)為什么一定要直接或間接調(diào)用本類(lèi)的指定構(gòu)造函數(shù)的原因之一,通過(guò)檢查兩種不同構(gòu)造函數(shù)的反匯編代碼僵控,可以很清楚的看到這一結(jié)論香到。
兩步構(gòu)造法
在Swift里面一個(gè)對(duì)象的繼承關(guān)系鏈和對(duì)象的初始化有著對(duì)應(yīng)的關(guān)系,按照兩步構(gòu)造法报破,第一步從子類(lèi)向上依次初始化成員變量悠就,當(dāng)根類(lèi)初始化完成之后,開(kāi)始第二階段的調(diào)用充易,第二階段就可以自由調(diào)用使用該對(duì)象的所有變量和函數(shù)梗脾,為了保證這兩步構(gòu)造,編譯器會(huì)進(jìn)行4種安全檢查蔽氨。這里看一些常見(jiàn)的錯(cuò)誤例子:
class Base {
convenience init() {
print("base")//錯(cuò)誤:沒(méi)有調(diào)用本類(lèi)的指定構(gòu)造函數(shù)
}
init(name : String) {
print(name)
}
init(name : String, age : Int) {
print("just for demo")
}
}
class Sample : Base {
let value : Int
func sayHello() {
print("Hello")
}
init () {
value = 10
//錯(cuò)誤:指定構(gòu)造必須調(diào)用super的指定構(gòu)造函數(shù)
}
override init(name : String) {
//錯(cuò)誤1:必須在init之前對(duì)value進(jìn)行初始化
self.sayHello() //錯(cuò)誤2:在super.init結(jié)束前藐唠,或者第一階段構(gòu)造結(jié)束前不能使用self關(guān)鍵字
super.init(name : name)
}
convenience init(test : Int) {
super.init()//錯(cuò)誤:便利構(gòu)造必須使用self調(diào)用本類(lèi)的構(gòu)造函數(shù),這樣才有機(jī)會(huì)對(duì)本類(lèi)的成員進(jìn)行初始化
}
}
對(duì)于一個(gè)指定構(gòu)造函數(shù)來(lái)講鹉究,可以認(rèn)為一個(gè)標(biāo)準(zhǔn)的模板是這樣的
init{
//第一階段:變量初始化
//如果有父類(lèi)宇立,構(gòu)造過(guò)程傳遞給父類(lèi),等待父類(lèi)初始化
//第二階段:自定義行為
}
兩步構(gòu)造保證了函數(shù)調(diào)用的安全性自赔,相對(duì)比妈嘹,C++就沒(méi)有這個(gè)特性,C++有一個(gè)常見(jiàn)的問(wèn)題就是:在構(gòu)造函數(shù)里面是否可以調(diào)用虛函數(shù)绍妨,C++沒(méi)有兩步構(gòu)造润脸,所以在父類(lèi)的構(gòu)造函數(shù)調(diào)用的時(shí)候,子類(lèi)還完全沒(méi)有開(kāi)始初始化他去,也就是說(shuō)虛函數(shù)表還沒(méi)有準(zhǔn)備就緒毙驯,所有在父類(lèi)的構(gòu)造函數(shù)里調(diào)用虛函數(shù)很可能不能給你想要的結(jié)果。
先復(fù)習(xí)到這里灾测,更多內(nèi)容請(qǐng)參考官方文檔關(guān)于構(gòu)造過(guò)程的章節(jié)爆价。
@3 問(wèn)題分析及預(yù)防
繼續(xù)回到內(nèi)存泄漏的問(wèn)題上,根據(jù)前面的分析,OC代碼把構(gòu)造過(guò)程傳遞給了子類(lèi)铭段,這明顯不符合Swift兩步構(gòu)造的安全檢查骤宣,但是OC并沒(méi)有這樣的檢查,OC同樣也是基于兩步構(gòu)造的序愚,只不過(guò)OC的成員變量統(tǒng)一被初始化為0或者nil憔披,OC的構(gòu)造函數(shù)傳遞也是基于消息的,這樣最終導(dǎo)致了開(kāi)頭的問(wèn)題出現(xiàn)爸吮。
OC的init函數(shù)里面調(diào)用[self init…]同樣是基于消息發(fā)送芬膝,最終調(diào)用的是子類(lèi)的方法。
Swift的init函數(shù)里面調(diào)用self.init(…)是函數(shù)調(diào)用拗胜,一定會(huì)調(diào)用本類(lèi)的實(shí)現(xiàn)蔗候,當(dāng)然這個(gè)調(diào)用者必須是便利構(gòu)造函數(shù)。
如何解決這個(gè)問(wèn)題呢埂软,直觀(guān)上來(lái)看不能夠調(diào)用LKSuperView的init函數(shù),因?yàn)樵趇nit里面調(diào)用initWithFrame是不符合兩步構(gòu)造的原則的纫事,第一個(gè)解決方法就是勘畔,在DerrivedView里面實(shí)現(xiàn)自己的init方法:
convenience override init() {
self.init(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
}
其實(shí),有經(jīng)驗(yàn)的同學(xué)也一定清楚UIView::init函數(shù)最后也會(huì)調(diào)用initWithFrame丽惶,這里感覺(jué)有些坑炫七,所以就算是在LKSuperView里面直接調(diào)用[super init]
也解決不了問(wèn)題,在Swift代碼繼承OC的類(lèi)的時(shí)候钾唬,需要注意以下幾點(diǎn):
- OC類(lèi)的init函數(shù)盡量正規(guī)化万哪,不要修改self的值
- Swift類(lèi)盡量去檢查OC構(gòu)造函數(shù)鏈,避免發(fā)生以上狀況抡秆,有時(shí)候這種情況會(huì)比較隱蔽
- 無(wú)論OC還是Swift盡量不去重寫(xiě)init構(gòu)造函數(shù)奕巍,而是重寫(xiě)標(biāo)記了 NS_DESIGNATED_INITIALIZER 的構(gòu)造函數(shù)
@4 結(jié)語(yǔ)
對(duì)于Swift和OC的代碼混用中,一定會(huì)存在各種意想不到的問(wèn)題儒士,畢竟兩種語(yǔ)言的設(shè)計(jì)思想存在較大的差異的止,在遇到問(wèn)題的時(shí)候,要善于利用Xcode提供的各種強(qiáng)大的工具來(lái)檢查着撩,解決诅福。
?