iOS底層原理-探尋OC對象本質

本篇主要是對小碼哥底層視頻學習的總結。方便日后復習。

本篇學習總結:

  • NSObject對象/自定義類的對象/繼承關系的類的類的對象內存分配情況以及類信息情況
  • OC對象類別有哪些呢?
  • OC對象的類信息存儲位置在哪里呢?
  • OC對象中常說的isa指針是怎么回事呢?
  • OC對象中常說的superclass指針怎么回事呢?

好了,帶著問題,我們一一開始閱讀吧 ??

一.NSObject 對象內存分配情況

1.面試題:在64bit環境下,一個NSObject對象占用多少內存?

探尋OC對象的本質,我們平時編寫的Objective-C代碼,轉化成底層都是C\C++代碼。

OC代碼轉化過程.png

OC代碼如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
    }
    return 0;
}

我們要想看到OC代碼轉化為C++文件,需要通過命令行進行操作,現在將OC的main.m 文件轉化為C++文件,
第一個方法是安裝插件轉換:


oc代碼轉c++插件.png

第二個方法是直接 cd main.m文件所在文件夾


cd main.m文件所在文件夾.png

然后再執行下面的命令行工具(用于mac命令行項目)

clang -rewrite-objc main.m -o main.cpp // 這種方式沒有指定架構例如arm64架構 其中cpp代表(c plus plus)
生成 main.cpp

我們還可以指定架構模式的命令行,使用Xcode工具xrun(用于iOS應用)

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 
生成 main-arm64.cpp 

下面提示代碼c++轉化成功:


轉化成功.png
轉化成功后的文件.png

我們將生成的文件添加到項目中,不需要編譯


C++轉換文件.png

我們打開main-arm64.cpp文件,搜索NSObject,可以找到NSObjcet_IMPL (IMPL代表 implementation 實現),代碼如下:

struct NSObject_IMPL {
    Class isa;
};

發現里面只有一個Class類型的isa成員變量,順勢點進入Class查看一下它的結構,代碼如下:

typedef struct objc_class *Class;
查看 objc_class,結構如下:
struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

原來typedef struct objc_class Class就是一種結構體的指針。

這時候我們回到第一個面試題,一個NSObject對象在內存中占用多少內存,其實就是isa結構體類型的指針在內存中占用的空間,如果64bit占用8個字節,如果32bit占用4個字節。咱們這里探討的是64bit,也就是說一個NSObjec對象所占用的內存是8個字節。到這里我們已經可以基本解答第一個問題。但是我們發現NSObject對象中還有很多方法,那這些方法不占用內存空間嗎?其實類的方法等也占用內存空間,但是這些方法所占用的存儲空間并不在NSObject對象中,如果是自定義的類,又是如何計算內存呢,我們繼續探討OC對象本質問題。

二.自定義類的實例對象內存分配情況

2.面試題:在64bit環境下,自定類的實例對象占用多少內存呢?

首先創建一個Student

@interface Student : NSObject
{
    @public
    int _no;
    int _age;
}
@end

@implementation Student

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Student *stu = [[Student alloc] init];
        stu->_no = 4;
        stu->_age = 5;
        
        NSLog(@"%zd", class_getInstanceSize([Student class]));
        NSLog(@"%zd", malloc_size((__bridge const void *)stu));

        struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)stu;
        NSLog(@"no is %d, age is %d", stuImpl->_no, stuImpl->_age);
    }
    return 0;
}

我們按照上面的OC代碼轉C++文件的方式進行轉換。我們從 main-arm64.cpp 文件中搜索 Student,搜索結果代碼如下:

struct Student_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _no;
    int _age;
};

我們發現Student類轉化為C++的結構體后第一項是struct NSObject_IMPL ,前面探討NSObject對象的時候寫過

struct NSObject_IMPL {
    Class isa;
};

我們將這部分代碼進行替換,替換結果如下:

struct Student_IMPL {
    Class *isa;
    int _no;
    int _age;
};

遵循上面計算NSObject對象內存的方式,結構體內的各個成員變量占用內存總和就是結構體占用總的內存大小,咱們給Student對象計算一下內存大小吧:
isa指針8個字節空間+int類型_no4個字節空間+int類型_age4個字節空間共16個字節空間

上面的方法是根據類型推算出來的內存大小,我們還可以根據代碼計算出來

 NSLog(@"NSObject = %zd",class_getInstanceSize([NSObject class]));
//類對象實際需要內存大小
 NSLog(@"Student = %zd", class_getInstanceSize([Student class]));
//系統分配
 NSLog(@"Student = %zd", malloc_size((__bridge const void *)stu));

OC對象本身占用內存大小.png

窺探內存結構
我們還需要進一步直觀的看到內存數據,那怎么做呢?
方式一:通過打斷點
Debug Workflow -> viewMemory address中輸入一個NSObject對象的地址,stu對象的內存地址查看方式也是同樣的操作。

查看內存地址方式.png

查看結果.png

從上圖中,我們可以發現讀取數據從高位數據開始讀,查看前16位字節,每四個字節讀出的數據為
16進制 0x0000004(4字節) 0x0000005(4字節) isa的地址為 00D1081000001119(8字節)

方式二:通過lldb指令xcode自帶的調試器
先看幾個常用的命令行

memory read 0x10074c450
// 簡寫  x 0x10074c450

// 增加讀取條件
// memory read/數量格式字節數  內存地址
// 簡寫 x/數量格式字節數  內存地址
// 格式 x是16進制,f是浮點,d是10進制
// 字節大小   b:byte 1字節,h:half word 2字節,w:word 4字節,g:giant word 8字節

示例:x/4xw    //   /后面表示如何讀取數據 w表示4個字節4個字節讀取,x表示以16進制的方式讀取數據,4則表示讀取4次

同時也可以通過lldb修改內存中的值

memory write 0x100400c68 6
將_no的值改為了6
libo 查看內存結果圖.png
三.繼承關系的類的類的對象內存分配情況

3.面試題:在64bit環境下,繼承關系的子父類占用內存情況如何呢?

// Person
@interface Person : NSObject
{
    @public
    int _age;
}
@property (nonatomic, assign) int height;
@end

@implementation Person

@end

//Student
@interface Student : Person
{
    int _no;
}
@end

@implementation Student

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"stu - %zd", class_getInstanceSize([Student class]));
        NSLog(@"person - %zd", class_getInstanceSize([Person class]));
       
    }
    return 0;
}
//打印結果如下:
Interview01-OC對象的本質[2872:67593] stu - 24
Interview01-OC對象的本質[2872:67593] person - 16

其實這道題主要考察的面試題是什么呢?繼承類的內存大小如何計算呢?
我們依次將上面的Student子類跟Person父類轉化成C++結構體寫出來

struct NSObject_IMPL {
    Class isa;//8
};

struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS; // 8
    int _age; // 4
}; // 16 內存對齊:結構體的大小必須是最大成員大小的倍數

struct Student_IMPL {
    struct Person_IMPL Person_IVARS; // 16
    int _no; // 4
}; // 16

這時候你會疑問了Person_IMPL 不是占用12個字節嗎,怎么顯示16呢?那是因為系統給對象分配內存時會遵循內存對齊:結構體的大小必須是最大成員大小的倍原則,也就說Person_IMPL結構體中的成員變量(isa_age)實際需要12字節空間,但是系統根據原則確分配了16字節,所以結果是16字節。
而** Student_IMPL怎么又成了16字節呢,上面說了系統給Person_IMPL分配了16字節,實際占用12字節,還留有4字節空余,恰好放_no**4字節的變量,這樣出來的結果就是系統分配16字節恰好夠Student_IMPL對象使用。

敲黑板了?。?!

所以,綜上,我們總結一下系統給對象分配存儲空間的原則:編譯器在給結構體開辟空間時,首先找到結構體中最寬的基本數據類型,然后尋找內存地址能是該基本數據類型的整倍的位置,作為結構體的首地址。將這個最寬的基本數據類型的大小作為對齊模數。
為結構體的一個成員開辟空間之前,編譯器首先檢查預開辟空間的首地址相對于結構體首地址的偏移是否是本成員的整數倍,若是,則存放本成員,反之,則在本成員和上一個成員之間填充一定的字節,以達到整數倍的要求,也就是將預開辟空間的首地址后移幾個字節。
我們可以總結內存對齊為兩個原則:
原則 1. 前面的地址必須是后面的地址正數倍,不是就補齊。
原則 2. 整個Struct的地址必須是最大字節的整數倍。

如果有興趣可以進一步研究底層實現,這里我就做個學習總結.

四.OC對象的類別以及存儲信息

4.面試題:OC對象都有哪些呢?
5.面試題:OC的類信息存儲在哪里呢?

先來一段代碼

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

/* Person */ 
@interface Person : NSObject <NSCopying>
{
    @public
    int _age;
}
@property (nonatomic, assign) int height;
- (void)personMethod;
+ (void)personClassMethod;
@end

@implementation Person
- (void)personMethod {}
+ (void)personClassMethod {}
@end

/* Student */
@interface Student : Person <NSCoding>
{
    @public
    int _no;
}
@property (nonatomic, assign) int score;
- (void)studentMethod;
+ (void)studentClassMethod;
@end

@implementation Student
- (void)studentMethod {}
+ (void)studentClassMethod {}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {      
        NSObject *object1 = [[NSObject alloc] init];
        NSObject *object2 = [[NSObject alloc] init];

        Student *stu = [[Student alloc] init];
        [Student load];

        Person *p1 = [[Person alloc] init];
        p1->_age = 10;
        [p1 personMethod];
        [Person personClassMethod];
        Person *p2 = [[Person alloc] init];
        p2->_age = 20;
    }
    return 0;
}

OC對象類分為幾類呢?

  • instance對象(實例對象)
  • class對象(類對象)
  • meta-class對象(元類對象)

instance對象
通過類alloc 出來的對象,每次調用alloc 都會產生新的instance對象。

NSObjcet *object1 = [[NSObjcet alloc] init];
NSObjcet *object2 = [[NSObjcet alloc] init];

//內存打印地址如下:
object1 = 0x100723a60 object2 = 0x100723720

object1和object2都是NSObject 的instance對象(實例對象),但是是兩個不同的對象,從打印結果就能看出來,它們分別占據不同的內存地址。
instance對象在內存中存儲的信息包括:
1.isa指針
2.成員變量具體的數據

instance對象存儲信息.png

class對象
我們通過class方法或者runtime方法得到一個class對象,class對象也就是類對象

Class objectClass1 = [object1 class];
Class objectClass2 = [object2 class];
Class objectClass3 = [NSObject class];

// runtime
Class objectClass4 = object_getClass(object1);
Class objectClass5 = object_getClass(object2);
NSLog(@"%p %p %p %p %p", objectClass1, objectClass2, objectClass3, objectClass4, objectClass5);

//內存打印地址如下:
objectClass1 = 0x7fff97528118 objectClass2 = 0x7fff97528118 objectClass3 = 0x7fff97528118 objectClass4 = 0x7fff97528118 objectClass5 = 0x7fff97528118

// 而調用類對象的class方法時得到還是類對象,無論調用多少次都是類對象
Class cls = [[NSObject class] class];
Class objectClass6 = [NSObject class];
NSLog(@"objectClass = %p cls = %p", objectClass6, cls); // 后面兩個地址相同,說明多次調用class得到的還是類對象

//打印結果如下:
objectClass = 0x7fff97528118 cls = 0x7fff97528118

每個類在內存中有且只有一個class對象(類對象),通過打印內存地址就可以看出來。
class對象在內存中存儲的信息包括:
1.isa指針
2.superclass指針
3.類的屬性信息(@property),類的成員變量信息(ivar)
4.類的方法信息(method),類的協議信息(protocol)

class對象存儲信息.png

寫到這里有人就有疑問了,剛才不是說在instance對象中存儲成員變量信息嗎,怎么class對象中也存儲成員變量和屬性變量呢,這里要特意說明一點:
成員變量的值時存儲在實例對象中的,因為只有當我們創建實例對象的時候才為成員變賦值。但是成員變量叫什么名字,是什么類型,只需要有一份就可以了。所以存儲在class對象中。
meta-class對象
只能是通過class對象獲取到meta-class對象,通過下面的方法獲取到。

//runtime中傳入類對象此時得到的就是元類對象
Class objectMetaClass = object_getClass([NSObject class]);
NSLog(@"objectMetaClass = %p",objectMetaClass);

//內存打印地址如下:
objectMetaClass = 0x7fff975280f0

//檢查是否為元類對象
BOOL ismetaclass = class_isMetaClass(objectMetaClass);// 判斷該對象是否為元類對象
NSLog(@"objectMetaClass 是否是元類對象 - %ld",ismetaclass);

//打印結果如下:
objectMetaClass 是否是元類對象 - 1

每一個類的meta-class對象在內存中有且只有一個,class對象跟meta-class對象結構一樣,都是*struct objc_class Class,但是用途不一樣。
meta-class對象在內存中存儲的信息包括:
1.isa指針
2.superclass指針
3.類的類方法信息(class-method)

meta-class存儲信息.png

既然class對象跟meta-class對象結構一樣,那么class對象中是不是也有類方法信息呢?meta-class對象中是不是也有class對象中存儲的屬性信息,成員變量信息,方法信息,協議信息呢?答案是有的,只不過對應的值可能是空的,所以忽略不計。

五.OC對象的isa指針指向問題

前面已經說到了一個NSObject對象轉化為C++文件后,

struct NSObject_IMPL {
    Class isa;
};

所以說任何一個繼承NSObject的對象都包含一個isa指針,那么各個對象的isa指針又分別指向哪里呢?

我們先看兩個常用的調用方法

MJStudent *student = [[MJStudent alloc]init];
//方法1:調用實例方法
[student studentInstanceMethod];

方法1:student實例對象調用了實例方法,我們在前面講到過,實例方法信息存儲在class對象中,這時候instace對象中存儲的isa指針起到作用了,instace對象中的isa指針指向class對象,我們通過isa指針找到class對象,進而找到實例方法列表,調用對應方法。


對象方法調用軌跡.png
//方法2:調用類方法
[MJStudent studentClassMethod];

方法2:MJStudent類對象調用了類方法,我們在前面講到過,類方法信息存儲在meta-class對象中,這時候class對象中存儲的isa指針起到作用了,class對象中的isa指針指向meta-class對象,我們通過isa指針找到meta-class對象,進而找到類方法列表,調用對應方法。仿照上圖“對象方法調用軌跡.png”

總結:instance對象——<isa指針>——class對象——<isa指針>——meta-class對象——<isa指針>——基類NSObject元類對象

詳細看下面isa指針圖例:

對象isa指針指向圖例.png

總結兩點比較坑的地方:

a.基類的元類對象的superclass指針指向基類的類對象
b.類的元類對象的isa指針指向基類的元類對象

六.OC對象的superclass指針指向位置

我們還是以Student類和Person類進行說明,不清楚類信息的小伙伴請往前瀏覽一下。Student類是子類,Person類是父類。

//方法1:調用父類的實例方法
[student personInstanceMethod];
//方法2:調用父類的類方法
[MJStudent personClassMethod];

方法1:當給student實例對象發送personInstanceMethod消息時,student實例對象會通過isa指針找到對應MJStudent類對象,因為類對象中存儲中對象方法信息,先從MJStudent類對象的實例方法信息中查找對應的方法,如果找到進行相應,沒找到則繼續向父類查找,那么子類怎么才能找到父類呢,這時候需要用到superclass指針了,通過superclass指針找到MJPerson的類對象,繼續從類對象那個的實例方法中查找,如果找到進行相應,沒找到則繼續通過superclass查找基類NSObject類對象方法列表,如果還沒找到,返回nil,就是咱們常見的報錯信息,找不到此方法。

方法2:跟方法1類似,簡單說一下吧
當給MJStudent類對象發送personClassMethod消息時,MJStudent類對象會通過isa指針找到對應MJStudent元類對象,因為元類對象中存儲中類方法信息,先從MJStudent元類對象的類信息中查找對應的方法,如果找到進行相應,沒找到則繼續向父類查找,那么子類怎么才能找到父類呢,這時候需要用到superclass指針了,通過superclass指針找到MJPerson的元類對象,繼續從元類對象那個的類方法中查找,如果找到進行相應,沒找到則繼續通過superclass查找基類NSObject元類對象方法列表,如果還沒找到,這個時候跟方法1的查找不太一樣了,如果NSObject的元類對象的類方法中找到,就從NSObject的類方法的實例方法中去查找,還沒有找到,則返回nil,就是咱們常見的報錯信息,找不到此方法。


子類調用父類方法查找順序.png

總結:
1.子類類對象——<superclass指針>——父類類對象——<superclass指針>——基類類對象
2.子類元類對象——<superclass指針>——父類元類對象——<superclass指針>——基類元類對象——<superclass指針>基類的類對象

看完以上的解析,再來看這經典的圖也不是那么的晦澀了


isa-superclass.png

對isa、superclass總結
instance的isa指向class
class的isa指向meta-class
meta-class的isa指向基類的meta-class,基類的isa指向自己
class的superclass指向父類的class,如果沒有父類,superclass指針為nil
meta-class的superclass指向父類的meta-class,基類的meta-class的superclass指向基類的class
instance調用對象方法的軌跡,isa找到class,方法不存在,就通過superclass找父類
class調用類方法的軌跡,isa找meta-class,方法不存在,就通過superclass找父類

七.代碼求證isa指針指向是否正確

我們先寫一段代碼:

NSObject *object = [[NSObject alloc] init];//instance對象
Class objectClass = [NSObject class];//類對象 
Class objectMetaClass = object_getClass([NSObject class]);//元類對象
NSLog(@"object - %p objectClass - %p objectMetaClass - %p", object, objectClass, objectMetaClass);

//打印結果如下:
object - 0x10051e0b0    //instance對象內存地址
objectClass - 0x7fff9abb6118 //類對象內存地址
objectMetaClass - 0x7fff9abb60f0  //元類對象內存地址

我們通過命令行打印如下:

instance對象isa指針內存地址.png

程序打?。侯悓ο髢却娴刂?- 0x7fff9abb6118
命令行打印:實例對象->isa指針獲取到的類對象內存地址 - 0x001dffff9abb6119
怎么不一樣了呢?
原因是:這是因為從64bit開始,isa需要進行一次位運算,才能計算出真實地址。而位運算的值我們可以通過下載objc源代碼找到。
arm64:表示真機應用
x86_64:表示mac應用
小碼哥視頻中創建的demo是mac應用,所以用 0x00007ffffffffff8計算
ISA_MASK.png

我們按照這個原則再來操作一遍:
實例對象位運算結果.png

果然跟程序打印出來的結果一樣,這足以證明上面總結的isa指針指向的正確性。

但我們再次嘗試驗證類方法的isa指針指向的元類對象的內存地址跟程序自然打印的是否一樣的時候,發現了如下問題


類對象isa指針內存地址.png

小碼哥教給的做法是:為了拿到isa指針的地址,我們自己創建一個同樣的結構體并通過強制轉化拿到isa指針。

struct mj_objc_class {
    Class isa;
    Class superclass;
};

struct mj_objc_class *nsobjectclass = (__bridge struct mj_objc_class *)([NSObject class]);

將OC中的類對象轉化成C語言的結構體指針的時候需要進行橋接,直接點fix就行


oc對象轉c語言結構體變量.png
類對象isa指針內存地址.png

程序打印:元類對象內存地址 - 0x7fff9abb60f0
命令行打?。侯悓ο?>isa指針獲取到的元類類對象內存地址 - 0x00007fff9abb60f0


類對象位運算結果.png

總結一下本文面試題:

  • 1.面試題:一個NSObject對象占用多少內存?

答:一個NSObject對象在內存中占用多少內存,其實就是isa結構體類型的指針在內存中占用的空間(64bit占8個字節,32bit占4個字節)

  • 2.面試題:自定義類的實例對象占用多少內存?

答:根據情況而定,具體分析方法看上述說明

  • 3.面試題:繼承關系的子父類的實例對象占用多少內存?

答:根據情況而定,具體分析方法看上述說明

  • 4.面試題:OC對象的類別有哪些呢?

instance對象(實例對象)
class對象(類對象)
meta-class對象(元類對象)

  • 5.面試題:OC對象的isa指針指向哪里呢?

答:instance對象的isa指針指向class對象,class對象的isa指針指向meta-class對象,meta-class對象的isa指針指向基類的meta-class對象,基類自己的isa指針也指向自己。

  • 6.面試題:OC的類信息存放在哪里?

instance對象(實例方法):存放isa指針,成員變量的具體數據
class對象(類對象):存放isa指針,superclass指針,類的成員變量(ivar),類的屬性信息(property),類的協議信息(protocol),類的方法列表(instance method list)
meta-class對象(元類對象:存放isa指針,superclass指針,類的方法列表(class method list)

本篇學習先記錄到此,感謝閱讀,如有錯誤,不吝賜教,
非常感謝大佬帶飛:http://www.lxweimin.com/u/3171707d8892

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

推薦閱讀更多精彩內容