OC对象的底层分析
OC底层原理探索文档汇总
从对象的创建过程、对象的底层结构两方面来分析对象
一、对象的创建
下面代码就是在对对象进行创建,可以看出包括自定义类的alloc 、init、new三个方法的底层实现。以及NSObject的创建。接下来分别对此进行讨论。
NSPerson *person1 = [[NSperson alloc] init];
NSPerson *person2 = [NSperson new];
NSObject *object = [[NSObject alloc] init];
1.1 alloc 的分析
1.1.1 案例分析
源码:
执行结果:
分析:
- 通过%@打印出来的是相同的地址空间
- 通过%p打印出来的是不一样的对象变量地址
- 因此可以看出来通过alloc其实也已经创建了对象
- alloc创建了一个内存空间,返回了一个对象地址
1.1.2 分析过程:
通过查看源码了解到alloc在底层是如何创建空间,并返回一个对象的。
0、查找源码
在objc源码中查找底层源码。
【第一步】 跳转到alloc方法
//alloc第一进入的就是这个方法,注意这里还是OC方法,而不是C函数
+ (id)alloc {
return _objc_rootAlloc(self);
}
tips:此时仍然是OC方法,而不是C函数,所以搜索时可以通过"alloc {"来查找
【第二步】 跳转到_objc_rootAlloc方法
// Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
【第三步】 跳转到callAlloc方法
// Call [cls alloc] or [cls allocWithZone:nil], with appropriate
// shortcutting optimizations.
//会使用适当的方式进行优化
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
if (slowpath(checkNil && !cls)) return nil;
//优化
//是否存在alloc/allocWithZone,类或父类默认会有(实现存储在元类中)
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif
// No shortcuts available.没有优化,需要执行这里
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
//NSObject的创建会进入这里,它是由系统自己调用的。
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
tips:
- 遇到分叉口,需要通过调试断点来判断从哪条分支执行
- 经过调试发现该方法会执行两次,第一次是执行alloc语句,也就是调用NSObject的alloc方法,第二次会执行objc_rootAllocWithZone(),具体的两次执行过程请看下面的NSObject与自定义
- slowpath和fastpath的具体使用过程请查看slowpath和fastpath的认识
【第四步】 跳转到_objc_rootAllocWithZone方法
NEVER_INLINE
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
// allocWithZone under __OBJC2__ ignores the zone parameter
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}
【第五步】 跳转到_class_createInstanceFromZone方法
到此就找到了alloc实现的底层源码,该方法就是真正创建空间的方法。共有三步,1)计算内存空间大小,2)申请内存空间,3)关联isa和类,下面分别进行了解。
代码:
/*
开辟空间
关联类
返回对象
*/
//当前zone是废弃掉了
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
ASSERT(cls->isRealized());
// Read class's info bits all at once for performance
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size;
// 1:计算要开辟多少内存
/*
extraBytes是0,后续看下这里什么时候不是0
就算什么都没有最起码也会有isa,这是8个字节,64位
*/
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
// 2;怎么去申请内存
//通过calloc来申请空间
obj = (id)calloc(1, size);
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
// 3: 申请之后将空间赋给对象变量,也就是得到isa
if (!zone && fast) {
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
//obj先是创建了个内存空间,是内存空间的地址值,其isa与类进行关联后,就变为了对象,所以此处返回的是一个对象指针
//4、返回obj
if (fastpath(!hasCxxCtor)) {
return obj;
}
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
1、计算内存空间
调用代码
size = cls->instanceSize(extraBytes);
说明:
- 断点查看此处extraBytes是0
- 这里虽然传入的是额外的字节数是0,但是还需要加上创建的内存空间大小,因为会有类的信息本身,比如肯定会存在的isa就占有8个字节。
点进去查看具体实现
【第1步】 拿到字节对齐后的对象大小
size_t instanceSize(size_t extraBytes) const {
//进入这里,第一步
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
通过断点调试,会执行到cache.fastInstanceSize(extraBytes);用来快速计算内存大小
【第二步】 执行cache.fastInstanceSize(extraBytes);
#define FAST_CACHE_ALLOC_DELTA16 0x0008
size_t fastInstanceSize(size_t extra) const
{
ASSERT(hasFastInstanceSize(extra));
if (__builtin_constant_p(extra) && extra == 0) {
return _flags & FAST_CACHE_ALLOC_MASK16;
} else {
size_t size = _flags & FAST_CACHE_ALLOC_MASK;
// remove the FAST_CACHE_ALLOC_DELTA16 that was added
// by setFastInstanceSize
//删除由setFastInstanceSize添加的FAST_CACHE_ALLOC_DELTA16 8个字节
//进行16字节对齐
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
}
}
核心代码就是进行16字节对齐,size是实例大小
这个size是通过_flags & FAST_CACHE_ALLOC_MASK计算得到的。
该内容需要通过了解类结构后才能明白,类的结构底层学习请看
此时可以先简单认为size为对象的成员变量大小即可。
【第三步】 字节对齐
代码:
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
此处简单解释:
1、该函数的作用是16字节对齐,也就是对一个数以16倍数向上取整
2、x + size_t(15)的作用是向上取整,而且是16的整数倍。试想,任何一个大于1的数再加上15,肯定会大于16而向前进一位,所以这里+15就是以16的整数倍向上取整。
3、&size_t(15)的作用是抹掉后四位,试想,上一步的操作已经得到了进位,也就是取整完成了,那么此时剩下的不够进位的数据就可以抹掉了。 size_t(15)就是对后四位取0,将源数据与其相与,就可以抹掉后四位了。
也可以通过代入几个数,真实的计算一下,这个比较简单,不再赘述了。
这里仅粗略概括性的进行解释,关于字节对齐的详细内容请查看苹果的内存对齐原理
总结: 传入的extraBytes+对象属性大小进行16字节对齐后得到的数据就是需要开辟的内存大小。
2、申请内存空间,返回内存空间地址
代码
// 2;怎么去申请内存
//通过calloc来申请空间
obj = (id)calloc(1, size);
通过calloc来申请内存,size是内存大小,此处返回的是内存的地址空间,并不是真正的对象指针
3、关联类,返回对象地址
代码:
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
if (!nonpointer) {
isa = isa_t((uintptr_t)cls);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
//初始化isa
isa_t newisa(0);
#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
//arm64位进入这里,具体的后面学习,这里只要知道是在对isa结构体的成员变量进行初始化
//掩码
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
//是否有析构函数
newisa.has_cxx_dtor = hasCxxDtor;
//把类信息存储到isa中,也就是类与isa的绑定
newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
// This write must be performed in a single store in some cases
// (for example when realizing a class because other threads
// may simultaneously try to use the class).
// fixme use atomics here to guarantee single-store and to
// guarantee memory order w.r.t. the class index table
// ...but not too atomic because we don't want to hurt instantiation
//赋值
isa = newisa;
}
}
这里涉及到了isa的认识,但也不妨碍该函数的解读。具体的认识可以在第三大节对象与类的关系中详细了解。
在此处只要知道newisa.shiftcls = (uintptr_t)cls >> 3;这条语句就将类信息赋值到isa中,通过这种方式将类与该对象的isa关联起来。
- 1、给一个对象初始化一个isa指针
- 2、指针与cls类进行 关联
- 3、关联之后objc就是一个对象了,而不仅仅是一个内存空间
流程图:
alloc源码查找流程图
1.1.3 总结:
1、alloc开辟的空间大小是16的整数倍,因为采用的是16字节对齐算法,最小不小于16个字节
2、影响内存大小的因素是属性
3、alloc生成了一个对象,并且返回的是一个对象地址,而不是内存空间地址
4、alloc通过三步创建:1)计算空间大小;2)开辟空间;3)将isa与类绑定
1.2、init的分析
主要分析init进行初始化做了什么操作?返回的是什么?
1.2.1 查看源码
类方法
代码:
// Replaced by CF (throws an NSException)
+ (id)init {
return (id)self;
}
说明: 此处可以看到什么都没做,只是直接返回当前对象
实例方法
代码:
- (id)init {
return _objc_rootInit(self);
}
id
_objc_rootInit(id obj)
{
// In practice, it will be hard to rely on this function.
// Many classes do not properly chain -init calls.
return obj;
}
说明:此处可以看到实例方法也是什么都没做,只是直接返回当前对象
tips:
既然init方法没有进行任何操作,那么它的作用是什么?
1、创建init 方法只是为了更好的复用,便于给其他的初始化方法调用,本身是没有任何操作的。
2、这里的init是一个最简单的构造方法,主要是用于给用户提供构造方法入口
1.2.2 总结
init没有进行任何操作,直接返回当前对象。
1.3 new的分析
主要分析new进行初始化做了什么操作?,返回的是什么?
代码:
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
说明:
new函数中直接调用了callAlloc函数,且调用了init函数,所以可以得出new 就等价于 [alloc init]的结论
总结:
如果子类没有重写父类的init,new会调用父类的init方法
如果子类重写了父类的init,new会调用子类重写的init方法
如果使用 alloc + 自定义的init,可以帮助我们自定义初始化操作,例如传入一些子类所需参数等,最终也会走到父类的init,相比new而言,扩展性更好,更灵活。
tips:
但是一般开发中并不建议使用new,主要是因为有时会重写init方法做一些自定义的操作,例如 initWithXXX,会在这个方法中调用[super init],用new初始化可能会无法走到自定义的initWithXXX部分。
1.4 总结:
知识点总结:
1、alloc开辟的空间大小是16的整数倍,因为采用的是16字节对齐算法,最小不小于16个字节
2、计算的内存大小是extraBytes+实例大小,实例大小是类所有属性的实际占用内存大小
3、alloc的目的是开辟内存,并返回的是一个对象地址,而不是内存空间地址
4、alloc创建三步骤:1)计算空间大小;2)开辟空间;3)将类与isa绑定
5、init没有进行任何操作,直接返回当前对象
6、对象的创建开辟的内存采用16字节对齐,对象实际占用的内存大小是8字节对齐
7、苹果自动进行的属性重排会按照属性的内存从大到小到大来排列,这样可以提高性能
8、内存对齐算法有两种,1)拿到最后三位为000,其他为111的值,这样就任何数值与他相与都可以将后三位抹零。2)先右移再左移,右移将后四位抹零,左移恢复其他位数的所在的位置
9、NSObject的alloc调用:NSObject的alloc是系统调用的,并且底层会转化为objc_alloc。基础类的alloc会调用两次,一次是调用的NSObject的alloc,一个是自己的类,原因就是底层会有一个判断,如果不是NSObject类则会执行一次alloc,而这个判断本身就会执行这个NSObject的alloc
一句话总结底层原理:
1、alloc的目的是开辟内存,返回的是一个对象地址,而不是内存空间地址,并且开辟内存大小是16字节对齐,init没有进行任何操作,直接返回当前对象
2、对象的创建开辟的内存采用16字节对齐,真正需要的内存大小是8字节对齐,为了优化性能,苹果内部会进行属性重排其他知识点总结:
1、slowpath和fastpath这两个宏定义可以告诉编译器最有可能执行的分支,减少指令跳转,可以优化性能
2、位运算,左移是加,右移是减,这里的移可以看做是指1的移动,比如1111>>3就为0001,0111<<3就为0011 1000。
3、new == alloc+init,开发中不建议使用new
2、对象的底层结构
分析对象的底层结构,对象的本质,上层对对象的操作在底层是如何实现的。
2.1 编译cpp文件
clang是一个由Apple主导编写,基于LLVM的C/C++/OC的编译器,它主要用于底层编译,将.m文件编译成.cpp文件,通过它我们可以更好的观察底层的一些结构 及 实现的逻辑,方便理解底层原理。
在main中先创建一个类LGPerson,有一个属性name,还有一个变量age
代码:
终端命令:
//1、将 main.m 编译成 main.cpp
clang -rewrite-objc main.m -o main.cpp
//2、将 ViewController.m 编译成 ViewController.cpp
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m
//以下两种方式是通过指定架构模式的命令行,使用xcode工具 xcrun
//3、模拟器文件编译
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
//4、真机文件编译
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp
编译得到的对象结构
2.2 对象的本质
2.2.1 类结构
打开编译好的main.cpp,找到LGPerson类的定义,发现LGPerson在底层会被编译成 struct 结构体
LGPerson_IMPL结构体
//LGPerson的底层编译
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS; // 等效于 Class isa;
NSString *_name;
int age;
};
说明:
- 第一个属性其实就是NSObject的所有属性
- 这种写法是伪继承,是结构体的继承方式
- 伪继承的方式是直接将父类的结构体定义为LGPerson_IMPL中的一个成员
- 意味着LGPerson拥有NSObject的所有成员
- 第二个第三个是LGPerson自身的属性
查看NSObject_IMPL结构体
//NSObject 的底层编译
struct NSObject_IMPL {
Class isa;
};
说明:
- 这个NSObject_IMPL是NSObject的底层编译
- 说明自定义类会拥有NSObject的所有成员变量
- NSObject中肯定包含有isa,因此可以说明自定义类的isa是继承自NSObject
Class的结构体
typedef struct objc_class *Class;
- 在ojbc源码中搜索发现Class结构体是以objc_class为模板创建的
总结:
- 从上往下推,这就说明了所有的类结构体都是基于objc_class为模板创建的
- 只不过NSObject_IMPL结构体是直接继承自Class结构体,也就是包含了所有Class结构体中的所有信息
- 而我们自定义类比如NSPerson_IMPL结构体也是伪继承自NSObject_IMPL的
- 这样就做到了所有的类都是都是以objc_class为模板创建的了
2.2.2 对象结构
objc_object结构体
//可以看到,objc_object中就是一个isa
struct objc_object {
private:
isa_t isa;
public://外部可以获取isa
// getIsa() allows this to be a tagged pointer object
Class getIsa();
}
自定义结构体
#ifndef _REWRITER_typedef_NSStudent
#define _REWRITER_typedef_NSStudent
typedef struct objc_object NSStudent;
typedef struct {} _objc_exc_NSStudent;
#endif
extern "C" unsigned long OBJC_IVAR_$_NSStudent$_age;
struct NSStudent_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
};
说明:
- 可以看到类的结构体中只有一个isa,其他的都没有
- 对象的所有属性方法成员都是保存在类或元类中的,并且通过isa来进行获取
2.2.3 总结
- OC对象的本质是一个结构体,通过objc_object结构体为模板创建的
- 对象结构体内仅有isa,其他的属性、方法列表这些都存储在类或元类中,而不是对象中
- 继承关系存在于类结构体中,通过伪继承将父类的结构体作为子类结构体的成员,以此在底层达到继承的作用
2.3 属性的理解
看一下属性的setter方法是如何调用的
我们会创建很多的属性,每个属性都有自己的方法,这样每次都创建一个方法就会开辟很多的内存,因此苹果其实只用了一个objc_setProperty来实现方法,通过传入不同的值来体现方法的不同之处。
开始查找
- 可以看到原理就是新值retain,旧值release
- 属性进行设置时,其实进入的是objc_setProperty这个方法,
- 因为所有属性进行的设置操作都是一样的,每个属性都有一个setXXX方法会占用更多的内存空间
- 所以就用objc_setProperty作为中间函数把共有的操作统一执行,每个属性只执行各自特殊的内容
- 苹果采用适配器设计模式,对外提供一个接口,供上层的set方法使用,对内调用底层的set方法,使其相互不受影响,即无论上层怎么变,下层都是不变的,或者下层的变化也无法影响上层,主要是达到上下层接口隔离的目的
- 以后源码查找时就用objc_setProperty来查找的
2.4 总结
- OC对象的本质 其实就是 结构体,通过objc_object作为模板来创建的
- objc_object中只有一个isa成员,这个对象的其他成员都是在类的底层结构体objc_class中的,所以我们看属性方法这些就要去看类的信息即可
- 上层类的继承关系是通过底层结构体的继承来实现的,将结构体的第一个成员定义为父类的结构体,通过这种伪继承方式来实现
- 自定义类是伪继承自NSObject_IMPL结构体的,NSObject_IMPL又是伪继承自Class的,而Class结构体是以objc_class结构体为模板创建的
- 因为获取isa对外反馈的其实只是Class,因此getIsa()得到的其实就是Class信息,因此可以直接通过Class强转
- objc_setProperty()函数作为中间函数将所有setter方法的共有操作统一执行,只将不同属性的差异点作为参数传入。达到更加高效率的操作
3、isa的认识
上文我们看到在objc_object结构体中只有一个isa,因此我们分析一下isa
3.1 isa类型
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
说明:
- isa_t有两个成员,cls和bits,共同占用了这一段内存空间,因此他们是互斥的
- 说明isa_t有两种存储形式,一个是直接存储类信息,一个是存储bits,bits中会带有Class信息
- 直接存储类信息一般来说是系统创建的类,比如元类就只有Class信息
- 存储bits是自定义的类,除了Class信息还会带有其他的信息
- 通过cls初始化得到的isa_t,bits没有默认值,通过bits初始化得到的isa_t,cls会有默认值,因为bits中本来就带有class信息
- 因此我们知道在初始化isa指针时,有两种初始化方式
- ISA_BITFIELD的内容解释了这段内存的每位存储的是什么信息
- isa_t使用联合体位域,可以优化内存空间
- 一个isa有8个字节,也就是64位,是可以存储很多信息了
3.2 查看位域ISA_BITFIELD
- nonpointer,表示是否是自定义的类,占1位
- 0:纯isa指针
- 1:不只是类对象地址,isa中包含了类信息、对象的引用计数等
- 在初始化isa的方法中可以看到如果是nonpointer=0,说明是纯isa指针,只包含Class信息
- has_assoc表示关联对象标志位,占1位
- 0:没有关联对象
- 1:存在关联对象
- has_cxx_dtor 表示该对象是否有C++/OC的析构器(类似于dealloc),占1位
- 1:有析构函数,需要做析构逻辑
- 0:没有析构函数,可以更快的释放对象
- shiftcls表示存储类的指针的值(类的地址), 即类信息
- arm64中占 33位,开启指针优化的情况下,在arm64架构中有33位用来存储类指针
- x86_64中占 44位
- magic 用于调试器判断当前对象是真的对象 还是 没有初始化的空间,占6位
- weakly_refrenced是 指对象是否被指向 或者 曾经指向一个ARC的弱变量
- 没有弱引用的对象可以更快释放
- deallocating 标志对象是是否正在释放内存
- has_sidetable_rc表示 当对象引用计数大于10时,则需要借用该变量存储进位
- extra_rc(额外的引用计数) ,用来存储引用计数的值
- 如果对象的引用计数为10,那么extra_rc为9(这个仅为举例说明),实际上iPhone 真机上的 extra_rc 是使用 19位来存储引用计数的
- 如果太大了,就将其折一半存储到散列表中
3.3 总结
- isa_t采用联合体位域的方式存储信息,有两种存储形式,一个是直接存储Class信息,还有就是存储bits,而bits就包含了Class,二者是互斥关系
- 一般来说系统的类采用直接存储Class信息,自定义类存储bits信息
- 我们在获取isa时其实内部会进行处理,其实获取的是Class信息
- 自定义类的isa_t的类信息存储在bits中的shiftcls位域
- cls与isa的关联原理就是在isa指针中的shiftcls位域中存储类信息