前言
我们在开发中最常用的就是 alloc跟init。先看下面的代码跟运行的结果,会发现p1,p2,p3打印的内存地址是一样的。
所以,可以得到下面的结论:
1.
alloc让对象有了内存空间跟指针指向。
2.init之后内存并没有发生变化,说明 init 并没有操作指针。而且变量地址是相差 8 字节,说明了栈内存是连续的并且指针占 8 字节内存空间(栈区内存从高地址到低地址,堆区从低地址到高地址).
定位源码的三种方法
1.断点调试
在 alloc 上加一个断点,按住 control+step into 单步往下走一步。发现 objc_alloc方法,添加 objc_alloc 符号断点。通过符号断点发现,alloc 在 libobjc.A.dylib 这个库中。
2.汇编调试
在 xcode 中 debug->debug workflow ->always show disassembly 中打开允许汇编,在 alloc 上在打上断点,会发现进入断点 进入了objc_alloc方法,然后在根据符号断点去查看该方法使用的动态库。
3.通过已知方法进行符号断点
直接添加 alloc enable symbolic breakpoint
结合源码还有汇编调试分析
打开下载的源码 搜索 alloc { :发现 alloc 里面调用的是_objc_rootAlloc 方法。在点击 _objc_rootAlloc,调用的是 callAlloc
调用
callAlloc之后 ,是执行_objc_rootAllocWithZone 还是objc_msgSend呢?
_objc_rootAllocWithZone
先看下 _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
在查看_class_createInstanceFromZone发现有三个重要方法
1.计算需要开辟的内存空间大小instanceSize
2. 申请内存空间calloc
3.将 cls 类和 isa关联initInstanceIsa
instanceSize
在继续查看instanceSize内部的代码
进入到这个函数,首先判断是否有缓存,如果有执行cache.fastInstanceSize函数直接返回,内存开辟结束,获得该对象内存大小。如果没有缓存,会执行alignedInstanceSize函数,执行word_align函数,此函数的参数是函数unalignedInstanceSize,而这个函数通过data()->ro()->instanceSize获取到对象的实例大小,也就是说,最终开辟内存空间的大小是根据对象的成员变量大小决定的。这里我们看一下unalignedInstanceSize()的返回值是多少,跟进去我们发现,该返回值的大小由实例变量的大小决定,依赖于成员变量(ivars):
默认情况下,不创建任何成员变量,类开辟的内存空间是8字节,因为继承NSObject造成的,NSObject内有成员变量isa,由于isa的类型是结构体指针,所以isa是8字节,所以创建一个新的对象,没有任何成员变量,默认内存大小是8字节
PS:
instanceSize疑问 走缓存还是下面的分支?
// May be unaligned depending on class's ivars.
uint32_t unalignedInstanceStart() const {
ASSERT(isRealized());
return data()->ro()->instanceStart;
}
// Class's instance start rounded up to a pointer-size boundary.
// This is used for ARC layout bitmaps.
uint32_t alignedInstanceStart() const {
return word_align(unalignedInstanceStart());
}
// May be unaligned depending on class's ivars.
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
inline size_t instanceSize(size_t extraBytes) const {
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
//16字节对齐
return cache.fastInstanceSize(extraBytes);
}
//8字节对齐,至少是16 字节
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
fastInstanceSize
在继续查看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
//这里进行 16 字节对齐
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
}
}
对齐算法
继续查看align16
#ifdef __LP64__
# define WORD_SHIFT 3UL
# define WORD_MASK 7UL
# define WORD_BITS 64
#else
# define WORD_SHIFT 2UL
# define WORD_MASK 3UL
# define WORD_BITS 32
#endif
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
static inline size_t word_align(size_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
static inline size_t align16(size_t x) {
//16字节对齐算法 &为与操作 ~为取反操作
return (x + size_t(15)) & ~size_t(15);
}
我们发现此方法是16字节对齐算法,在解析该算法前,我们需要了解为什么要进行16字节对齐。
我们需要知道CPU在读取数据时,是以字节快为单位进行读取的,如果频繁读取没有对齐的数据,会严重加大CPU的开销,降低效率为什么16字节对齐而不是8字节对齐,我们都知道在一个对象中,第一个属性isa占8字节,如果只有8字节的话,不预留空间,可能造成这个对象的isa和另一个对象的isa紧挨着,容易造成访问混乱。同时一个对象也不会只有isa一个属性。由此可见:16字节对齐后,可以加快CPU读取速度,同时访问也会更加安全。
内存对齐是 --- 16字节对齐 16字节对齐算法的过程,如下所示
x + size_t(15)) & ~size_t(15)
&为与操作 ~为取反操作
&(与)的规则是:全部为1则为1,反之则0
~(取反)的规则是:1变0,0变1
此处我们以9为例
9+15=24
24的二进制为:0001 1000
15的二进制位:0000 1111
15的取反二进制位:1111 0000
0001 1000
1111 0000
= 0001 0000 也就是10进制的16
8字节对齐
算法讲解:
首先我们知道此时:x = 8 && WORD_MASK = 7;
那么函数中的计算公式就是:(8 + 7) & ~7 也就是 15 & ~7。
15的二进制是 ---> 0000 1111。
7的二进制是 ---> 0000 0111,那么~7为 ---> 1111 1000。
那么15 & ~7 就是 0000 1111 & 1111 1000,结果为0000 1000 == 8
calloc
calloc分析:申请内存,返回地址指针
通过instanceSize方法计算的内存大小,向内存中申请大小为size的内存,并赋值给obj,因此 obj是指向内存地址的指针。如图所示
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
initInstanceIsa
接着分析initInstanceIsa方法,初始化指针 ,和类关联起来,查看其实现
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
执行calloc之后,内存已经分配,initInstanceIsa初始化isa指针,并将isa指针指向已经分配好的内存地址,再将isa指针与cls类进行关联
总结
alloc的主要目的就是开辟内存,并使得isa指针和cls类进行关联。
跳转到的地方
在类的实现过程中,类是否为非懒加载,如果是非懒加载就会在main函数之前;如果懒加载,就会在第一次发送消息的时候,会对类进行初始化,也就是实现类!过程中会将fastInstanceSize设置到缓存中。
alloc的流程图
补充一下[NSObject alloc]流程,因为在调用NSObject的alloc方法时,alloc已经放入缓存,(系统初始化时,已经被其他的类调用,放入了缓存!)。所以NSObjec alloc流程会直接调用_objc_rootAllocWithZone。
init 跟 new
init
return _objc_rootInit(self);
}
断点进入 _objc_rootInit
_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方法返回的是对象本身
- init可以提供给开发者更多的自由去自定义 ,通过id实现强转,返回我们需要的类型
new
return [callAlloc(self, false/*checkNil*/) init];
}
源码显示 走了callAlloc的方法流程,然后走了init方法 ,所以 new看做是alloc + init
总结
alloc 的核心作用就是开辟内存,通过isa指针与类进行关联,init方法,提供开发者更多的自由,new 是对(alloc+init)进行了封装,无法在初始化的时候添加其它的需求。
补充说明
这里面有
slowpath 跟 fastpath fastpathfastpath(!cls->ISA()->hasCustomAWZ())
bool hasCustomAWZ() const {
return !cache.getBit(FAST_CACHE_HAS_DEFAULT_AWZ);
}
继续查看FAST_CACHE_HAS_DEFAULT_AWZ
// class or superclass has default alloc/allocWithZone: implementation
// Note this is is stored in the metaclass.
#define FAST_CACHE_HAS_DEFAULT_AWZ (1<<14)
也就是说cls->ISA()->hasCustomAWZ()是用来获取类或父类中,是否有alloc/allocWithZone:的实现。再次查看fastpath
#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))
fastpath&slowpath是两个objc源码中定义的宏。
调用的都是__builtin_expect。
__builtin_expect(bool exp, probability)的主要作用是进行条件分支预测。
函数主要有两个参数:
第一个参数:是一个布尔表达式
第二个参数:表明第一个参数为真值的概率,这个参数只能是1或0;当取值为1时,表示布尔表达式大部分情况下的值为真值;当取值为0时,表示布尔表达式大部分情况下的值是假值。
函数的返回值,就是第一个参数的表达式的值。
在一条指令执行时,由于流水线的作用,CPU可以完成下一条指令的取值,这样可以提高CPU的利用率。在执行一条条分支指令时,CPU也会预取下一条执行,但是如果条件分支跳转到其他指令,那么CPU预取的下一条指令就没用了,这样就降低了流水线的效率。__builtin_expect函数可以优化程序编译后的指令序列,使指令尽可能的顺序执行,从而提高CPU预取指令的正确率
例如:
if (__builtin_expect (x, 0))
foo();
表示:x的值大部分情况下可能为假,因此foo()函数得到执行的机会比较少。这样编译器在编译这段代码的时候,就不会将foo()函数的汇编指令紧挨着if条件跳转指令。
再比如:
if (__builtin_expect (x, 1))
foo();
表示:x的值大部分情况下可能为真,因此foo()函数得到执行的机会比较大。这样编译器在编译这段代码的时候,就会将foo()函数的汇编指令紧挨着if条件跳转指令。
为了简化函数使用,iOS系统使用两个宏fastpath和slowpath来实现这种分支优化判断处理:
#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))
那也就是说fastpath(!cls->ISA()->hasCustomAWZ())的结果,其实就是!cls->ISA()->hasCustomAWZ()的结果。
这里还有一个判断__OBJC2__是用来判断是否有编译优化。