Swift數(shù)組越界引發(fā)的猜想

前言

iOS開發(fā)很多年了,之前一直主要用OC開發(fā)叠赐,今年開始漸漸主用Swift開發(fā)了偿洁。最近在開發(fā)中發(fā)現(xiàn)一個遇到一個數(shù)組越界的問題

Fatal error: Index out of range: 

對于數(shù)組越界相信大家都不會陌生了,在OC里面轿钠,我們Hook了數(shù)組的底層實現(xiàn)巢钓,所以業(yè)務上不管怎么使用都不會有問題。

但是在Swift中疗垛,如果數(shù)組越獄還是會Crash症汹,很明顯,之前實現(xiàn)的那一套運行時Hook方案對Swift無效贷腕;什么會這樣烈菌?為了測試阵幸,我寫了如下代碼:

let arr1: NSMutableArray = [1,2,3,4]
arr1.removeObject(at: 5)

這段代碼在運行時會被Hook,并不會產生崩潰。但如果換成下面這樣就會產生崩潰

let arr1: Array = [1,2,3,4]
arr1.remove(at: 5)

二者在運行時的實現(xiàn)有何不同芽世,我通過SIL來分析挚赊。這里介紹一下SIL:

Swift 和 Objective-C 使用的相同的編譯架構 LLVM,LLVM 分為前端济瓢、中端和后端三部分荠割,通過中間語言 LLVM IR 將前端和后端串聯(lián)起來。swiftc 作為 Swift 語言的的編譯器旺矾,負責 LLVM 前端的工作蔑鹦。swiftc 與其它編譯器工作類似,進行詞法分析箕宙、語法分析嚎朽、語義分析后構建抽象語法樹(AST),然后生成 LLVM IR 交由 LLVM 的中端和后端柬帕。在這個流程當中哟忍,swiftc 相比 Objective-C 使用的 clang ,swiftc 在構建完成 AST 后陷寝,生成最終的 LLVM IR 之前锅很,加入了 SIL。

SIL (Swift Intermediate Language) 基于 SSA 形式凤跑,它針對 Swift 語言設計爆安,是一門具備高級語義信息的中間語言。

image.png

當使用NSMutableArray時仔引,我們生成SIL可以發(fā)現(xiàn)扔仓,函數(shù)的派發(fā)用的是objc_method

image.png

當使用Array時,我們生成SIL可以發(fā)現(xiàn)咖耘,函數(shù)的派發(fā)用的是function_ref

image.png

看到這里当辐,就不得不說一下Swift的消息派發(fā)機制了

派發(fā)(dispatch)是一個比較通用的概念,一般是指為了完成某個目的把一個東西發(fā)送到某個位置的行為鲤看。在計算機科學中缘揪,這個術語在很多地方都會用到,比如派發(fā)一個調用給某個函數(shù)义桂,派發(fā)一個事件給一個監(jiān)聽者找筝,派發(fā)一個中斷給中斷處理程序,或者派發(fā)一個進程給 CPU慷吊。

在這篇文章中袖裕,我們主要研究 Swift 中的派發(fā),也就是派發(fā)一個調用到某個方法上溉瓶,Swift 中的方法派發(fā)包括類的方法派發(fā)和基于協(xié)議的派發(fā)急鳄。

方法派發(fā)

Swift 中類的方法的派發(fā)有以下三種方式:
● 靜態(tài)派發(fā)(Static Dispatch)
● 動態(tài)派發(fā)(Dynamic Dispatch)
● 消息派發(fā)(Messaging Dispatch)

靜態(tài)派發(fā)

靜態(tài)派發(fā)谤民,又叫做早期綁定,是指在編譯期將方法調用綁定到方法的實現(xiàn)上疾宏,這種派發(fā)方式非痴抛悖快。在編譯期坎藐,編譯器可以看到調用方和被調方的所有信息为牍,直接生成跳轉代碼,這樣在運行期就不會有其它額外的開銷岩馍。并且編譯器可以根據(jù)自己知道的信息進行優(yōu)化碉咆,比如內聯(lián),可以極大提高程序運行效率蛀恩。
在 Swift 中疫铜,結構體和枚舉的方法調用,以及被 final 標記的類和類的方法双谆,都會采用這種派發(fā)方式壳咕。

動態(tài)派發(fā)

動態(tài)派發(fā)是在運行時決定方法調用地址,因此需要有個查找方法地址的機制佃乘,在 Swift 中是通過虛函數(shù)表(Virtual Method Table)囱井,簡稱 V-Table 實現(xiàn)的驹尼,因此動態(tài)派發(fā)也被稱為表派發(fā)(Table Dispatch)
在編譯期趣避,編譯器會給每個包含動態(tài)派發(fā)方法的類型創(chuàng)建一個虛函數(shù)表,這個表會被放在內存的靜態(tài)區(qū)新翎,表中是方法名到方法實現(xiàn)地址的映射程帕。當這個類型的方法被調用時,運行時會去這個類型的虛函數(shù)表中尋找這個方法名對應的實現(xiàn)地址地啰,然后再跳轉到這個地址執(zhí)行代碼愁拭。
動態(tài)派發(fā)主要是用來實現(xiàn)繼承多態(tài),繼承多態(tài)是多態(tài)的一種亏吝。例如以下代碼:

class Animal {
    func makeNoise() {
        fatalError("此方法必須通過子類調用")
    }
}

class Dog: Animal {
    override func makeNoise() {
        print("Wang Wang!")
    }
}

class Cat: Animal {
    override func makeNoise() {
        print("Miao!")
    }
}

這段代碼在編譯時岭埠,編譯器會把 makeNoise 方法采用動態(tài)派發(fā)來處理,會給 Animal蔚鸥、Dog惜论、Cat 這三個類分別生成一個虛函數(shù)表,每個表中包含了方法實現(xiàn)地址的列表和方法列表的索引止喷。
我們可以使用一個容器來裝一些列 Animal 和其子類馆类,然后統(tǒng)一調用 makeNoise 方法,這樣的好處是忽略每個具體類型的信息弹谁,提供高級的抽象乾巧,這種做法在很多地方都很有用句喜。這種做法在面向對象中也被稱為開放遞歸(Open recursion)。

let animals: [Animal] = [Dog(), Cat()]
for animal in animals {
    animal.makeNoise()
}
// 輸出:
// Wang Wang!
// Miao!

相對于靜態(tài)派發(fā)的直接跳轉沟于,動態(tài)派發(fā)要經(jīng)過 3 個步驟咳胃,找到虛函數(shù)表、找到方法地址社裆、跳轉到方法地址拙绊,并且編譯器無法對動態(tài)派發(fā)做優(yōu)化,因此其性能要比靜態(tài)派發(fā)慢得多泳秀。
默認情況下标沪,如果繼承了一個 Objective-C 類,子類中的方法派發(fā)是采用動態(tài)派發(fā)而不是消息派發(fā)嗜傅。

消息派發(fā)

關于消息派發(fā)金句,這就是 Objective-C 的知識了,就是 OC 運行時通過 isa 和 super 指針查找方法實現(xiàn)吕嘀,并包含一系列消息轉發(fā)流程违寞,在此不表。
在 Swift 類中使用 @objc dynamic 關鍵字可以強制方法使用消息派發(fā)偶房。

協(xié)議的派發(fā)

類繼承是一個很好用的東西趁曼,但是它也存在一些問題,比如子類只能繼承一個父類棕洋,并且子類會被強制包含父類的內存布局挡闰。
Swift 提供了一個解決方案來解決上述類繼承的不足,這個解決方案提供了良好的封裝掰盘,支持多態(tài)摄悯,不會和某個特定的內存布局綁定,并且可以基于值類型工作愧捕,這就是利用面向協(xié)議編程(POP)奢驯。
協(xié)議定義了一個類型具備的能力,和繼承不同次绘,我們可以給讓一個類型符合任意多個協(xié)議瘪阁,可以讓不是自己寫的類型去符合一個協(xié)議,可以給協(xié)議提供默認實現(xiàn)邮偎。在 Swift 中管跺,類、結構體钢猛、枚舉都可以去符合協(xié)議伙菜。
用面向協(xié)議的思想來編程,我們就會摒棄類繼承命迈,而是從設計一個協(xié)議開始贩绕,比如上面的代碼火的,我們會將 Animal 設計為一個協(xié)議:

protocol Animal {
    func makeNoise()
}

然后可以用一個協(xié)議類型的變量來保存一個對象:

let animal: Animal = ...

在類繼承中,由于 Animal 是一個類淑倾,編譯器知道 Animal 占用多大的內存空間馏鹤,因此知道 animal 對象應該占用多大空間,但是如果 Animal 是一個協(xié)議類型娇哆,編譯器怎樣知道 animal 應該占用多大空間呢湃累?

class Dog: Animal {
    let name: String
    func makeNoise() { ... }
}

class Cat: Animal {
    let age: Int
    func makeNoise() { ... }
}

協(xié)議并不限制符合協(xié)議的類型的內存布局,上面代碼中碍讨,Dog 占 3 個字的大小治力,Cat 占 1 個字的大小。
Swift 引入了存在容器(Existential Container) 來解決這個問題勃黍。每個存在容器由以下幾個部分組成:
● Value Buffer ValueBuffer 占 3 個字的長度宵统,如果符合協(xié)議的對象是值類型且小于等于 3 個字,則直接放入 ValueBuffer 中覆获,如果對象是引用類型或者大于 3 個字的值類型马澈,則將對象放在堆上,在 ValueBuffer 中保存一個指向堆上對象的引用弄息。
● 一個指向 值目擊表(Value Witness Table, VWT) 的指針痊班,用來創(chuàng)建、拷貝和銷毀值摹量,表中保存了創(chuàng)建涤伐、拷貝、銷毀等函數(shù)的地址荆永,其中創(chuàng)建废亭、銷毀函數(shù)的地址僅在當對象分配在堆上時才會有国章。
● 一個指向 協(xié)議目擊表(Protocol Witness Table, PWT) 的指針具钥,每個符合了某個協(xié)議的類型都有自己的協(xié)議目擊表,保存了實現(xiàn)協(xié)議中方法的方法地址液兽。
● 如果類型符合了多個協(xié)議骂删,后面還會有第二個協(xié)議的協(xié)議目擊表指針,以及第三個四啰,第四個等宁玫。符合的協(xié)議越多,存在容器占用內存空間就越大柑晒。
這樣對于某個協(xié)議類型欧瘪,它的存在容器的大小總是相同的,編譯器即可確定它的大小匙赞。

let animal: Animal = Dog()
animal.makeNoise()

上面的代碼佛掖,animal 會被處理成一個存在容器妖碉,占用 5 個字大小的空間,由于 Dog 的大小小于等于 3 個字芥被,它被直接放入存在容器的 ValueBuffer 中欧宜,也就是頭 3 個字的空間。第 4 個字的位置是 VWT拴魄,保存了對象拷貝等函數(shù)的地址冗茸。在 PWT 中保存了 makeNoise 方法的實現(xiàn)地址,用存在容器第 5 個字的位置指向 PWT匹中。
當調用 makeNoise 時夏漱,運行時會去 PWT 中尋找方法的地址,然后跳轉指令顶捷,這其實和虛函數(shù)表差不多麻蹋。

總結

理解了 Swift 中的方法派發(fā)方式后,可以知道焊切,應該優(yōu)先使用靜態(tài)派發(fā)扮授,可以獲得最佳的性能,只有在需要和 Objective-C 代碼交互時才應該使用消息派發(fā)专肪。在需要動態(tài)派發(fā)的地方刹勃,應該優(yōu)先使用面向協(xié)議設計使用基于協(xié)議的派發(fā),然后根據(jù)具體情況使用類本身的動態(tài)派發(fā)嚎尤。


image.png

派發(fā)效率從高到底:Static dispatch > Table dispatch > Message dispatch

1.1 static dispatch

Static dispatch 靜態(tài)派發(fā)荔仁,即直接地址調用。這個函數(shù)指針在編譯芽死、鏈接完成后就確定了乏梁,存放在代碼段。優(yōu)點:派發(fā)速度最快关贵,因為需要調用的指令集少遇骑,且編譯器還有很大的優(yōu)化空間(如:函數(shù)內斂 inline)。缺點:局限也是最大的揖曾,因為缺乏動態(tài)性落萎,所以沒法支持繼承。

1.2 table dispatch

Table dispatch 函數(shù)表派發(fā)炭剪,是編譯型語言實現(xiàn)動態(tài)行為最常見的實現(xiàn)方式练链。函數(shù)表使用一個數(shù)組來存儲類聲明的每個函數(shù)的指針。大部分語言把這個稱之為 Virtual Table 虛函數(shù)表奴拦,Swift 里的協(xié)議則為 Witness Table 媒鼓。每個類維護一個虛函數(shù)表,記錄著類的所有函數(shù)。如果被 override 的話绿鸣,表里只會保存 override 后的函數(shù)瓷产。子類新增函數(shù)會被插到這個數(shù)組的最后,沒有位置可以讓 extension 安全的插入函數(shù)枚驻。優(yōu)點:可擴展缺點:速度慢濒旦,編譯器對某些含有副作用的函數(shù)無法優(yōu)化

1.3 objc_msgSend

基于 Objc RunTime 實現(xiàn),沿著實例的 isa 指針進行查找再登,找不到最后還有3次拯救機會尔邓。詳細可見:iOS_Objective-C 消息發(fā)送(消息查找 及 消息轉發(fā))過程優(yōu)點:最動態(tài)的方式,可在運行時改變函數(shù)行為锉矢。不只可以通過 swizzling 來改變梯嗽,甚至可以用 isa-swizzling 修改對象繼承關系,可以在面向對象基礎上實現(xiàn)自定義派發(fā)缺點:速度最慢

為了方便理解沽损,我整理了如下表格:


image.png

Swift數(shù)組越界的處理

回歸正題灯节,通過對Swift消息派發(fā)機制的了解,我們可以知道對于NSMutableArray系統(tǒng)用的是消息派發(fā)绵估,對于Array用的是直接(靜態(tài))派發(fā)炎疆。對于普通的class用的是函數(shù)表派發(fā)。
前面2種国裳,上文已經(jīng)介紹過了形入,函數(shù)表派發(fā)的SILl類似下面:

sil_vtable xxx {
  ......
}

防止Array的越界,給數(shù)組添加擴展,下面是一個安全的數(shù)組取值在擴展中的實現(xiàn),其他增刪改查方法類似吸祟。

import Foundation

extension Array {
  subscript (safe index: Index) -> Iterator.Element? {
        return indices.contains(index) ? self[index] : nil
    } 
}   

展望未來

  1. 方法交換的那種hook方案,無法在函數(shù)表派發(fā)和協(xié)議派發(fā)時生效音半,很明顯,函數(shù)沒有存在isa指針聯(lián)合結構體的method列表中,那么它在哪?對于虛函數(shù)表派發(fā)要怎么hook?
  2. swift的Mach-o文件和oc不一樣耳舅,以前做的從data段獲取無用類和無用函數(shù)的功能會失效,要如何兼容英妓?
  3. 蘋果正在計劃重構Foundation框架挽放,若以后Foundation和UIKit都用swift重構绍赛,我們之前做的全埋點方案蔓纠、行為日志以及其他hook方案的功能是否將會失效,要如何兼容吗蚌?
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末腿倚,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子蚯妇,更是在濱河造成了極大的恐慌敷燎,老刑警劉巖暂筝,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異硬贯,居然都是意外死亡焕襟,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門饭豹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鸵赖,“玉大人,你說我怎么就攤上這事拄衰∷剩” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵翘悉,是天一觀的道長茫打。 經(jīng)常有香客問我,道長妖混,這世上最難降的妖魔是什么老赤? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮制市,結果婚禮上诗越,老公的妹妹穿的比我還像新娘。我一直安慰自己息堂,他們只是感情好嚷狞,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著荣堰,像睡著了一般床未。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上振坚,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天薇搁,我揣著相機與錄音,去河邊找鬼渡八。 笑死啃洋,一個胖子當著我的面吹牛,可吹牛的內容都是我干的屎鳍。 我是一名探鬼主播宏娄,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼逮壁!你這毒婦竟也來了孵坚?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎卖宠,沒想到半個月后巍杈,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡扛伍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年筷畦,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片刺洒。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡汁咏,死狀恐怖,靈堂內的尸體忽然破棺而出作媚,到底是詐尸還是另有隱情攘滩,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布纸泡,位于F島的核電站漂问,受9級特大地震影響,放射性物質發(fā)生泄漏女揭。R本人自食惡果不足惜蚤假,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望吧兔。 院中可真熱鬧磷仰,春花似錦、人聲如沸境蔼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽箍土。三九已至逢享,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間吴藻,已是汗流浹背瞒爬。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留沟堡,地道東北人侧但。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像航罗,于是被迫代替她去往敵國和親禀横。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內容