2018年08月26日
- 添加參考使用源碼版本說明
2018年08月23日
- 添加實例對象映琳、類對象机隙、元類對象信息存放指示圖,更加直觀解析了OC的類信息存放位置
2018年08月12日
- 修改了獲取類對象時萨西,傳入?yún)?shù)錯誤問題
- 添加objc_getClass 和 object_getClass方法源碼區(qū)別
2018年08月05日
- 修改第1點的回答方式有鹿,更詳細(xì)
注:分析步驟參考 MJ底層原理班 內(nèi)容,本著自己學(xué)習(xí)原則記錄
本文使用的源碼為objc4-723
1 一個NSObject對象占用多少內(nèi)存谎脯?
- OC底層實現(xiàn)是C/C++葱跋,OC 對象的底層表現(xiàn)為 C/C++的
結(jié)構(gòu)體
- 結(jié)構(gòu)體的大小,實際上是指它內(nèi)部所有
成員變量
占用內(nèi)存的大小
(存在內(nèi)存對齊
原則源梭,指的是結(jié)構(gòu)體的內(nèi)存大小必須是最大成員變量內(nèi)存的大小的倍數(shù)關(guān)系)- 在64bit 下娱俺,NSObject類的結(jié)構(gòu)體對象只包含一個
Class 類型指針的 isa
成員變量- 按照第3點的理解,NSObject 對象占用的內(nèi)存應(yīng)該就只有
8個字節(jié)
的空間
(64bit 下废麻,可以通過class_getInstanceSize
函數(shù)獲得荠卷,其內(nèi)部會進行內(nèi)存對齊
操作)- 但實際情況是:系統(tǒng)分配了
16個字節(jié)
給 NSObject 對象
(通過malloc_size
函數(shù)獲得)
以下是上述5點的解析:
1.1 OC 代碼通過兩種方法獲得的大小
- 使用 Xcode 創(chuàng)建 macOS 類的 command line 項目,代碼如下:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
// 獲得 NSObject 類實例對象的成員變量所占用的大小
NSLog(@"%zd", class_getInstanceSize([NSObject class]));
// 獲得 obj 指針指向內(nèi)存的大小
NSLog(@"%zd", malloc_size((__bridge const void *)obj));
}
return 0;
}
>>>> 打印結(jié)果
8
16
1.2 class_getInstanceSize
和malloc_size
說明
- API 說明
extern size_t malloc_size(const void *ptr);
/* Returns size of given ptr */
/**
* Returns the size of instances of a class.
*
* @param cls A class object.
*
* @return The size in bytes of instances of the class \e cls, or \c 0 if \e cls is \c Nil.
*/
OBJC_EXPORT size_t
class_getInstanceSize(Class _Nullable cls)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
- 兩個函數(shù)使用場景
創(chuàng)建一個實例對象烛愧,至少需要多少內(nèi)存油宜?
#import <objc/runtime.h>
class_getInstanceSize([NSObject class]);
等價于 sizeof()獲得的值。
sizeof 獲取類型大小怜姿,它是一個運算符并非函數(shù)慎冤,在編譯時即計算到給定類型的大小,即如 sizeof(int) 在編譯后會直接替換為 4沧卢。
由于在編譯時計算蚁堤,因此sizeof不能用來返回動態(tài)分配的內(nèi)存空間的大小,而class_getInstanceSize則屬于動態(tài)獲取
創(chuàng)建一個實例對象但狭,實際上分配了多少內(nèi)存披诗?
#import <malloc/malloc.h>
malloc_size((__bridge const void *)obj);
1.3 通過源碼解析class_getInstanceSize
方法返回8個字節(jié)原因
- 下載最新源碼: 蘋果開源源碼地址
https://opensource.apple.com/tarballs/objc4/
解壓后打開項目撬即,查看class_getInstanceSize
實現(xiàn):
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
- 函數(shù)
alignedInstanceSize
的描述是 Class's ivar size rounded up to a pointer-size - 這個方法是獲取類 ivar(成員變量) 的大小,這就解析為什么方法
class_getInstanceSize
返回的是8個字節(jié)了
因為 NSObject 對象中只有一個 isa 指針成員變量藤巢,而且 isa 的類型是一個指針搞莺。在64bit 設(shè)備下指針大小為8個字節(jié)
1.4 將 OC 轉(zhuǎn)成 C/C++代碼, 解析NSObject本質(zhì)
- OC 中 NSObject 的定義掂咒,只有一個 isa 指針
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
- 通過命令行將 OC 的 mian.m 文件轉(zhuǎn)化為 C++ 文件
方式一:簡單轉(zhuǎn)換
clang -rewrite-objc main.m -o main.cpp // 這種方式?jīng)]有指定架構(gòu)才沧,如 arm64 架構(gòu)生成 main.cpp
方式二:使用xcode工具 xcrun,指定架構(gòu)模式
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
在生成的 main-arm64.cpp 文件中搜索NSObjcet
绍刮,可以找到NSObjcet_IMPL
(IMPL代表 implementation 實現(xiàn))
- C++ 的結(jié)構(gòu)體
struct NSObject_IMPL {
Class isa;
};
// Class其實就是一個指針温圆,類型如下
// typedef struct objc_class *Class;
1.5 為什么有8個字節(jié)又有16個字節(jié)的問題呢?
- 通過在源碼中追蹤
allocWithZone
函數(shù)獲得解答
我們知道孩革,創(chuàng)建對象時岁歉,NSObject *obj = [[NSObject alloc] init];
會調(diào)用alloc
類方法,而其底層就是調(diào)用allocWithZone
的
-
底層都是調(diào)用
callAlloc
搜索 allocWithZone 獲取對應(yīng)信息 -
class_createInstance
方法創(chuàng)建 obj
class_createInstance -
找到分配內(nèi)存函數(shù)
instanceSize
-
原因: corefoundation 要求所有 objects 最少16 bytes
最少16 bytes
1.6 簡單繼承的對象內(nèi)存占用分析
- 一個Person對象膝蜈、一個Student對象占用多少內(nèi)存空間锅移?
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
/* Person */
@interface Person : NSObject {
int _age;
}
@end
@implementation Person
@end
/* Student */
@interface Student : Person {
int _no;
}
@end
@implementation Student
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
NSLog(@"person - %zd", class_getInstanceSize([Person class]));
NSLog(@"person - %zd", malloc_size((__bridge const void *)person));
Student *stu = [[Student alloc] init];
NSLog(@"stu - %zd", class_getInstanceSize([Student class]));
NSLog(@"stu - %zd", malloc_size((__bridge const void *)stu));
}
return 0;
}
>>>>打印結(jié)果
person - 16
person - 16
stu - 16
stu - 16
1.6.1 轉(zhuǎn)成 C++ 后結(jié)構(gòu)體成員分析
-
下述代碼分析可以通過上述對 NSObject 對象的分析步驟獲得
摘自 MJ 底層原理班課程 PPT
1.6.2 內(nèi)存對齊
- 內(nèi)存對齊:結(jié)構(gòu)體的大小必須是最大成員大小的倍數(shù)
- Person 中的實際應(yīng)該分配應(yīng)該是12個字節(jié),因為 isa 為8字節(jié)饱搏,int 類型的 _age 為4字節(jié)非剃,為什么
class_getInstanceSize
返回的還是16字節(jié)呢?此時就要考慮內(nèi)存對齊
了推沸。以最大成員大小备绽,即8字節(jié)的 isa 的倍數(shù)算,最少就是8的2倍鬓催,16字節(jié)了肺素。
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS; // 8
int _age; // 4
}; // 16 內(nèi)存對齊:結(jié)構(gòu)體的大小必須是最大成員大小的倍數(shù)
1.6.3 優(yōu)先利用空的連續(xù)的內(nèi)存
- 上述中 Person 實例實際使用的內(nèi)存是12字節(jié),但是內(nèi)存占用是16字節(jié)宇驾,那么多余的4個字節(jié)在 Student 實例創(chuàng)建時就需要被考慮使用了倍靡。
struct Student_IMPL {
struct Person_IMPL Person_IVARS; // 16
int _no; // 4
}; // 16,剛好 Person 分配的16字節(jié)中空余的4個字節(jié)可以放下 int 類型的 _no 成員變量
1.7 帶@property的對象內(nèi)存占用分析
// Person
@interface Person : NSObject
{
int _age;
}
@property (nonatomic, assign) int height;
@end
@implementation Person
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
NSLog(@"person - %zd", class_getInstanceSize([Person class]));
NSLog(@"person - %zd", malloc_size((__bridge const void *)person));
}
return 0;
}
>>>>打印結(jié)果
person - 16
person - 16
- @property 是作用是自動生成一個帶下劃線的實例變量课舍,同時生成對應(yīng)的getter 和 setter 方法
- 那么轉(zhuǎn)換成 C++ 后代碼如下
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS; // 8
int _age; // 4
int _height; // 4
}; // 16
- 也就如運行所得菌瘫,占用內(nèi)存為16個字節(jié)
1.8 為什么實例方法
不在實例對象
里呢?
- 實例方法是公用的布卡,一份足以應(yīng)付同一類型的多個實例對象。因為除了實例變量的值會變之外雇盖,方法的調(diào)用是不會變的忿等。也就是 person1、person2崔挖、person3 它們調(diào)用 Person 類的方法都是一樣的贸街。
1.9 附加課程介紹的內(nèi)存分析和修改內(nèi)存的操作
- 分析 stu 實例內(nèi)存
OC 代碼
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
@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));
}
return 0;
}
C++ 代碼
struct NSObject_IMPL {
Class isa; // 8
};
struct Student_IMPL {
Class isa;
int _no;
int _age;
};
1.9.1 實時查看內(nèi)存數(shù)據(jù)
- 在 stu 生成后庵寞,打斷點。
- 在 Xcode 的控制器查看 stu 實時地址
- 在Xcode 工具欄 選擇
Debug -> Debug Workfllow -> View Memory (Shift + Command + M)
然后在address
中輸入對象的地址
View Memory
- 注意薛匪,上圖讀取內(nèi)存時捐川,存在大端小端讀取方向問題。
從上圖中逸尖,我們可以發(fā)現(xiàn)讀取數(shù)據(jù)從高位數(shù)據(jù)開始讀古沥,查看前16位字節(jié),每四個字節(jié)讀出的數(shù)據(jù)為 16進制 0x00 00 00 04(4字節(jié))娇跟、 0x00 00 00 05(4字節(jié))岩齿、 isa的地址為 0x 00 D1 08 10 00 00 11 19(8字節(jié))
1.9.2 LLDB 指令查看且修改內(nèi)存值
1.9.2.1 在生成 stu 實例后,打斷點苞俘,啟動 LLDB盹沈。
1.9.2.2 通過 p
指令獲得 stu 的地址
(lldb) p stu
(Student *) $0 = 0x000000010062cdd0
1.9.2.3 通過指令memory read
讀取對應(yīng)的地址內(nèi)存
(lldb) memory read 0x000000010062cdd0
0x10062cdd0: c9 11 00 00 01 80 1d 00 04 00 00 00 05 00 00 00 ................
0x10062cde0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
(lldb)
指令memory read
可以簡寫成 x
(lldb) x 0x000000010062cdd0
0x10062cdd0: c9 11 00 00 01 80 1d 00 04 00 00 00 05 00 00 00 ................
0x10062cde0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
(lldb)
1.9.2.4 增加讀取條件
memory read/[數(shù)量][格式][字節(jié)數(shù)] 內(nèi)存地址
簡寫:
x/[數(shù)量][格式][字節(jié)數(shù)] 內(nèi)存地址
格式:
x
是16進制,f
是浮點吃谣,d
是10進制
字節(jié)大衅蚍狻:b
:byte 1字節(jié),h
:half word 2字節(jié)岗憋,w
:word 4字節(jié)肃晚,g
:giant word 8字節(jié)
示例:x/4xw
/
后面表示如何讀取數(shù)據(jù)
w
:表示4個4個字節(jié)讀取
x
:表示以16進制的方式讀取數(shù)據(jù)
4
:則表示讀取4次
(lldb) memory read/4xw 0x000000010062cdd0
0x10062cdd0: 0x000011c9 0x001d8001 0x00000004 0x00000005
簡寫
(lldb) x/4xw 0x000000010062cdd0
0x10062cdd0: 0x000011c9 0x001d8001 0x00000004 0x00000005
(lldb)
1.9.2.5 修改內(nèi)存中的值
- 如,修改_no 的值為8則:
// 對象地址 +8個字節(jié)就是 _no 的地址
(lldb) memory write 0x000000010062cdd8 8
- 通過 log 日志查看執(zhí)行步驟
(lldb) p stu
(Student *) $0 = 0x0000000100600590
2018-07-08 11:08:51.663461+0800 Test1 [21943:3939579] no is 4, age is 5
(lldb) memory write 0x0000000100600598 8
2018-07-08 11:09:15.525190+0800 Test1[21943:3939579] -------------
2018-07-08 11:09:15.525299+0800 Test1[21943:3939579] no is 8, age is 5
Program ended with exit code: 0
- _no 的值在第一個斷點執(zhí)行前澜驮,通過命令
memory write 0x0000000100600598 8
進行修改了陷揪。值從4 變成 8
1.10 OC對象內(nèi)存分配對齊規(guī)則為16的倍數(shù)(最大是256)
- 下述對象中即使實際占大小為24,但由于內(nèi)存分配對齊原則杂穷,最終分配給對象內(nèi)存就是32
#import <Foundation/Foundation.h>
#import <malloc/malloc.h>
#import <objc/runtime.h>
// C++代碼中對象的結(jié)構(gòu)體表示
//struct NSObject_IMPL
//{
// Class isa;
//};
//
//struct Person_IMPL
//{
// struct NSObject_IMPL NSObject_IVARS; // 8
// int _age; // 4
// int _height; // 4
// int _no; // 4
//}; // 24
@interface Person : NSObject {
int _age;
int _height;
int _no;
}
@end
@implementation Person
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
NSLog(@"%zd", sizeof(struct Person_IMPL)); // 24
NSLog(@"%zd %zd",
class_getInstanceSize([Person class]), // 24
malloc_size((__bridge const void *)(p))); // 32
}
return 0;
}
PS:更復(fù)雜的內(nèi)存分配后續(xù)補充
2. 對象的isa指針指向哪里悍缠?
問題簡答:
- instance對象的isa指針指向class對象
- class對象的isa指針指向meta-class對象
- meta-class對象的isa指針指向基類的meta-class對象
- 基類自己的isa指針也指向自己
問題理解方向如下:
2.1 OC對象的分類
- 主要可以分為3種
instance對象(實例對象)
class對象(類對象)
meta-class對象(元類對象)
2.2 instance對象在內(nèi)存中存儲的信息,主要包括
- isa指針
- 其他成員變量(具體的值的信息等)
- ...
2.3 class對象在內(nèi)存中存儲的信息耐量,主要包括
- isa指針
- superclass指針
- 類的成員變量信息(ivar)(變量名稱之類)
- 類的屬性信息(@property)
- 類的協(xié)議信息(protocol)
- 類的對象方法信息(instance method)
- ...
2.4 meta-class對象和class對象的內(nèi)存結(jié)構(gòu)是一樣的飞蚓,但是用途不一樣,在內(nèi)存中存儲的信息廊蜒,主要包括
- isa指針
- superclass指針
- 類的類方法信息(class method)
- ...
2.5 instance趴拧、class、meta-class 存儲區(qū)別
- 每一個類通過 alloc 創(chuàng)建的 instance都是獨立占用一塊內(nèi)存的
NSObject *obj1 = [[NSObject alloc] init];
NSObject *obj2 = [[NSObject alloc] init];
obj1 和 obj2 是NSObject的instance對象(實例對象)山叮,分別占用兩塊不同的內(nèi)存
- 每個類在內(nèi)存中有且只有一個class對象
Class objClass1 = [obj1 class];
Class objClass2 = [obj2 class];
Class objClass3 = [NSObject class];
Class objClass4 = object_getClass(obj1); //Runtime API
Class objClass5 = object_getClass(obj2); //Runtime API
objClass1~5 都是NSObject 的 class 對象著榴,它們都是同一個對象
- 每個類在內(nèi)存中有且只有一個meta-class對象
Class objMetaClass = object_getClass([NSObject class]); // Runtime API
//類對象作為參數(shù)獲取元類對象
objMetaClass是NSObject的meta-class對象(元類對象)
- 注意:
以下方式獲得的是 class 對象,不是 meta-class對象
查看 Class 是否為meta-classClass objClass = [[NSObject class] class];
#import <objc/runtime.h> BOOL result = class_isMetaClass([NSObject class]);
2.6 isa 和 superclass 指向總結(jié)
- 上圖解讀
instance的isa指向class
class的isa指向meta-class
meta-class的isa指向基類的meta-class
class的superclass指向父類的class
如果沒有父類脑又,superclass指針為nilmeta-class的superclass指向父類的meta-class
基類的meta-class的superclass指向基類的classinstance調(diào)用對象方法的軌跡
isa找到class,方法不存在,就通過superclass找父類class調(diào)用類方法的軌跡
isa找meta-class问麸,方法不存在往衷,就通過superclass找父類
2.7 objc_getClass 和 object_getClass
1. objc_getClass
- 根據(jù)類名字符串返回一個類對象
- 與 -class、+clas 方法返回結(jié)果一樣严卖,都只返回類對象席舍,即使繼續(xù)多次調(diào)用都不會返回 meta-class對象
2. object_getClass
- 如果傳入的是 instance對象 則返回 class對象
- 如果傳入的是 class對象 則返回 meta-class對象
- 如果傳入是 meta-class對象 則返回 NSObject(基類/rootObject)的 meta-class對象
3. objc_getClass、object_getClass 和 class 小結(jié)
3. OC的類信息存放在哪里哮笆?
instance對象含有信息為:
1. 成員變量的具體值(ivar value)class對象含有信息為:
1.對象方法(instance method)
2. 協(xié)議(protocol)
3. 屬性(property)
4. 成員變量信息(ivar type and name etc.info)-
meta-class對象含有信息為:
1. 類方法(class method)
摘自 MJ 底層課課件
文/Jacob_LJ(簡書作者)
PS:如非特別說明来颤,所有文章均為原創(chuàng)作品,著作權(quán)歸作者所有疟呐,轉(zhuǎn)載需聯(lián)系作者獲得授權(quán)脚曾,并注明出處,所有打賞均歸本人所有启具!