Swift 內(nèi)存安全詳解

默認(rèn)情況下承边,Swift可以防止代碼中出現(xiàn)不安全行為。例如毡鉴,Swift確保變量在使用之前被初始化崔泵,內(nèi)存在被釋放后不被訪問秒赤,數(shù)組索引被檢查是否越界。
Swift還確保對(duì)同一內(nèi)存區(qū)域的多次訪問不會(huì)發(fā)生沖突憎瘸,這是因?yàn)橹挥行枰薷膬?nèi)存中某個(gè)位置的代碼才擁有對(duì)該內(nèi)存的訪問權(quán)限入篮。因?yàn)镾wift是自動(dòng)管理內(nèi)存的,通常情況下根本不需要考慮訪問內(nèi)存幌甘。但是潮售,了解潛在沖突可能發(fā)生在哪里是很重要的,這樣可以避免編寫訪問內(nèi)存的沖突代碼含潘。如果代碼中確實(shí)包含沖突饲做,則在編譯或運(yùn)行時(shí)發(fā)生錯(cuò)誤。

一遏弱、內(nèi)存訪問沖突

當(dāng)執(zhí)行諸如設(shè)置變量值或向函數(shù)傳遞參數(shù)等操作時(shí),就會(huì)在代碼中發(fā)生對(duì)內(nèi)存的訪問塞弊。例如漱逸,以下代碼包含讀訪問和寫訪問:

// A write access to the memory where one is stored.
var one = 1

// A read access from the memory where one is stored.
print("We're number \(one)!")

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

通過思考如何更新寫在紙上的預(yù)算,你可以看到類似的問題眯勾。更新預(yù)算需要兩個(gè)步驟:首先添加商品的名稱和價(jià)格枣宫,然后更改總額以反映當(dāng)前清單上的商品。在更新之前和之后吃环,您可以從預(yù)算中讀取任何信息并得到正確的答案也颤,如下圖所示。
memory_shopping_2x.png
當(dāng)你向預(yù)算中添加商品時(shí)郁轻,它處于臨時(shí)的翅娶、無效的狀態(tài),因?yàn)榭偨痤~尚未更新來反映新添加的商品好唯。在添加一個(gè)商品的過程中竭沫,讀取總金額會(huì)獲取錯(cuò)誤的信息。

這個(gè)例子還演示了在修復(fù)內(nèi)存的訪問沖突時(shí)可能遇到的一個(gè)挑戰(zhàn):有時(shí)有多種方法可以解決沖突骑篙,同時(shí)也產(chǎn)生不同的答案蜕提,而且無法確定哪個(gè)答案是正確的。在本例中替蛉,根據(jù)您想要原始的總額還是更新后的總額贯溅,5美元或320美元可能是正確的答案拄氯。在修復(fù)沖突訪問之前,你必須確定要執(zhí)行的操作它浅。

注意:如果你編寫過并發(fā)或多線程代碼译柏,則對(duì)內(nèi)存的沖突訪問可能是一個(gè)熟悉的問題。但是姐霍,此處討論的沖突訪問可能發(fā)生在單個(gè)線程上鄙麦,并且不涉及并發(fā)或多線程代碼。
如果在單線程中存在內(nèi)存訪問沖突镊折,Swift會(huì)保證在編譯時(shí)或運(yùn)行時(shí)都會(huì)收到錯(cuò)誤胯府。對(duì)于多線程代碼,請(qǐng)使用Thread Sanitizer幫助檢測(cè)跨線程的沖突訪問恨胚。

二骂因、內(nèi)存訪問的特征

在沖突訪問的上下文中,需要考慮內(nèi)存訪問的三個(gè)特征:訪問是讀還是寫赃泡,訪問的持續(xù)時(shí)間寒波,以及在內(nèi)存中的訪問位置。具體來說升熊,如果您有兩個(gè)滿足以下所有條件的訪問俄烁,就會(huì)發(fā)生沖突:

  • 至少有一個(gè)是寫訪問。
  • 它們?cè)L問內(nèi)存中的相同位置级野。
  • 它們的時(shí)間重疊页屠。
    讀訪問和寫訪問之間的區(qū)別通常很明顯:寫訪問改變內(nèi)存中的位置,但讀訪問不會(huì)蓖柔。內(nèi)存中的位置是指正在訪問的內(nèi)容 - 例如辰企,變量,常量或?qū)傩栽ǔ椤?nèi)存訪問的持續(xù)時(shí)間可以是瞬時(shí)的蟆豫,也可以是長(zhǎng)期的。
    如果在訪問開始之后但在訪問結(jié)束之前不能運(yùn)行其他代碼懒闷,則訪問是瞬時(shí)的十减。從本質(zhì)上講,兩次瞬時(shí)訪問不可能同時(shí)發(fā)生愤估。大多數(shù)內(nèi)存訪問是瞬時(shí)的帮辟。例如,下面代碼清單中的所有讀寫訪問都是瞬時(shí)的:
func oneMore(than number: Int) -> Int {
    return number + 1
}

var myNumber = 1
myNumber = oneMore(than: myNumber)
print(myNumber)
// Prints "2"

但是玩焰,有幾種訪問內(nèi)存的方法由驹,稱為長(zhǎng)期訪問,可以跨越其他代碼的執(zhí)行。瞬時(shí)訪問和長(zhǎng)期訪問的區(qū)別在于蔓榄,其他代碼可以在長(zhǎng)期訪問開始后結(jié)束之前運(yùn)行并炮,這稱為重疊。長(zhǎng)期訪問可以與其他長(zhǎng)期訪問和瞬時(shí)訪問重疊甥郑。
重疊訪問主要出現(xiàn)在在函數(shù)和方法中使用in-out參數(shù)的代碼中逃魄,或者是結(jié)構(gòu)體的可變方法中。使用長(zhǎng)期訪問的特定Swift代碼類型將在下面的部分中討論澜搅。

三伍俘、對(duì)In-Out參數(shù)的訪問沖突

函數(shù)擁有對(duì)其所有in-out參數(shù)的長(zhǎng)期寫訪問權(quán)。一個(gè)in-out參數(shù)的寫訪問在所有非in-out參數(shù)被計(jì)算之后開始勉躺,并持續(xù)到整個(gè)函數(shù)調(diào)用期間癌瘾。如果有多個(gè)in-out參數(shù),則寫訪問的開始順序與參數(shù)出現(xiàn)的順序相同饵溅。
這種長(zhǎng)期寫訪問的一個(gè)后果是妨退,您不能訪問作為in-out傳遞的原始變量,即使范圍規(guī)則和訪問控制允許這樣做——任何對(duì)原始變量的訪問都會(huì)產(chǎn)生沖突概说。例如:

var stepSize = 1

func increment(_ number: inout Int) {
    number += stepSize
}

increment(&stepSize)
// Error: conflicting accesses to stepSize

在上面的代碼中碧注,stepSize是一個(gè)全局變量,通程桥猓可以從increment(_:)訪問它。但是轩端,對(duì)stepSize的讀訪問與對(duì)number的寫訪問重疊放典。如下圖所示,numberstepSize都指向內(nèi)存中的同一個(gè)位置基茵。讀和寫訪問引用相同的內(nèi)存奋构,它們重疊,產(chǎn)生沖突拱层。

memory_increment_2x.png

解決這個(gè)沖突的一種方法是顯式復(fù)制stepSize:

// Make an explicit copy.
var copyOfStepSize = stepSize
increment(&copyOfStepSize)

// Update the original.
stepSize = copyOfStepSize
// stepSize is now 2

當(dāng)在調(diào)用increment(_:)之前復(fù)制一個(gè)stepSize時(shí)弥臼,很明顯copyOfStepSize的值是由當(dāng)前stepSize遞增的。讀訪問在寫訪問開始之前結(jié)束根灯,所以沒有沖突径缅。
對(duì)in-out參數(shù)進(jìn)行長(zhǎng)期寫訪問的另一個(gè)后果是,將單個(gè)變量作為同一個(gè)函數(shù)的多個(gè)in-out參數(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)  // OK
balance(&playerOneScore, &playerOneScore)
// Error: conflicting accesses to playerOneScore

上面balance(_:_:)函數(shù)修改了它的兩個(gè)參數(shù)纳猪,使它們之間的值相等。用playerOneScoreplayerTwoScore調(diào)用它不會(huì)產(chǎn)生沖突——有兩個(gè)寫訪問在時(shí)間上重疊桃笙,但它們?cè)L問的是不同的內(nèi)存位置氏堤。相反,將playerOneScore作為兩個(gè)參數(shù)的值傳遞會(huì)產(chǎn)生沖突搏明,因?yàn)樗噲D同時(shí)執(zhí)行對(duì)內(nèi)存中相同位置的兩次寫訪問鼠锈。

注意:由于操作符是函數(shù)闪檬,它們也可以長(zhǎng)期訪問它們的in-out參數(shù)。
因?yàn)檫\(yùn)算符是函數(shù)购笆,所以它們也可以長(zhǎng)期訪問其in-out參數(shù)粗悯。例如,如果balance(_:_:)是一個(gè)名為<^>的運(yùn)算符函數(shù)由桌,則寫入playerOneScore <^> playerOneScore將導(dǎo)致與balance(&playerOneScore, &playerOneScore)相同的沖突为黎。

四、方法中self的訪問沖突

結(jié)構(gòu)體上的可變方法在方法調(diào)用期間對(duì)self具有寫訪問權(quán)行您。例如铭乾,考慮這樣一個(gè)游戲,每個(gè)玩家都有一個(gè)生命值(在受到傷害時(shí)減少)和一個(gè)能量值(在使用特殊技能時(shí)減少)娃循。

struct Player {
    var name: String
    var health: Int
    var energy: Int

    static let maxHealth = 10
    mutating func restoreHealth() {
        health = Player.maxHealth
    }
}

在上面的restoreHealth()方法中炕檩,對(duì)self的寫訪問從方法的開頭開始,一直持續(xù)到方法返回為止捌斧。在本例中笛质,restoreHealth()中沒有其他代碼可以重疊訪問Player實(shí)例的屬性。下面的 shareHealth(with:)方法將另一個(gè)Player實(shí)例作為一個(gè)in-out參數(shù)捞蚂,創(chuàng)建了重疊訪問的可能性妇押。

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)  // OK

在上面的例子中,調(diào)用Oscar玩家的shareHealth(with:)方法與Maria 玩家共享生命值不會(huì)引起沖突姓迅。在方法調(diào)用期間有對(duì)oscar的寫訪問敲霍,因?yàn)?code>oscar是可變方法中的self值,在相同的時(shí)間內(nèi)也有對(duì)maria的寫訪問丁存,因?yàn)?code>maria是作為 in-out參數(shù)傳遞的肩杈。如下圖所示,它們?cè)L問內(nèi)存中的不同位置解寝。盡管兩個(gè)寫訪問在時(shí)間上是重疊的男翰,但它們并不沖突绑谣。

memory_share_health_maria_2x.png

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

oscar.shareHealth(with: &oscar)
// Error: conflicting accesses to oscar

在該方法的持續(xù)時(shí)間內(nèi)纤泵,可變方法需要對(duì)self的寫訪問薯定。而在相同的持續(xù)時(shí)間內(nèi)毡泻,in-out參數(shù)需要對(duì)teammate的寫訪問刘离。在方法中,selfteamate引用內(nèi)存中的相同位置—如下圖所示抑片。這兩個(gè)寫訪問引用相同的內(nèi)存卵佛,它們重疊,產(chǎn)生沖突。

memory_share_health_oscar_2x.png

五截汪、屬性的訪問沖突

結(jié)構(gòu)體疾牲、元組和枚舉等類型由單個(gè)組成值組成,例如結(jié)構(gòu)體的屬性或元組的元素衙解。由于這些都是值類型阳柔,因此改變值的任何部分會(huì)改變整個(gè)值,這意味著對(duì)其中一個(gè)屬性的讀或?qū)懺L問需要對(duì)整個(gè)值進(jìn)行讀或?qū)懺L問蚓峦。例如舌剂,對(duì)元組元素的重疊寫訪問會(huì)產(chǎn)生沖突:

var playerInformation = (health: 10, energy: 20)
balance(&playerInformation.health, &playerInformation.energy)
// Error: conflicting access to properties of playerInformation

在上面的例子中,調(diào)用元組元素上的balance(_:_:)會(huì)產(chǎn)生沖突暑椰,因?yàn)閷?duì)playerInformation的寫訪問有重疊霍转。playerInformation.healthplayerInformation.energy作為in-out參數(shù)傳遞,這意味著balance(_:_:)在函數(shù)調(diào)用期間需要對(duì)它們進(jìn)行寫訪問一汽。在這兩種情況下避消,對(duì)tuple元素的寫訪問都需要對(duì)整個(gè)tuple進(jìn)行寫訪問。這意味著有兩個(gè)對(duì)playerInformation的寫訪問召夹,它們的持續(xù)時(shí)間重疊岩喷,導(dǎo)致沖突。
下面的代碼顯示监憎,對(duì)存儲(chǔ)在全局變量中的結(jié)構(gòu)體屬性的重疊寫訪問纱意,會(huì)出現(xiàn)相同的錯(cuò)誤。

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

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

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

在上面的例子中,oscarhealthenergy 作為兩個(gè)輸入?yún)?shù)傳遞給balance(_:_:)跑筝。編譯器可以證明保留了內(nèi)存安全性死讹,因?yàn)檫@兩個(gè)存儲(chǔ)屬性不以任何方式交互。

反對(duì)重疊訪問結(jié)構(gòu)體屬性的限制并不總是保證內(nèi)存安全的必要條件曲梗。內(nèi)存安全是理想的保證赞警,但唯一的訪問是比內(nèi)存安全更嚴(yán)格的要求 - 這意味著一些代碼可以維護(hù)內(nèi)存安全,即使它違反了對(duì)內(nèi)存的唯一訪問權(quán)限虏两。如果編譯器可以證明對(duì)內(nèi)存的非單獨(dú)訪問仍然是安全的愧旦,那么Swift允許這種內(nèi)存安全的代碼。具體而言定罢,如果滿足以下條件笤虫,則可以證明對(duì)結(jié)構(gòu)屬性的重疊訪問是安全的:

  • 只訪問實(shí)例的存儲(chǔ)屬性,而不是計(jì)算屬性或類屬性。
  • 結(jié)構(gòu)體是局部變量的值琼蚯,而不是全局變量酬凳。
  • 該結(jié)構(gòu)體要么不被任何閉包捕獲,要么僅由非逃離閉包捕獲遭庶。
    如果編譯器無法證明訪問是安全的宁仔,則不允許訪問。

六峦睡、其他專題模塊

Swift 4.2 基礎(chǔ)專題詳解

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末翎苫,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子榨了,更是在濱河造成了極大的恐慌煎谍,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件阻逮,死亡現(xiàn)場(chǎng)離奇詭異粱快,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)叔扼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門事哭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人瓜富,你說我怎么就攤上這事鳍咱。” “怎么了与柑?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵谤辜,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我价捧,道長(zhǎng)丑念,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任结蟋,我火速辦了婚禮脯倚,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘嵌屎。我一直安慰自己推正,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布宝惰。 她就那樣靜靜地躺著植榕,像睡著了一般。 火紅的嫁衣襯著肌膚如雪尼夺。 梳的紋絲不亂的頭發(fā)上尊残,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天炒瘸,我揣著相機(jī)與錄音,去河邊找鬼夜郁。 笑死什燕,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的竞端。 我是一名探鬼主播屎即,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼事富!你這毒婦竟也來了技俐?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤统台,失蹤者是張志新(化名)和其女友劉穎雕擂,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體贱勃,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡井赌,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了贵扰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片仇穗。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖戚绕,靈堂內(nèi)的尸體忽然破棺而出纹坐,到底是詐尸還是另有隱情,我是刑警寧澤舞丛,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布耘子,位于F島的核電站,受9級(jí)特大地震影響球切,放射性物質(zhì)發(fā)生泄漏谷誓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一吨凑、第九天 我趴在偏房一處隱蔽的房頂上張望片林。 院中可真熱鬧,春花似錦怀骤、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至焚鹊,卻和暖如春痕届,著一層夾襖步出監(jiān)牢的瞬間韧献,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工研叫, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留锤窑,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓嚷炉,卻偏偏與公主長(zhǎng)得像渊啰,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子申屹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

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

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,089評(píng)論 1 32
  • 不知道大家有沒有這樣的經(jīng)歷... 平時(shí)已經(jīng)不怎么聯(lián)系的朋友哗讥,突然發(fā)來一條信息:客服正在幫我清理僵尸粉嚷那,感謝微信有你...
    稍磨制就閱讀 14,517評(píng)論 0 0
  • 在霧汽濛濛的早上 知更鳥不要迷失方向 日復(fù)一日的習(xí)慣 即使在遲來的朝陽(yáng) 知更鳥 不知道 沒有什么需要你去阻擋 沒...
    肖魁之閱讀 588評(píng)論 0 2
  • 2017年12月10日决乎,星期日队询,伍哥讀報(bào)時(shí)間: 1、【京津冀公立醫(yī)院耗材聯(lián)合采購(gòu)】12月7日瑞驱,北京市衛(wèi)生計(jì)生委印發(fā)...
    邢五閱讀 106評(píng)論 0 0
  • 1996到2016娘摔,今年我20歲,讀大三唤反。 打開朋友圈凳寺,經(jīng)常看著和我年齡相仿或是稍大的已婚女性5-8連發(fā)式刷屏彤侍,內(nèi)...
    童圓圓O_O閱讀 340評(píng)論 1 2