QLJPerson *qp1 = [QLJPerson alloc];
QLJPerson *qp2 = [qp1 init];
QLJPerson *qp3 = [qp1 init];
// 输出是:QLJPerson对象 - 指向地址 - 对象地址
NSLog(@"%@ - %p - %p",qp1,qp1,&qp1);
NSLog(@"%@ - %p - %p",qp2,qp2,&qp2);
NSLog(@"%@ - %p - %p",qp3,qp3,&qp3);
思考:qp1、qp2、qp3的输出是什么?
实际运行打印结果:
由打印结果可以看到qp1、qp2、qp3的指针都是指向同一个内存地址,所以 QLJPerson对象 - 指向地址都是 相同的;这三个对象分别有单独的内存地址,所以只有最后一项的打印是不同的。
思考:qp1是使用alloc创建,qp2、qp3都是使用qp1的init创建,为什么会指向同一个内存地址? 那么让我们带着这个问题开始分析alloc、init和new的源码。
1. 准备环境
- 下载最新的 objc4 源码:当前最新是objc4-781
- 配置编译源码,可参考此文章:https://github.com/LGCooci/objc4_debug
2. Alloc分析
alloc + init 整体源码的探索流程如下
(注:图片参考自“Style_月月”的博客)
执行alloc时,在NSObject中可以发现其调用的_objc_rootAlloc方法;继续跟进源码,下面执行的是callAlloc方法;继续… 执行_objc_rootAllocWithZone方法;然后执行obj-runtime-new中_class_createInstanceFromZone方法,在_class_createInstanceFromZone方法中,执行了三个重要步骤分别是instanceSize(计算内存大小)、calloc(开辟内存)和initInstanceIsa(将cls类与objc指针绑定)。
- 执行[QLJPerson alloc] 时进入alloc源码,在objc4-781中alloc的源码是:(在NSObject中实现的)
+ (id)alloc {
return _objc_rootAlloc(self);
}
- 继续跟进,进入_objc_rootAlloc(在NSObject中实现的)
// 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中(在NSObject中实现的)
// Call [cls alloc] or [cls allocWithZone:nil], with appropriate
// shortcutting optimizations.
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
/**
* 其作用是将最有可能执行的分支告诉编译器
* x很可能为真, fastpath 可以简称为 真值判断
* #define fastpath(x) (__builtin_expect(bool(x), 1))
* x很可能为假,slowpath 可以简称为 假值判断
* #define slowpath(x) (__builtin_expect(bool(x), 0))
**/
#if __OBJC2__
if (slowpath(checkNil && !cls)) return nil;
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);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
- 跟进到_objc_rootAllocWithZone方法中(在run-time-new中),发现执行的是_class_createInstanceFromZone
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,我们就会发现重点来了:
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:要开辟多少内存
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (zone) {//在iOS8及以上废弃了zone,早期开辟内存的方式
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
// 2;怎么去申请内存
obj = (id)calloc(1, size);
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
// 3: 类和内存地址指针相关联
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);
}
if (fastpath(!hasCxxCtor)) {
return obj;
}
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
根据源码分析到_class_createInstanceFromZone中主要就干了三件事:
- 计算需要申请开辟的内存空间
- 开辟空间
- 类和内存地址指针相关联
2.1 alloc计算需要开辟的内存空间大小 cls->instanceSize(extraBytes)
在上述源码分析中就会发现extraBytes为0(额外的字节数)。
那么在instanceSize中,就会发现类的内存大小是由其属性决定的 :
size_t instanceSize(size_t extraBytes) const {
//编译器快速计算内存大小
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
//alignedInstanceSize():属性的大小
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
- 在fastInstanceSize中计算内存大小
在调试中,会发现执行的align16方法,
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
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
}
}
在调试时传入参数:size + extra - FAST_CACHE_ALLOC_DELTA16
-
size:16
-
extra : 0
-
FAST_CACHE_ALLOC_DELTA16 : #define FAST_CACHE_ALLOC_DELTA16 0x0008 八个字节
-
align16 – 16字节对齐算法
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
什么是字节对齐呢?
在计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是字节对齐。
简而言之:以16字节对齐为例,类的大小都是以16的整数倍进行存储,如果不足以16的倍数就加一个偏移量变成16的整数倍。 这样在访问时大大节省了访问时间。
OC中由于在一个对象中,第一个属性isa占8字节,当然一个对象肯定还有其他属性,当无属性时,会预留8字节,即16字节对齐,如果不预留,相当于这个对象的isa和其他对象的isa紧挨着,容易造成访问混乱。
那么(x + size_t(15)) & ~size_t(15);是如何计算的呢?
我们以 8 + 15 = 23为例:
(8 + size_t(15)) & ~size_t(15)其中:
- &:是与计算,真真得真,反之为假;
- ~:取反运算符。
- 需要转成2进制计算
8 + size_t(15)) ==》 23 的二进制: 0000 0000 0001 0111
15的二进制:0000 0000 0000 1111 =》 取反(~15): 1111 1111 1111 0000
0000 0000 0001 0111 & 1111 1111 1111 0000 = 0000 0000 0001 0000
结果是16,正好是16的整数倍。
总结: 在OC中类的内存大小是16的整数倍,且其大小是由属性决定的。
2.2 开辟空间(id)calloc(1, size);
通过instanceSize计算的内存大小,向内存中申请 大小 为 size的内存,并赋值给obj,因此 obj是指向内存地址的指针.
obj = (id)calloc(1, size);
在执行完calloc后,打印obj就会发现obj指向了一个地址:
2.3 类和内存地址指针相关联initInstanceIsa
主要过程就是初始化一个isa指针,并将isa指针指向申请的内存地址,在将指针与cls类进行 关联。
在执行完 obj->initInstanceIsa(cls, hasCxxDtor)后,打印Obj就会发现LGPerson类与指针相互关联了。
3. init分析
在NSObject.mm文件中,有两个init方法分别为:
// 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;
}
根据源码分析发现,init就是返回的self。我们再看文章开头的问题,我的天呐,原来是这么回事,竟然这么如此简单粗暴。
4. new分析
在NSObject.mm文件中,new的源码如下:
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
new就是执行alloc与init,但是一般在实际开发中,不建议使用new方法,因为类的init方法我们一般会重写。如果不进行重写的话,使用new方法也是可行的