24_內(nèi)存安全

默認情況下,Swift 會阻止你代碼里不安全的行為。例如椎镣,Swift 會保證變量在使用之前就完成初始化,在內(nèi)存被回收之后就無法被訪問兽赁,并且數(shù)組的索引會做越界檢查状答。

Swift 也保證同時訪問同一塊內(nèi)存時不會沖突,通過約束代碼里對于存儲地址的寫操作刀崖,去獲取那一塊內(nèi)存的訪問獨占權(quán)惊科。因為 Swift 自動管理內(nèi)存,所以大部分時候你完全不需要考慮內(nèi)存訪問的事情亮钦。然而馆截,理解潛在的沖突也是很重要的,可以避免你寫出訪問沖突的代碼蜂莉。而如果你的代碼確實存在沖突蜡娶,那在編譯時或者運行時就會得到錯誤。

理解內(nèi)存訪問沖突

內(nèi)存的訪問映穗,會發(fā)生在你給變量賦值窖张,或者傳遞參數(shù)給函數(shù)時。例如蚁滋,下面的代碼就包含了讀和寫的訪問:

// 向 one 所在的內(nèi)存區(qū)域發(fā)起一次寫操作
var one = 1
 
// 向 one 所在的內(nèi)存區(qū)域發(fā)起一次讀操作
print("We're number \(one)!")

內(nèi)存訪問的沖突會發(fā)生在你的代碼嘗試同時訪問同一個存儲地址的時侯宿接。同一個存儲地址的多個訪問同時發(fā)生會造成不可預(yù)計或不一致的行為赘淮。在 Swift 里,有很多修改值的行為都會持續(xù)好幾行代碼睦霎,在修改值的過程中進行訪問是有可能發(fā)生的梢卸。

你思考一下預(yù)算表更新的過程也可以看到同樣的問題。更新預(yù)算表總共有兩步:首先你把預(yù)算項的名字和費用加上副女,然后你再更新總數(shù)以體現(xiàn)預(yù)算表的現(xiàn)況蛤高。在更新之前和之后,你都可以從預(yù)算表里讀取任何信息并獲得正確的答案肮塞,就像下面展示的那樣襟齿。

image

而當(dāng)你添加預(yù)算項進入表里的時候,它只是一個臨時的枕赵,錯誤的狀態(tài),因為總數(shù)還沒有更新位隶。在添加預(yù)算項的過程中讀取總數(shù)就會讀取到錯誤的信息拷窜。

這個例子也演示了你在修復(fù)內(nèi)存訪問沖突時會遇到的問題:有時修復(fù)的方式會有很多種,但哪一種是正確的就不總是那么明顯了涧黄。在這個例子里篮昧,根據(jù)你是否需要更新后的總數(shù),$5 和 $320 都可能是正確的值笋妥。在你修復(fù)訪問沖突之前懊昨,你需要決定它的傾向。

注意

如果你寫過并發(fā)和多線程的代碼春宣,內(nèi)存訪問沖突也許是同樣的問題酵颁。然而,這里訪問沖突的討論是在單線程的情境下討論的月帝,并沒有使用并發(fā)或者多線程躏惋。

如果你曾經(jīng)在單線程代碼里有訪問沖突,Swift 可以保證你在編譯或者運行時會得到錯誤嚷辅。對于多線程的代碼簿姨,可以使用 "Thread Sanitizer" 去幫助檢測多線程的沖突。

內(nèi)存訪問的典型狀況

內(nèi)存訪問沖突有三種典型的狀況:訪問是讀還是寫簸搞,訪問的時長扁位,以及被訪問的存儲地址。特別是趁俊,當(dāng)你有兩個訪問符合下列的情況:

  • 至少有一個是寫訪問
  • 它們訪問的是同一個存儲地址
  • 它們的訪問在時間線上部分重疊

讀和寫訪問的區(qū)別很明顯:一個寫訪問會改變存儲地址域仇,而讀操作不會。存儲地址會指向真正訪問的位置 —— 例如则酝,一個變量殉簸,常量或者屬性闰集。內(nèi)存訪問的時長要么是瞬時的,要么是長期的般卑。

如果一個訪問不可能在其訪問期間被其它代碼訪問武鲁,那么就是一個瞬時訪問◎鸺欤基于這個特性沐鼠,兩個瞬時訪問是不可能同時發(fā)生。大多數(shù)內(nèi)存訪問都是瞬時的叹谁。例如饲梭,下面列舉的所有讀和寫訪問都是瞬時的:

func oneMore(than number: Int) -> Int {
    return number + 1
}
 
var myNumber = 1
myNumber = oneMore(than: myNumber)
print(myNumber)
// 打印 "2"

然而,有幾種被稱為長期訪問的內(nèi)存訪問方式焰檩,會在別的代碼執(zhí)行時持續(xù)進行憔涉。瞬時訪問和長期訪問的區(qū)別在于別的代碼有沒有可能在訪問期間同時訪問,也就是在時間線上的重疊析苫。一個長期訪問可以被別的長期訪問或瞬時訪問重疊兜叨。

重疊的訪問主要出現(xiàn)在使用 in-out 參數(shù)的函數(shù)和方法或者結(jié)構(gòu)體的 mutating 方法里。Swift 代碼里典型的長期訪問會在后面進行討論衩侥。

In-Out 參數(shù)的訪問沖突

一個函數(shù)會對它所有的 in-out 參數(shù)進行長期寫訪問国旷。in-out 參數(shù)的寫訪問會在所有非 in-out 參數(shù)處理完之后開始,直到函數(shù)執(zhí)行完畢為止茫死。如果有多個 in-out 參數(shù)跪但,則寫訪問開始的順序與參數(shù)的順序一致。

長期訪問的存在會造成一個結(jié)果峦萎,你不能在原變量以 in-out 形式傳入后訪問原變量屡久,即使作用域原則和訪問權(quán)限允許 —— 任何訪問原變量的行為都會造成沖突。例如:

var stepSize = 1
 
func increment(_ number: inout Int) {
    number += stepSize
}
 
increment(&stepSize)
// 錯誤:stepSize 訪問沖突

在上面的代碼里骨杂,stepSize 是一個全局變量涂身,并且它可以在 increment(_:) 里正常訪問。然而搓蚪,對于 stepSize 的讀訪問與 number 的寫訪問重疊了蛤售。就像下面展示的那樣,numberstepSize 都指向了同一個存儲地址妒潭。同一塊內(nèi)存的讀和寫訪問重疊了悴能,就此產(chǎn)生了沖突。

image

解決這個沖突的一種方式雳灾,是復(fù)制一份 stepSize 的副本:

// 復(fù)制一份副本
var copyOfStepSize = stepSize
increment(&copyOfStepSize)
 
// 更新原來的值
stepSize = copyOfStepSize
// stepSize 現(xiàn)在的值是 2

當(dāng)你在調(diào)用 increment(_:) 之前復(fù)制一份副本漠酿,顯然 copyOfStepSize 就會根據(jù)當(dāng)前的 stepSize 增加。讀訪問在寫操作之前就已經(jīng)結(jié)束了谎亩,所以不會有沖突炒嘲。

長期寫訪問的存在還會造成另一種結(jié)果宇姚,往同一個函數(shù)的多個 in-out 參數(shù)里傳入同一個變量也會產(chǎn)生沖突,例如:

func balance(_ x: inout Int, _ y: inout Int) {
    let sum = x + y
    x = sum / 2
    y = sum - x
}
var playerOneScore = 42
var playerTwoScore = 30
balance(&playerOneScore, &playerTwoScore)  // 正常
balance(&playerOneScore, &playerOneScore)
// 錯誤:playerOneScore 訪問沖突

注意
因為操作符也是函數(shù)夫凸,它們也會對 in-out 參數(shù)進行長期訪問浑劳。例如,假設(shè) balance(_:_:) 是一個名為 <^> 的操作符函數(shù)夭拌,那么 playerOneScore <^> playerOneScore 也會造成像 balance(&playerOneScore, &playerOneScore) 一樣的沖突魔熏。

方法里 self 的訪問沖突

一個結(jié)構(gòu)體的 mutating 方法會在調(diào)用期間對 self 進行寫訪問。例如鸽扁,想象一下這么一個游戲蒜绽,每一個玩家都有血量,受攻擊時血量會下降桶现,并且有敵人的數(shù)量躲雅,使用特殊技能時會減少敵人數(shù)量。

struct Player {
    var name: String
    var health: Int
    var energy: Int
    
    static let maxHealth = 10
    mutating func restoreHealth() {
        health = Player.maxHealth
    }
}

在上面的 restoreHealth() 方法里巩那,一個對于 self 的寫訪問會從方法開始直到方法 return吏夯。在這種情況下,restoreHealth() 里的其它代碼不可以對 Player 實例的屬性發(fā)起重疊的訪問即横。下面的 shareHealth(with:) 方法接受另一個 Player 的實例作為 in-out 參數(shù),產(chǎn)生了訪問重疊的可能性裆赵。

extension Player {
    mutating func shareHealth(with teammate: inout Player) {
        balance(&teammate.health, &health)
    }
}
 
var oscar = Player(name: "Oscar", health: 10, energy: 10)
var maria = Player(name: "Maria", health: 5, energy: 10)
oscar.shareHealth(with: &maria)  // 正常

上面的例子里东囚,調(diào)用 shareHealth(with:) 方法去把 oscar 玩家的血量分享給 maria 玩家并不會造成沖突。在方法調(diào)用期間會對 oscar 發(fā)起寫訪問战授,因為在 mutating 方法里 self 就是 oscar页藻,同時對于 maria 也會發(fā)起寫訪問,因為 maria 作為 in-out 參數(shù)傳入植兰。過程如下份帐,它們會訪問內(nèi)存的不同位置。即使兩個寫訪問重疊了楣导,它們也不會沖突废境。

當(dāng)然,如果你將 oscar 作為參數(shù)傳入 shareHealth(with:) 里筒繁,就會產(chǎn)生沖突:

oscar.shareHealth(with: &oscar)
// 錯誤:oscar 訪問沖突

mutating 方法在調(diào)用期間需要對 self 發(fā)起寫訪問噩凹,而同時 in-out 參數(shù)也需要寫訪問。在方法里毡咏,selfteammate 都指向了同一個存儲地址 —— 就像下面展示的那樣驮宴。對于同一塊內(nèi)存同時進行兩個寫訪問,并且它們重疊了呕缭,就此產(chǎn)生了沖突堵泽。

屏幕快照 2018-04-08 16.43.50.png

屬性的訪問沖突

如結(jié)構(gòu)體修己,元組和枚舉的類型都是由多個獨立的值組成的,例如結(jié)構(gòu)體的屬性或元組的元素迎罗。因為它們都是值類型睬愤,修改值的任何一部分都是對于整個值的修改,意味著其中一個屬性的讀或?qū)懺L問都需要訪問整一個值佳谦。例如戴涝,元組元素的寫訪問重疊會產(chǎn)生沖突:

var playerInformation = (health: 10, energy: 20)
balance(&playerInformation.health, &playerInformation.energy)
// 錯誤:playerInformation 的屬性訪問沖突

上面的例子里,傳入同一元組的元素對 balance(_:_:) 進行調(diào)用钻蔑,產(chǎn)生了沖突啥刻,因為 playerInformation 的訪問產(chǎn)生了寫訪問重疊。playerInformation.healthplayerInformation.energy 都被作為參數(shù)傳入咪笑,意味著 balance(_:_:) 需要在函數(shù)調(diào)用期間對它們發(fā)起寫訪問可帽。任何情況下,對于元組元素的寫訪問都需要對整個元組發(fā)起寫訪問窗怒。這意味著對于 playerInfomation 發(fā)起的兩個寫訪問重疊了映跟,造成沖突。

下面的代碼展示了一樣的錯誤扬虚,對于一個存儲在全局變量里的結(jié)構(gòu)體屬性的寫訪問重疊了努隙。

var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy)  // 錯誤

在實踐中,大多數(shù)對于結(jié)構(gòu)體屬性的訪問都會安全的重疊辜昵。例如荸镊,將上面例子里的變量 holly 改為本地變量而非全局變量,編譯器就會可以保證這個重疊訪問時安全的:

func someFunction() {
    var oscar = Player(name: "Oscar", health: 10, energy: 10)
    balance(&oscar.health, &oscar.energy)  // 正常
}

上面的例子里堪置,oscarhealthenergy 都作為 in-out 參數(shù)傳入了 balance(_:_:) 里躬存。編譯器可以保證內(nèi)存安全,因為兩個存儲屬性任何情況下都不會相互影響舀锨。

限制結(jié)構(gòu)體屬性的重疊訪問對于內(nèi)存安全并不總是必要的岭洲。內(nèi)存安全是必要的,但訪問獨占權(quán)的要求比內(nèi)存安全還要更嚴(yán)格 —— 意味著即使有些代碼違反了訪問獨占權(quán)的原則坎匿,也是內(nèi)存安全的盾剩。如果編譯器可以保證這種非專屬的訪問是安全的,那 Swift 就會允許這種內(nèi)存安全的行為碑诉。特別是當(dāng)你遵循下面的原則時彪腔,它可以保證結(jié)構(gòu)體屬性的重疊訪問是安全的:

  • 你訪問的是實例的存儲屬性,而不是計算屬性或類的屬性
  • 結(jié)構(gòu)體是本地變量的值进栽,而非全局變量
  • 結(jié)構(gòu)體要么沒有被閉包捕獲德挣,要么只被非逃逸閉包捕獲了

如果編譯器無法保證訪問的安全性,它就不會允許訪問快毛。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末格嗅,一起剝皮案震驚了整個濱河市番挺,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌屯掖,老刑警劉巖玄柏,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異贴铜,居然都是意外死亡粪摘,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門绍坝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來徘意,“玉大人,你說我怎么就攤上這事轩褐∽颠郑” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵把介,是天一觀的道長勤讽。 經(jīng)常有香客問我,道長拗踢,這世上最難降的妖魔是什么脚牍? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮巢墅,結(jié)果婚禮上莫矗,老公的妹妹穿的比我還像新娘。我一直安慰自己砂缩,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布三娩。 她就那樣靜靜地躺著庵芭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪雀监。 梳的紋絲不亂的頭發(fā)上双吆,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機與錄音会前,去河邊找鬼好乐。 笑死,一個胖子當(dāng)著我的面吹牛瓦宜,可吹牛的內(nèi)容都是我干的蔚万。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼临庇,長吁一口氣:“原來是場噩夢啊……” “哼反璃!你這毒婦竟也來了昵慌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤淮蜈,失蹤者是張志新(化名)和其女友劉穎斋攀,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體梧田,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡淳蔼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了裁眯。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鹉梨。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡竞川,死狀恐怖操灿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情两踏,我是刑警寧澤司草,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布艰垂,位于F島的核電站,受9級特大地震影響埋虹,放射性物質(zhì)發(fā)生泄漏猜憎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一搔课、第九天 我趴在偏房一處隱蔽的房頂上張望胰柑。 院中可真熱鬧,春花似錦爬泥、人聲如沸柬讨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽踩官。三九已至,卻和暖如春境输,著一層夾襖步出監(jiān)牢的瞬間蔗牡,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工嗅剖, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留辩越,地道東北人。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓信粮,卻偏偏與公主長得像黔攒,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345

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

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理亏钩,服務(wù)發(fā)現(xiàn)莲绰,斷路器,智...
    卡卡羅2017閱讀 134,601評論 18 139
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法姑丑,類相關(guān)的語法蛤签,內(nèi)部類的語法,繼承相關(guān)的語法栅哀,異常的語法震肮,線程的語...
    子非魚_t_閱讀 31,587評論 18 399
  • 從三月份找實習(xí)到現(xiàn)在,面了一些公司留拾,掛了不少戳晌,但最終還是拿到小米、百度痴柔、阿里沦偎、京東、新浪咳蔚、CVTE豪嚎、樂視家的研發(fā)崗...
    時芥藍閱讀 42,192評論 11 349
  • 風(fēng)吹來你的消息 那不再是種情愫 而是種莫名的恍惚 我走進虛擬但包含你情緒的圈子里 看著你青澀的舊照 嗅著你發(fā)燙的文...
    柚寶媽咪閱讀 155評論 0 1
  • lavender鈺閱讀 399評論 3 14