Swift-進(jìn)階 02:類唇礁、對象、屬性

Swift 進(jìn)階之路 文章匯總

本文主要介紹以下幾點(diǎn)

  • 通過SIL來理解對象的創(chuàng)建

  • Swift類結(jié)構(gòu)分析

  • 存儲屬性 & 計算屬性

  • 延遲存儲屬性 & 單例創(chuàng)建方式

SIL

在底層流程中娶吞,OC代碼和SWift代碼時通過不同的編譯器進(jìn)行編譯,然后通過LLVM械姻,生成.o可執(zhí)行文件妒蛇,如下所示

SIL-1

下面是Swift中的編譯流程欢揖,其中SIL(Swift Intermediate Language)陶耍,是Swift編譯過程中的中間代碼,主要用于進(jìn)一步分析和優(yōu)化Swift代碼她混。如下圖所示烈钞,SIL位于在ASTLLVM IR之間

SIL-2

注意:這里需要說明一下泊碑,Swift與OC的區(qū)別在于 Swift生成了高級的SIL

我們可以通過swiftc -h終端命令,查看swiftc的所有命令

SIL-3

例如:在main.swift文件定義如下代碼

class CJLTeacher{
    var age: Int = 18
    var name: String = "CJL"
}

var t = CJLTeacher()
  • 查看抽象語法樹:swiftc -dump-ast main.swift
    SIL-4
  • 生成SIL文件:swiftc -emit-sil main.swift >> ./main.sil && code main.sil毯欣,其中main的入口函數(shù)如下
// main
//`@main`:標(biāo)識當(dāng)前main.swift的`入口函數(shù)`馒过,SIL中的標(biāo)識符名稱以`@`作為前綴
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
//`%0、%1` 在SIL中叫做寄存器酗钞,可以理解為開發(fā)中的常量腹忽,一旦賦值就不可修改,如果還想繼續(xù)使用砚作,就需要不斷的累加數(shù)字(注意:這里的寄存器窘奏,與`register read`中的寄存器是有所區(qū)別的,這里是指`虛擬寄存器`葫录,而`register read`中是`真寄存器`)
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
//`alloc_global`:創(chuàng)建一個`全局變量`着裹,即代碼中的`t`
  alloc_global @$s4main1tAA10CJLTeacherCvp        // id: %2
//`global_addr`:獲取全局變量地址,并賦值給寄存器%3
  %3 = global_addr @$s4main1tAA10CJLTeacherCvp : $*CJLTeacher // user: %7
//`metatype`獲取`CJLTeacher`的`MetaData`賦值給%4
  %4 = metatype $@thick CJLTeacher.Type           // user: %6
//將`__allocating_init`的函數(shù)地址賦值給 %5
  // function_ref CJLTeacher.__allocating_init()
  %5 = function_ref @$s4main10CJLTeacherCACycfC : $@convention(method) (@thick CJLTeacher.Type) -> @owned CJLTeacher // user: %6
//`apply`調(diào)用 `__allocating_init` 初始化一個變量压昼,賦值給%6
  %6 = apply %5(%4) : $@convention(method) (@thick CJLTeacher.Type) -> @owned CJLTeacher // user: %7
//將%6的值存儲到%3求冷,即全局變量的地址(這里與前面的%3形成一個閉環(huán))
  store %6 to %3 : $*CJLTeacher                   // id: %7
//構(gòu)建`Int`,并`return`
  %8 = integer_literal $Builtin.Int32, 0          // user: %9
  %9 = struct $Int32 (%8 : $Builtin.Int32)        // user: %10
  return %9 : $Int32                              // id: %10
} // end sil function 'main'

注意:code命令是在.zshrc中做了如下配置窍霞,可以在終端中指定軟件打開相應(yīng)文件

$ open .zshrc
//****** 添加以下別名
alias subl='/Applications/SublimeText.app/Contents/SharedSupport/bin/subl'
alias code='/Applications/Visual\ Studio\ Code.app/Contents/Resources/app/bin/code'

//****** 使用
$ code main.sil

//如果想SIL文件高亮匠题,需要安裝插件:VSCode SIL
  • 從SIL文件中,可以看出但金,代碼是經(jīng)過混淆的韭山,可以通過以下命令還原,以s4main1tAA10CJLTeacherCvp為例:xcrun swift-demangle s4main1tAA10CJLTeacherCvp

    SIL-5

  • 在SIL文件中搜索s4main10CJLTeacherCACycfC冷溃,其內(nèi)部實現(xiàn)主要是分配內(nèi)存+初始化變量

    • allocing_ref:創(chuàng)建一個CJLTeacher的實例對象钱磅,當(dāng)前實例對象的引用計數(shù)為1
    • 調(diào)用init方法
//********* main入口函數(shù)中代碼 *********
%5 = function_ref @$s4main10CJLTeacherCACycfC : $@convention(method) (@thick CJLTeacher.Type) -> @owned CJLTeacher 

// s4main10CJLTeacherCACycfC 實際就是__allocating_init()
// CJLTeacher.__allocating_init()
sil hidden [exact_self_class] @$s4main10CJLTeacherCACycfC : $@convention(method) (@thick CJLTeacher.Type) -> @owned CJLTeacher {
// %0 "$metatype"
bb0(%0 : $@thick CJLTeacher.Type):
// 堆上分配內(nèi)存空間
  %1 = alloc_ref $CJLTeacher                      // user: %3
  // function_ref CJLTeacher.init() 初始化當(dāng)前變量
  %2 = function_ref @$s4main10CJLTeacherCACycfc : $@convention(method) (@owned CJLTeacher) -> @owned CJLTeacher // user: %3
  // 返回
  %3 = apply %2(%1) : $@convention(method) (@owned CJLTeacher) -> @owned CJLTeacher // user: %4
  return %3 : $CJLTeacher                         // id: %4
} // end sil function '$s4main10CJLTeacherCACycfC'

SIL語言對于Swift源碼的分析是非常重要的,關(guān)于其更多的語法信息似枕,可以在這個網(wǎng)站進(jìn)行查詢

符號斷點(diǎn)調(diào)試

  • 在demo中設(shè)置_allocing_init符號斷點(diǎn)
    SIL-6

    發(fā)現(xiàn)其內(nèi)部調(diào)用的是swift_allocObject
    SIL-7

源碼調(diào)試

下面我們就通過swift_allocObject來探索swift中對象的創(chuàng)建過程

  • REPL(命令交互行盖淡,類似于python的,可以在這里編寫代碼)中編寫如下代碼(也可以拷貝)凿歼,并搜索swift_allocObject函數(shù)加一個斷點(diǎn)褪迟,然后定義一個實例對象t

    SIL-8

  • 斷點(diǎn)斷住,查看左邊local有詳細(xì)的信息


    SIL-9
  • 其中requiredSize是分配的實際內(nèi)存大小,為40

  • requiredAlignmentMask是swift中的字節(jié)對齊方式答憔,這個和OC中是一樣的味赃,必須是8的倍數(shù),不足的會自動補(bǔ)齊虐拓,目的是以空間換時間心俗,來提高內(nèi)存操作效率

swift_allocObject 源碼分析

swift_allocObject的源碼如下,主要有以下幾部分

  • 通過swift_slowAlloc分配內(nèi)存,并進(jìn)行內(nèi)存字節(jié)對齊
  • 通過new + HeapObject + metadata初始化一個實例對象
  • 函數(shù)的返回值是HeapObject類型城榛,所以當(dāng)前對象的內(nèi)存結(jié)構(gòu)就是HeapObject的內(nèi)存結(jié)構(gòu)
static HeapObject *_swift_allocObject_(HeapMetadata const *metadata,
                                       size_t requiredSize,
                                       size_t requiredAlignmentMask) {
  assert(isAlignmentMask(requiredAlignmentMask));
  auto object = reinterpret_cast<HeapObject *>(
      swift_slowAlloc(requiredSize, requiredAlignmentMask));//分配內(nèi)存+字節(jié)對齊

  // NOTE: this relies on the C++17 guaranteed semantics of no null-pointer
  // check on the placement new allocator which we have observed on Windows,
  // Linux, and macOS.
  new (object) HeapObject(metadata);//初始化一個實例對象

  // If leak tracking is enabled, start tracking this object.
  SWIFT_LEAKS_START_TRACKING_OBJECT(object);

  SWIFT_RT_TRACK_INVOCATION(object, swift_allocObject);

  return object;
}
  • 進(jìn)入swift_slowAlloc函數(shù)揪利,其內(nèi)部主要是通過malloc中分配size大小的內(nèi)存空間,并返回內(nèi)存地址吠谢,主要是用于存儲實例變量
void *swift::swift_slowAlloc(size_t size, size_t alignMask) {
  void *p;
  // This check also forces "default" alignment to use AlignedAlloc.
  if (alignMask <= MALLOC_ALIGN_MASK) {
#if defined(__APPLE__)
    p = malloc_zone_malloc(DEFAULT_ZONE(), size);
#else
    p = malloc(size);// 堆中創(chuàng)建size大小的內(nèi)存空間土童,用于存儲實例變量
#endif
  } else {
    size_t alignment = (alignMask == ~(size_t(0)))
                           ? _swift_MinAllocationAlignment
                           : alignMask + 1;
    p = AlignedAlloc(size, alignment);
  }
  if (!p) swift::crash("Could not allocate memory.");
  return p;
}
  • 進(jìn)入HeapObject初始化方法,需要兩個參數(shù):metadata工坊、refCounts
    源碼分析-1
    • 其中metadata類型是HeapMetadata献汗,是一個指針類型,占8字節(jié)
    • refCounts(引用計數(shù)王污,類型是InlineRefCounts罢吃,而InlineRefCounts是一個類RefCounts的別名,占8個字節(jié))昭齐,swift采用arc引用計數(shù)
      源碼分析-2

總結(jié)

  • 對于實例對象t來說尿招,其本質(zhì)是一個HeapObject 結(jié)構(gòu)體,默認(rèn)16字節(jié)內(nèi)存大汹寮荨(metadata 8字節(jié) + refCounts 8字節(jié))就谜,與OC的對比如下

    • OC中實例對象的本質(zhì)是結(jié)構(gòu)體,是以objc_object為模板繼承的里覆,其中有一個isa指針丧荐,占8字節(jié)

    • Swift中實例對象,默認(rèn)的比OC中多了一個refCounted引用計數(shù)大小喧枷,默認(rèn)屬性占16字節(jié)

  • Swift中對象的內(nèi)存分配流程是:__allocating_init --> swift_allocObject_ --> _swift_allocObject --> swift_slowAlloc --> malloc

  • init在其中的職責(zé)就是初始化變量虹统,這點(diǎn)與OC中是一致的

針對上面的分析,我們還遺留了兩個問題:metadata是什么隧甚,40是怎么計算的车荔?下面來繼續(xù)探索

在demo中,我們可以通過Runtime方法獲取類的內(nèi)存大小

源碼分析-3

這點(diǎn)與在源碼調(diào)試時左邊local的requiredSize值是相等的戚扳,從HeapObject的分析中我們知道了忧便,一個類在沒有任何屬性的情況下,默認(rèn)占用16字節(jié)大小帽借,

對于Int珠增、String類型,進(jìn)入其底層定義,兩個都是結(jié)構(gòu)體類型宜雀,那么是否都是8字節(jié)呢切平?可以通過打印其內(nèi)存大小來驗證

//********* Int底層定義 *********
@frozen public struct Int : FixedWidthInteger, SignedInteger {...}

//********* String底層定義 *********
@frozen public struct String {...}

//********* 驗證 *********
print(MemoryLayout<Int>.stride)
print(MemoryLayout<String>.stride)

//********* 打印結(jié)果 *********
8
16

從打印的結(jié)果中可以看出握础,Int類型占8字節(jié)辐董,String類型占16字節(jié)(后面文章會進(jìn)行詳細(xì)講解),這點(diǎn)與OC中是有所區(qū)別的

所以這也解釋了為什么CJLTeacher的內(nèi)存大小等于40禀综,即40 = metadata(8字節(jié)) +refCount(8字節(jié))+ Int(8字節(jié))+ String(16字節(jié))

這里驗證了40的來源简烘,但是metadata是什么還不知道苔严,繼續(xù)往下分析

探索Swift中類的結(jié)構(gòu)

在OC中類是從objc_class模板繼承過來的,具體的參考這篇文章iOS-底層原理 08:類 & 類結(jié)構(gòu)分析

而在Swift中孤澎,類的結(jié)構(gòu)在底層是HeapObject届氢,其中有 metadata + refCounts

HeapMetadata類型分析

下面就來分析metadata,看看它到底是什么覆旭?

  • 進(jìn)入HeapMetadata定義退子,是TargetHeapMetaData類型的別名,接收了一個參數(shù)Inprocess
using HeapMetadata = TargetHeapMetaData<Inprocess>;
  • 進(jìn)入TargetHeapMetaData定義型将,其本質(zhì)是一個模板類型寂祥,其中定義了一些所需的數(shù)據(jù)結(jié)構(gòu)。這個結(jié)構(gòu)體中沒有屬性七兜,只有初始化方法丸凭,傳入了一個MetadataKind類型的參數(shù)(該結(jié)構(gòu)體沒有,那么只有在父類中了)這里的kind就是傳入的Inprocess
//模板類型
template <typename Runtime>
struct TargetHeapMetadata : TargetMetadata<Runtime> {
  using HeaderType = TargetHeapMetadataHeader<Runtime>;

  TargetHeapMetadata() = default;
  //初始化方法
  constexpr TargetHeapMetadata(MetadataKind kind)
    : TargetMetadata<Runtime>(kind) {}
#if SWIFT_OBJC_INTEROP
  constexpr TargetHeapMetadata(TargetAnyClassMetadata<Runtime> *isa)
    : TargetMetadata<Runtime>(isa) {}
#endif
};
  • 進(jìn)入TargetMetaData定義腕铸,有一個kind屬性惜犀,kind的類型就是之前傳入的Inprocess。從這里可以得出狠裹,對于kind虽界,其類型就是unsigned long,主要用于區(qū)分是哪種類型的元數(shù)據(jù)
//******** TargetMetaData 定義 ********
struct TargetMetaData{
   using StoredPointer = typename Runtime: StoredPointer;
    ...
    
    StoredPointer kind;
}

//******** Inprocess 定義 ********
struct Inprocess{
    ...
    using StoredPointer = uintptr_t;
    ...
}

//******** uintptr_t 定義 ********
typedef unsigned long uintptr_t;

TargetHeapMetadata酪耳、TargetMetaData定義中浓恳,均可以看出初始化方法中參數(shù)kind的類型是MetadataKind

  • 進(jìn)入MetadataKind定義,里面有一個#include "MetadataKind.def"碗暗,點(diǎn)擊進(jìn)入颈将,其中記錄了所有類型的元數(shù)據(jù),所以kind種類總結(jié)如下
name value
Class 0x0
Struct 0x200
Enum 0x201
Optional 0x202
ForeignClass 0x203
Opaque 0x300
Tuple 0x301
Function 0x302
Existential 0x303
Metatype 0x304
ObjCClassWrapper 0x305
ExistentialMetatype 0x306
HeapLocalVariable 0x400
HeapGenericLocalVariable 0x500
ErrorObject 0x501
LastEnumerated 0x7FF
  • 回到TargetMetaData結(jié)構(gòu)體定義中言疗,找方法getClassObject晴圾,在該方法中去匹配kind返回值是TargetClassMetadata類型
    • 如果是Class,則直接對this(當(dāng)前指針噪奄,即metadata)強(qiáng)轉(zhuǎn)為ClassMetadata
 const TargetClassMetadata<Runtime> *getClassObject() const;
 
//******** 具體實現(xiàn) ********
template<> inline const ClassMetadata *
  Metadata::getClassObject() const {
    //匹配kind
    switch (getKind()) {
      //如果kind是class
    case MetadataKind::Class: {
      // Native Swift class metadata is also the class object.
      //將當(dāng)前指針強(qiáng)轉(zhuǎn)為ClassMetadata類型
      return static_cast<const ClassMetadata *>(this);
    }
    case MetadataKind::ObjCClassWrapper: {
      // Objective-C class objects are referenced by their Swift metadata wrapper.
      auto wrapper = static_cast<const ObjCClassWrapperMetadata *>(this);
      return wrapper->Class;
    }
    // Other kinds of types don't have class objects.
    default:
      return nullptr;
    }
  }

這一點(diǎn)死姚,我們可以通過lldb來驗證

  • po metadata->getKind(),得到其kind是Class
  • po metadata->getClassObject()勤篮、x/8g 0x0000000110efdc70都毒,這個地址中存儲的是元數(shù)據(jù)信息!
    源碼分析-4

所以,TargetMetadataTargetClassMetadata 本質(zhì)上是一樣的碰缔,因為在內(nèi)存結(jié)構(gòu)中账劲,可以直接進(jìn)行指針的轉(zhuǎn)換,所以可以說,我們認(rèn)為的結(jié)構(gòu)體瀑焦,其實就是TargetClassMetadata

  • 進(jìn)入TargetClassMetadata定義腌且,繼承自TargetAnyClassMetadata,有以下這些屬性榛瓮,這也是類結(jié)構(gòu)的部分
template <typename Runtime>
struct TargetClassMetadata : public TargetAnyClassMetadata<Runtime> {
    ...
    //swift特有的標(biāo)志
    ClassFlags Flags;
    //實力對象內(nèi)存大小
    uint32_t InstanceSize;
    //實例對象內(nèi)存對齊方式
    uint16_t InstanceAlignMask;
    //運(yùn)行時保留字段
    uint16_t Reserved;
    //類的內(nèi)存大小
    uint32_t ClassSize;
    //類的內(nèi)存首地址
    uint32_t ClassAddressPoint;
  ...
}
  • 進(jìn)入TargetAnyClassMetadata定義铺董,繼承自TargetHeapMetadata
template <typename Runtime>
struct TargetAnyClassMetadata : public TargetHeapMetadata<Runtime> {
    ...
    ConstTargetMetadataPointer<Runtime, swift::TargetClassMetadata> Superclass;
    TargetPointer<Runtime, void> CacheData[2];
    StoredSize Data;
    ...
}

總結(jié)

綜上所述,當(dāng)metadatakind為Class時禀晓,有如下繼承鏈:

源碼分析-5

  • 當(dāng)前類返回的實際類型是 TargetClassMetadata,而TargetMetaData中只有一個屬性kind精续,TargetAnyClassMetaData中有4個屬性,分別是kind粹懒, superclass驻右,cacheData、data(圖中未標(biāo)出)
  • 當(dāng)前Class在內(nèi)存中所存放的屬性TargetClassMetadata屬性 + TargetAnyClassMetaData屬性 + TargetMetaData屬性 構(gòu)成崎淳,所以得出的metadata的數(shù)據(jù)結(jié)構(gòu)體如下所示
struct swift_class_t: NSObject{
    void *kind;//相當(dāng)于OC中的isa堪夭,kind的實際類型是unsigned long
    void *superClass;
    void *cacheData;
    void *data;
    uint32_t flags; //4字節(jié)
    uint32_t instanceAddressOffset;//4字節(jié)
    uint32_t instanceSize;//4字節(jié)
    uint16_t instanceAlignMask;//2字節(jié)
    uint16_t reserved;//2字節(jié)
    
    uint32_t classSize;//4字節(jié)
    uint32_t classAddressOffset;//4字節(jié)
    void *description;
    ...
}

與OC對比

  • 實例對象 & 類
    • OC中的實例對象本質(zhì)結(jié)構(gòu)體,是通過底層的objc_object模板創(chuàng)建拣凹,類是繼承自objc_class

    • Swift中的實例對象本質(zhì)也是結(jié)構(gòu)體森爽,類型是HeapObject,比OC多了一個refCounts

  • 方法列表
    • OC中的方法存儲在objc_class結(jié)構(gòu)體class_rw_tmethodList

    • swift中的方法存儲在 metadata 元數(shù)據(jù)中

  • 引用計數(shù)
    • OC中的ARC維護(hù)的是散列表

    • Swift中的ARC是對象內(nèi)部有一個refCounts屬性

Swift屬性

在swift中嚣镜,屬性主要分為以下幾種

  • 存儲屬性

  • 計算屬性

  • 延遲存儲屬性

  • 類型屬性

存儲屬性

存儲屬性爬迟,又分兩種:

  • 要么是常量存儲屬性,即let修飾

  • 要么是變量存儲屬性菊匿,即var修飾

定義如下代碼

class CJLTeacher{
    var age: Int = 18
    var name: String = "CJL"
}

let t = CJLTeacher()

其中代碼中的age付呕、name來說,都是變量存儲屬性跌捆,這一點(diǎn)可以在SIL中體現(xiàn)

class CJLTeacher {
    //_hasStorage 表示是存儲屬性
  @_hasStorage @_hasInitialValue var age: Int { get set }
  @_hasStorage @_hasInitialValue var name: String { get set }
  @objc deinit
  init()
}

存儲屬性特征:會占用占用分配實例對象的內(nèi)存空間

下面我們同斷點(diǎn)調(diào)試來驗證

  • po t

  • x/8g 內(nèi)存地址徽职,即HeapObject存儲的地址


    屬性-1

    屬性-2

計算屬性

計算屬性:是指不占用內(nèi)存空間,本質(zhì)是set/get方法的屬性

我們通過一個demo來說明佩厚,以下寫法正確嗎姆钉?

class CJLTeacher{
    var age: Int{
        get{
            return 18
        }
        set{
            age = newValue
        }
    }
}

在實際編程中,編譯器會報以下警告抄瓦,其意思是在age的set方法中又調(diào)用了age.set

屬性-3

然后運(yùn)行發(fā)現(xiàn)崩潰了潮瓶,原因是age的set方法中調(diào)用age.set導(dǎo)致了循環(huán)引用,即遞歸
屬性-4

驗證:不占內(nèi)存
對于其不占用內(nèi)存空間這一特征钙姊,我們可以通過以下案例來驗證毯辅,打印以下類的內(nèi)存大小

class Square{
    var width: Double = 8.0
    var area: Double{
        get{
            //這里的return可以省略,編譯器會自動推導(dǎo)
            return width * width
        }
        set{
            width = sqrt(newValue)
        }
    }
}

print(class_getInstanceSize(Square.self))

//********* 打印結(jié)果 *********
24

從結(jié)果可以看出類Square的內(nèi)存大小是24煞额,等于 (metadata + refCounts)類自帶16字節(jié) + width(8字節(jié)) = 24思恐,是沒有加上area的赤屋。從這里可以證明 area屬性沒有占有內(nèi)存空間

驗證:本質(zhì)是set/get方法

  • 將main.swift轉(zhuǎn)換為SIL文件:swiftc -emit-sil main.swift >> ./main.sil
  • 查看SIL文件壁袄,對于存儲屬性,有_hasStorage的標(biāo)識符
class Square {
  @_hasStorage @_hasInitialValue var width: Double { get set }
  var area: Double { get set }
  @objc deinit
  init()
}
  • 對于計算屬性媚媒,SIL中只有setter嗜逻、getter方法
    屬性-5

屬性觀察者(didSet、willSet)

  • willSet:新值存儲之前調(diào)用 newValue

  • didSet:新值存儲之后調(diào)用 oldValue

驗證

  • 可以通過demo來驗證
class CJLTeacher{
    var name: String = "測試"{
        //新值存儲之前調(diào)用
        willSet{
            print("willSet newValue \(newValue)")
        }
        //新值存儲之后調(diào)用
        didSet{
            print("didSet oldValue \(oldValue)")
        }
    }
}
var t = CJLTeacher()
t.name = "CJL"

//**********打印結(jié)果*********
willSet newValue CJL
didSet oldValue 測試
  • 也可以通過編譯來驗證缭召,將main.swift編譯成mail.sil栈顷,在sil文件中找nameset方法
    屬性-6

問題1:init方法中是否會觸發(fā)屬性觀察者?
以下代碼中嵌巷,init方法中設(shè)置name萄凤,是否會觸發(fā)屬性觀察者?

class CJLTeacher{
    var name: String = "測試"{
        //新值存儲之前調(diào)用
        willSet{
            print("willSet newValue \(newValue)")
        }
        //新值存儲之后調(diào)用
        didSet{
            print("didSet oldValue \(oldValue)")
        }
    }
    
    init() {
        self.name = "CJL"
    }
}

運(yùn)行結(jié)果發(fā)現(xiàn)搪哪,并沒有走willSet靡努、didSet中的打印方法,所以有以下結(jié)論:

  • init方法中晓折,如果調(diào)用屬性惑朦,是不會觸發(fā)屬性觀察者的
  • init中主要是初始化當(dāng)前變量,除了默認(rèn)的前16個字節(jié)漓概,其他屬性會調(diào)用memset清理內(nèi)存空間(因為有可能是臟數(shù)據(jù)漾月,即被別人用過),然后才會賦值

【總結(jié)】:初始化器(即init方法設(shè)置)和定義時設(shè)置默認(rèn)值(即在didSet中調(diào)用其他屬性值)都不會觸發(fā)

問題2:哪里可以添加屬性觀察者胃珍?

主要有以下三個地方可以添加:

  • 1梁肿、中定義的存儲屬性
  • 2、通過類繼承的存儲屬性
class CJLMediumTeacher: CJLTeacher{
    override var age: Int{
        //新值存儲之前調(diào)用
        willSet{
            print("willSet newValue \(newValue)")
        }
        //新值存儲之后調(diào)用
        didSet{
            print("didSet oldValue \(oldValue)")
        }
    }
}
  • 3觅彰、通過類繼承的計算屬性
class CJLTeacher{
    var age: Int = 18
    
    var age2: Int {
        get{
            return age
        }
        set{
            self.age = newValue
        }
    }
}
var t = CJLTeacher()


class CJLMediumTeacher: CJLTeacher{
    override var age: Int{
        //新值存儲之前調(diào)用
        willSet{
            print("willSet newValue \(newValue)")
        }
        //新值存儲之后調(diào)用
        didSet{
            print("didSet oldValue \(oldValue)")
        }
    }
    
    override var age2: Int{
        //新值存儲之前調(diào)用
        willSet{
            print("willSet newValue \(newValue)")
        }
        //新值存儲之后調(diào)用
        didSet{
            print("didSet oldValue \(oldValue)")
        }
    }
}

問題3:子類和父類的計算屬性同時存在didset吩蔑、willset時,其調(diào)用順序是什么填抬?

有以下代碼哥纫,其調(diào)用順序是什么?

class CJLTeacher{
    var age: Int = 18{
        //新值存儲之前調(diào)用
        willSet{
            print("父類 willSet newValue \(newValue)")
        }
        //新值存儲之后調(diào)用
        didSet{
            print("父類 didSet oldValue \(oldValue)")
        }
    }
    
    var age2: Int {
        get{
            return age
        }
        set{
            self.age = newValue
        }
    }
}


class CJLMediumTeacher: CJLTeacher{
    override var age: Int{
        //新值存儲之前調(diào)用
        willSet{
            print("子類 newValue \(newValue)")
        }
        //新值存儲之后調(diào)用
        didSet{
            print("子類 didSet oldValue \(oldValue)")
        }
    }
    
}

var t = CJLMediumTeacher()
t.age = 20

運(yùn)行結(jié)果如下:


屬性-7

結(jié)論:對于同一個屬性痴奏,子類和父類都有屬性觀察者蛀骇,其順序是:先子類willset,后父類willset读拆,在父類didset擅憔, 子類的didset,即:子父 父子

問題4:子類調(diào)用了父類的init檐晕,是否會觸發(fā)觀察屬性暑诸?

在問題3的基礎(chǔ)蚌讼,修改CJLMediumTeacher

class CJLMediumTeacher: CJLTeacher{
    override var age: Int{
        //新值存儲之前調(diào)用
        willSet{
            print("子類 willSet newValue \(newValue)")
        }
        //新值存儲之后調(diào)用
        didSet{
            print("子類 didSet oldValue \(oldValue)")
        }
    }
    
    override init() {
        super.init()
        self.age = 20
    }
}

//****** 打印結(jié)果 ******
子類 willSet newValue 20
父類 willSet newValue 20
父類 didSet oldValue 18
子類 didSet oldValue 18

從打印結(jié)果發(fā)現(xiàn),會觸發(fā)屬性觀察者个榕,主要是因為子類調(diào)用了父類init篡石,已經(jīng)初始化過了,而初始化流程保證了所有屬性都有值(即super.init確保變量初始化完成了)西采,所以可以觀察屬性了

延遲屬性

延遲屬性主要有以下幾點(diǎn)說明:

  • 1凰萨、使用lazy修飾的存儲屬性

  • 2、延遲屬性必須有一個默認(rèn)的初始值

  • 3械馆、延遲存儲在第一次訪問的時候才被賦值

  • 4胖眷、延遲存儲屬性并不能保證線程安全

  • 5、延遲存儲屬性對實例對象大小的影響

下面來一一進(jìn)行分析

1霹崎、使用lazy修飾的存儲屬性

class CJLTeacher{
    lazy var age: Int = 18
}

2珊搀、延遲屬性必須有一個默認(rèn)的初始值

如果定義為可選類型,則會報錯尾菇,如下所示


屬性-8

3境析、延遲存儲在第一次訪問的時候才被賦值
可以通過調(diào)試,來查看實例變量的內(nèi)存變化

  • age第一次訪問前的內(nèi)存情況:此時的age是沒值的派诬,為0x0
    屬性-9
  • age第一次訪問后的內(nèi)存情況:此時age是有值的簿晓,為30
    屬性-10

    從而可以驗證,懶加載存儲屬性只有在第一次訪問時才會被賦值

我們也可以通過sil文件來查看千埃,這里可以在生成sil文件時憔儿,加上還原swift中混淆名稱的命令(即xcrun swift-demangle):swiftc -emit-sil main.swift | xcrun swift-demangle >> ./main.sil && code main.sil,demo代碼如下

class CJLTeacher{
    lazy var age: Int = 18
}

var t = CJLTeacher()
t.age = 30
  • 類+main:lazy修飾的存儲屬性在底層是一個optional類型

    屬性-11

  • setter+getter:從getter方法中可以驗證放可,在第一次訪問時谒臼,就從沒值變成了有值的操作

    屬性-12

通過sil,有以下兩點(diǎn)說明:

  • 1耀里、lazy修飾的屬性蜈缤,在底層默認(rèn)是optional,在沒有被訪問時冯挎,默認(rèn)是nil底哥,在內(nèi)存中的表現(xiàn)就是0x0。在第一次訪問過程中房官,調(diào)用的是屬性的getter方法趾徽,其內(nèi)部實現(xiàn)是通過當(dāng)前enum的分支,來進(jìn)行一個賦值操作

  • 2翰守、可選類型是16字節(jié)嗎孵奶?可以通過MemoryLayout打印

    • size:實際大小
    • stride:分配大小(主要是由于內(nèi)存對齊)
print(MemoryLayout<Optional<Int>>.stride)
print(MemoryLayout<Optional<Int>>.size)

//*********** 打印結(jié)果 ***********
16
9

為什么實際大小是9蜡峰?Optional其本質(zhì)是一個enum了袁,其中Int8字節(jié)朗恳,另一個字節(jié)主要用于存儲case值(這個后續(xù)會詳細(xì)講解)

4、延遲存儲屬性并不能保證線程安全

繼續(xù)分析3中sil文件载绿,主要是查看age的getter方法粥诫,如果此時有兩個線程:

  • 線程1此時訪問age,其age是沒有值的崭庸,進(jìn)入bb2流程

  • 然后時間片將CPU分配給了線程2怀浆,對于optional來說,依然是none冀自,同樣可以走到bb2流程

  • 所以,在此時秒啦,線程1會走一遍賦值熬粗,線程2也會走一遍賦值,并不能保證屬性只初始化了一次

5余境、延遲存儲屬性對實例對象大小的影響
下面來繼續(xù)看下不使用lazy的內(nèi)存與使用lazy的內(nèi)存是否有變化驻呐?

  • 不使用lazy修飾的情況,的內(nèi)存大小是24
    屬性-13
  • 使用lazy修飾的情況下芳来,類的內(nèi)存大小是32
    屬性-14

從而可以證明含末,使用lazy和不使用lazy,其實例對象的內(nèi)存大小是不一樣的

類型屬性

類型屬性即舌,主要有以下幾點(diǎn)說明:

  • 1佣盒、使用關(guān)鍵字static修飾,且是一個全局變量

  • 2顽聂、類型屬性必須有一個默認(rèn)的初始值

  • 3肥惭、類型屬性只會被初始化一次

1、使用關(guān)鍵字static修飾

class CJLTeacher{
    static var age: Int = 18
}

// **** 使用 ****
var age = CJLTeacher.age

生成SIL文件

  • 查看定義紊搪,發(fā)現(xiàn)多了一個全局變量蜜葱,說以,類型屬性是一個全局變量
    屬性-15
  • 查看入口函數(shù)中age的獲取


    屬性-16
  • 查看age的getter方法


    屬性-17
    • 其中 globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0是全局變量初始化函數(shù)
      屬性-18
    • builtin "once" 耀石,通過斷點(diǎn)調(diào)試牵囤,發(fā)現(xiàn)調(diào)用的是swift_once,表示屬性只初始化一次
      屬性-19
  • 源碼中搜索swift_once滞伟,其內(nèi)部是通過GCDdispatch_once_f 單例實現(xiàn)揭鳞。從這里可以驗證上面的第3點(diǎn)
void swift::swift_once(swift_once_t *predicate, void (*fn)(void *),
                       void *context) {
#if defined(__APPLE__)
  dispatch_once_f(predicate, context, fn);
#elif defined(__CYGWIN__)
  _swift_once_f(predicate, context, fn);
#else
  std::call_once(*predicate, [fn, context]() { fn(context); });
#endif
}

2、類型屬性必須有一個默認(rèn)的初始值

如下圖所示梆奈,如果沒有給默認(rèn)的初始值汹桦,會報錯


屬性-20

所以對于類型屬性來說,一是全局變量鉴裹,只初始化一次舞骆,二是線程安全的

單例的創(chuàng)建

//****** Swift單例 ******
class CJLTeacher{
    //1钥弯、使用 static + let 創(chuàng)建聲明一個實例對象
    static let shareInstance = CJLTeacher.init()
    //2、給當(dāng)前init添加private訪問權(quán)限
    private init(){ }
}
//使用(只能通過單例督禽,不能通過init)
var t = CJLTeacher.shareInstance

//****** OC單例 ******
@implementation CJLTeacher
+ (instancetype)shareInstance{
    static CJLTeacher *shareInstance = nil;
    dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        shareInstance = [[CJLTeacher alloc] init];
    });
    return shareInstance;
}
@end

總結(jié)

  • 存儲屬性會占用實例變量的內(nèi)存空間脆霎,且

  • 計算屬性不會占用內(nèi)存空間,其本質(zhì)是set/get方法

  • 屬性觀察者

    • willset:新值存儲之前調(diào)用狈惫,先通知子類睛蛛,再通知父類(因為父類中可能需要做一些額外的操作),即子父

    • didSet:新值存儲完成后胧谈,先告訴父類忆肾,再通知子類(父類的操作優(yōu)先于子類),即父子

    • 類中的init方法賦值不會觸發(fā)屬性觀察

    • 屬性可以添加在 類定義的存儲屬性菱肖、繼承的存儲屬性客冈、繼承的計算屬性

    • 子類調(diào)用父類的init方法,會觸發(fā)觀察屬性

  • 延遲存儲屬性

    • 使用lazy修飾存儲屬性稳强,且必須有一個默認(rèn)值

    • 只有在第一次被訪問時才會被賦值场仲,且是線程不安全

    • 使用lazy和不使用lazy,會對實例對象的內(nèi)存大小有影響退疫,主要是因為lazy在底層是optional類型渠缕,optional的本質(zhì)是enum,除了存儲屬性本身的內(nèi)存大小褒繁,還需要一個字節(jié)用于存儲case

  • 類型屬性

    • 使用static 修飾亦鳞,且必須有一個默認(rèn)初始值

    • 是一個全局變量,只會被初始化一次棒坏,是線程安全

    • 用于創(chuàng)建單例對象:

      • 使用static + let創(chuàng)建實例變量

      • init方法的訪問權(quán)限為private

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蚜迅,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子俊抵,更是在濱河造成了極大的恐慌谁不,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件徽诲,死亡現(xiàn)場離奇詭異刹帕,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)谎替,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進(jìn)店門偷溺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人钱贯,你說我怎么就攤上這事挫掏。” “怎么了秩命?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵尉共,是天一觀的道長褒傅。 經(jīng)常有香客問我,道長袄友,這世上最難降的妖魔是什么殿托? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘堪伍。我一直安慰自己,他們只是感情好乙埃,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪馒吴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天卑雁,我揣著相機(jī)與錄音募书,去河邊找鬼绪囱。 笑死测蹲,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的鬼吵。 我是一名探鬼主播扣甲,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼齿椅!你這毒婦竟也來了琉挖?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤涣脚,失蹤者是張志新(化名)和其女友劉穎示辈,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體遣蚀,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡矾麻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了芭梯。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片险耀。...
    茶點(diǎn)故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖玖喘,靈堂內(nèi)的尸體忽然破棺而出甩牺,到底是詐尸還是另有隱情,我是刑警寧澤累奈,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布贬派,位于F島的核電站急但,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏赠群。R本人自食惡果不足惜羊始,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望查描。 院中可真熱鬧突委,春花似錦、人聲如沸冬三。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽勾笆。三九已至敌蚜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間窝爪,已是汗流浹背弛车。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蒲每,地道東北人纷跛。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像邀杏,于是被迫代替她去往敵國和親贫奠。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評論 2 353

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