我们先从下面案例看看alloc和init分别做了什么事?
BKPerson *p1 = [BKPerson alloc];
BKPerson *p2 = [p1 init];
BKPerson *p3 = [p1 init];
BKNSLog(@"%@ - %p - %p",p1,p1,&p1);
BKNSLog(@"%@ - %p - %p",p2,p2,&p2);
BKNSLog(@"%@ - %p - %p",p3,p3,&p3);
输出打印结果
打印的分别是对象描述、指针指向的地址(即当前指针存放的对象的地址)、当前指针地址(当前指针被存储在哪里)。
可以看出p1,p2,p3指向了同一个内存地址
0x600001138790
,而分别使用了3个指针保存这个内存地址的值。
猜想:在alloc一步就已经创建出了一个对象??
我们再通过objc源码来验证这个猜想
源码探究
一步一步跟进alloc的源码
- alloc ——> _objc_rootAlloc
+ (id)alloc {
return _objc_rootAlloc(self);
}`
- _objc_rootAlloc——> callAlloc
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
- callAlloc——> _objc_rootAllocWithZone
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#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));
}
这里面系统用到了两个宏定义
#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))
-
__builtin_expect
这个指令是gcc引入的,作用是允许程序员将最有可能执行的分支告诉编译器。这个指令的写法为:__builtin_expect(EXP, N)
。意思是:EXP==N的概率很大。 -
__builtin_expect()
是 GCC (version >= 2.96)提供给程序员使用的,目的是将“分支转移”的信息提供给编译器,这样编译器可以对代码进行优化,以减少指令跳转带来的性能下降。 - 也就是说
fastpath(x)
告诉编译器x值为真的可能性最大。编译器会更大可能编译fastpath
条件分支里的指令,也就是告诉编译器很大概率走这条分支。 -
slowpath(x)
告诉编译器x值为假的可能性最大,也就是x
为真的可能性很小,当x==0
为真才执行这个条件分支下的语句。编译器会更大可能编译else
部分的指令,也就是告诉编译器很小概率会走if slowpath(x)
这条分支的指令。 - 通过这种方式,编译器在编译过程中,会将可能性更大的代码紧跟着前面的代码,从而减少指令跳转带来的性能上的下降。
在回到callAlloc
方法里,由于这里有几个分支,可以通过断点调试跟方法,看是走的哪一个分支。
在if (fastpath(!cls->ISA()->hasCustomAWZ()))
中,cls->ISA()->hasCustomAWZ())
判断一个类是否有自定义的 +allocWithZone
实现,所以fastpath(!cls->ISA()->hasCustomAWZ())
表示这个类没有自定义的+allocWithZone
时走这部分代码。所以执行了_objc_rootAllocWithZone(cls, nil);
- _objc_rootAllocWithZone——> _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:需要开辟的内存大小,可以看到外部传入的extraBytes为0
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;向系统申请内存,返回地址指针
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);
}
这个方法主要做了三个事情:计算需要的内存大小、申请内存返回地址指针、关联到对应的类。下面分析这个三个方法都做了哪些工作
alloc核心方法
1.计算内存大小
计算需要开辟的内存空间大小是通过size = cls->instanceSize(extraBytes);
内部实现过程如下
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;
}
在这里可以发现,if (size < 16) size = 16;
不够16个字节,会手动分配16个字节。之后调试跟进走的是cache.fastInstanceSize
快速计算实例大小的方法。
在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);
}
}
align16
的实现,可以看到当前使用的是16字节对齐的方式。
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
图示16字节对齐运算
16字节对齐的目的
-
提高性能,加快存取速度
:通常内存是由一个个字节组成的,cpu在存取数据时,并不是以字节为单位存储,而是以块为单位存取。频繁存取字节未对齐的数据,会极大降低cpu的性能。固定16字节的存取长度,可以更快存取数据。 -
更安全
:苹果如今采用16字节对齐,由于在一个对象中,isa占8字节,而对象每个属性也占8字节,当对象无属性时,会预留8字节,即16字节对齐,如果不预留,CPU存取时以16字节长度会导致访问到相邻的其他对象,造成访问混乱。
在执行完cls->instanceSize(extraBytes)
就可以打印出计算出的内存大小size为16
我们知道OC代码底层是用C/C++实现的,将OC转成C/C++代码可以发现,NSObject实际是一个结构体,结构体里只有个isa指针,既然是指针,在64位机环境下占了8个字节,在32位环境下占了4个字节。而NSObject这个结构体内部只有这么一个指针,那NSObject本身也是占用8字节。
struct NSObject_IMPL {
Class isa;
};
可以用runtime的一个函数,获取类的实例大小
class_getInstanceSize(Class _Nullable cls)
需要引入#import <objc/runtime.h>
,打印结果果然是8个字节
NSLog(@"InstanceSize:%zd",class_getInstanceSize([NSObject class]));
结果:InstanceSize:8
我们还能通过导入#import <malloc/malloc.h>
,查看到这个函数
extern size_t malloc_size(const void *ptr);
使用这个函数打印
NSLog(@"%zd",malloc_size((__bridge const void *)(obejct)));
结果是 16
由此看出对象本身占用8个字节,而系统开辟了16个字节用来保存这个对象。
进一步验证
上面只是NSObject
类的情况,而通常类都会有相关属性。
给BKPerson
类增加4个属性,两个字符串类型,两个int
类型
@interface BKPerson : NSObject
@property (nonatomic,strong) NSString *name; // Dog
@property (nonatomic,strong) NSString *nick; // KK
@property (nonatomic) int age;
@property (nonatomic) int hobby;
@end
运行调试
查看对象内存地址
可以看到对象isa指针占据了8字节,两个string属性每个占据8字节,一共8+8+8=24个字节,但是系统开辟了32字节,也就是有8个字节为空。
再赋值两个int
属性,而64位机下int
类型占据了4个字节,可以看到之前空的8个字节,刚好放了age和hobby两个属性的值。
结合以上16字节对齐分配空间法则,进一步得出结论:
类的实例占用8字节,而每一次申请的内存空间是16字节。类的实例在第一次申请内存空间就申请了包括isa和所有属性的内存空间大小,跟属性有没有赋值无关
2.申请内存,返回地址指针
return _class_createInstanceFromZone(cls, 0, nil, OBJECT_CONSTRUCT_CALL_BADALLOC);
从第三个参数可以看到传入的zone
为nil,由于iOS 8以后废除了用zone
开辟内存的方式,所以是用obj = (id)calloc(1, size);
的方式申请内存。这里面size就是我们上面字节对齐算法得出的内存大小。
打印执行calloc
之后的地址
而这里只打印出了开辟的内存地址,没有类的信息例如
<BKPerson: 0x101019160>
,验证了calloc
这一步只是向系统申请内存空间。
3.关联相应的类
将地址指针与类相关联
obj->initInstanceIsa(cls, hasCxxDtor);
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
这一步会init一个isa指针,与类关联起来
打印指针描述,可以看到已经关联上类了
总结:以上对alloc源码的探究,可以得知alloc的主要作用就是使用16字节对齐算法计算内存,开辟内存,关联类。
alloc的整体流程图示
那么init帮我们做了什么事呢?
+ (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
的类方法和对象方法返回的都是对象本身。 - 不同的是类方法返回了一个
id
类型的self
,这是为了可以给开发者提供自定义构造方法的入口,通过id
强转类型实现工厂设计,返回我们定义的类型。
new
我们习惯于用new
一个对象,可以更省略代码,通过源码可以知道它跟alloc+init
的方式本质并没有区别
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
但是一般开发中并不建议使用new
,主要是工厂设计重载init方法我们如果做一些业务的操作,用new
初始化则无法执行到里面去。