Swift編程二十六(內(nèi)存安全)

案例代碼下載

內(nèi)存安全

默認(rèn)情況下飒筑,Swift可以防止代碼中發(fā)生不安全行為。例如,Swift確保變量在使用之前進(jìn)行初始化超营,在取消分配后不訪問(wèn)內(nèi)存,并檢查數(shù)組索引是否存在越界錯(cuò)誤阅虫。

Swift還確保對(duì)同一內(nèi)存區(qū)域的多次訪問(wèn)不會(huì)發(fā)生沖突演闭,因?yàn)樾枰薷膬?nèi)存中某個(gè)位置的代碼才能對(duì)該內(nèi)存進(jìn)行獨(dú)占訪問(wèn)。因?yàn)镾wift自動(dòng)管理內(nèi)存颓帝,所以大多數(shù)時(shí)候根本不需要考慮訪問(wèn)內(nèi)存米碰。但是,了解潛在沖突可能發(fā)生的位置非常重要购城,這樣就可以避免編寫(xiě)對(duì)內(nèi)存具有沖突訪問(wèn)權(quán)限的代碼吕座。如果代碼確實(shí)包含沖突,那么將收到編譯時(shí)或運(yùn)行時(shí)錯(cuò)誤瘪板。

了解對(duì)內(nèi)存的沖突訪問(wèn)

當(dāng)執(zhí)行諸如設(shè)置變量的值或?qū)?shù)傳遞給函數(shù)之類的操作時(shí)米诉,會(huì)在代碼中訪問(wèn)內(nèi)存。例如篷帅,以下代碼包含讀寫(xiě)內(nèi)存:

var one = 1
print("We're number \(one)!")

/*
打印結(jié)果:

We're number 1!
*/

當(dāng)代碼的不同部分試圖同時(shí)訪問(wèn)內(nèi)存中的相同位置時(shí)史侣,可能會(huì)發(fā)生沖突的內(nèi)存訪問(wèn)拴泌。同時(shí)多次訪問(wèn)內(nèi)存中的某個(gè)位置會(huì)產(chǎn)生不可預(yù)測(cè)或不一致的行為。在Swift中惊橱,有一些方法可以修改跨越多行代碼的值蚪腐,從而可以嘗試在自己修改的過(guò)程中訪問(wèn)一個(gè)值。

通過(guò)考慮如何更新寫(xiě)在紙上的預(yù)算税朴,可以看到類似的問(wèn)題回季。更新預(yù)算的過(guò)程分為兩步:首先添加項(xiàng)目的名稱和價(jià)格,然后更改總金額以反映列表中當(dāng)前的項(xiàng)目正林。在更新之前和之后泡一,可以從預(yù)算中讀取任何信息并獲得正確答案,如下圖所示觅廓。


image

當(dāng)在預(yù)算中添加項(xiàng)目時(shí)鼻忠,它處于臨時(shí)無(wú)效狀態(tài),因?yàn)榭偨痤~尚未更新以反映新添加的項(xiàng)目杈绸。在添加項(xiàng)目的過(guò)程中讀取總金額會(huì)導(dǎo)致不正確的信息帖蔓。

此示例還演示了在修復(fù)對(duì)內(nèi)存的沖突訪問(wèn)時(shí)可能遇到的挑戰(zhàn):有時(shí)有多種方法可以解決產(chǎn)生不同答案的沖突,并且并不總是很明顯哪個(gè)答案是正確的瞳脓。在此示例中塑娇,根據(jù)是否需要原始總金額或更新的總金額,5或 320可能是正確的答案劫侧。在修復(fù)沖突訪問(wèn)之前埋酬,必須確定要執(zhí)行的操作。

注意

如果編寫(xiě)了并發(fā)或多線程代碼烧栋,則對(duì)內(nèi)存的沖突訪問(wèn)可能是一個(gè)熟悉的問(wèn)題写妥。但是,此處討論的沖突訪問(wèn)可能發(fā)生在單個(gè)線程上劲弦,并且不涉及并發(fā)或多線程代碼。

如果在單個(gè)線程中存在對(duì)內(nèi)存的沖突訪問(wèn)醇坝,Swift會(huì)保證您在編譯時(shí)或運(yùn)行時(shí)都會(huì)收到錯(cuò)誤邑跪。對(duì)于多線程代碼,請(qǐng)使用Thread Sanitizer來(lái)幫助檢測(cè)跨線程的沖突訪問(wèn)呼猪。

內(nèi)存訪問(wèn)的特征

在沖突訪問(wèn)的上下文中要考慮存儲(chǔ)器訪問(wèn)的三個(gè)特征:訪問(wèn)是讀取還是寫(xiě)入画畅,訪問(wèn)的持續(xù)時(shí)間以及訪問(wèn)的存儲(chǔ)器中的位置。具體而言宋距,如果有兩個(gè)滿足以下所有條件的訪問(wèn)轴踱,則會(huì)發(fā)生沖突:

  • 至少一個(gè)是寫(xiě)訪問(wèn)。
  • 他們?cè)L問(wèn)內(nèi)存中的相同位置谚赎。
  • 他們的持續(xù)時(shí)間重疊淫僻。

讀寫(xiě)訪問(wèn)之間的區(qū)別通常很明顯:寫(xiě)訪問(wèn)會(huì)更改內(nèi)存中的位置诱篷,但讀訪問(wèn)不會(huì)。內(nèi)存中的位置是指正在訪問(wèn)的內(nèi)容 - 例如雳灵,變量棕所,常量或?qū)傩浴4鎯?chǔ)器訪問(wèn)的持續(xù)時(shí)間是瞬時(shí)的或長(zhǎng)期的悯辙。

如果在訪問(wèn)開(kāi)始之后但在結(jié)束之前其他代碼無(wú)法運(yùn)行琳省,則訪問(wèn)是即時(shí)的。就其本質(zhì)而言躲撰,兩次即時(shí)訪問(wèn)不可能同時(shí)發(fā)生针贬。大多數(shù)內(nèi)存訪問(wèn)都是即時(shí)的。例如拢蛋,下面代碼清單中的所有讀寫(xiě)訪問(wèn)都是即時(shí)的:

func oneMore(than number: Int) -> Int {
    return number + 1
}
var myNumber = 1
myNumber = oneMore(than: myNumber)
print(myNumber)

/*
打印結(jié)果:

2
*/

但是桦他,有幾種方法可以訪問(wèn)內(nèi)存,稱為長(zhǎng)期訪問(wèn)瓤狐,跨越其他代碼的執(zhí)行瞬铸。即時(shí)訪問(wèn)和長(zhǎng)期訪問(wèn)之間的區(qū)別在于,其他代碼可以在長(zhǎng)期訪問(wèn)開(kāi)始之后但在結(jié)束之前運(yùn)行础锐,這稱為重疊嗓节。長(zhǎng)期訪問(wèn)可以與其他長(zhǎng)期訪問(wèn)和即時(shí)訪問(wèn)重疊。

重疊訪問(wèn)主要出現(xiàn)在使用函數(shù)和方法中的輸入輸出參數(shù)或結(jié)構(gòu)的mutating方法的代碼中皆警。使用長(zhǎng)期訪問(wèn)的特定Swift代碼類型將在下面的部分中討論拦宣。

對(duì)In-Out參數(shù)的訪問(wèn)沖突

函數(shù)具有對(duì)其所有輸入輸出參數(shù)的長(zhǎng)期寫(xiě)訪問(wèn)權(quán)。對(duì)in-out參數(shù)的寫(xiě)訪問(wèn)開(kāi)始于所有非in-out參數(shù)評(píng)估之后并且持續(xù)該函數(shù)調(diào)用的整個(gè)持續(xù)時(shí)間信姓。如果有多個(gè)輸入輸出參數(shù)鸵隧,則寫(xiě)訪問(wèn)的開(kāi)始順序與參數(shù)顯示的順序相同。

這種長(zhǎng)期寫(xiě)入訪問(wèn)的一個(gè)結(jié)果是無(wú)法訪問(wèn)作為輸入輸出傳遞的原始變量意推,即使范圍規(guī)則和訪問(wèn)控制允許它 - 任何對(duì)原始數(shù)據(jù)的訪問(wèn)都會(huì)產(chǎn)生沖突豆瘫。例如:

var stepSize = 1
func increment(_ number: inout Int) {
    number += stepSize
}
increment(&stepSize) // 錯(cuò)誤:沖突反問(wèn)stepSize

在上面的代碼中,stepSize是一個(gè)全局變量菊值,它通惩馇可以從increment(_:)內(nèi)部訪問(wèn)。但是腻窒,讀訪問(wèn)stepSize與寫(xiě)訪問(wèn)重疊number昵宇。如在下文中的圖所示,兩者number并stepSize在存儲(chǔ)器中指代相同的位置儿子。讀和寫(xiě)訪問(wèn)指的是相同的內(nèi)存瓦哎,它們重疊,產(chǎn)生沖突。


image

解決這種沖突的一種方法是制作一份明確的副本stepSize:

var copyOfStepSize = stepSize // 制作一個(gè)副本
increment(&copyOfStepSize)
stepSize = copyOfStepSize

當(dāng)stepSize在increment(_:)調(diào)用之前復(fù)制蒋譬,很明顯割岛,copyOfStepSize的值通過(guò)當(dāng)前的stepSize增加。讀訪問(wèn)在寫(xiě)訪問(wèn)開(kāi)始之前結(jié)束羡铲,因此不存在沖突蜂桶。

對(duì)輸入輸出參數(shù)進(jìn)行長(zhǎng)期寫(xiě)訪問(wèn)的另一個(gè)后果是,將單個(gè)變量作為同一函數(shù)的多個(gè)輸入輸出參數(shù)的參數(shù)傳遞會(huì)產(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) // 錯(cuò)誤:沖突反問(wèn)playerOneScore

上面的balance(::)函數(shù)修改了它的兩個(gè)參數(shù)扑媚,以便在它們之間平均分配總值。使用playerOneScore和playerTwoScore作為參數(shù)調(diào)用它不會(huì)產(chǎn)生沖突 - 有兩個(gè)寫(xiě)訪問(wèn)在時(shí)間上重疊雷恃,但它們?cè)L問(wèn)內(nèi)存中的不同位置疆股。相反,playerOneScore作為兩個(gè)參數(shù)的值傳遞會(huì)產(chǎn)生沖突倒槐,因?yàn)樗噲D同時(shí)對(duì)內(nèi)存中的同一位置執(zhí)行兩次寫(xiě)訪問(wèn)旬痹。

注意

因?yàn)檫\(yùn)算符是函數(shù),所以它們也可以長(zhǎng)期訪問(wèn)其輸入輸出參數(shù)讨越。例如两残,如果balance(::)是一個(gè)名為<^>的運(yùn)算符函數(shù),則寫(xiě)playerOneScore <^> playerOneScore將導(dǎo)致與balance(&playerOneScore, &playerOneScore)相同的沖突把跨。

方法中的self沖突

結(jié)構(gòu)上的mutating方法在方法調(diào)用期間具有對(duì)self的寫(xiě)訪問(wèn)權(quán)人弓。例如,考慮一種游戲着逐,其中每個(gè)玩家具有健康量崔赌,其在受到傷害時(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寫(xiě)入訪問(wèn)從方法的開(kāi)頭開(kāi)始并持續(xù)到方法返回秀姐。在這種情況下慈迈,內(nèi)部沒(méi)有其他restoreHealth()代碼可以重疊訪問(wèn)Player實(shí)例的屬性。下面的shareHealth(with:)方法將另一個(gè)Player實(shí)例作為輸入輸出參數(shù)省有,從而創(chuàng)建重疊訪問(wè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)用Oscar游戲者的shareHealth(with:)方法與Maria共享健康狀態(tài)不會(huì)導(dǎo)致沖突锥咸。在方法調(diào)用期間對(duì)oscar存在寫(xiě)訪問(wèn)權(quán)狭瞎,因?yàn)閛scar是的mutating方法中self的值细移,并且maria在相同的持續(xù)時(shí)間內(nèi)存在寫(xiě)訪問(wèn)權(quán)搏予,因?yàn)樗黰aria是作為輸入輸出參數(shù)傳遞的。如下圖所示弧轧,它們?cè)L問(wèn)內(nèi)存中的不同位置雪侥。即使兩個(gè)寫(xiě)訪問(wèn)在時(shí)間上重疊碗殷,它們也不會(huì)發(fā)生沖突。


image

但是速缨,如果oscar作為參數(shù)傳遞給shareHealth(with:)锌妻,則存在沖突:

oscar.shareHealth(with: &oscar) // 錯(cuò)誤:沖突訪問(wèn)oscar

mutating方法需要在方法的持續(xù)時(shí)間內(nèi)對(duì)self進(jìn)行寫(xiě)訪問(wèn),并且需要對(duì)輸入輸出參數(shù)teammate相同持續(xù)時(shí)間的寫(xiě)訪問(wèn)權(quán)旬牲。在該方法中仿粹,無(wú)論是self和teammate指的是相同的位置在內(nèi)存如示于下圖中。兩次寫(xiě)訪問(wèn)指的是相同的內(nèi)存原茅,它們重疊吭历,產(chǎn)生沖突。


image

對(duì)屬性的沖突訪問(wèn)

結(jié)構(gòu)擂橘,元組和枚舉等類型由單個(gè)組成值組成晌区,例如結(jié)構(gòu)的屬性或元組的元素。因?yàn)檫@些是值類型通贞,所以改變值的任何部分都會(huì)改變整個(gè)值朗若,這意味著對(duì)其中一個(gè)屬性的讀取或?qū)懭朐L問(wèn)需要對(duì)整個(gè)值進(jìn)行讀取或?qū)懭朐L問(wèn)。例如昌罩,重疊對(duì)元組元素的寫(xiě)訪問(wèn)會(huì)產(chǎn)生沖突:

var playerInformation = (health: 10, enertgy: 10)
balance(&playerInformation.health, &playerInformation.enertgy) // 錯(cuò)誤:沖突訪問(wèn)playerInformation

在上面的示例中哭懈,調(diào)用balance(::)元組的元素會(huì)產(chǎn)生沖突,因?yàn)榇嬖谥丿B的playerInformation寫(xiě)訪問(wèn)峡迷。playerInformation.health和playerInformation.energy雙方都在出參數(shù)银伟,這意味著調(diào)用balance(::)函數(shù)的持續(xù)時(shí)間需要寫(xiě)訪問(wèn)。在這兩種情況下绘搞,對(duì)元組元素的寫(xiě)訪問(wèn)都需要對(duì)整個(gè)元組進(jìn)行寫(xiě)訪問(wèn)彤避。這意味著有兩次寫(xiě)訪問(wèn)playerInformation,持續(xù)時(shí)間重疊夯辖,導(dǎo)致沖突琉预。

下面顯示的代碼,對(duì)于存儲(chǔ)在全局變量中的結(jié)構(gòu)屬性的重寫(xiě)寫(xiě)訪問(wèn)蒿褂,會(huì)出現(xiàn)相同的錯(cuò)誤圆米。

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

實(shí)際上,大多數(shù)對(duì)結(jié)構(gòu)屬性的訪問(wèn)都可以安全地重疊啄栓。例如娄帖,如果上例中的變量holly更改為局部變量而不是全局變量,則編譯器可以證明對(duì)結(jié)構(gòu)的存儲(chǔ)屬性的重疊訪問(wèn)是安全的:

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

在上面的例子中昙楚,oscar的健康和能量作為兩個(gè)in-out參數(shù)傳遞給balance(::)近速。編譯器可以證明保留了內(nèi)存安全性,因?yàn)閮蓚€(gè)存儲(chǔ)的屬性不會(huì)以任何方式進(jìn)行交互。

為了保持存儲(chǔ)器安全性削葱,并不總是必須限制對(duì)結(jié)構(gòu)屬性的重疊訪問(wèn)奖亚。內(nèi)存安全是理想的保證,但獨(dú)占訪問(wèn)是比內(nèi)存安全更嚴(yán)格的要求 - 這意味著一些代碼可以保持內(nèi)存安全析砸,即使它違反了內(nèi)存的獨(dú)占訪問(wèn)權(quán)限昔字。如果編譯器能夠證明對(duì)內(nèi)存的非獨(dú)占訪問(wèn)仍然是安全的,那么Swift允許這種內(nèi)存安全的代碼首繁。具體而言作郭,如果滿足以下條件,則可以證明對(duì)結(jié)構(gòu)屬性的重疊訪問(wèn)是安全的:

  • 只訪問(wèn)實(shí)例的存儲(chǔ)屬性弦疮,而不是計(jì)算屬性或類屬性所坯。
  • 結(jié)構(gòu)是局部變量的值,而不是全局變量挂捅。
  • 結(jié)構(gòu)要么不被任何閉包捕獲芹助,要么僅由非逃逸閉包捕獲。

如果編譯器無(wú)法證明訪問(wèn)是安全的闲先,則不允許訪問(wèn)状土。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市伺糠,隨后出現(xiàn)的幾起案子蒙谓,更是在濱河造成了極大的恐慌,老刑警劉巖训桶,帶你破解...
    沈念sama閱讀 217,734評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件累驮,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡舵揭,警方通過(guò)查閱死者的電腦和手機(jī)谤专,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)午绳,“玉大人置侍,你說(shuō)我怎么就攤上這事±狗伲” “怎么了蜡坊?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,133評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)赎败。 經(jīng)常有香客問(wèn)我秕衙,道長(zhǎng),這世上最難降的妖魔是什么僵刮? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,532評(píng)論 1 293
  • 正文 為了忘掉前任据忘,我火速辦了婚禮峡钓,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘若河。我一直安慰自己,他們只是感情好寞宫,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布萧福。 她就那樣靜靜地躺著,像睡著了一般辈赋。 火紅的嫁衣襯著肌膚如雪鲫忍。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,462評(píng)論 1 302
  • 那天钥屈,我揣著相機(jī)與錄音悟民,去河邊找鬼。 笑死篷就,一個(gè)胖子當(dāng)著我的面吹牛射亏,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播竭业,決...
    沈念sama閱讀 40,262評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼智润,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了未辆?” 一聲冷哼從身側(cè)響起窟绷,我...
    開(kāi)封第一講書(shū)人閱讀 39,153評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎咐柜,沒(méi)想到半個(gè)月后兼蜈,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,587評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡拙友,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評(píng)論 3 336
  • 正文 我和宋清朗相戀三年为狸,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片遗契。...
    茶點(diǎn)故事閱讀 39,919評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡钥平,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出姊途,到底是詐尸還是另有隱情涉瘾,我是刑警寧澤,帶...
    沈念sama閱讀 35,635評(píng)論 5 345
  • 正文 年R本政府宣布捷兰,位于F島的核電站立叛,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏贡茅。R本人自食惡果不足惜秘蛇,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評(píng)論 3 329
  • 文/蒙蒙 一其做、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧赁还,春花似錦妖泄、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,855評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至朋蔫,卻和暖如春罚渐,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背驯妄。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,983評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工荷并, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人青扔。 一個(gè)月前我還...
    沈念sama閱讀 48,048評(píng)論 3 370
  • 正文 我出身青樓源织,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親微猖。 傳聞我的和親對(duì)象是個(gè)殘疾皇子雀鹃,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評(píng)論 2 354

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