Runtime源碼 —— property和ivar

我原本以為這兩個東西沒啥好寫的,結果是property確實沒啥好寫的,但是ivar就不少了。

本文不探討何時該選擇property,何時該選擇ivar

我會把我研究這兩東西的過程原原本本的展示出來。

試探期

Runtime源碼 —— 方法加載的過程這篇文章中,我提到過兩個結構體:

  • class_ro_t
    記錄編譯期就已經確定的信息
  • class_rw_t
    運行期拷貝class_ro_t中的部分信息存入此結構體中,并存放運行期添加的信息

細心的同學應該發現在ro和rw結構體中,method、protocol和property都是存在的,拷貝也就是拷貝的這部分信息,但是ro中還存在一個字段叫做:

const ivar_list_t * ivars;

這玩意兒就是本文研究的重點了。

例子

在寫代碼之前,我心里是這樣想的:
ivar都應該存在ro的ivars字段中,property存在ro的baseProperties字段中,在運行期,將property拷貝到rw中。獲取property的時候直接從rw中獲取,獲取ivar則從ro中獲取。

寫代碼測試一下:

// ZNObjectFather.h
@interface ZNObjectFather : NSObject {
    NSInteger ivarInt;
    BOOL ivarBool;
}
@property (nonatomic, assign) NSInteger propertyInt;
@end

// ZNObjectFather.m
#import "ZNObjectFather.h"
@implementation ZNObjectFather
@end

還是通過lldb驗證一下:

// 獲取ZNObjectFather class的內存地址
2017-02-21 10:06:12.772188 TestOSX[6560:288465] 0x100002e10
(lldb) p (class_data_bits_t *)0x100002e30
(class_data_bits_t *) $0 = 0x0000000100002e30
(lldb) p $0->data()
(class_rw_t *) $1 = 0x000060800007e140
(lldb) p (*$1).ro
(const class_ro_t *) $2 = 0x0000000100002318
(lldb) p *$2
(const class_ro_t) $3 = {
  flags = 128
  instanceStart = 8
  instanceSize = 32
  reserved = 0
  ivarLayout = 0x0000000000000000 <no value available>
  name = 0x000000010000139a "ZNObjectFather"
  baseMethodList = 0x0000000100002260
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000100002298
  weakIvarLayout = 0x0000000000000000 <no value available>
  baseProperties = 0x0000000100002300
}
// 獲取ivars
(lldb) p $3.ivars
(const ivar_list_t *const) $4 = 0x0000000100002298
(lldb) p *$4
// $5的內容顯示count = 3,但是實際只聲明了兩個ivar
(const ivar_list_t) $5 = {
  entsize_list_tt<ivar_t, ivar_list_t, 0> = {
    entsizeAndFlags = 32
    count = 3
    first = {
      offset = 0x0000000100002d88
      name = 0x0000000100001448 "ivarInt"
      type = 0x0000000100001aee "q"
      alignment_raw = 3
      size = 8
    }
  }
}
(lldb) p $5.get(1)
// ivar_t的結構體后面會分析
(ivar_t) $6 = {
  offset = 0x0000000100002d90
  name = 0x0000000100001450 "ivarBool"
  type = 0x0000000100001af0 "c"
  alignment_raw = 0
  size = 1
}
(lldb) p $5.get(2)
// 發現聲明的屬性自動生成了一個_propertyName的ivar
(ivar_t) $7 = {
  offset = 0x0000000100002d80
  name = 0x0000000100001459 "_propertyInt"
  type = 0x0000000100001aee "q"
  alignment_raw = 3
  size = 8
}
// 獲取property
(lldb) p $3.baseProperties
(property_list_t *const) $8 = 0x0000000100002300
(lldb) p *$8
// 結果符合預期
(property_list_t) $9 = {
  entsize_list_tt<property_t, property_list_t, 0> = {
    entsizeAndFlags = 16
    count = 1
    first = (name = "propertyInt", attributes = "Tq,N,V_propertyInt")
  }
}

property的測試結果很正常,ivar不完全相同,如果直接聲明的2個之外,屬性也自動生成了一個ivar。

另外$3的baseMethodList也不為空,存的就是property自動生成的get/set方法,感興趣自己打印一下。

看到這里也就不難理解為什么:

property = ivar + get + set

但也并不總是這樣,如果重寫了屬性的get/set方法,就不會生成_propertyName這樣的ivar了,本文不做深入。

再看看$3里面的這么兩個屬性:

instanceStart = 8
instanceSize = 32
  • instanceStart之所以等于8,是因為每個對象的isa占用了前8個字節。
  • instanceSize = isa + 3個ivar,$6的size只有1,但是為了對齊,也占用了8,對齊是怎么計算的后面再講。

到這里對ivar和property已經有一個大概的理解了,下面繼續深入。

深入期

根據上半部分的分析,我們已經知道了

  • ivars在編譯期就已經確定了
  • 屬性會生成 _propertyName格式的ivar,也在編譯期確定
  • 對象的大小是由 isa + ivars決定的

但是這就引出了如下幾個問題:

  • 帶有繼承體系的對象是怎么表示的?
  • 繼承體系中對象的 instanceStart和 instanceSize是怎么計算的?
  • ivar_t中的 alignment_raw和 offset是什么意思?
  • class_ro_t中的 ivarLayout和 weakIvarLayout是什么意思?

現在我們都知道class_ro_t中的ivar,property,protocol和method都是在編譯期就確定的,在運行期時,通過realizeClass()方法將部分信息拷貝到class_rw_t中。

在realizeClass()方法中有這么一段代碼:

// Reconcile instance variable offsets / layout.
// This may reallocate class_ro_t, updating our ro variable.
if (supercls  &&  !isMeta) reconcileInstanceVariables(cls, supercls, ro);

注釋里面講了,這一步會調整ivar的offset值,并且更新ro的信息,看起來這一步就是關鍵,看看方法是怎么實現的:

static void reconcileInstanceVariables(Class cls, Class supercls, const class_ro_t*& ro) 
{
    class_rw_t *rw = cls->data();
    ...
    const class_ro_t *super_ro = supercls->data()->ro;
    ...// 省略了用于debug的相關代碼
    if (ro->instanceStart >= super_ro->instanceSize) {
        // Superclass has not overgrown its space. We're done here.
        return;
    }
    if (ro->instanceStart < super_ro->instanceSize) {
        ...
        class_ro_t *ro_w = make_ro_writeable(rw);
        ro = rw->ro;
        moveIvars(ro_w, super_ro->instanceSize);
        gdb_objc_class_changed(cls, OBJC_CLASS_IVARS_CHANGED, ro->name);
    } 
}

只保留了最關鍵的代碼,關注一下其中的if判斷,比較的是當前類的instanceStart和父類的instanceSize,當start < size的時候調整了一下當前類ro的相關信息。

這給了我一個信息,也就是在這一步之前,ro中的instanceStart和instanceSize其實并不是最終值。

具體調整的過程在moveIvars(ro_w, super_ro->instanceSize)這個方法中完成:

static void moveIvars(class_ro_t *ro, uint32_t superSize)
{
    ...
    uint32_t diff;
    ...
    diff = superSize - ro->instanceStart;

    if (ro->ivars) {
        uint32_t maxAlignment = 1;
        for (const auto& ivar : *ro->ivars) {
            if (!ivar.offset) continue;  // anonymous bitfield

            uint32_t alignment = ivar.alignment();
            if (alignment > maxAlignment) maxAlignment = alignment;
        }

        uint32_t alignMask = maxAlignment - 1;
        diff = (diff + alignMask) & ~alignMask;

        for (const auto& ivar : *ro->ivars) {
            if (!ivar.offset) continue;  // anonymous bitfield

            uint32_t oldOffset = (uint32_t)*ivar.offset;
            uint32_t newOffset = oldOffset + diff;
            *ivar.offset = newOffset;

            ...
        }
    }

    *(uint32_t *)&ro->instanceStart += diff;
    *(uint32_t *)&ro->instanceSize += diff;
}

這個方法做了這些事情:

  • 更新當前類ivar中的offset字段
  • 更新當前類ro的instanceStart和instanceSize

先按照源代碼分析,最后寫代碼驗證。

part1
diff = superSize - ro->instanceStart;

獲取了當前類的instanceStart和父類的instanceSize的偏移量,但這并不是最終的結果,因為存在對齊的問題。這就是后面這個if判斷內部做的事情。

part2

先看第一個for循環:

for (const auto& ivar : *ro->ivars) {
    if (!ivar.offset) continue;  // anonymous bitfield

    uint32_t alignment = ivar.alignment();
    if (alignment > maxAlignment) maxAlignment = alignment;
}

遍歷了ivars,獲取了最大得alignment。這個ivar.alignment()是ivar_t結構體中的方法:

struct ivar_t {
    int32_t *offset;
    const char *name;
    const char *type;
    // alignment is sometimes -1; use alignment() instead
    uint32_t alignment_raw;
    uint32_t size;

    uint32_t alignment() const {
        if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
        return 1 << alignment_raw;
    }
}

#   define WORD_SHIFT 3UL

備注:這里有這么一個字段:alignment_raw,這個字段據我的理解,應該是在編譯期確定的,但是是按照什么規則確定的就不清楚了。根據測試的結果來看,一般都是0或者3。

通過ivar.alignment()得到的結果是1 << 3,也就是8。

part3
uint32_t alignMask = maxAlignment - 1;
diff = (diff + alignMask) & ~alignMask;

這一步確定了diff的值,那個&運算的結果就是把diff按8對齊,比如本來diff = 9,這一步之后diff = 16。

part4
for (const auto& ivar : *ro->ivars) {
    if (!ivar.offset) continue;  // anonymous bitfield

    uint32_t oldOffset = (uint32_t)*ivar.offset;
    uint32_t newOffset = oldOffset + diff;
    *ivar.offset = newOffset;
}

這一步調整ivar的offset字段,調整的過程就是用原來的offset加上上一步得到的diff。說白了就是當前類的ivar是在父類的ivar之后的。

part5
*(uint32_t *)&ro->instanceStart += diff;
*(uint32_t *)&ro->instanceSize += diff;

最后更新了當前類的instanceStart和instanceSize,過程也是加上diff。其實就是把父類的instanceSize給空出來了。

到這里的時候,已經回答了這部分最開始提出的4個問題中的前3個。先來驗證一下。

例子

為了驗證前3個問題,需要給增加一個類:

// ZNObjectSon.h
#import "ZNObjectFather.h"
@interface ZNObjectSon : ZNObjectFather {
    NSInteger ivarIntSon;
    BOOL ivarBoolSon;
}
@property (nonatomic, assign) NSInteger propertyIntSon;

@end
// ZNObjectSon.m
#import "ZNObjectSon.h"
@implementation ZNObjectSon
@end

此類繼承于ZNObjectFather,按照老套路,還是先獲取一下類的地址:

2017-02-21 11:43:49.962750 TestOSX[6743:331148] father address: 0x100002e30
2017-02-21 11:43:49.962803 TestOSX[6743:331148] son address: 0x100002de0

接著在reconcileInstanceVariables()方法中添加一個條件斷點,進入斷點后,通過lldb獲取一下相關值,請看圖:

ZNObjectFather相關信息

條件斷點設置的是ZNObjectFather的地址,所以:

  • ro的信息就是ZNObjectFather的ro

ZNObjectFather繼承于NSObject,所以:

  • super_ro是NSObject的ro

根據控制臺打印的信息,這一步的if判斷結果為true,所以直接return了,調整一下條件斷點的內容,把地址設置為ZNObjectSon的地址再試一下:

ZNObjectSon相關信息

可以看到father的start和size沒有發生變化,因為上一步做過說明直接return了。

再來看看son的start值,說實話看到這個24我是無法理解的。在這之前我預期start = 8,這多出來的16是怎么回事?

我做了一個猜測:instanceStart的值在編譯期已經計算了父類直接聲明的ivar,由property生成的沒有計算。

我做了一些驗證,先把father類中的屬性注釋掉了:

// ZNObjectFather.h
@interface ZNObjectFather : NSObject {
    NSInteger ivarInt;
    BOOL ivarBool;
}
//@property (nonatomic, assign) NSInteger propertyInt;
@end

這時候打印出來的start和size如下:

// ZNObjectFather
instanceStart = 8
instanceSize = 24

// ZNObjectSon
instanceStart = 24
instanceSize = 48

沒有問題,father的size少了8,son沒有變化,這個時候son的start >= father的size,所以直接return。

如果把father中的一個ivar注釋掉:

// ZNObjectFather.h
@interface ZNObjectFather : NSObject {
    NSInteger ivarInt;
//    BOOL ivarBool;
}
@property (nonatomic, assign) NSInteger propertyInt;
@end

這時候打印出來的start和size如下:

// ZNObjectFather
instanceStart = 8
instanceSize = 24

// ZNObjectSon
instanceStart = 16
instanceSize = 40

跟預期的一樣,因為只有一個ivar,所以son的start只多了8,那是不是可以證明上面的猜測是對的呢?

回到上面的截圖,這個時候那一步if判斷是沒法通過的,因為24 < 32,這個時候就進到了moveIvars()方法了,再進這個方法之前,先把son的ivars全打印出來,看看offset的原始值:

(lldb) p $2.ivars
(const ivar_list_t *const) $4 = 0x0000000100002170
(lldb) p *$4
(const ivar_list_t) $5 = {
  entsize_list_tt<ivar_t, ivar_list_t, 0> = {
    entsizeAndFlags = 32
    count = 3
    first = {
      offset = 0x0000000100002d90
      name = 0x00000001000013e5 "ivarIntSon"
      type = 0x0000000100001ace "q"
      alignment_raw = 3
      size = 8
    }
  }
}
(lldb) p $5.get(0)
(ivar_t) $6 = {
  offset = 0x0000000100002d90
  name = 0x00000001000013e5 "ivarIntSon"
  type = 0x0000000100001ace "q"
  alignment_raw = 3
  size = 8
}
(lldb) p $5.get(1)
(ivar_t) $7 = {
  offset = 0x0000000100002d98
  name = 0x00000001000013f0 "ivarBoolSon"
  type = 0x0000000100001ad0 "c"
  alignment_raw = 0
  size = 1
}
(lldb) p $5.get(2)
(ivar_t) $8 = {
  offset = 0x0000000100002d88
  name = 0x00000001000013fc "_propertyIntSon"
  type = 0x0000000100001ace "q"
  alignment_raw = 3
  size = 8
}
(lldb) p $6.offset
(int32_t *) $9 = 0x0000000100002d90
(lldb) p *$9
(int32_t) $10 = 24
(lldb) p $7.offset
(int32_t *) $11 = 0x0000000100002d98
(lldb) p *$11
(int32_t) $12 = 32
(lldb) p $8.offset
(int32_t *) $13 = 0x0000000100002d88
(lldb) p *$13
(int32_t) $14 = 40

$6和$7是直接聲明的ivar,排在前2位,屬性生成的$8排在后面,打印出各自的offset,第一個ivar的offset即$10就是instanceStart,最后一個offset即$14加上ivar的size就是instanceSize,結果很清晰。

moveIvars()方法前面已經分析過源碼了,這里不再贅述,直接看看方法結束之后的結果,在moveIvars()方法之后加一個斷點:

moveIvars()方法調用結果.png

這個時候ro的start和size已經是這樣的了:

// ZNObjectSon
instanceStart = 32
instanceSize = 56

調整結果符合預期,繼續打印出ivar的offset也是沒問題的,這里就不截圖了。

到這里,前3個問題基本驗證完畢了,還剩最后一個問題:

class_ro_t中的 ivarLayout和 weakIvarLayout是什么意思?

這個問題之所以單獨講,是因為在尋找答案的過程中,出現了一些有趣的結果,怎么個有趣法,一起來看看。

首先依然是一個猜測,weakIvarLayout名字中有個weak,是不是統計weak類型的ivar用的。又因為ivar默認類型是strong,所以ivarLayout是不是用于統計strong類型的ivar呢?

當然這里默認strong是不針對基本類型的

這時候又要修改一下測試的代碼了,son類已經不需要了,只用一個father類就可以了:

// ZNObjectFather.h
@interface ZNObjectFather : NSObject {
    NSInteger ivarInt;
    BOOL ivarBool;
    __strong NSArray *ivarArray;
}
@end
// .m文件就不寫了,因為什么也沒有

runtime也提供了方法用于獲取 ivarLayout和 weakIvarLayout

const uint8_t *
class_getIvarLayout(Class cls)
{
    if (cls) return cls->data()->ro->ivarLayout;
    else return nil;
}

const uint8_t *
class_getWeakIvarLayout(Class cls)
{
    if (cls) return cls->data()->ro->weakIvarLayout;
    else return nil;
}

其實就是返回ro的那兩個值,直接用這兩個方法就不需要用lldb慢慢打印了,測試的代碼是這樣的:

const uint8_t *ivarLayout = class_getIvarLayout([ZNObjectFather class]);
const uint8_t *weakIvarLayout = class_getWeakIvarLayout([ZNObjectFather class]);

使用上面修改之后的father代碼測試一下,有趣的事情就發生了:

ivarLayout = "!"
weakIvarLayout = NULL

說實話,看到這個結果的時候,我的第一反應是: 臥槽,這個!是什么鬼

第二行為空我裝作可以理解,因為沒有weak類型的ivar。

我在想,是不是因為在strong之前有兩個基本類型,去掉那兩個基本類型再試試:

// ZNObjectFather.h
@interface ZNObjectFather : NSObject {
    __strong NSArray *ivarArray;
}
@end

結果:
ivarLayout = "\x01"
weakIvarLayout = NULL

這個結果看起來還像點樣子,那個01中的1應該就表示有一個strong類型的ivar吧,接著做測試:

// ZNObjectFather.h
@interface ZNObjectFather : NSObject {
    __strong NSArray *ivarArray;
}
@property (nonatomic, weak) NSArray *propertyArrayWeak;
@end

結果:
ivarLayout = "\x01"
weakIvarLayout = "\x11"

看到這里我又不能理解了,這個"\x11"怎么解釋呢?

沒辦法,只能搜索一下,發現了Objective-C Class Ivar Layout 探索

這篇文章里面的結果輸出并不完全正確,可能作者并沒有真正寫代碼測試吧,但是關于layout編碼的規則猜測看起來是沒問題的:

layout 就是一系列的字符,每兩個一組,比如 \xmn,每一組 ivarLayout 中第一位表示有 m 個非強屬性,第二位表示接下來有 n 個強屬性。

再回過去看之前的結果:

  • ivarLayout = "\x01",表示在先有0個弱屬性,接著有1個連續的強屬性。若之后沒有強屬性了,則忽略后面的弱屬性,對weakIvarLayout也是同理。
  • weakIvarLayout = "\x11",表示先有1個強屬性,然后才有1個連續的弱屬性。

但是文章中并沒有出現過那個神奇的"!",我繼續做測試。

中間過程比較艱辛,省略無數次結果

直到發現下面這兩次結果:

// ZNObjectFather.h
@interface ZNObjectFather : NSObject {
    __weak NSArray *ivarArrayWeak;
    __weak NSArray *ivarArrayWeak2;
    __strong NSArray *ivarArray;
}

結果:
ivarLayout = "!"
weakIvarLayout = "\x02"

這個感嘆號又來了,這個時候根據上面的規則,ivarLayout = "\x21" 才對。

// ZNObjectFather.h
@interface ZNObjectFather : NSObject {
    __weak NSArray *ivarArrayWeak;
    __weak NSArray *ivarArrayWeak2;
    __strong NSArray *ivarArray;
    __strong NSArray *ivarArray2;
}

結果:
ivarLayout = "\""
weakIvarLayout = "\x02"

居然輸出了一個引號("),結果難道不應該是:ivarLayout = "\x22" 嗎?

這個時候我靈光一閃!

當然在閃之前已經搜索了好久,但沒有找到答案,不過這個時候真的是一閃!

我去搜索了ASCII碼表,結果真讓我猜中了:

ASCII碼表.png

所以結果其實是正確的,只是被轉成了ASCII碼,至于xcode為什么要這么做,我就不得而知了...

總結

原本以為很簡單的property和ivar,其實一點也不簡單,特別是ivar,真的是花了很多時間。順便把class_ro_t中幾個之前沒有分析的屬性也一并理解了一下,還是很不錯的。

  • property在編譯期會生成_propertyName的ivar,和相應的get/set屬性
  • ivars在編譯期確定,但不完全確定,offset屬性在運行時會修改
  • 對象的大小是由ivars決定的,當有繼承體系時,父類的ivars永遠放在子類之前
  • class_ro_t的instanceStart和instanceSize會在運行時調整
  • class_ro_t的ivarLayout和weakIvarLayout存放的是強ivar和弱ivar的存儲規則
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容