上一節(jié),我們分析了屬性(存儲性、計算型)和屬性觀察者(willSet、didSet)
- Lazy 懶加載
- static 單例
- struct 結(jié)構(gòu)體
- mutating & inout
- 靜態(tài)函數(shù)調(diào)用
- 函數(shù)重載
- 靜態(tài)尋址
準備工作:
MachoView軟件: 下載地址
MachoView是查看機器執(zhí)行文件的工具。蘋果的應(yīng)用經(jīng)過LLVM編譯處理后,會輸出Mach-O格式(全稱Mach Object)的可執(zhí)行文件。在這個文件中,我們可以查看APP運行需要的代碼資源和執(zhí)行指令。
1. Lazy 懶加載
1.1 創(chuàng)建
swift中懶加載是使用Lazy進行修飾
- 必須是
var(可變存儲屬性),不可以是let(不可變屬性),也不能是option(可選值)。
class HTPerson {
// 懶加載屬性
lazy var name: String = "ht"
}
初始時,沒有值。
image.png
首次訪問后,有值
image.png所以
Lazy修飾的屬性,具備延時加載功能。(首次訪問時才加載)
1.2 大小
-
懶加載屬性的大小,與本身屬性大小不同:
swift中int(64位系統(tǒng))原本8字節(jié),但lazy修飾后,就變成16字節(jié)
image.png
1.3 SIL分析
- 將
main.swift輸出SIL文件,使用VSCode打開SIL文件:
swiftc -emit-sil main.swift >> ./main.sil

- 可以清晰看到:
懶加載屬性,創(chuàng)建時,是可選值。但是在首次訪問(getter)時,進行初始賦值,返回非可選類型的值。
注意
懶加載是線程不安全的。 讀寫未加鎖,多線程同時訪問(getter)時,可能多次賦值。
Q: 為何lazy修飾的Int屬性是16字節(jié):
- 因為
lazy修飾的屬性,會變成可選類型。
(option: 可選類型。本質(zhì)是枚舉,值類型)
包含some<Int>和none兩個枚舉類型。其中none是0x0。打印
image.png- 其中:
none占1字節(jié),some<Int>占8字節(jié)。所以實際大小(size)為9字節(jié)。- 對外遵循
align8(8字節(jié)對齊)原則,系統(tǒng)會開辟16字節(jié)空間(8的倍數(shù))來存儲真實大小為9字節(jié)的數(shù)據(jù)
(align8原則:為了避免五花八門的空間大小,增加系統(tǒng)讀取數(shù)據(jù)的困難性。所以統(tǒng)一以8字節(jié)為一個單位,進行一段一段的截取,提高讀取效率。)
lazy總結(jié)
lazy必須修飾var(可變類型)存儲屬性,- 必須有
默認初始值,但初始值會延遲到首次加載時賦值。
(所以lazy修飾的屬性,叫延遲存儲屬性,也叫懶加載屬性)延遲存儲屬性是線程不安全的(可能多次賦值)延遲存儲屬性影響實例對象的大小
2. static 單例
2. 1 類屬性
-
類屬性使用static修飾
class HTPerson {
static var age: Int = 18
}
print(HTPerson.age) // 打印18
print("end")
- 生成
SIL文件,getter方法調(diào)用了builtin "once",內(nèi)部是調(diào)用swift_once:
image.png -
swift源碼查看swift_once,內(nèi)部調(diào)用gcd的dispatch_once_f,創(chuàng)建單例,線程安全(內(nèi)部有鎖,讀寫安全)。
image.png
2.2 OC & swift 單例
- OC單例:
使用gcd創(chuàng)建,使用父類alloc初始化,攔截alloc,任何方式實例化,返回的都是單例對象。
@implementation HTPerson
static HTPerson *sharedInstance = nil;
+ (instancetype)sharedInstance{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 不使用alloc方法,而是調(diào)用[[super allocWithZone:NULL] init]
// 重載allocWithZone基本的對象分配方法,所以要借用父類(NSObject)的功能處理底層內(nèi)存分配
// 此時不管外部使用設(shè)么方式創(chuàng)建對象,最終返回的都是單例對象
sharedInstance = [[super allocWithZone:NULL] init] ;
});
return sharedInstance;
}
+(id)allocWithZone:(struct _NSZone *)zone {
return [HTPerson sharedInstance] ;
}
-(id)copyWithZone:(NSZone *)zone {
return [HTPerson sharedInstance] ;
}
-(id)mutablecopyWithZone:(NSZone *)zone {
return [HTPerson sharedInstance] ;
}
@end
- Swift單例:
直接static創(chuàng)建,將init方法藏起來(private私有重寫)。
class HTPerson {
// 創(chuàng)建單例對象
static let sharedInstance = HTPerson()
// 重寫init方法,設(shè)為私有方法
private init(){}
}
3. struct 結(jié)構(gòu)體
對比struct和class:
init初始化方法
struct: 沒有init時,默認生成。創(chuàng)建init后,使用自己創(chuàng)建的。
class: 必須手動創(chuàng)建
image.png
- 類型:
struct:值類型(不可更改,分配在棧區(qū)。copy是值拷貝(深拷貝),不共享狀態(tài))
class:引用類型(可更改,分配在堆區(qū)。copy是指針拷貝(淺拷貝),共享狀態(tài))
image.png
值類型是直接存儲值,所以讀取和拷貝都是值。引用類型是存儲指針地址,所以讀取和拷貝都是指針地址。
(需要通過指針地址去取值)檢驗:(
withUnsafeMutablePointer函數(shù)讀取對象的指針地址)
image.png
struct實例化時,是棧區(qū)alloc_stack使用let創(chuàng)建并返回self。(沒有看到malloc相關(guān)函數(shù),沒有在堆區(qū)開辟空間)。讀取值是通過棧區(qū)地址偏移,直接讀取。
- 寫時復(fù)制(Copy On Write)
- 當
struct對象賦值給一個新對象時,2個對象指向的地址是同一個。- 只有當新對象
被引用時,才會在新的內(nèi)存空間,完整拷貝一個對象,并存儲新值。
目的:提升性能,節(jié)約內(nèi)存空間
(2個完全一樣的對象,沒必要占用2個內(nèi)存空間,當它真正被使用時,再開辟空間)
struct值類型中,應(yīng)避免包含引用類型對象。因為保存的是引用類型的地址,同樣會調(diào)用strong_retain對引用類型對象進行引用計數(shù)+1。我們默認struct內(nèi)部都使用值類型,減少使用的困擾
(可使用CFGetRetainCount()打印引用計數(shù),進行觀察)
4. mutating & inout
-
struct屬性不可修改,除非有mutating聲明。
struct HTStack {
var items = [Int]()
// 使用 mutating 修飾函數(shù)
mutating func push(item: Int) {
self.items.append(item)
}
}

- 生成
SIL文件,可以查看到mutating修飾的函數(shù),內(nèi)部使用@inout聲明了入?yún)?,讀取的是入?yún)⒌刂?/code>,而不是值。所以可以更改。
image.png
5. 靜態(tài)函數(shù)調(diào)用
struct是值類型,屬性是直接讀取,那它怎么調(diào)用函數(shù)呢?
創(chuàng)建
測試項目,編譯生成Demo(.o可執(zhí)行文件):
image.png打開
終端,輸入nm空格,拖入.o可執(zhí)行文件讀取完整路徑,回車。
可以看到eat函數(shù)編譯后的符號名(_$s4Demo8HTPersonV3eatyyF)。
image.png使用
machoView軟件,打開編譯好的.o文件,點擊Assembly,搜索eat,比對函數(shù)名,定位eat函數(shù)在符號表中的位置:
image.png選擇
Symbol Table下的Symbol符號表,搜索eat,比對Value,確實可通過Assembly中記錄的地址找到eat函數(shù)在符號表中的位置。而String Table Index記錄了該符號在字符串表中的位置。(第2位開始)
image.png選擇
String Table字符串表,核對可發(fā)現(xiàn),從第2個字符開始,就是eat函數(shù)命名重整后的符號名:_$s4Demo8HTPersonV3eatyyF
image.png
- 總結(jié):
- 項目
編譯后,machoView查看.o文件。c語言函數(shù)和結(jié)構(gòu)體函數(shù)都是靜態(tài)調(diào)用(直接調(diào)用函數(shù)地址)靜態(tài)函數(shù)的調(diào)用都在__TEXT段中,__TEXT中:記錄符號在Symbol符號表中的位置Symbol符號表中:記錄符號在StringTable字符串表中的位置StringTable字符串表:以字符串形式,存儲所有變量名和函數(shù)名
靜態(tài)執(zhí)行,執(zhí)行效率非常高!(靜態(tài)調(diào)用,就是地址調(diào)用)
DSYM文件: 用于還原符號表。捕獲崩潰,定位線上BUG
- 選擇
release模式,編譯,會多生成DSYM文件,使用MachoView查看.O文件。發(fā)現(xiàn)找不到eat函數(shù),比Debug模式下少很多內(nèi)容。
重點
靜態(tài)鏈接函數(shù)是不需要符號的,一旦地址確定,可直接調(diào)用。在
Debug調(diào)試環(huán)節(jié)可以便于開發(fā),但release模式下(iOS項目),為了減小包體積,strip(剝掉)這些靜態(tài)鏈接函數(shù)的符號。
release包中存在的符號,是不能直接確定地址的。
如:Lazy Symbol Pointers(懶加載的符號):在首次被調(diào)用時才會生成地址,所以不能strip掉。
再比如:外部庫的函數(shù)調(diào)用:運行時斷點,在匯編中可以看到,調(diào)用了dyld_stub_binder。 因為不在當前函數(shù)庫中,編譯文件記錄了print函數(shù)所在的庫,并進行了動態(tài)綁定。 在調(diào)用這個函數(shù)時,需要沿著這個路徑去找?guī)?/code>,找到它真正的地址。再進行調(diào)用。
靜態(tài)鏈接函數(shù)的名稱和地址,在編譯期就已經(jīng)確定,可優(yōu)化,直接使用地址。
(可多次編譯查看,會發(fā)現(xiàn)代碼沒改變時,名稱和地址都是不會變的)
而動態(tài)庫的鏈接,是dyld在運行時去動態(tài)查找的,無法直接確定地址。所以需要符號表記錄它存放在哪個庫的哪個地址,再順應(yīng)摸瓜去找到并調(diào)用它。
6. 函數(shù)重載
函數(shù)重載: 使用相同的函數(shù)名,但入?yún)⒉灰粯?/code>的函數(shù)。
struct HTPerson {
// 函數(shù)名都是eat,但參數(shù)類型不一樣。
func eat(num: Int){
print("吃\(num)個")
}
func eat(name: String) {
print("吃\(name)")
}
}
Q: 為什么C、OC語言不支持函數(shù)重載,而C++、swift支持函數(shù)重載?
區(qū)別: 是否有
命名重整規(guī)則。C和OC沒有,C++和swift有。
- 通過打印
MachO可執(zhí)行文件的符號表。就清楚了:
【方法】 打開
終端,輸入nm,拖入MachO可執(zhí)行文件讀取完整路徑,回車:
- 我們以
各類語言的test函數(shù)為例:func test() { }【C語言】函數(shù)名:
_test
image.png【OC語言】函數(shù)名:
-[HTPerson test]
image.png【C++語言】函數(shù)名:
__Z4testv
image.png【Swift語言】函數(shù)名:
_$s4Demo4testyyF
image.png這就是
C++、swift支持函數(shù)重載,而C、OC語言不支持的原因。
- 因為他們
函數(shù)符號(名稱)沒處理,相同命名函數(shù),無法區(qū)分。
拓展
可以通過xcrun swift-demangle s4Demo4testyyF,將swift的test函數(shù)符號名還原:
(其中s4Demo4testyyF是命名重整后的test函數(shù))
image.png
7. 靜態(tài)尋址
-
新建一個iOS項目,以
test函數(shù)為例,在調(diào)用test函數(shù)處加斷點。
image.png -
運行代碼,打開匯編模式,可以看到test函數(shù)在運行時的調(diào)用地址為0x10cbd51e0
image.png -
編譯后,用MachoView中打開.o文件,在__TEXT中搜索tes,找到編譯后的test函數(shù)地址:0x1000041D0
image.png
發(fā)現(xiàn)test函數(shù)調(diào)用地址在編譯期和運行時有偏差(ASLR隨機地址偏移)。
ASLR:隨機地址偏移
- 為
保證APP的數(shù)據(jù)安全,每次APP啟動時,都會隨機生成一個地址偏移值。
運行時查找所有符號,必須在編譯期確定的符號地址上,加上隨機生成的ASLR偏移值,才是運行時正確的符號地址。
驗證
公式:
ASLR隨機偏移值=運行時基地址-編譯期基地址運行時函數(shù)地址=編譯期函數(shù)地址+ASLR隨機偏移值獲取信息:(每次APP啟動,運行時的數(shù)據(jù)都會變)
程序運行到斷點,輸入image list打印鏡像文件的地址。第一個鏡像文件地址就是運行時的基地址。
【運行時基地址】:0x000000010897f000
【運行時函數(shù)地址】0x1089831d0
image.png【編譯期函數(shù)地址】
0x1000041D0
image.png【編譯期基地址】
0x100000000(Load Comand的_TEXT中找到VM Address)
image.png【計算】:
ASLR偏移值 = 【運行時基地址】:
0x000000010897f000- 【編譯期基地址】0x100000000=0x0x000000000897f000
image.png運行時函數(shù)地址 = 【編譯期函數(shù)地址】
0x1000041D0+ ASLR偏移值0x0x000000000897f000= 0x00000001089831d0
image.png與我們打印的【運行時函數(shù)地址】
0x1089831d0一抹抹一樣樣。完美??!
- 至此。我相信你對
ASLR隨機偏移值,靜態(tài)尋址都十分熟悉了。



























