Objective-C(八)對象的本質及分類

本文是Objective-C系列的第8篇,主要講述OC對象的底層結構,以及分類:實例對象、類對象、元類對象。git

在上一篇Objective-C(七)對象內存分析分析後,咱們得知了一個類在內存中的存儲。github

可是,咱們只分析了類的成員變量和屬性,咱們知道,一個OC對象,還包括方法、協議等極其重要的信息,那麼它們在哪裏,又是如何存儲,如何用的呢?數組

本篇在此進一步分析Objective-C類體系的分類及其在內存中的完整分佈。bash

爲此,咱們先要進行一些準備工做。app

1、準備

1.1 對象的分類

Objective-C中的對象,簡稱OC對象,主要能夠分爲3種:佈局

  • instance對象(實例對象)post

  • class對象(類對象)測試

  • meta-class對象(元類對象)ui

咱們針對OC中對象分爲三種進行了測試,測試結果以下:this

image-20181121182028637

從上圖咱們能夠獲得如下結論:

  • 實例對象能夠有多個:每alloc一個對象,就會建立一個實例對象;
  • 類對象只能有一個:不一樣實例對象的類對象爲同一個;
  • 元類對象只能有一個:不一樣實例對象(類對象)的元類對象只有一個;

那麼測試代碼中的幾個方法,下面也簡要說明下,部分截取源碼項目-BFOCClass分類-01

1.1.1 [NSObject class]

NSObject.mm中:

+ (Class)class {
    return self;
}
複製代碼

返回類對象(元類對象)自己。

因此,[[[NSObject class] class] class] 不管調用多少次class,其返回都是NSObject class對象。

1.1.2 [person1 class]

返回實例對象的類對象

- (Class)class {
    return object_getClass(self);
}
複製代碼

由(2)中可知,

  • [person1 class]:返回person1中isa指向的對象,就是BFPerson class對象;

1.1.3 objc_getClass

該方法與object_getClass從方法名看極其類似。

  • 參數:字符串類名;
  • 返回:對應的類對象。
Class objc_getClass(const char *aClassName)
{
    if (!aClassName) return Nil;
    // NO unconnected, YES class handler
    return look_up_class(aClassName, NO, YES);
}
複製代碼

1.1.4 object_getClass

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}
複製代碼

返回傳入objisa所指向的對象,傳入的obj多是instance對象、class對象、meta-class對象

其返回值對應爲:

a) 若是是instance對象,返回class對象

b) 若是是class對象,返回meta-class對象

c) 若是是meta-class對象,返回NSObject(基類)的meta-class對象

因此上面測試中:

  • object_getClass(person1):返回即class對象;

  • object_getClass(personClass5):返回即meta-class對象;

1.1.5 class_isMetaClass

返回是否爲meta-class對象。

1.2 一些從源碼來的結構體

咱們會從源碼摘抄一部分結構體,用來進行後面的論證。

1.2.1 NSObject

NSObject從源碼中獲得,其第一個成員變量爲isa,該Class結構體第一個成員變量名稱也是isa

isa裏則包含了(元)類信息的存放地址。

image-20181122114646926

1.2.2 class_rw_t

class_rw_tclass_ro_t均是存放類中信息的結構體,包括成員變量、屬性列表、協議列表及方法列表,其具體以下:

image-20181122114716755

目前,咱們class_ro_t當前類在編譯期就已經肯定的屬性、方法以及遵循的協議

class_rw_t則是在運行時,runtime從新加載佈局的當前類的屬性、方法和協議等。

1.2.3 method_t

  • method_t是 方法最底層的結構。
  • method_list_t
    • 方法列表,一維數組
    • 內部存放method_t
    • class_ro_t中的成員變量。
  • method_array_t
  • 一樣是方法列表,可是是二維數組
  • 內部存放method_t
  • class_rw_t中的成員變量;

三者的關係以下圖:

image-20181122114848636

與method相似,property、protocol的相似。

至於爲何會有一維和二維數組,後續文章會詳述該問題。

1.2.4 property_t

property_array_tproperty_list_t的結構與上面的method中的數組相似,分別是二維、一維數組。

struct property_t {
    const char *name;  		 //屬性名
    const char *attributes;  //字符串,代表了該屬性的特性
};
複製代碼

假如屬性名爲name,類型爲NSString,那麼對應的attributes:T@"NSString",C,N,V_name

1.2.5 protocol_t

protocol_array_tprotocol_list_t分別是存放protocol_t的二維及一維數組。

相似的,咱們關注protocol_t結構體:

struct protocol_t : objc_object {
    const char *mangledName;
    struct protocol_list_t *protocols;
    method_list_t *instanceMethods;
    method_list_t *classMethods;
    method_list_t *optionalInstanceMethods;
    method_list_t *optionalClassMethods;
    property_list_t *instanceProperties;
    uint32_t size;   // sizeof(protocol_t)
    uint32_t flags;
    // Fields below this point are not always present on disk.
    const char **_extendedMethodTypes;
    const char *_demangledName;
    property_list_t *_classProperties;
};
複製代碼

1.2.6 ivar_t

咱們發現,針對成員變量,只有ivar_list_t,而沒有ivar_array_t這樣的結構。

從這裏咱們能夠知道,就算是runtime,也沒法在運行時往類中添加成員變量。

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;
};
複製代碼

1.3 NSObject實例對象的內存

咱們已經對NSObject實例對象的內存結構至關熟悉了。咱們再闡述如下觀點:

  • NSObject實例對象內存中有且只有一個變量爲isa,isa指向NSObject的class對象;

下面,咱們針對編譯期isa,結合源碼,咱們獲得以下:

image-20181122162823924

運行期的isa:

image-20181122162910315

2、對象的內存存儲

上面,咱們作了一些準備工做,瞭解了NSObject如何存儲各類各樣信息的結構體及存儲在什麼地方。

下面咱們將會針對,三種類型作一個簡單的描述。

2.1 instance對象

image-20181123041001470

其內存佈局以下:

image-20181123041041229

instance對象在內存中存儲的信息有:

  • isa指針
  • 其餘成員變量

2.2 class對象

image-20181123063510205

class對象在內存中存儲的信息主要包括

  • isa指針
  • superclass指針
  • 類的對象方法信息(instance method)
  • 類的屬性信息(property)
  • 類的協議信息(protocol)
  • 類的成員變量信息(ivar)

2.3 meta-class對象

image-20181123071237895

meta-class對象和class對象的內存結構是同樣的,可是用途不同,在內存中存儲的信息主要包括

  • isa指針
  • superclass指針
  • 類的類方法信息(class method)
  • 其餘包括屬性、協議、成員變量均爲NULL

3、isa和superclass

isa在平時開發中不多使用,可是相信不少iOS開發者並不陌生。

在前面瞭解了NSObject、各類結構體及對象三大分類以後,蘋果爲咱們提供了的Cocoa/Cocoa Touch中,類蔟體系的創建依賴兩個指針:isasuperclass。那麼又是如何串聯的?

橫向聯繫:isa的做用就是,將某一個類的instance對象、class對象、meta-class對象創建了橫向聯繫,爲查找對象存儲各種信息提供指引。

縱向聯繫:superclass則是面嚮對象語言中最爲重要的特性——繼承的體現,它體現的是類與類之間的聯繫,即爲類圖中創建了縱向聯繫

請注意區分:一個類各類對象之間的聯繫,以及不一樣類之間的聯繫!

3.1 isa

3.1.1 isa指針

image-20181123102401283

3.1.2 ISA_MASK

image-20181123102411439

咱們爲了驗證該結論,在真機設備上,運行項目-BFOCClass分類-01,嘗試獲取了BFPerson對象,並打印類相關地址:

BFPerson instance - 0x12fd1f570 0x12fd366e0
BFPerson class - 0x100091e28 0x100091e28 0x100091e28 0x100091e28 0x100091e28 0
BFPerson meta class - 0x100091e00 1 0x100091e28 0
(lldb) x/2xw 0x12fd1f570
0x12fd1f570: 0x00091e2d 0x000001a1
複製代碼

從上面打印咱們得知:

  • person1實例對象的地址爲0x12fd1f570
  • person1對象地址起始的8個字節,就是isa,此處爲:0x1a100091e2d
  • BFPerson class 地址爲0x100091e28不等於0x1a100091e2d
  • 0x1a100091e2d(isa) & 0x0000000ffffffff8(ISA_MASK) = 0x100091e28

3.2 superclass

3.2.1 class對象

image-20181123100901305

3.2.2 meta-class對象

image-20181123102215644

3.3 類圖體系

據此,isasuperclass聯手織起的下面這張類圖:

class-superclas

3.3.1 聯繫

  • isa指向

    • instance的isa指向class
    • class的isa指向meta-class
    • meta-class的isa指向基類的meta-class
  • superclass指向

    • class的superclass指向父類的class 若是沒有父類,superclass指針爲nil
    • meta-class的superclass指向父類的meta-class 基類的meta-class的superclass指向基類的class

3.3.2 方法調用

  1. instance調用對象方法的軌跡 isa找到class,方法不存在,就經過superclass找父類
  2. class調用類方法的軌跡 isa找meta-class,方法不存在,就經過superclass找父類

代碼參考BFOCClass分類-01

繼承關係:BFStudent->BFPerson->NSObject

3.3.2.1 狀況1

BFStudent *stu = [[BFStudent alloc] init];
[stu test];
複製代碼
  • BFStudent實現-(void)test;,調用BFStudent的實現方法;

  • BFStudent未實現-(void)test;,若BFPerson實現,調用BFPerson實現;

  • BFStudent和BFPerson均未實現,NSObject若實現,調用NSObject實現;

    • 以上都未實現,代碼是不能直接調用[stu test];,會編譯報錯,須要改調用方式:

      ((**void** (*)(**id**, **SEL**))objc_msgSend)(stu, **@selector**(test));
      複製代碼
      • NSObject若實現+(void)test;,崩潰;
      • NSObject若實現-(void)test;,調用-(void)test;
  • 繼承體現中的類,均未實現,崩潰—— -[BFStudent test]: unrecognized selector sent to instance

3.3.2.2 狀況2

[BFStudent test];
複製代碼
  • BFStudent實現+(void)test;,調用BFStudent的實現;

  • BFStudent未實現+(void)test;,若BFPerson實現,調用BFPerson實現;

  • BFStudent和BFPerson均未實現,調用NSObject實現;

    • 沒法[BFStudent test];調用,調用方式需改成:

      Class stuClass = [BFStudent class];
      ((void (*)(id, SEL))objc_msgSend)(stuClass, @selector(test))
      複製代碼
      • NSObject若實現+(void)test;,調用+(void)test;
      • NSObject若實現-(void)test;,調用-(void)test;
      • NSObject若實現+(void)test;-(void)test;,調用+(void)test;
  • 繼承體現中的類,均未實現,崩潰—— +[BFStudent test]: unrecognized selector sent to instance

3.3.3 分析

針對狀況1和2,大部分咱們都能理解,但有一種狀況。是有差異的。

在BFStudent和BFPerson中均未實現對應的對象方法(類方法)時,都會去NSObject中尋找。

  • 對象方法:NSObject尋找對應的對象方法,未找到對象方法,崩潰,參考下面的藍色箭頭。
  • 類方法:NSObject先尋找對應的類方法,未找到類方法,而後繼續尋找對應的實例方法,若二者未找到,纔會崩潰,見下面的黃色箭頭。

image-20181210131420601

4、窺探對象完整的內部結構

到此,咱們針對對象已經瞭解的差很少了,可是咱們仍是沒有完整的看到運行時,對象的內部結構。

4.1 如何窺探

有兩種方式:

  • 方式一:編譯objc源碼,直接經過objc4源碼調試;
  • 方式二:抽象出[一些從源碼來的結構體](#2. 一些從源碼來的結構體)中提到的結構體,將正常對象轉換爲對應的結構體,進行調試;

咱們採用方式二。

4.2 一探究竟

  1. 抽象出的結構體從這裏下載。

  2. 導入後,將編譯項目調整爲命令行項目,並將main.m改成main.mm,以支持C++編譯。

對應的項目源碼

4.2.1 類對象

image-20181123170300802

4.2.2 元類對象

image-20181123170320360

如今,你知道OC對象的本質了嗎?它們的分類呢?

參考

連接

  1. objc4
  2. Runtime(一)Runtime 簡介

示例代碼

  1. 本文源碼項目-BFOCClass分類-01
  2. 本文源碼項目-BFOCClass分類-02
相關文章
相關標籤/搜索