從一個(gè)內(nèi)存泄漏復(fù)習(xí)swift對(duì)象的構(gòu)造過(guò)程

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ù)齿桃,如圖:

構(gòu)造函數(shù)調(diào)用示意圖

實(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)檢查着撩,解決诅福。

?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市拖叙,隨后出現(xiàn)的幾起案子氓润,更是在濱河造成了極大的恐慌,老刑警劉巖薯鳍,帶你破解...
    沈念sama閱讀 211,290評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件咖气,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)采章,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)运嗜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人悯舟,你說(shuō)我怎么就攤上這事担租。” “怎么了抵怎?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,872評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵奋救,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我反惕,道長(zhǎng)尝艘,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,415評(píng)論 1 283
  • 正文 為了忘掉前任姿染,我火速辦了婚禮背亥,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘悬赏。我一直安慰自己狡汉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布闽颇。 她就那樣靜靜地躺著盾戴,像睡著了一般。 火紅的嫁衣襯著肌膚如雪兵多。 梳的紋絲不亂的頭發(fā)上尖啡,一...
    開(kāi)封第一講書(shū)人閱讀 49,784評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音剩膘,去河邊找鬼衅斩。 笑死,一個(gè)胖子當(dāng)著我的面吹牛援雇,可吹牛的內(nèi)容都是我干的矛渴。 我是一名探鬼主播,決...
    沈念sama閱讀 38,927評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼惫搏,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼具温!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起筐赔,我...
    開(kāi)封第一講書(shū)人閱讀 37,691評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤铣猩,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后茴丰,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體达皿,經(jīng)...
    沈念sama閱讀 44,137評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡天吓,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評(píng)論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了峦椰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片龄寞。...
    茶點(diǎn)故事閱讀 38,622評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖汤功,靈堂內(nèi)的尸體忽然破棺而出物邑,到底是詐尸還是另有隱情,我是刑警寧澤滔金,帶...
    沈念sama閱讀 34,289評(píng)論 4 329
  • 正文 年R本政府宣布色解,位于F島的核電站,受9級(jí)特大地震影響餐茵,放射性物質(zhì)發(fā)生泄漏科阎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評(píng)論 3 312
  • 文/蒙蒙 一忿族、第九天 我趴在偏房一處隱蔽的房頂上張望锣笨。 院中可真熱鬧,春花似錦肠阱、人聲如沸票唆。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至衅金,卻和暖如春噪伊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背氮唯。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工鉴吹, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人惩琉。 一個(gè)月前我還...
    沈念sama閱讀 46,316評(píng)論 2 360
  • 正文 我出身青樓豆励,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親瞒渠。 傳聞我的和親對(duì)象是個(gè)殘疾皇子良蒸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容