Swift內存管理

OC一樣,Swift中也是通過引用計數的方式來管理對象的內存的。在Swift類 結構探究中,分析過引用計數refCounts,它是RefCounts類型,class類型,占8字節大小。

一、強引用

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

斷點,查看t的的內存,refCounts0x0000000600000003

image.png

1.1

HeapObject開始分析引用計數的真正表示形態,源碼 HeapObject -> InlineRefCounts

struct HeapObject {
  HeapMetadata const *metadata;

  SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;
  ...
}

#define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS       \
  InlineRefCounts refCounts

進入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其實質是將RefCountBitsInt中的type屬性取了一個別名,所以bits的真正類型是uint64_t,即64位整型數組

1.2 對象創建

然后,繼續分析swift中對象創建的底層方法swift_allocObject,源碼:

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

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

進入Initialized定義,是一個枚舉,其對應的refCounts方法中,干事的是RefCountBits

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

進入RefCountBits定義,也是一個模板定義

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

重點:最終,真正的初始化地方是下面這個,實際上是做了一個位域操作,根據的是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的本質是RefCountBitsInt中的type屬性,而RefCountBitsInt又是模板類RefCountBitsT的模板類型T,所以RefCountBits(0, 1)實質調用的是模板類RefCountBitsT構造方法strongExtraCount傳值為0unownedCount傳值為1
RefCountsBit的結構圖,如下所示:

image.png

isImmortal(0)
UnownedRefCount(1-31)unowned的引用計數
isDeinitingMask(32):是否進行釋放操作
StrongExtraRefCount(33-62): 強引用計數
UseSlowRC(63)

其中需要重點關注UnownedRefCountStrongExtraRefCount。至此,我們分析一下上面的引用計數值0x0000000600000003,用二進制展示:

image.png

如圖,33位置開始的強引用計數StrongExtraRefCount0011,轉換成十進制就是3
下面通過sil驗證一下:
image.png

關于copy_addrsil文檔有解釋,其實現是對object的引用計數作+1操作。有興趣的可以自己去查一下。
需要注意的是:var t = Animal()此時已經有了引用計數,即,OC中創建實例對象時為0;Swift中創建實例對象時默認為1
可以通過CFGetRetainCount獲取引用計數:
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的引用計數:

image.png

弱引用聲明變量是一個可選值,因為在程序運行過程中是允許將當前變量設置為nil的。
t1處加斷點,查看匯編,發現:swift_weakInit。查看源碼,這個函數是由WeakReference來調用的,相當于weak字段編譯器聲明過程中就自定義了一個WeakReference的對象,其目的在于管理弱引用。

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

進入nativeInit

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

進入formWeakReference,創建sideTable

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

進入allocateSideTable

template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting)
{
  // 1、先拿到原本的引用計數
  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、創建sideTable
  HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject());
  // 3、將創建的地址給到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;
}

總結:

  1. 先拿到原本的引用計數;
  2. 創建sideTable
  3. 將創建的sideTable地址給InlineRefCountBits,并查看其初始化方法,根據sideTable地址作了偏移操作并存儲到內存,相當于將sideTable直接存儲到了64位的變量中。

通過上面的底層流程分析,我們可以get到關鍵的2點:
通過HeapObjectSideTableEntry初始化散列表

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

上述源碼中可知,弱引用對象對應的引用計數refCountsSideTableRefCounts類型,而強引用對象的是InlineRefCounts類型。
接下來我們看看SideTableRefCounts

typedef RefCounts<SideTableRefCountBits> SideTableRefCounts;

繼續搜索SideTableRefCountBits

image.png

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

通過InlineRefCountBits初始化散列表的數據

  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構造方法,而bits定義

BitsType bits;

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

強引用一樣,來到了RefCountBitsInt,這個之前分析過,就是uint64_t類型,存的是64位域信息。

綜合12兩點的論述可得出:64位 用于記錄 原有引用計數32位 用于記錄 弱引用計數

為何t的引用計數是:0xc0000000200e08ba

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

static const size_t SideTableUnusedLowBits = 3;

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

image.png

三、循環引用

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

//打印結果
11

從輸出結果中可以看出:閉包內部對變量的修改會改變外部原始變量的值,原因是閉包會捕獲外部變量,這個與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()

//打印結果
//Animal deinit

可見,deinit是在當前實例對象即將被回收時觸發。
接下來,我們把age放到類中,閉包中再去修改時會怎樣:

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

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

//打印結果
//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()
//打印結果
//10

從運行結果發現,t.age還是1,并且沒有執行deinit方法,這里存在循環引用

循環引用的處理

1 weak修飾閉包傳入的參數。weak修飾后的變量是optional類型,所以t?.age += 1

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

2 unowned修飾閉包參數

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

[weak t][unowned t],稱為捕獲列表。定義在參數列表之前,如果使用捕獲列表,那么即使省略參數名稱、參數類型和返回類型,也必須使用in關鍵字

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

//打印結果
//1
//1.85

如上,age值改變了,height未被捕獲,值未變。

捕獲列表特點: 捕獲列表中的常量是值拷貝,而不是引用拷貝,因此,它是只讀的,即不可修改

Runtime

Swift是一門靜態語言,本身不具備動態性,不像OCRuntime運行時的機制(此處指OC提供運行時API供程序員操作)。但由于Swift兼容OC,所以可以轉成OC類和函數,利用OC的運行時機制,來實現動態性

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修飾。只打印了初始化方法,因為在swift.h文件中暴露出來的只有init方法。
注意:如果要讓OC調用,那么必須 繼承NSObject + @objc修飾
4、去掉@objc修飾,改成dynamic修飾 + NSObject,同3。
5、@objc修飾 + dynamic修飾 + NSObject
關于方法調用,參考Swift方法調用

補充

AnyObject:代表任意類的instance,類的類型,類遵守的協議,但struct?不行。
Any:代表任意類型,包括funcation類型或者Optional類型。
AnyClass:代表任意實例的類型。
T.self:如果T為實例對象,返回的就是它本身T是類,返回的是Metadata
type(of:):用于獲取一個值的動態類型,編譯期時,value的類型是Any類型運行期時,type(of:)獲取的是真實類型

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容