OC底層探究(4)-- 類的結(jié)構(gòu)分析

類的結(jié)構(gòu)

老規(guī)矩,還是從源碼搞起。我們先在main.m中定義一個類ZPerson,繼承自NSObject。

@interface ZPerson : NSObject{
   NSString *hobby;
}
@property (nonatomic, copy) NSString *name;

- (void)sayHello;
+ (void)sayHappy;

@end

@implementation ZPerson
- (void)sayHello{
   NSLog(@"ZPerson say : Hello!!!");
}

+ (void)sayHappy{
   NSLog(@"ZPerson say : Happy!!!");
}
@end

然后通過通過命令行進(jìn)行編譯。

clang -rewrite-objc main.m
typedef struct objc_class *Class;

編譯后得到一個main.cpp文件。之后我們可以在文件中查找關(guān)于類Class的定義,即Class是一個objc_class類型的結(jié)構(gòu)體。在objc的源碼中,繼續(xù)查找關(guān)于objc_class結(jié)構(gòu)體的一些信息。

struct objc_class : objc_object {
    // Class ISA;  繼承自objc_object  //8
    Class superclass;  //8
    cache_t cache;  //16           // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
   
    //bits 從bits中讀取數(shù)據(jù)
    class_rw_t *data() { 
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
    ...
}

根據(jù)源碼我們可以知道,objc_class集成了objc_object。哈哈,原來類也是一個對象啊,類對象。萬物皆對象!objc_class中包含了isa(從objc_object集成而來)、superclass(它的父類)、cache_t(緩存, 以后再分析)、bits(存儲的數(shù)據(jù),屬性、方法、協(xié)議等)等一系列信息。

補(bǔ)充:NSObject是一個類,而類的底層是objc_class,所以我們可以認(rèn)為NSObject就是對底層的objc_class的上層封裝。

類中的數(shù)據(jù)存儲

從上面類的結(jié)構(gòu)中,我們知道類的數(shù)據(jù)存貯才bits中,可以通過data()方法得到。data()返回一個class_rw_t結(jié)構(gòu),我們可以從中拿到我們想要的喲。rw代表可讀可寫,類中的屬性、方法還有遵循的協(xié)議等信息都保存在 class_rw_t 中。下面看一下class_rw_t的源碼結(jié)構(gòu):

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    ...
}

再看一下class_ro_t的數(shù)據(jù)結(jié)構(gòu):

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;//類名
    method_list_t * baseMethodList;//原始方法列表
    protocol_list_t * baseProtocols;//原始協(xié)議列表
    const ivar_list_t * ivars;//成員變量列表

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;//屬性列表

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

ro是一個class_ro_t 結(jié)構(gòu)體類型的指針,占8個bytes大小。class_ro_t與class_rw_t名字類似,rw是可讀可寫,那ro就是只讀(readonly)了吧,哈哈。class_ro_t與class_rw_t結(jié)構(gòu)也有些類似,class_ro_t中存儲了當(dāng)前類在編譯期就已經(jīng)確定的屬性、方法以及遵循的協(xié)議,不包括分類和后來動態(tài)添加的東西。baseProperties存貯了屬性,屬性在編譯時會將一個“_”+屬性名成員變量添加到ivars中。ivar中存貯了聲明的所有成員變量。屬性在編譯時還會自動生成set、get方法存儲在baseMethodList方法列表中,成員變量不會生成set、get方法。baseMethodList存儲了類中的所有原始方法。
下面我們就會匯編探究一下是不是這樣。
創(chuàng)建一個person對象


image.png

然后在控制臺做如下打印:
打印出person.class的類

(lldb) x/6gx person.class
0x1000024e0: 0x001d8001000024b9 0x0000000100b37140
0x1000024f0: 0x0000000101e633f0 0x0000000200000003
0x100002500: 0x0000000101e62d24 0x0000000100b370f0

0x1000024e0 為類在內(nèi)存中起始指針,指向isa

(lldb) po 0x1000024e0
ZPerson

根據(jù)上面類的結(jié)構(gòu)和內(nèi)存偏移,再偏移8位,就是superclass

(lldb) po 0x1000024e8
<NSObject: 0x1000024e8>

同理0x1000024e0指針偏移32位, 就是bits, 強(qiáng)轉(zhuǎn)為class_data_bits_t類型

(lldb) p (class_data_bits_t *)0x100002500
(class_data_bits_t *) $3 = 0x0000000100002500

調(diào)用data()方法,獲取bits里面的數(shù)據(jù),返回一個class_rw_t結(jié)構(gòu)體

(lldb) p $3->data()
(class_rw_t *) $4 = 0x0000000101872d30

打印出$4指針?biāo)赶虻闹?看一下class_rw_t結(jié)構(gòu)

(lldb) p *$4
(class_rw_t) $5 = {
  flags = 2148139008
  version = 0
  ro = 0x00000001000022e8
  methods = {
    list_array_tt<method_t, method_list_t> = {
       = {
        list = 0x0000000100002220
        arrayAndFlag = 4294976032
      }
    }
  }
  properties = {
    list_array_tt<property_t, property_list_t> = {
       = {
        list = 0x00000001000022d0
        arrayAndFlag = 4294976208
      }
    }
  }
  protocols = {
    list_array_tt<unsigned long, protocol_list_t> = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
  demangledName = 0x0000000100001f75 "ZPerson"
}

打印一下ro

(lldb) p $5.ro
(const class_ro_t *) $6 = 0x00000001000022e8
(lldb) p *$6
(const class_ro_t) $7 = {
  flags = 388
  instanceStart = 8
  instanceSize = 24
  reserved = 0
  ivarLayout = 0x0000000100001f7d "\x02"
  name = 0x0000000100001f75 "ZPerson"
  baseMethodList = 0x0000000100002220
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000100002288
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x00000001000022d0
}

打印ro中的baseProperties屬性列表, 我們找到了我們聲明的屬性nickName

(lldb) p *$7.baseProperties
(property_list_t) $8 = {
  entsize_list_tt<property_t, property_list_t, 0> = {
    entsizeAndFlags = 16
    count = 1
    first = (name = "nickName", attributes = "T@\"NSString\",C,N,V_nickName")
  }
}

在打印一下ivars成員變量列表

(lldb) p *$7.ivars
(const ivar_list_t) $9 = {
  entsize_list_tt<ivar_t, ivar_list_t, 0> = {
    entsizeAndFlags = 32
    count = 2 //總有2個成員變量
    first = {
      offset = 0x0000000100002498
      name = 0x0000000100001e62 "hobby"
      type = 0x0000000100001fa2 "@\"NSString\""
      alignment_raw = 3
      size = 8
    }
  }
}

在打印一下第二個成員變量

(lldb) p $9.get(1)
(ivar_t) $10 = {
  offset = 0x00000001000024a0
  name = 0x0000000100001e68 "_nickName"
  type = 0x0000000100001fa2 "@\"NSString\""
  alignment_raw = 3
  size = 8
}

找到了nickName, 這是自動生成的,驗證了上面所說的屬性在編譯時會將一個“”+屬性名成員變量添加到ivars中

再找一下方法

(lldb) p *$7.baseMethodList
(method_list_t) $11 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 4
    first = {
      name = "sayHello"
      types = 0x0000000100001f87 "v16@0:8"
      imp = 0x0000000100001a80 (LGTest`-[ZPerson sayHello] at main.m:109)
    }
  }
}
(lldb) p $11.get(0)
(method_t) $13 = {
  name = "sayHello"
  types = 0x0000000100001f87 "v16@0:8"
  imp = 0x0000000100001a80 (LGTest`-[ZPerson sayHello] at main.m:109)
}
(lldb) p $11.get(1)
(method_t) $12 = {
  name = ".cxx_destruct"
  types = 0x0000000100001f87 "v16@0:8"
  imp = 0x0000000100001b50 (LGTest`-[ZPerson .cxx_destruct] at main.m:108)
}
(lldb) p $11.get(2)
(method_t) $14 = {
  name = "setNickName:"
  types = 0x0000000100001f97 "v24@0:8@16"
  imp = 0x0000000100001b10 (LGTest`-[ZPerson setNickName:] at main.m:101)
}
(lldb) p $11.get(3)
(method_t) $15 = {
  name = "nickName"
  types = 0x0000000100001f8f "@16@0:8"
  imp = 0x0000000100001ae0 (LGTest`-[ZPerson nickName] at main.m:101)
}

總共四個方法我們創(chuàng)建的sayHello()、系統(tǒng)自動為屬性nickName自動添加的get/set方法和一個系統(tǒng)默認(rèn)添加的方法。并沒有找到我們創(chuàng)建的sayHappy(),說明類方法并沒有存在類里。那么類方法存在哪里呢?類方法存在元類里!那么我們怎么去驗證呢?
根據(jù)isa的走位圖我們知道,類的isa指向元類,所以我們要先拿到當(dāng)前類isa,再按照上面的流程,去元類中查看class_ro_t結(jié)構(gòu),就能找到了。
先拿到類的isa的指向地址,也就是元類

(lldb) p/x 0x001d8001000024b9 & 0x00007ffffffffff8ULL
(unsigned long long) $3 = 0x00000001000024b8

打印元類內(nèi)存地址信息

(lldb) x/6gx 0x00000001000024b8
0x1000024b8: 0x001d800100b370f1 0x0000000100b370f0
0x1000024c8: 0x0000000102d1a9a0 0x0000000100000003
0x1000024d8: 0x0000000102d1a900 0x001d8001000024b9

參考上面拿到類的class_ro_t信息流程,我們可以拿到了元類的class_ro_t,打印一下baseMethodList,我們看到sayHappy方法。驗證了類方法存貯在元類中。還有誰!

(lldb) p *$11
(method_list_t) $12 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 1
    first = {
      name = "sayHappy"
      types = 0x0000000100001f87 "v16@0:8"
      imp = 0x0000000100001ab0 (LGTest`+[ZPerson sayHappy] at main.m:113)
    }
  }
}

我們再看一下除了ro以外的其他信息:
method_list_t methods:方法列表(如果是類對象存儲的是對象方法,元類對象存儲的是類方法)。methods是一個二維數(shù)組,外層是method_list_t,每個method_list_t又包含了多個method_t。這個是可寫的,因為后期可能會有多個分類需要合并到類的方法列表中,還有可能動態(tài)添加方法。關(guān)于動態(tài)方法我們放在后面再探究,目前先簡單看一下method_array_t和method_t的數(shù)據(jù)結(jié)構(gòu):

class method_array_t : 
    public list_array_tt<method_t, method_list_t> 
{
    typedef list_array_tt<method_t, method_list_t> Super;

 public:
    method_list_t **beginCategoryMethodLists() {
        return beginLists();
    }
    
    method_list_t **endCategoryMethodLists(Class cls);

    method_array_t duplicate() {
        return Super::duplicate<method_array_t>();
    }
};

struct method_t {
    //方法\函數(shù)名,一般叫做選擇器
    SEL name;
   //包含了函數(shù)返回值,參數(shù)編碼的字符串。通過字符串拼接的方式將返回值和參數(shù)拼接成一個字符串,來代表函數(shù)返回值及參數(shù)。
    const char *types;
    //代表函數(shù)的具體實現(xiàn),存儲的內(nèi)容是函數(shù)地址。也就是說當(dāng)找到imp的時候就可以找到函數(shù)實現(xiàn),進(jìn)而對函數(shù)進(jìn)行調(diào)用。
    MethodListIMP imp;

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};

我們發(fā)現(xiàn)method_array_t中有一個beginCategoryMethodLists()方法。我們猜想,能否通過它來讀取存儲在method_array_t中的方法呢?
先打印一下methods

(lldb) p $8.methods
(method_array_t) $20 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x00000001000021f8
      arrayAndFlag = 4294975992
    }
  }
}

//再打印一下beginCategoryMethodLists()方法的返回,返回一個指向method_list_t的二級指針

(lldb) p $20.beginCategoryMethodLists()
(method_list_t **) $21 = 0x0000000101b53bb0

//取一下二級指針的值,變成指向method_list_t的指針

(lldb) p *$21
(method_list_t *) $22 = 0x00000001000021f8

//取一下method_list_t指針指向的內(nèi)存空間,找到了跟ro中存儲的一樣的方法,method_array_t取值成功。

(lldb) p *$22
(method_list_t) $23 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 5
    first = {
      name = "sayHello"
      types = 0x0000000100001f82 "v16@0:8"
      imp = 0x0000000100001a30 (LGTest`-[ZPerson sayHello] at main.m:124)
    }
  }
}
(lldb) p $23.get(0)
(method_t) $24 = {
  name = "sayHello"
  types = 0x0000000100001f82 "v16@0:8"
  imp = 0x0000000100001a30 (LGTest`-[ZPerson sayHello] at main.m:124)
}

property_array_t、protocol_array_t與method_list_t類似,就不重復(fù)列舉了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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