日常咱们使用Objective-C语法来编写代码,可是它的底层其实都是C或C++代码。Objective-C实际上是在C语言的基础上增长了面向对象的特性。咱们能够经过如下命令将Objective-C代码转换成C++代码:ios
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC文件 -o 输出目标cpp文件
复制代码
若是OC文件须要连接其它的框架,可使用-framework参数:面试
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC文件 -o 输出目标cpp文件 -framework 框架名称
复制代码
与此同时,还须要下载runtime的源码,经过objc源码地址下载最新版本的objc源码,以便于后续使用。数组
在开发过程当中,最经常使用到的就是OC的对象。几乎全部的类对象都是NSObject的子类,可是抛开OC的限制,NSObject底层是如何实现的呢?上文说到,全部的OC代码最后都会转换成C代码,因此咱们经过一个例子来认识NSObject的底层实现。缓存
@interface XLPerson : NSObject
@end
@implementation XLPerson
@end
复制代码
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc XLPerson.m -o XLPerson_cpp.cpp
复制代码
struct XLPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
};
复制代码
在XLPerson_IMPL中包含一个结构体成员NSObject_IVARS,它是NSObject_IMPL类型,查看NSObject_IMPL的代码以下:bash
struct NSObject_IMPL {
Class isa;
};
复制代码
由此能够看出,OC中的对象其实就是经过结构体来实现的。在NSObject_IMPL包含了一个Class类型的成员isa。继续查看Class的定义:app
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
复制代码
能够发现其实Class就是一个objc_class类型的结构体指针。在最新的objc4的源码中的objc-runtime-new.h文件中,能够找到最新的objc_class的定义以下:框架
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
}
复制代码
objc_class继承自结构体objc_object,而结构体objc_object的具体定义以下,内部只有一个isa指针iphone
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
复制代码
因为继承关系,结构体objc_class天然也就继承了objc_object的isa指针,因此objc_class也能够转换成以下写法:函数
struct objc_class {
Class isa;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
}
复制代码
查看class_data_bits_t的具体实现以下:学习
//此处只列出核心的代码
struct class_data_bits_t {
......
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
......
}
复制代码
这时候发现了经过bits的内部函数data()能够拿到class_rw_t类型的数据,查看class_rw_t的源码以下:
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; //只读的属性ro
method_array_t methods; //方法列表
property_array_t properties; //属性列表
protocol_array_t protocols; //协议列表
Class firstSubclass;
Class nextSiblingClass;
}
复制代码
在结构体class_rw_t中存放着
继续查看class_ro_t的源码以下:
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize; //当前instance对象占用内存的大小
const uint8_t * ivarLayout;
const char * name; //类名
method_list_t * baseMethodList; //基础的方法列表
protocol_list_t * baseProtocols;//基础协议列表
const ivar_list_t * ivars; //成员变量列表
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;//基本属性列表
}
复制代码
此处就不得不说class_rw_t和class_ro_t的区别了,class_ro_t中存放着类最原始的方法列表,属性列表等等,这些在编译期就已经生成了,并且它是只读的,在运行期没法修改。而class_rw_t不只包含了编译器生成的方法列表、属性列表,还包含了运行时动态生成的方法和属性。它是可读可写的。至于class_rw_t和class_ro_t更深层次的区别,我会放在介绍runtime的时候详细说明。
在iOS中通常使用以下[[NSObject alloc] init]建立对象,其中[NSObject alloc]就是为NSObject分配内存空间,下面,咱们就从源码入手,来理解OC对象是如何分配内存的。
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
{
id obj;
#if __OBJC2__
// allocWithZone under __OBJC2__ ignores the zone parameter
(void)zone;
obj = class_createInstance(cls, 0);
#else
if (!zone) {
obj = class_createInstance(cls, 0);
}
else {
obj = class_createInstanceFromZone(cls, 0, zone);
}
#endif
if (slowpath(!obj)) obj = callBadAllocHandler(cls);
return obj;
}
复制代码
id class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
复制代码
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
......
size_t size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (!zone && fast) {
obj = (id)calloc(1, size);
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
}
......
}
复制代码
能够看出,其中真正用来分配内存的是C函数calloc,calloc函数传入了两个参数,第一个参数表示对象的个数,第二个参数size表示对象占据的内存字节数。所以size就表示当前对象所须要的内存大小。
// May be unaligned depending on class's ivars. uint32_t unalignedInstanceSize() { assert(isRealized()); return data()->ro->instanceSize; } // Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
复制代码
其中cls->unalignedInstanceSize()表示未进行内存对齐的内存大小,cls->alignedInstanceSize()是对未对齐的内存进行内存对齐操做,获得最终所需的内存大小。
这里有个细节,就是执行对齐操做获得的内存大小若是小于16个字节,那么最后分配的内存大小为16个字节,也就是说,咱们建立对象时,分配的内存最少是16个字节。
在iOS中,咱们能够经过三种方式来获取一个对象的内存大小。
sizeof,它其实不是一个函数,而是一个运算符,它和宏定义相似,在编译期就将传入的类型转换成具体的占用内存的大小。例如int是4个字节,那么sizeof(int)在编译期就会直接被替换成4
注意:sizeof须要传入一个类型过去,它返回的是一个类型所占用的内存空间
class_getInstanceSize(Class _Nullable cls),传入一个Class类型的对象就能获得当前Class所占用的内存大小。例如,class_getInstanceSize([NSObject class]),最后返回的是8,也就说明NSObject对象在内存中占用8个字节,并且因为NSObject最后会转化成结构体NSObject_IMPL,并且内部只有一个isa指针,因此也就能够理解为isa指针占用8个字节的存储空间。
class_getInstanceSize函数内部其实就是调用alignedInstanceSize函数获取到对象所须要的真实内存大小。
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
复制代码
在调用calloc函数进行内存分配的时候,是将alignedInstanceSize的值看成参数赋值给calloc函数,所以calloc函数能够有以下写法:
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
......
size_t size = class_getInstanceSize(cls);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (!zone && fast) {
obj = (id)calloc(1, size);
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
}
......
}
复制代码
由此能够看出,class_getInstanceSize(Class _Nullable cls)所返回的实际上是对象实际所须要的内存大小。
malloc_size(const void *ptr)函数,传入const void *类型的参数,就能够获取到当前操做系统所分配的内存大小。例如:仍是利用NSObject来进行测试,malloc_size((__bridge const void *)([[NSObject alloc] init])),将NSObject类型的实例对象做为参数,最后获得的值为16,和咱们以前使用class_getInstanceSize([NSObject class])获得的8不相同。
这是由于在iOS中,在分配内存时,若是对象所须要的内存大小小于16个字节,那么就分配给这个对象16个字节的内存空间。也就是每一个对象至少分配16个字节的内存空间
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
复制代码
@interface XLPerson : NSObject{
int _height;
int _age;
long _num;
}
复制代码
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
@interface XLPerson : NSObject{
@public
int _height;
int _age;
long _num;
}
@end
@implementation XLPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
XLPerson *p = [[XLPerson alloc] init];
p->_height = 10;
p->_age = 20;
p->_num = 25;
NSLog(@"sizeof --> %lu", sizeof(p));
NSLog(@"class_getInstanceSize --> %lu", class_getInstanceSize([XLPerson class]));
NSLog(@"malloc_size --> %lu", malloc_size((__bridge const void *)(p)));
}
return 0;
}
复制代码
能够看出此时sizeof(p)返回8个字节,class_getInstanceSize返回24个字节,malloc_size则返回32个字节。3个方法返回的内存大小都不同,这是为何呢?
其实sizeof(p)返回8个字节,这个很好理解,由于sizeof传入的是p,而p在此处表示的是一个指向XLPerson实例对象的一个指针,在iOS中,指针类型所占用的内存大小为8个字节。所以sizeof(p)所返回的并非XLPerson对象的内存大小。
要想使用sizeof获取到XLPerson对象的内存大小,就须要知道XLPerson最终会转换成什么类型。经过上文的学习,咱们知道,XLPerson内部实际上是一个结构体,经过xcrun指令将文件转换成.cpp文件
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
复制代码
分析main.cpp文件能够得出,XLPerson最终会转换成以下结构体类型
struct NSObject_IMPL {
Class isa;
};
struct XLPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _height;
int _age;
long _num;
};
复制代码
此时调用sizeof(struct XLPerson_IMPL)就能够得出struct XLPerson_IMPL类型所占用的内存为24字节,其实也就是XLPerson所占用的内存是24个字节。
由此可看出运算符sizeof(struct XLPerson_IMPL)和函数class_getInstanceSize([XLPerson class]返回的是对象真正所须要的内存大小。
在了解malloc_size函数以前,咱们先来分析一下XLPerson内部结构体所须要的真实内存大小。
struct XLPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _height;
int _age;
long _num;
};
复制代码
因此,单纯从结构体层面分析的话,咱们能够看出XLPerson_IMPL结构体所须要的内存是24个字节,这和上文的sizeof(struct XLPerson_IMPL)以及函数class_getInstanceSize([XLPerson class]返回的内存大小一致。因而可知,XLPerson所须要的内存就是24个字节。
但是为何malloc_size所返回的内存大小确是32个字节呢?这就要说到内存对齐操做。
首先,咱们先将上文中提到的XLPerson属性进行修改,去掉其中的_age属性
@interface XLPerson : NSObject{
@public
int _height;
long _num;
}
@end
复制代码
而后从新运行项目,能够看到XLPerson实际占用的内存仍是24个字节,而经过分析咱们能够发现XLPerson只须要20个字节的内存空间。
这就是结构体内存对齐操做所致使的,也就是上文中所说的alignedInstanceSize函数的做用。那么什么是结构体的内存对齐操做?
结构体不像数组,结构体中能够存放不一样类型的数据,它的大小也不是简单的各个数据成员大小之和,限于读取内存的要求,而是每一个成员在内存中的存储都要按照必定偏移量来存储,根据类型的不一样,每一个成员都要按照必定的对齐数进行对齐存储,最后整个结构体的大小也要按照必定的对齐数进行对齐。
结构体的内存对齐规则以下:
这就是为什么XLPerson的内存大小为24个字节的缘由。
既然XLPerson的内存占用为24个字节,那么为何系统会给它分配32个字节呢?其实在iOS系统中也存在内存对齐操做。
咱们能够经过打印内存信息来查看是否分配了32个字节,依旧是使用上面的例子
int main(int argc, const char * argv[]) {
@autoreleasepool {
XLPerson *p = [[XLPerson alloc] init];
p->_height = 1;
p->_num = 3;
}
return 0;
}
复制代码
(lldb) po p
<XLPerson: 0x1005b4fd0>
复制代码
其中前8个字节存储着isa指针,蓝色框中的四个字节存放着_height=1,而绿色框中的8个字节存放着_num=3,这里由于结构体内存对齐原则,因此_num=3的内存地址从第17个字节开始,整个红色框的32个字节,就是系统分配给XLPerson实例对象的内存空间,这也证实了malloc_size((__bridge const void *)(p))返回的确实是系统分配给p对象的内存空间。
OC对象主要分为3种
instance对象就是经过alloc操做建立出来的对象,每次调用alloc操做都会建立出不一样的instance对象,它们拥有各自独立分配的内存空间。例如上文中使用的XLPerson的实例对象
XLPerson *p1 = [[XLPerson alloc] init];
XLPerson *p2 = [[XLPerson alloc] init];
复制代码
其中p一、p2就是实例对象,在内存中能够同时拥有多个同一类对象的实例对象。它们各自拥有一块内存空间,用来存储独有的信息。实例对象内部存放的内容以下(以XLPerson的实例对象为例):
XLPerson *p1 = [[XLPerson alloc] init];
p->_height = 10;
p->_num = 25;
复制代码
由于经过[XLPerson alloc]就能建立一个实例对象,因此每一个实例对象内部会存放着一个isa指针,指向它的类对象,还存放着定义好的其它的成员变量的具体值。
类对象是将具备类似属性和方法的对象抽象出来,从而造成类对象。它能够定义一些类似的方法和属性,不一样的实例对象去引用类对象的属性或者方法,能减小代码的重复率。
运行以下代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
XLPerson *p1 = [[XLPerson alloc] init];
XLPerson *p2 = [[XLPerson alloc] init];
Class c1 = [XLPerson class];
Class c2 = [p1 class];
Class c3 = [p2 class];
Class c4 = object_getClass(p1);
Class c5 = object_getClass(p2);
NSLog(@"\n c1 -> %p,\n c2 -> %p,\n c3 -> %p,\n c4 -> %p,\n c5 -> %p", c1, c2, c3, c4, c5);
}
return 0;
}
复制代码
能够获得结果为:
经过结果能够发现,全部的class对象的内存地址都是相同的,这也就说明在内存中只有一个class对象,不论是使用上面的哪一种方法获取到的class对象都是同一个。
class对象内部其实就是一个object_class的结构体,具体的结构定义在上文已经介绍过,这里只列举出class对象存储的主要信息:
元类其实也是一个class类型的对象,它内部的结构和类对象一致,可是元类对象中只存放了以下信息:
元类和类同样,在内存中只会存在一个元类对象。能够经过runtime的方法获取元类对象
int main(int argc, const char * argv[]) {
@autoreleasepool {
XLPerson *p1 = [[XLPerson alloc] init];
XLPerson *p2 = [[XLPerson alloc] init];
Class c1 = object_getClass(p1);
Class c2 = object_getClass(p2);
Class mataC1 = object_getClass(c1);
Class mataC2 = object_getClass(c2);
BOOL c1_isMataClass = class_isMetaClass(c1);
BOOL c2_isMataClass = class_isMetaClass(c2);
BOOL mataC1_isMataClass = class_isMetaClass(mataC1);
BOOL mataC2_isMataClass = class_isMetaClass(mataC2);
NSLog(@"\n c1_isMataClass:%d,\n c2_isMataClass:%d,\n mataC1_isMataClass:%d,\n mataC2_isMataClass:%d"
,c1_isMataClass, c2_isMataClass, mataC1_isMataClass, mataC2_isMataClass);
NSLog(@"\n c1 -> %p,\n c2 -> %p,\n mataC1 -> %p,\n mataC2 -> %p",
c1, c2, mataC1, mataC2);
}
return 0;
}
复制代码
调用结果以下:
在上图中,c1和c2都是类对象,因此返回0,mataC1和mataC2都是元类对象,因此返回1。同时mataC1和mataC2的内存地址彻底相同,这也说明了元类对象在内存中确实只存在一份。
上文屡次提到,在Class对象内部都会有一个isa指针,那么这个isa指针的做用是什么呢?其实isa指针是instance对象、class对象和mata-class对象之间的桥梁。
superClass其实就是指向class对象或者mata-class对象的父类,下面咱们以一个简单的例子来具体说明:
@interface XLPerson : NSObject
- (void)run;
+ (void)sleep;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
XLPerson *p1 = [[XLPerson alloc] init];
[p1 run];
[XLPerson sleep];
}
}
复制代码
XLPerson继承自NSObject,而且声明了一个类方法sleep()和一个对象方法run(),当p1调用对象方法run()时
仍是以上面的例子来讲明,当XLPerson调用类方法sleep()时
首先先看一张很是经典的描述instance对象、类对象以及元类对象之间关系的图片。途中虚线表明isa指针,实线表明superClass指针。
系统给一个NSObject对象分配了16个字节的内存空间(经过malloc_size函数申请内存),可是NSObject对象内部只有一个isa指针,因此它实际使用到了8个字节的内存,而因为ios的内存对齐原则,系统最少分配16个字节的内存空间。
能够经过class_getInstanceSize函数来获取NSObject占用内存大小
以上内容纯属我的理解,若是有什么不对的地方欢迎留言指正。
一块儿学习,一块儿进步~~~