从头开始
从事了几年的iOS开发,我们写的最多的或者说最开始写的一句代码,大概就是[[XXX alloc] init]了吧,以前只知道这是创建了一个对象,开辟了一块空间,并且对其初始化,那么到底句代码有做了什么呢,今天我想去看看。
准备工作
为了知道其底层到底做了什么,需要准备一份objc的源码,这里由于笔者的版本是10.15,那么也就去最新的代码里看看,objc-781,这里将地址贴出来objc源码地址下载,那么准备工作做好了,迫不及待的想去看看了。
小例子
正式进入之前先来看一段代码
KKSearch *p1 = [KKSearch alloc];
KKSearch *p2 = [p1 init];
KKSearch *p3 = [p2 init];
NSLog(@"%@ - %p - %p",p1, p1, &p1);
NSLog(@"%@ - %p - %p",p2, p2, &p2);
NSLog(@"%@ - %p - %p",p3, p3, &p3);
来看看结果,可以看出p2和p3除了指针地址与p1是不一样的,其实它们指向的都是同一块内存地址,也是同一个对象,那么这里有一个猜想,如果是这样的话,是不是init方法并不影响对象的内存地址,那么init究竟做了什么。
init初探
从源码中可以看到init方法中什么也没做,这也就解释了上面的例子中为什么指向了同一块内存地址,那么重点显而易见的的转移到了alloc方法中,看来主要的工作都在这里了。
- (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;
}
#alloc探索
现将alloc的整个流程图贴出来,让大家对整个过程先有个清晰的认识
接下来将从objc源码中一步步的跟进看看究竟发生了什么
- 从
alloc进入
+ (id)alloc {
return _objc_rootAlloc(self);
}
- 进入
_objc_rootAlloc
id _objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
callAlloc
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));
}
这里有两个宏定义需要看一下,slowpath和fastpath
#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的概率很大fastpath(x)等价于x为真的可能性很大slowpath(x)等价于x为假的可能性很大 该指令带来的好处就是:编译器可以对代码进行优化,减少了由于指令跳转带来的性能损耗
cls->ISA()->hasCustomAWZ()这里是判断当前类是否有自定义实现的allocWithZone,这里未实现所以走入_objc_rootAllocWithZone方法
4. _objc_rootAllocWithZone
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方法中,在这个方法可分为三个重要的步骤
-
`cls->instanceSize`计算内存大小 -
`calloc`根据计算结果开辟内存空间 -
`obj->initInstanceIsa`将类与isa指针相关联
cls->instanceSize
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;
}
这里看缓存中如果已经有计算好的大小则走入fastInstanceSize方法
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);
}
}
根据断点调试后发现走入else分支,这里涉及到aling16这个16字节对齐的方法
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
16字节对齐的方法清晰可见,将x与15相加的结果再与15取反后相与,这里可以注意到为什么16字节对齐会与15有关,15的二进制是0000 0000 0000 1111取反后得到 1111 1111 1111 0000,可以看到最后四位都是0,那么与任何数都能保持第四位均为0,这样地址上就做到了16字节对齐,那么可见如果是8字节对齐,关键数字则为7。
如果缓存中没有的话会进入alignedInstanceSize()这个方法,在这里同样能看到有字节对齐的方法
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
同样是要进行字节对齐,这里看到WORD_MASK是7,那么说明首先是进行8字节对齐的,但是这种结果如果不足16字节,将统一按照16字节处理。
内存对齐
这里既然谈到了内存对齐,那么有必要去探究一下究竟何为iOS的内存对齐。在iOS中获取内存大小的方式有三种
sizeofclass_getInstanceSizemalloc_size
NSObject *obj = [[NSObject alloc] init];
// objc对象类型占用的内存大小
NSLog(@"objc对象类型占用的内存大小:%lu",sizeof(objc));
// 成员变量占空间大小(实际使用空间)
NSLog(@"%zu", class_getInstanceSize(NSObject.class));
// obj所指向的分配的空间大小(实际分配空间)
NSLog(@"%zu", malloc_size((__bridge const void *)(obj)));
最后结果是:8,8,16
总结
sizeof
sizeof计算的是类型占用内存的大小,其中可以计算基本数据类型、对象、指针。对于基本数据类型sizeof指的是数据类型占用的内存大小。那么对于实例对象obj来说,它指的则是objc_object结构体指针的大小,和isa指针没有任何关系。
下面来佐证这个观点,首先定义个结构体指针pst1
struct test_struct
{
}str1 = {},*pst1 = &str1;
可以看到
sizeof的结果是8,说明了sizeof计算指针类型时,度量的就是结构体指针的大小,与其中的内容并无关系。
#####class_getInstanceSize
此方法是计算对象实际占用的内存空间大小,是根据类的属性来变化,其源码中是这样:
# define WORD_MASK 7UL
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
可以简单理解为8字节对齐。
#####malloc_size
此方法是计算实际分配给对象的内存空间,那么为什么与class_getInstanceSize结果不同呢?可以参考上面分析到的,最终分配的内存是以16字节对齐的方法来计算的。
calloc
calloc方法仅仅是开辟了一块空间,此时没有任何信息,因为此时obj的地址并未与传入的类cls发生任何关联。
##obj->initInstanceIsa
主要过程就是初始化一个isa指针,并将isa指针指向申请的内存地址,在将指针与cls类进行关联。
NSObject 的 alloc
上面分析了NSObject的子类的alloc流程,再重新尝试的时候发现NSObject的alloc并不像上面分析的那样,首先并没有走到_objc_rootAlloc方法,what happened,重新进入后开始分析,首先看看函数调用栈
那么简单了,找到
objc_alloc函数并打上断点,发现的确是走到了这里
可以看到此时
cls的确是NSObject,看到这里就想到Person这个类在我们看到走到alloc是真的是直接调起了alloc方法吗?一波操作直接回到之前的分析来看看
这里就很清楚的可以看到同样
Person这个类第一步也是调起了objc_alloc方法,同时这也解释了为什么我们在之前的分析中发现callAlloc函数进入了两次,同样打印栈信息也可以得到
那么现在是得到两个问题:
Person中alloc为什么走了两遍?NSObject的alloc为什么走到了objc_alloc方法?
Person中alloc为什么走了两遍
对于这个问题,通过以上的调试基本可以确认的是在查找alloc的方法编号的过程中,系统在底层进行了一些操作,最后找到了objc_alloc,这是在llvm级别所做的优化
大意即是当指定的方法返回为
true时将会把alloc方法指定为objc_alloc方法
同时也在llvm的代码中找到依据,详情可见EmitObjCAlloc函数
所以这个优化是在llvm级别来完成的。