前言
写代码一般都离不开创建对象,那这个过程到底是怎样的呢,今天我们就来探究一下。
alloc和init初探
我们先来看下对象alloc和init对指针的影响:
这里分别打印了
对象p
,指针p
,指针地址p
,得出如下结果:
可以看出来,三个对象指向的是同一个内存空间,我猜测在
alloc
中开辟了内存,而init
操作却没有,到底是不是这样呢,接下来我们继续探究
工具准备
- 下载 objc4-818.2 源码,编译可以参考这个男人的文章 源码编译调试
- 编译好的源码 objc4_debug
alloc源码探究
alloc的整体流程图如下
第一步:在main
中找到LGPerson
的alloc
,点击进入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__ // 判断是否有可用的OC编译器
// 传入的checkNil为nil, !cls为nil, 所以 slowpath为false
// slowpath是假值判断,不会走到if
if (slowpath(checkNil && !cls)) return nil;
// 判断累或父类是否有alloc/allocWithZone的默认实现
// fastpath为真值判断
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
// x很可能为真, fastpath 可以简称为 真值判断
#define slowpath(x) (__builtin_expect(bool(x), 0))
// x很可能为假,slowpath 可以简称为 假值判断
#define fastpath(x) (__builtin_expect(bool(x), 1))
引荐自文章 __builtin_expect说明
__builtin_expect
这个指令是gcc引入的,作用是 允许程序员将最有可能执行的分支告诉编译器,这个指令的写法为__builtin_expect(EXP, N)
意思是:EXP==N 的概率很大
__builtin_expect(bool(x), 0)
表示x值为假的可能性比较大__builtin_expect(bool(x), 1)
表示x值为真的可能性比较大slowpath
和fastpath
的作用是对编译器进行优化,减少指令的跳转而使性能降低- XCode中也可以通过设置来达到性能优化的目的:
Build Settings
->Optimization Level
->Debug
->Fastest, Smallest
,具体变化可以通过写一个简单的代码,然后修改该设置,再在汇编代码中查看变化
备注二:hasCustomAWZ()
bool hasCustomAWZ() const {
return !cache.getBit(FAST_CACHE_HAS_DEFAULT_AWZ);
}
// class or superclass has default alloc/allocWithZone: implementation (该类或者父类有没有默认的 alloc/allocWithZone:)
// Note this is is stored in the metaclass.
#define FAST_CACHE_HAS_DEFAULT_AWZ (1<<14)
- 表示 该类或者父类有没有默认的
alloc/allocWithZone:
实现
第四步:进入 _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
函数,该步骤为alloc
的核心
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;
// 计算要开辟的内存空间大小
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id 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;
}
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);
}
该流程分为三步:
instanceSize
:计算要开辟的内存空间大小calloc
:根据内存空间向系统申请地址指针initInstanceIsa
:关联类与地址
instanceSize
:计算内存大小
主要流程如下图:
备注一:align16
16字节对齐算法
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
- 举例:
~
是取反,也就是1变0,0变1
,&(与)
是同1则为1,反之为0
word_align
与align16
算法类似,不同点是前者+7, 然后 ~-7
- 为什么要字节对齐:
-
5s
以上的手机都是64位cpu
,64位的cpu
一次性可以处理64bit
数据,而1字节=8bit
,也就是64位的cpu
一次性可以处理8字节
数据 -
苹果采取
16字节对齐
,是因为OC
的对象中,第一位叫isa
指针,它是必然存在的,而且它就占了8字节
,就算你的对象中没有其他的属性了,也一定有一个isa
,那对象就至少要占用8字节
-
如果以
8字节
对齐的话,如果有连续的两块内存都是没有属性
的对象,它们的内存空间就会完全的挨在一起,容易混乱。以16字节
为一块,这就保证了CPU
在按块读取的时候效率更高,同时还不容易混乱。
calloc
:根据内存空间向系统申请地址
obj = (id)calloc(1, size);
我们可以通过断点来得出这个结论,未执行calloc
前 p obj
为 0x00000001002e3dc5
, 那么问题来了,没有创建怎么会有地址呢,这是系统分配的一个脏内存地址,执行后打印,得到一个地址:
通常我们打印的对象都是<LGPerson: 0x66666666>
这种形式,而这里却只有地址,为什么呢?主要是因为 obj
还没有传入 类
进行关联
initInstanceIsa
:关联类与地址指针
通过calloc
我们已经得到了指针地址,接下来我们需要将类与地址进行关联
- 进入
initInstanceIsa
的initIsa
函数,流程如下:
- 执行完这个流程后,我们通过断点打印
obj
:
总结:
- 通过阅读分析源码,我们印证了之前的猜想,
alloc
主要是创建对象 - 从三个步骤来进行创建:
计算
->申请空间
->关联
init
探究
类init
// Replaced by CF (throws an NSException)
+ (id)init {
return (id)self;
}
- 源码中的
init
方法是返回self
实例init
- (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;
}
- 该方法也是返回
self
,是一个构造方法
,可以重新该函数对类
进行一些拓展,也就是工厂模式
new
探究
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
- 本质上与
alloc
没有什么区别, 只不过如果项目中一个类用init
写了构造方法,而使用new
去创建对象,则不会走这个自定义的构造,这样可能会导致一些错误产生 - 相比
init
更加灵活,建议使用init
,根据场景来使用new