Swift內(nèi)存管理

OC一樣踩娘,Swift中也是通過引用計(jì)數(shù)的方式來管理對象的內(nèi)存的橘沥。在Swift類 結(jié)構(gòu)探究中毅戈,分析過引用計(jì)數(shù)refCounts,它是RefCounts類型骡尽,class類型遣妥,占8字節(jié)大小。

一攀细、強(qiáng)引用

class Animal {
    var age: Int = 10
    var name: String = "dog"
}
var t = Animal()
var t1 = t
var t2 = t

斷點(diǎn)箫踩,查看t的的內(nèi)存,refCounts0x0000000600000003

image.png

1.1

HeapObject開始分析引用計(jì)數(shù)的真正表示形態(tài)谭贪,源碼 HeapObject -> InlineRefCounts

struct HeapObject {
  HeapMetadata const *metadata;

  SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;
  ...
}

#define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS       \
  InlineRefCounts refCounts

進(jìn)入InlineRefCounts定義境钟,是RefCounts類型的別名,而RefCounts是模板類俭识,真正決定的是傳入的類型InlineRefCountBits

typedef RefCounts<InlineRefCountBits> InlineRefCounts;

template <typename RefCountBits>
class RefCounts {
  std::atomic<RefCountBits> refCounts;
  ...
}

InlineRefCountBits慨削,是RefCountBitsT類的別名:

typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;

RefCountBitsT,有bits屬性:

template <RefCountInlinedness refcountIsInline>
class RefCountBitsT {
    ...
      typedef typename RefCountBitsInt<refcountIsInline, sizeof(void*)>::Type
    BitsType;
    ...
    BitsType bits;
    ...
}

template <>
struct RefCountBitsInt<RefCountNotInline, 4> {
  //類型
  typedef uint64_t Type;
  typedef int64_t SignedType;
};

其中bits其實(shí)質(zhì)是將RefCountBitsInt中的type屬性取了一個別名套媚,所以bits的真正類型是uint64_t缚态,即64位整型數(shù)組

1.2 對象創(chuàng)建

然后堤瘤,繼續(xù)分析swift中對象創(chuàng)建的底層方法swift_allocObject玫芦,源碼:

static HeapObject *_swift_allocObject_(HeapMetadata const *metadata,
                                       size_t requiredSize,
                                       size_t requiredAlignmentMask) {
    ...
    new (object) HeapObject(metadata);
    ...
}

<!--構(gòu)造函數(shù)-->
 constexpr HeapObject(HeapMetadata const *newMetadata) 
    : metadata(newMetadata)
    , refCounts(InlineRefCounts::Initialized)
  { }

進(jìn)入Initialized定義,是一個枚舉本辐,其對應(yīng)的refCounts方法中桥帆,干事的是RefCountBits

  enum Initialized_t { Initialized };
  
  //對應(yīng)的RefCounts方法
// Refcount of a new object is 1.
constexpr RefCounts(Initialized_t)
: refCounts(RefCountBits(0, 1)) {}

進(jìn)入RefCountBits定義医增,也是一個模板定義

template <typename RefCountBits>
class RefCounts {
  std::atomic<RefCountBits> refCounts;
  ...
}

重點(diǎn):最終,真正的初始化地方是下面這個老虫,實(shí)際上是做了一個位域操作叶骨,根據(jù)的是Offsets

LLVM_ATTRIBUTE_ALWAYS_INLINE
constexpr
RefCountBitsT(uint32_t strongExtraCount, uint32_t unownedCount)
: bits((BitsType(strongExtraCount) << Offsets::StrongExtraRefCountShift) |
       (BitsType(1)                << Offsets::PureSwiftDeallocShift) |
       (BitsType(unownedCount)     << Offsets::UnownedRefCountShift))
{ }

referCounts的本質(zhì)是RefCountBitsInt中的type屬性,而RefCountBitsInt又是模板類RefCountBitsT的模板類型T祈匙,所以RefCountBits(0, 1)實(shí)質(zhì)調(diào)用的是模板類RefCountBitsT構(gòu)造方法忽刽,strongExtraCount傳值為0unownedCount傳值為1菊卷。
RefCountsBit的結(jié)構(gòu)圖缔恳,如下所示:

image.png

isImmortal(0)
UnownedRefCount(1-31)unowned的引用計(jì)數(shù)
isDeinitingMask(32):是否進(jìn)行釋放操作
StrongExtraRefCount(33-62): 強(qiáng)引用計(jì)數(shù)
UseSlowRC(63)

其中需要重點(diǎn)關(guān)注UnownedRefCountStrongExtraRefCount。至此洁闰,我們分析一下上面的引用計(jì)數(shù)值0x0000000600000003,用二進(jìn)制展示:

image.png

如圖万细,33位置開始的強(qiáng)引用計(jì)數(shù)StrongExtraRefCount0011扑眉,轉(zhuǎn)換成十進(jìn)制就是3
下面通過sil驗(yàn)證一下:
image.png

關(guān)于copy_addr赖钞,sil文檔有解釋腰素,其實(shí)現(xiàn)是對object的引用計(jì)數(shù)作+1操作。有興趣的可以自己去查一下雪营。
需要注意的是:var t = Animal()此時已經(jīng)有了引用計(jì)數(shù)弓千,即,OC中創(chuàng)建實(shí)例對象時為0献起;Swift中創(chuàng)建實(shí)例對象時默認(rèn)為1洋访。
可以通過CFGetRetainCount獲取引用計(jì)數(shù):
image.png

二、弱引用

class Animal {
    var age: Int = 10
    var name: String = "cat"
    var dog: Dog?
}

class Dog {
    var age = 20
    var animal: Animal?
}

func test(){
    var t = Animal()
    weak var t1 = t
    
    print("end")
}

test()

查看 t的引用計(jì)數(shù):

image.png

弱引用聲明變量是一個可選值谴餐,因?yàn)樵诔绦蜻\(yùn)行過程中是允許將當(dāng)前變量設(shè)置為nil的姻政。
t1處加斷點(diǎn),查看匯編岂嗓,發(fā)現(xiàn):swift_weakInit汁展。查看源碼,這個函數(shù)是由WeakReference來調(diào)用的厌殉,相當(dāng)于weak字段編譯器聲明過程中就自定義了一個WeakReference的對象食绿,其目的在于管理弱引用。

WeakReference *swift::swift_weakInit(WeakReference *ref, HeapObject *value) {
  ref->nativeInit(value);
  return ref;
}

進(jìn)入nativeInit

void nativeInit(HeapObject *object) {
auto side = object ? object->refCounts.formWeakReference() : nullptr;
nativeValue.store(WeakReferenceBits(side), std::memory_order_relaxed);
}

進(jìn)入formWeakReference公罕,創(chuàng)建sideTable

template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference()
{
  //創(chuàng)建 sideTable
  auto side = allocateSideTable(true);
  if (side)
  // 如果創(chuàng)建成功器紧,則增加弱引用
    return side->incrementWeak();
  else
    return nullptr;
}

進(jìn)入allocateSideTable

template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting)
{
  // 1、先拿到原本的引用計(jì)數(shù)
  auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
  
  // Preflight failures before allocating a new side table.
  if (oldbits.hasSideTable()) {
    // Already have a side table. Return it.
    return oldbits.getSideTable();
  } 
  else if (failIfDeiniting && oldbits.getIsDeiniting()) {
    // Already past the start of deinit. Do nothing.
    return nullptr;
  }

  // Preflight passed. Allocate a side table.
  
  // FIXME: custom side table allocator
  //2熏兄、創(chuàng)建sideTable
  HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject());
  // 3品洛、將創(chuàng)建的地址給到InlineRefCountBits
  auto newbits = InlineRefCountBits(side);
  
  do {
    if (oldbits.hasSideTable()) {
      // Already have a side table. Return it and delete ours.
      // Read before delete to streamline barriers.
      auto result = oldbits.getSideTable();
      delete side;
      return result;
    }
    else if (failIfDeiniting && oldbits.getIsDeiniting()) {
      // Already past the start of deinit. Do nothing.
      return nullptr;
    }
    
    side->initRefCounts(oldbits);
    
  } while (! refCounts.compare_exchange_weak(oldbits, newbits,
                                             std::memory_order_release,
                                             std::memory_order_relaxed));
  return side;
}

總結(jié):

  1. 先拿到原本的引用計(jì)數(shù)树姨;
  2. 創(chuàng)建sideTable
  3. 將創(chuàng)建的sideTable地址給InlineRefCountBits桥状,并查看其初始化方法帽揪,根據(jù)sideTable地址作了偏移操作并存儲到內(nèi)存,相當(dāng)于將sideTable直接存儲到了64位的變量中辅斟。

通過上面的底層流程分析转晰,我們可以get到關(guān)鍵的2點(diǎn):
通過HeapObjectSideTableEntry初始化散列表

class HeapObjectSideTableEntry {
  // FIXME: does object need to be atomic?
  std::atomic<HeapObject*> object;
  SideTableRefCounts refCounts;
...
}

上述源碼中可知,弱引用對象對應(yīng)的引用計(jì)數(shù)refCountsSideTableRefCounts類型士飒,而強(qiáng)引用對象的是InlineRefCounts類型查邢。
接下來我們看看SideTableRefCounts

typedef RefCounts<SideTableRefCountBits> SideTableRefCounts;

繼續(xù)搜索SideTableRefCountBits

image.png

里面包含了成員uint32_t weakBits,即一個32位域的信息酵幕。

通過InlineRefCountBits初始化散列表的數(shù)據(jù)

  LLVM_ATTRIBUTE_ALWAYS_INLINE
  RefCountBitsT(HeapObjectSideTableEntry* side)
    : bits((reinterpret_cast<BitsType>(side) >> Offsets::SideTableUnusedLowBits)
           | (BitsType(1) << Offsets::UseSlowRCShift)
           | (BitsType(1) << Offsets::SideTableMarkShift))
  {
    assert(refcountIsInline);
  }

這里繼承的bits構(gòu)造方法扰藕,而bits定義

BitsType bits;

typedef typename RefCountBitsInt<refcountIsInline, sizeof(void*)>::Type
    BitsType;

強(qiáng)引用一樣,來到了RefCountBitsInt芳撒,這個之前分析過邓深,就是uint64_t類型,存的是64位域信息笔刹。

綜合12兩點(diǎn)的論述可得出:64位 用于記錄 原有引用計(jì)數(shù)芥备;32位 用于記錄 弱引用計(jì)數(shù)

為何t的引用計(jì)數(shù)是:0xc0000000200e08ba舌菜?

上述分析中我們知道萌壳,在 InlineRefCountBits初始化散列表的數(shù)據(jù)時,執(zhí)行了(reinterpret_cast<BitsType>(side) >> Offsets::SideTableUnusedLowBits這句代碼日月,而

static const size_t SideTableUnusedLowBits = 3;

side右移了3位袱瓮,所以此時,將0xc0000000200e08ba 左移3位0x1007045D0山孔,就是散列表的地址懂讯。再x/8g查看:

image.png

三、循環(huán)引用

var age = 10
let clourse = {
    age += 1
}
clourse()
print(age)

//打印結(jié)果
11

從輸出結(jié)果中可以看出:閉包內(nèi)部對變量的修改會改變外部原始變量的值台颠,原因是閉包會捕獲外部變量褐望,這個與OC中的block一致的。

deinit
class Animal {
    var age = 10
    deinit {
        print("Animal deinit")
    }
}
func test(){
    var t = Animal()
    let clourse = {
        t.age += 10
    }
    clourse()
    print(t.age)
}
test()

//打印結(jié)果
//Animal deinit

可見串前,deinit是在當(dāng)前實(shí)例對象即將被回收時觸發(fā)瘫里。
接下來,我們把age放到類中荡碾,閉包中再去修改時會怎樣:

class Animal {
    var age = 10
    deinit {
        print("Animal deinit")
    }
}

var t = Animal()
let clourse = {
    t.age += 10
}
clourse()
print(t.age)

//打印結(jié)果
//20
//Animal deinit

Animal類增加閉包屬性

class Animal {
    var age = 10
    var completionBlock: (() ->())?
    deinit {
        print("Animal deinit")
    }
}
func test(){
    var t = Animal()
    t.completionBlock = {
        t.age += 10
    }
    print(t.age)
}
test()
//打印結(jié)果
//10

從運(yùn)行結(jié)果發(fā)現(xiàn)谨读,t.age還是1,并且沒有執(zhí)行deinit方法坛吁,這里存在循環(huán)引用劳殖。

循環(huán)引用的處理

1 weak修飾閉包傳入的參數(shù)铐尚。weak修飾后的變量是optional類型,所以t?.age += 1哆姻。

func test(){
    var t = Animal()
    t.completionBlock = { [weak t] in
        t?.age += 10
    }
    print(t.age)
}

2 unowned修飾閉包參數(shù)

func test(){
    var t = Animal()
    t.completionBlock = { [unowned t] in
        t.age += 10
    }
    print(t.age)
}

[weak t][unowned t]宣增,稱為捕獲列表。定義在參數(shù)列表之前矛缨,如果使用捕獲列表爹脾,那么即使省略參數(shù)名稱、參數(shù)類型和返回類型箕昭,也必須使用in關(guān)鍵字灵妨。

捕獲列表的值
func test(){
    var age = 1
    var height = 1.0
    let clourse = {[age] in
        print(age)
        print(height)
    }
    age = 10
    height = 1.85
    clourse()
}
test()

//打印結(jié)果
//1
//1.85

如上,age值改變了落竹,height未被捕獲泌霍,值未變。

捕獲列表特點(diǎn): 捕獲列表中的常量是值拷貝述召,而不是引用拷貝烹吵,因此,它是只讀的桨武,即不可修改

Runtime

Swift是一門靜態(tài)語言锈津,本身不具備動態(tài)性呀酸,不像OCRuntime運(yùn)行時的機(jī)制(此處指OC提供運(yùn)行時API供程序員操作)。但由于Swift兼容OC琼梆,所以可以轉(zhuǎn)成OC類和函數(shù)性誉,利用OC的運(yùn)行時機(jī)制,來實(shí)現(xiàn)動態(tài)性茎杂。

class Animal {
    var age: Int = 18
    func eat(){
        print("eat")
    }
}

let t = Animal()

func test(){
    var methodCount: UInt32 = 0
    let methodList = class_copyMethodList(Animal.self, &methodCount)
    for i in 0..<numericCast(methodCount) {
        if let method = methodList?[i]{
            let methodName = method_getName(method)
            print("=======方法名稱:\(methodName)")
        }else{
            print("not found method")
        }
    }
    
    var count: UInt32 = 0
    let proList = class_copyPropertyList(Animal.self, &count)
    for i in 0..<numericCast(count) {
        if let property = proList?[i]{
            let propertyName = String(utf8String: property_getName(property))
            print("------成員屬性名稱:\(propertyName!)")
        }else{
            print("沒有找到你要的屬性")
        }
    }
    print("test run")
}
test()

嘗試
1错览、以上代碼,沒有打印煌往。
2倾哺、給方法和屬性添加@objc修飾,可以打印刽脖。
3羞海、類Animal繼承NSObject,不用@objc修飾曲管。只打印了初始化方法却邓,因?yàn)樵趕wift.h文件中暴露出來的只有init方法。
注意:如果要讓OC調(diào)用院水,那么必須 繼承NSObject + @objc修飾腊徙。
4简十、去掉@objc修飾,改成dynamic修飾 + NSObject撬腾,同3螟蝙。
5、@objc修飾 + dynamic修飾 + NSObject时鸵。
關(guān)于方法調(diào)用胶逢,參考Swift方法調(diào)用

補(bǔ)充

AnyObject:代表任意類的instance,類的類型饰潜,類遵守的協(xié)議初坠,但struct?不行。
Any:代表任意類型彭雾,包括funcation類型或者Optional類型碟刺。
AnyClass:代表任意實(shí)例的類型。
T.self:如果T為實(shí)例對象薯酝,返回的就是它本身半沽;T是類,返回的是Metadata吴菠。
type(of:):用于獲取一個值的動態(tài)類型者填,編譯期時,value的類型是Any類型做葵;運(yùn)行期時占哟,type(of:)獲取的是真實(shí)類型

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末酿矢,一起剝皮案震驚了整個濱河市榨乎,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌瘫筐,老刑警劉巖蜜暑,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異策肝,居然都是意外死亡肛捍,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門驳糯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來篇梭,“玉大人,你說我怎么就攤上這事酝枢√裢担” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵帘睦,是天一觀的道長袍患。 經(jīng)常有香客問我坦康,道長,這世上最難降的妖魔是什么诡延? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任滞欠,我火速辦了婚禮,結(jié)果婚禮上肆良,老公的妹妹穿的比我還像新娘筛璧。我一直安慰自己,他們只是感情好惹恃,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布夭谤。 她就那樣靜靜地躺著,像睡著了一般巫糙。 火紅的嫁衣襯著肌膚如雪朗儒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天参淹,我揣著相機(jī)與錄音醉锄,去河邊找鬼。 笑死浙值,一個胖子當(dāng)著我的面吹牛恳不,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播开呐,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼妆够,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了负蚊?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤颓哮,失蹤者是張志新(化名)和其女友劉穎家妆,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體冕茅,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡伤极,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了姨伤。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片哨坪。...
    茶點(diǎn)故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖乍楚,靈堂內(nèi)的尸體忽然破棺而出当编,到底是詐尸還是另有隱情,我是刑警寧澤徒溪,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布忿偷,位于F島的核電站金顿,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏鲤桥。R本人自食惡果不足惜揍拆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望茶凳。 院中可真熱鬧嫂拴,春花似錦、人聲如沸贮喧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽塞淹。三九已至窟蓝,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間饱普,已是汗流浹背运挫。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留套耕,地道東北人谁帕。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像冯袍,于是被迫代替她去往敵國和親匈挖。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評論 2 345

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