面試題:一個(gè)NSObject對(duì)象占用多少內(nèi)存?
作為一個(gè)iOS開(kāi)發(fā)人員來(lái)說(shuō),iOS底層原理是必須要掌握的知識(shí)。雖然iOS底層原理更多的是在面試中被問(wèn)到餐济,但是在實(shí)際工作中,掌握iOS底層原理更有助于我們提高編寫(xiě)代碼的質(zhì)量胆剧、速度以及更方便的解決bug絮姆。
想要弄清楚iOS底層原理的本質(zhì),首先要清楚以下兩點(diǎn)
1秩霍、OC對(duì)象在內(nèi)存中是怎么布局的篙悯?
2、OC對(duì)象中包含了哪些內(nèi)容铃绒?
一鸽照、NSObject對(duì)象在內(nèi)存中的布局
我們平時(shí)編寫(xiě)的Objective-C代碼,底層都是C/C++語(yǔ)言來(lái)支持的匿垄。
iOS代碼運(yùn)行流程如下:
接下來(lái)通過(guò)創(chuàng)建OC項(xiàng)目移宅,并將OC相應(yīng)文件轉(zhuǎn)化為C++文件來(lái)探尋OC對(duì)象的本質(zhì)。
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSObject *isa_obj= [[NSObject alloc] init];
}
@end
將OC的ViewController.m文件轉(zhuǎn)化為c++文件椿疗,可以通過(guò)以下兩種命令行來(lái)執(zhí)行漏峰。
第一種方式是不指定架構(gòu)來(lái)轉(zhuǎn)化為C++。
clang -rewrite-objc main.m -o main.cpp
其中cpp代表C++(c plus plus)
第二種方式是指定架構(gòu)模式(現(xiàn)在iOS是arm64架構(gòu)) 來(lái)轉(zhuǎn)化為C++届榄。
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m -o ViewController-arm64.cpp
在ViewController-arm64.cpp文件中全局搜索NSObjcet_IMPL浅乔,可以找到NSObject的實(shí)現(xiàn)代碼
struct NSObject_IMPL {
Class isa;
};
點(diǎn)擊Class,我們發(fā)現(xiàn)铝条,Class是一個(gè)指向結(jié)構(gòu)體的指針靖苇,如下:
typedef struct objc_class *Class;
從上面可以看出,NSObject對(duì)象的底層是基于C++的數(shù)據(jù)類(lèi)型實(shí)現(xiàn)班缰。這個(gè)數(shù)據(jù)類(lèi)型是結(jié)構(gòu)體類(lèi)型贤壁。
我們打印下NSObject內(nèi)存,來(lái)看下NSObject對(duì)象在內(nèi)存中的占用大小
#import "ViewController.h"
#import <objc/runtime.h>
#import <malloc/malloc.h>
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSObject *isa_obj= [[NSObject alloc] init];
//獲得NSObject類(lèi)的實(shí)例對(duì)象的成員變量所占用的大小
NSLog(@"%zd",class_getInstanceSize([NSObject class]));
//獲得isa_objl指針?biāo)赶騼?nèi)存的大小
NSLog(@"%zd",malloc_size((__bridge const void *)isa_obj));
}
輸出結(jié)果
2018-10-10 15:42:53.776715+0800 NSObject本質(zhì)-01[5132:224858] 8
2018-10-10 15:42:53.776875+0800 NSObject本質(zhì)-01[5132:224858] 16
根據(jù)打印結(jié)果埠忘,NSObject對(duì)象的指針占用了8個(gè)字節(jié)脾拆,但是NSObject對(duì)象確占用了16個(gè)字節(jié)。其實(shí)class_getInstanceSize返回的是成員變量的大小莹妒,上例中這個(gè)成員變量只是isa指針名船。malloc_size返回的才是NSObject對(duì)象的在內(nèi)存中的大小。
回到開(kāi)頭的面試題
面試題:一個(gè)NSObject對(duì)象占用多少內(nèi)存旨怠?
NSObject的面向?qū)ο笫且訡/C++的數(shù)據(jù)類(lèi)型實(shí)現(xiàn)的渠驼,這種數(shù)據(jù)類(lèi)型是結(jié)構(gòu)體。在NSObject頭文件中鉴腻,Class是一個(gè)指向結(jié)構(gòu)體的指針迷扇。在64位環(huán)境下,指針占用8個(gè)字節(jié)拘哨。但實(shí)際上系統(tǒng)給NSObject對(duì)象分配了16個(gè)字節(jié)谋梭。但是NSObject對(duì)象內(nèi)部占用了8個(gè)字節(jié)。
在NSObject的初始化中倦青,系統(tǒng)為NSObject對(duì)象分配16個(gè)字節(jié)的內(nèi)存空間瓮床,其中8個(gè)字節(jié)用來(lái)存放一個(gè)成員isa指針。那么isa指針這個(gè)變量的地址就是結(jié)構(gòu)體的地址产镐,也就是NSObjcet對(duì)象的地址隘庄。
假設(shè)isa的地址為0x100400110,那么系統(tǒng)分配存儲(chǔ)空間給NSObject對(duì)象癣亚,然后將存儲(chǔ)空間的地址賦值給objc指針丑掺。objc存儲(chǔ)的就是isa的地址。objc指向內(nèi)存中NSObject對(duì)象地址述雾,即指向內(nèi)存中的結(jié)構(gòu)體街州,也就是isa的位置兼丰。
我們通過(guò)自定義的類(lèi)來(lái)說(shuō)明內(nèi)部布局
#import <Foundation/Foundation.h>
@interface Student : NSObject
{
@public
int _no;
int _age;
}
@end
@implementation Student
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *student = [[Student alloc] init];
student -> _no = 1;
student -> _age = 18;
}
return 0;
}
按照以上c++生成步驟生成文件,并查找Student唆缴,C++文件中如下:
struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _no;
int _age;
};
通過(guò)搜索NSObject_IMPL鳍征,我們查找到NSObject_IMPL實(shí)現(xiàn)方式
struct NSObject_IMPL {
Class isa;
};
所以Student_IMPL的結(jié)構(gòu)體就相當(dāng)于
struct Student_IMPL {
Class *isa;
int _no;
int _age;
};
因此,此結(jié)構(gòu)體占用多少存儲(chǔ)空間面徽,Student對(duì)象就占用多少存儲(chǔ)空間艳丛。結(jié)構(gòu)體占用的存儲(chǔ)空間為:
isa指針(8個(gè)字節(jié))+int _no(4個(gè)字節(jié))+int _age(4個(gè)字節(jié))= 16個(gè)字節(jié)
我們用另外一種方式驗(yàn)證Student內(nèi)存分布
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
struct Student_IMPL {
Class isa;
int _no;
int _age;
};
@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 = 1;
stu->_age = 18;
struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)stu;
NSLog(@"_no = %d, _age = %d", stuImpl->_no, stuImpl->_age);
}
return 0;
}
首先給Student的成員變量_no,_age賦值。用結(jié)構(gòu)體指針stuImpl訪(fǎng)問(wèn)Student的成員變量趟紊,輸出成員變量值氮双。這說(shuō)明,stu這個(gè)指針指向的就是Student_IMPL的結(jié)構(gòu)體霎匈。
當(dāng)存在繼承關(guān)系的時(shí)候戴差,對(duì)象在內(nèi)存中是如何分布的呢?我們來(lái)看一個(gè)例子
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
@interface Person : NSObject
{
int _no;
}
@end
@implementation Person
@end
@interface Student : Person
{
int _age;
}
@end
@implementation Student
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
return 0;
}
上面代碼中Person對(duì)象铛嘱,Student對(duì)象分別占用多少內(nèi)存空間造挽?
通過(guò)將OC文件轉(zhuǎn)化為C++文件,我們查找Student_IMPL弄痹,發(fā)現(xiàn)Student對(duì)象在內(nèi)存中的分布
struct Student_IMPL {
struct Person_IMPL Person_IVARS;
int _age;
};
Person對(duì)象在內(nèi)存中的分布如下
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _no;
};
在看上面的面試題饭入,不難回答,Student對(duì)象和Person對(duì)象都是占用16個(gè)字節(jié)的內(nèi)存肛真。雖然Person對(duì)象占用了16個(gè)字節(jié)的內(nèi)存空間谐丢,但是Person的地址指針和成員變量只占用了12個(gè)字節(jié),空出來(lái)4個(gè)字節(jié)的內(nèi)存蚓让。這4個(gè)字節(jié)乾忱,就被Student的成員變量占據(jù)。
二历极、OC對(duì)象中包含的內(nèi)容
Objective_C中的對(duì)象窄瘟,簡(jiǎn)稱(chēng)OC對(duì)象,主要可以分為3種對(duì)象
1趟卸、instance對(duì)象(實(shí)例對(duì)象)
2蹄葱、class對(duì)象(類(lèi)對(duì)象)
3、meta-class對(duì)象(元類(lèi)對(duì)象)
1锄列、instance對(duì)象(實(shí)例對(duì)象)
instance對(duì)象是通過(guò)類(lèi)alloc出來(lái)的對(duì)象图云,每次調(diào)用alloc都會(huì)產(chǎn)生新的實(shí)例對(duì)象。
instance對(duì)象在內(nèi)存中存儲(chǔ)的信息包括成員以下內(nèi)容:
isa指針邻邮;
成員變量值竣况。
- 注意instance對(duì)象
不存儲(chǔ)方法。
- isa指針也是一種成員變量值筒严。
2丹泉、class對(duì)象(類(lèi)對(duì)象)
每個(gè)類(lèi)在內(nèi)存中有且只有一個(gè)class對(duì)象
class對(duì)象在內(nèi)存中存儲(chǔ):
isa指針;
superclass指針;
類(lèi)的屬性信息(@property);
類(lèi)的對(duì)象方法信息(instance method);
類(lèi)的協(xié)議信息(protocol);
類(lèi)的成員變量信息(ivar)
情萤。
3、meta-class對(duì)象(元類(lèi)對(duì)象)
每個(gè)類(lèi)中有且只有一個(gè)meta-class對(duì)象
meta-class對(duì)象在內(nèi)存中存儲(chǔ):
isa指針;
superclass指針;
類(lèi)方法信息摹恨。
- 獲取類(lèi)對(duì)象紫岩、元類(lèi)對(duì)象的方法
a) Class objc_getClass(const char *aClassName)
1>傳入類(lèi)名的字符串
2>返回對(duì)應(yīng)的類(lèi)對(duì)象
b) Class object_getClass(id obj)
1> 傳入的obj可能是instance、class對(duì)象睬塌、meta-class對(duì)象
2>返回值
如果傳入的obj是instance對(duì)象,返回class對(duì)象
如果傳入的obj是class對(duì)象歇万,返回meta-class對(duì)象
如果傳入的obj是meta-class對(duì)象揩晴,返回NSObject(基類(lèi))的meta-class對(duì)象
c) 判斷是否為元類(lèi)對(duì)象
class_isMetaClass(Class cls)
isa指針的指向
- instance的isa指向class
當(dāng)調(diào)用對(duì)象方法時(shí),通過(guò)instance對(duì)象的isa指針找到class贪磺,最后找到對(duì)象方法的實(shí)現(xiàn)進(jìn)行調(diào)用硫兰。 - class的isa指針指向meta-class
當(dāng)調(diào)用類(lèi)方法時(shí),通過(guò)class的isa找到meta-class寒锚,最后找到類(lèi)方法的實(shí)現(xiàn)進(jìn)行調(diào)用劫映。
class對(duì)象的superclass指針
- 當(dāng)Student的instance對(duì)象要調(diào)用Person的對(duì)象方法時(shí),會(huì)先通過(guò)isa找到Student的superclass刹前,然后通過(guò)superclass找到Person的class泳赋,最后找到對(duì)象方法的實(shí)現(xiàn)進(jìn)行調(diào)用
meta-class對(duì)象的superclass指針
- 當(dāng)Student的class要調(diào)用Person的類(lèi)方法時(shí),會(huì)先通過(guò)isa找到Student的meta-class喇喉,然后通過(guò)superclass找到person的meta-class祖今,最后找到類(lèi)方法的實(shí)現(xiàn)進(jìn)行調(diào)用。
isa拣技、superclass的總結(jié)
- instance的isa指向class
- class的isa指向meta-class
- meta-class的isa指向基類(lèi)的meta-class
- class的superclass指向父類(lèi)的class
如果沒(méi)有父類(lèi)千诬,superclass的指針為nil - meta-class的superclass指向父類(lèi)的meta-class
基類(lèi)的meta-class的superclass指向基類(lèi)的class - instance調(diào)用對(duì)象方法的軌跡
isa找到class,方法不存在膏斤,就通過(guò)superclass找父類(lèi) - class調(diào)用類(lèi)方法的軌跡
isa找meta-class徐绑,方法不存在,就通過(guò)superclass找父類(lèi)
isa地址值的計(jì)算
64bit之前莫辨,instance的isa存的值為class的地址值傲茄。從64bit開(kāi)始,isa需要進(jìn)行一次位運(yùn)算沮榜,才能計(jì)算出真實(shí)地址烫幕。
如果平臺(tái)是arm64,isa & ISA_MASK 的值是class的isa
如果平臺(tái)是x86敞映,isa & ISA_MASK的值是class的isa