前言
这篇文章主要是用来记录一下对alloc底层原理的探索。
源码:objc4
alloc & init & new
探索之前先大概了解一下alloc、init、new分别做了什么
- alloc:创建新的对象
- init:返回对象本身,没做其他任何处理,
- new:等价于alloc+init
NSObject * obj = [NSObject alloc];
NSObject * obj1 = [obj init];
NSObject * obj2 = [NSObject new];
NSLog(@"obj :%p----%p",&obj,obj);
NSLog(@"obj1:%p----%p",&obj1,obj1);
NSLog(@"obj2:%p----%p",&obj2,obj2);
打印结果为:
obj: 0x16b0e92d8----0x1c4010f10
obj1:0x16b0e92d0----0x1c4010f10
obj2:0x16b0e92c8----0x1c4010fd0
根据打印结果可以看出:
- obj、obj1、obj2是一串连续的间隔8字节的指针地址,地址从高到底的顺序排列。由此得以验证:栈区是一段连续的内存空间,内存从高到低,64位下指针占用8字节的空间。
- init直接返回原对象,所以obj、obj1指向的对象相同,而new则会重新创建一个对象,所以obj2会指向一个新的对象。
alloc源码探索
1、定位到源码库
1.1、在需要调试的位置打上断点。
- Step Into 单步执行,遇到子函数就进入而且继续单步执行。
- Step Over 在单步执行时,在函数内遇到子函数时不会进入子函数内单步执行,而是将整个子函数做为一个总体,至关于一条语句,只执行一步。
- 上述两种单步调试配合control使用则变成汇编单步调试。
1.2、按住control键+step into进行单步调试进入到汇编,可以看到alloc将会调用到objc_alloc
1.3、接下来点击xcode左下角➕,选择Symbolic Breakpoint选项
1.4、输入需要添加断点的方法,比如objc_alloc(alloc会有很多次调用,最好先把符号断点设为disable,到我们需要的位置再打开,以避免干扰。)
1.5、根据符号断点可知,alloc流程源码位于libobjc.A.dylib中,我们可以在opensource下载objc4源码。
除此之外还可以打上断点,并勾选Debug -> Debug Workflow -> Always Show Disassembly。
运行程序到断点位置,就会直接进入到汇编代码。
2、分析alloc执行流程
2.1、首先在main函数中创建所需对象。
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject * obj = [NSObject alloc];
JLObject * obj0 = [JLObject alloc];//继承于NSObject
JLObject * obj1 = [JLObject alloc];//偶然发现第一次调用会进入到NSObject的alloc,第二次调用将会直接进入_objc_rootAllocWithZone
}
return 0;
}
2.2通过可编译objc4运行会发现,而是会进入objc_alloc函数,在汇编代码中我们看到了同样的结果。
2.3、跳转进入到callAlloc的源码:
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
if (slowpath(checkNil && !cls)) return nil;
/*
* class或superclass中是否存在默认的alloc/allocWithZone实现。
*/
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
2.4、进入到_objc_rootAllocWithZone,发现会跳转到_class_createInstanceFromZone方法中。
_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);
}
2.5、_class_createInstanceFromZone里面包含了alloc的核心操作。
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
......
bool fast = cls->canAllocNonpointer();
size_t size;
//计算实例大小,实际占用空间8字节对齐,外部16字节对齐
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 (!zone && fast) {
//是否有C++或Objc的析构器
//nonpointer
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
//纯地址指针,
obj->initIsa(cls);
}
......
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
3、alloc的核心方法
- instanceSize计算需要的内存空间大小;
- calloc根据计算的大小开辟内存;
- initInstanceIsa初始化实例的isa;
4、分析objc_alloc
- 问题:断点到alloc调用的行,通过汇编与运行源码发现调用alloc后并不会进入到alloc的实现,而是会进入到objc_alloc中,这是如何做到的呢?
- 猜测:既然在调用alloc时会进入到objc_alloc的实现,那首先想到的就是sel所对应的imp在某一个时间被替换了。
alloc的实现:
+ (id)alloc {
return _objc_rootAlloc(self);
}
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
4.1、根据上述的猜测,首先对objc_alloc进行全局的搜索,果然找到了fixupMessageRef方法。打断点并运行,发现全程并未计入到断点。
4.2、根据fixupMessageRef的调用向上追溯,发现是在_read_images中进行了调用,它的作用是对objc_msgSend进行修复,而这里并没有执行,alloc的imp指向却变了,而在源码中我们没有再找到其他的修改alloc imp的地方。
4.3、既然不是运行时修改的,那会不会是在编译阶段就进行了替换呢,打开llvm,全局搜索objc_alloc,在tryGenerateSpecializedMessageSend中找到了我们想要的结果,进入到EmitObjCAlloc,
4.4、如图就是在这里llvm将alloc的imp修改到了objc_alloc。
5、分析hasCustomAWZ()
我们回过头来分析一下hasCustomAWZ()方法,从这里开始NSObject与其子类调用流程会有所不同
!hasCustomAWZ()
- NSObject会直接进入到_objc_rootAllocWithZone中,
- JLObject首次调用回进入objc_msgSend流程,调用NSObject的alloc,然后执行_objc_rootAlloc之后又再次调用callAlloc。
- JLObject第二次调用会直接进入到_objc_rootAllocWithZone,此处与NSObject相同。 猜测可能JLObject第一次调用时,通过慢速方法查找流程,将父类中的方法加入到了cache中,JLObject第二次调用的时候方法在cache中已经存在,所以就不需要再次进入到慢速查找流程,从而直接进入_objc_rootAllocWithZone。
下面我们去验证一下这个猜想。
第一步:进入hasCustomAWZ() 我们发现如下代码:
bool hasCustomAWZ() const {
return !cache.getBit(FAST_CACHE_HAS_DEFAULT_AWZ);
}
其中FAST_CACHE_HAS_DEFAULT_AWZ是一个宏定义
第二步:进入getBit,flags是cache_t的一个成员,用来以二进制的方式记录一些信息,将FAST_CACHE_HAS_DEFAULT_AWZ和_flags做与操作进行验证是否实现了alloc/allocWithZone。
bool getBit(uint16_t flags) const {
return _flags & flags;//
}
分析:能否进入_objc_rootAllocWithZone则是由_flags来决定,JLObject第一次不能直接进入,而第二次调用却可以,那说明_flags的AWZ标记位在JLObject第一次调用后被置为1。
第三步:全局搜索FAST_CACHE_HAS_DEFAULT_AWZ发现只在三个地方出现,其中包含一个get和两个set方法,hasCustomAWZ方法运行过之后在set方法打上断点,发现会进入到setHasDefaultAWZ()
第四步:查看堆栈信息
第五步:根据堆栈信息,我们进入到objc_class::setInitialized()从注释可以看出,NSObject默认就会被标记为AWZ,所以NSObject第一次就可以进入到_objc_rootAllocWithZone中。
第六步:继续沿着调用栈我们发现lookUpImpOrForward(),这是在消息流程,继续往回追溯又一次进入到了callAlloc方法。进入到callAlloc中,发现是在通过objc_msgSend调用alloc,到了这里思路大概就清晰了。
结论:
- NSObject默认标记AWZ为1,所以第一次
if (fastpath(!cls->ISA()->hasCustomAWZ()))为真。直接进入了_objc_rootAllocWithZone。- JLObject第一次
if (fastpath(!cls->ISA()->hasCustomAWZ()))为假。所以会跳过_objc_rootAllocWithZone方法。allocWithZone= nil,跳过allocWithZone,运行到消息查找alloc,从而进入到lookUpImpOrForward()。- 而
alloc/allocWithZone在NSObject已经实现,所以消息查找一定可以找到并存到cache中。- 然后经过一系列的方法调用到了
setHasDefaultAWZ(),将AWZ标记改为1。- 第二次再调用时因为已经标记了AWZ,所以
if (fastpath(!cls->ISA()->hasCustomAWZ()))为真,直接进入到_objc_rootAllocWithZone中。
流程图
补充
init源码探索
接着我们再去看一看init的源码实现
点击进入_objc_rootInit,没有做其他任何事情,直接返回了对象本身。
其实init是一个构造方法,用于给用户提供构造方法入口,是工厂方法模式的一种运用。
new源码探索
进入到new的源码,发现调用到了callAlloc,这与alloc->objc_alloc->callAlloc是一致的,只是参数不同,allocWithZone参数不传的话默认为false所以此参数两个方法是一致的,只有checkNil不一致。
点进到callAlloc发现checkNil只参与了一个判断检测cls是否为空,为空直接return,而且是一个小概率的事件,所以我们可以认为在callAlloc方法的调用上,两者是基本相同的。不同之处在于new方法中直接调用了init。
建议:new会自动帮我们调用init,看起来很方便,但是一般并不提倡使用new,因为开发过程中重写init是很常见的,而形式千变万化,例如initWithxxx,如果使用了new进行初始化,将走不到自定义的initWithxxx中。