swift進階四:懶加載 & 單例 & Struct

swift進階 學(xué)習(xí)大綱

上一節(jié),我們分析了屬性(存儲性、計算型)和屬性觀察者(willSet、didSet)

  1. Lazy 懶加載
  2. static 單例
  3. struct 結(jié)構(gòu)體
  4. mutating & inout
  5. 靜態(tài)函數(shù)調(diào)用
  6. 函數(shù)重載
  7. 靜態(tài)尋址

準備工作:

  1. 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
image.png
  • 可以清晰看到:懶加載屬性創(chuàng)建時,是可選值。但是在首次訪問(getter)時,進行初始賦值,返回非可選類型的值。

注意
懶加載線程不安全的。 讀寫未加鎖,多線程同時訪問(getter)時,可能多次賦值。

Q: 為何lazy修飾的Int屬性是16字節(jié):

  • 因為lazy修飾的屬性,會變成可選類型
    option: 可選類型。本質(zhì)是枚舉,值類型
    包含some<Int>none兩個枚舉類型。其中none0x0。打印
    image.png
  • 其中:none1字節(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)用gcddispatch_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)體

對比structclass:

  1. init初始化方法
    struct: 沒有init時,默認生成。創(chuàng)建init后,使用自己創(chuàng)建的
    class: 必須手動創(chuàng)建
    image.png
  1. 類型:
    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ū)地址偏移,直接讀取。

  1. 寫時復(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)
    }
}
image.png
  • 生成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)用:print。 運行時斷點,在匯編中可以看到,調(diào)用了dyld_stub_binder。 因為print函數(shù)不在當前函數(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ī)則。COC沒有,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 ,將swifttest函數(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偏移值,才是運行時正確的符號地址。

驗證

  • 公式:

    1. ASLR隨機偏移值 = 運行時基地址 - 編譯期基地址
    2. 運行時函數(shù)地址 = 編譯期函數(shù)地址 + ASLR隨機偏移值
  • 獲取信息:(每次APP啟動,運行時的數(shù)據(jù)都會變)
    程序運行斷點,輸入image list打印鏡像文件的地址。第一個鏡像文件地址就是運行時基地址。
    【運行時基地址】:0x000000010897f000
    【運行時函數(shù)地址】0x1089831d0

    image.png

【編譯期函數(shù)地址】0x1000041D0

image.png

【編譯期基地址】0x100000000Load Comand_TEXT中找到VM Address

image.png

【計算】:

  1. ASLR偏移值 = 【運行時基地址】:0x000000010897f000 - 【編譯期基地址】0x100000000 = 0x0x000000000897f000

    image.png

  2. 運行時函數(shù)地址 = 【編譯期函數(shù)地址】0x1000041D0 + ASLR偏移值0x0x000000000897f000 = 0x00000001089831d0

    image.png

與我們打印的【運行時函數(shù)地址】0x1089831d0 一抹抹一樣樣。完美??!

  • 至此。我相信你對ASLR隨機偏移值,靜態(tài)尋址都十分熟悉了。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容