一、alloc探索
我们写代码的时候总是会alloc,如下,我们知道它是在为对象开辟内存空间,那么具体的开辟流程是怎么样的呢,接下来我们开始对其研究。
XDPerson *person = [XDPerson alloc];
1 准备工作
- 下断点调试。
- 打开
Debug->Debug Workflow->Always Show Disassembly。 - 真机运行。
我们可以看到汇编代码,并且在汇编文件的顶部看到
libobjc.A.dylib objc_alloc。
- 这里对源码做一下说明,它分为两个版本
legecy->objc1,modern->objc2。 - 我们现在使用的是
objc2版本。
llvm源码,源码比较大,可以自行下载查看。
2 探索过程
源码里面写代码来探索,同样是这么一行代码。
XDPerson *person = [XDPerson alloc];
-
现象: 我们断点在
alloc这行,运行起来之后可以进入到源码里面。发现直接进入到alloc类方法,下一步就是_objc_rootAlloc这个函数。 -
这个时候我们有疑问,不是应该
alloc类方法之后的下一步是objc_alloc吗?没错,这个过程是编译器直接给我们优化了,也就间接了说明了
objc4源码并没有真正的开源,只做到了部分的开源,这里分为两个阶段,编译阶段llvm源码 + 运行阶段objc源码。
2.1 源码llvm探索
llvm很友好,它提供了一系列的测试代码,可以供我们去理性理解分析,接下来开始我们的探索之路。
-
测试代码:
test_alloc_class_ptr搜索// Make sure we get a bitcast on the return type as the // call will return i8* which we have to cast to A* // CHECK-LABEL: define {{.*}}void @test_alloc_class_ptr A* test_alloc_class_ptr() { // CALLS: {{call.*@objc_alloc}} // CALLS-NEXT: bitcast i8* // CALLS-NEXT: ret return [B alloc]; } // Make sure we get a bitcast on the return type as the // call will return i8* which we have to cast to A* // CHECK-LABEL: define {{.*}}void @test_alloc_class_ptr A* test_allocWithZone_class_ptr() { // CALLS: {{call.*@objc_allocWithZone}} // CALLS-NEXT: bitcast i8* // CALLS-NEXT: ret return [B allocWithZone:nil]; }全局搜索
test_alloc_class_ptr我们可以看到一段测试的代码的相关说明,意思就是告诉开发者调用objc_alloc之后会继续走alloc的流程。 -
objc_alloc探索
-
查找目标:
objc_alloc搜索llvm::Value *CodeGenFunction::EmitObjCAlloc(llvm::Value *value, llvm::Type *resultType) { return emitObjCValueOperation(*this, value, resultType, CGM.getObjCEntrypoints().objc_alloc, "objc_alloc"); } -
往上追溯:
EmitObjCAlloc搜索static Optional<llvm::Value *> tryGenerateSpecializedMessageSend(CodeGenFunction &CGF, QualType ResultType, llvm::Value *Receiver, const CallArgList& Args, Selector Sel, const ObjCMethodDecl *method, bool isClassMessage) { auto &CGM = CGF.CGM; if (!CGM.getCodeGenOpts().ObjCConvertMessagesToRuntimeCalls) return None; // objc_alloc // 2: 只是去读取字符串 // 3: // 4: auto &Runtime = CGM.getLangOpts().ObjCRuntime; switch (Sel.getMethodFamily()) { case OMF_alloc: if (isClassMessage && Runtime.shouldUseRuntimeFunctionsForAlloc() && ResultType->isObjCObjectPointerType()) { // [Foo alloc] -> objc_alloc(Foo) or // [self alloc] -> objc_alloc(self) if (Sel.isUnarySelector() && Sel.getNameForSlot(0) == "alloc") return CGF.EmitObjCAlloc(Receiver, CGF.ConvertType(ResultType)); // [Foo allocWithZone:nil] -> objc_allocWithZone(Foo) or // [self allocWithZone:nil] -> objc_allocWithZone(self) if (Sel.isKeywordSelector() && Sel.getNumArgs() == 1 && Args.size() == 1 && Args.front().getType()->isPointerType() && Sel.getNameForSlot(0) == "allocWithZone") { const llvm::Value* arg = Args.front().getKnownRValue().getScalarVal(); if (isa<llvm::ConstantPointerNull>(arg)) return CGF.EmitObjCAllocWithZone(Receiver, CGF.ConvertType(ResultType)); return None; } } break; case OMF_autorelease: if (ResultType->isObjCObjectPointerType() && CGM.getLangOpts().getGC() == LangOptions::NonGC && Runtime.shouldUseARCFunctionsForRetainRelease()) return CGF.EmitObjCAutorelease(Receiver, CGF.ConvertType(ResultType)); break; case OMF_retain: if (ResultType->isObjCObjectPointerType() && CGM.getLangOpts().getGC() == LangOptions::NonGC && Runtime.shouldUseARCFunctionsForRetainRelease()) return CGF.EmitObjCRetainNonBlock(Receiver, CGF.ConvertType(ResultType)); break; case OMF_release: if (ResultType->isVoidType() && CGM.getLangOpts().getGC() == LangOptions::NonGC && Runtime.shouldUseARCFunctionsForRetainRelease()) { CGF.EmitObjCRelease(Receiver, ARCPreciseLifetime); return nullptr; } break; default: break; } return None; }看到这段代码,我们好熟悉呀,
alloc、autorelease、retain、release,这里其实就是编译阶段符号绑定symblos的相关信息。 -
继续往上追溯:
tryGenerateSpecializedMessageSend搜索CodeGen::RValue CGObjCRuntime::GeneratePossiblySpecializedMessageSend( CodeGenFunction &CGF, ReturnValueSlot Return, QualType ResultType, Selector Sel, llvm::Value *Receiver, const CallArgList &Args, const ObjCInterfaceDecl *OID, const ObjCMethodDecl *Method, bool isClassMessage) { if (Optional<llvm::Value *> SpecializedResult = tryGenerateSpecializedMessageSend(CGF, ResultType, Receiver, Args, Sel, Method, isClassMessage)) { return RValue::get(SpecializedResult.getValue()); } return GenerateMessageSend(CGF, Return, ResultType, Sel, Receiver, Args, OID, Method); }截止追踪源头到这里就结束了。
- 首先尽量的去走通用的特殊名称的消息发送。
我们从源码的流程中大致的可以得到这个一个过程,
alloc特殊名称消息来了之后就会走objc_alloc消息调用流程。 - 然后再走通过的消息发送。
objc_alloc在objc4源码里面会最终调用[cls alloc],它是一个没有返回值的函数,虽然也是alloc特殊函数名称,但是在我们追踪到源头的位置if条件里面没有成立,于是就直接走了通用消息查找流程。
- 首先尽量的去走通用的特殊名称的消息发送。
笔者只对llvm做了一下简单的流程分析,里面还有很多小细节,可以去探索发现。
2.2 源码objc探索
下面我们开始对我们感兴趣的objc源码进行分析。有了前面的llvm的过程,我们就可以直接进入源码查看alloc的流程了。在下面的分析中,我们分为两块,alloc主线流程 + 具体函数分支流程。
alloc主线流程
-
alloc的入口+ (id)alloc { return _objc_rootAlloc(self); }- 一个简单的有
OC方法进入到C函数里面。
- 一个简单的有
-
_objc_rootAlloc分析id _objc_rootAlloc(Class cls) { return callAlloc(cls, false/*a*/, true/*allocWithZone*/); }- 一个调用流程,调用
callAlloc,入参cls类,false->checkNil,true->allocWithZone。
- 一个调用流程,调用
-
callAlloc分析static ALWAYS_INLINE id callAlloc(Class cls, bool checkNil, bool allocWithZone=false) { if (slowpath(checkNil && !cls)) return nil; #if __OBJC2__ if (fastpath(!cls->ISA()->hasCustomAWZ())) { // No alloc/allocWithZone implementation. Go straight to the allocator. // fixme store hasCustomAWZ in the non-meta class and // add it to canAllocFast is summary if (fastpath(cls->canAllocFast())) { // No ctors, raw isa, etc. Go straight to the metal. bool dtor = cls->hasCxxDtor(); id obj = (id)calloc(1, cls->bits.fastInstanceSize()); if (slowpath(!obj)) return callBadAllocHandler(cls); obj->initInstanceIsa(cls, dtor); return obj; } else { // Has ctor or raw isa or something. Use the slower path. id obj = class_createInstance(cls, 0); if (slowpath(!obj)) return callBadAllocHandler(cls); return obj; } } #endif // No shortcuts available. if (allocWithZone) return [cls allocWithZone:nil]; return [cls alloc]; }-
if (slowpath(checkNil && !cls)) return nil;条件判断,slowpath表示条件很可能为false。 -
if (fastpath(!cls->ISA()->hasCustomAWZ())),fastpath表示条件很可能为true。 -
if (fastpath(cls->canAllocFast()))当前cls是否能快速alloc。 -
bool dtor = cls->hasCxxDtor();当前的cls是否有c++的析构函数。 -
id obj = (id)calloc(1, cls->bits.fastInstanceSize());让系统开辟内存空间。 -
callBadAllocHandler(cls);表示alloc失败。 -
obj->initInstanceIsa(cls, dtor);初始化isa。 -
id obj = class_createInstance(cls, 0);创建实例化对象,我们会在下面具体针对这个函数做讲解。 -
if (allocWithZone) return [cls allocWithZone:nil];当前类实现了allocWithZone函数,就调用类的allocWithZone方法。
-
通过callAlloc函数的调用流程,alloc的主线流程我们大致了解了。
具体函数分支流程
下面我们针对具体函数做分析
-
cls->ISA()->hasCustomAWZ()cls->ISA()通过对象的isa的值 通过一个&运算isa.bits & ISA_MASK找到当前类的元类。hasCustomAWZ()判断元类里面是否有自定义的allocWithZone,这个与我们在类里面写的allocWithZone是不同。
-
cls->canAllocFast()- 这里我们可以在源码里面看到最终直接返回的值是
false,这段部分涉及到objc_class结构体里面的相关解释,这里就不做展开说明了。
- 这里我们可以在源码里面看到最终直接返回的值是
-
[cls allocWithZone:nil]- 调用
_objc_rootAllocWithZone。 - 我们已经了解到目前的是
objc2版本,直接进入class_createInstance(cls, o)。
- 调用
-
class_createInstance(cls, 0)笔者会对这个函数着重分析,我们可以发现
_objc_rootAllocWithZone最后也是调用的这个函数。流程分析:
id class_createInstance(Class cls, size_t extraBytes) { return _class_createInstanceFromZone(cls, extraBytes, nil); }我们可以看到实际调用是
_class_createInstanceFromZone,入参cls类,extraBytes值为0。static __attribute__((always_inline)) id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, bool cxxConstruct = true, size_t *outAllocatedSize = nil) { if (!cls) return nil; assert(cls->isRealized()); // Read class is info bits all at once for performance bool hasCxxCtor = cls->hasCxxCtor(); bool hasCxxDtor = cls->hasCxxDtor(); bool fast = cls->canAllocNonpointer(); size_t size = cls->instanceSize(extraBytes); if (outAllocatedSize) *outAllocatedSize = size; id obj; if (!zone && fast) { obj = (id)calloc(1, size); if (!obj) return nil; obj->initInstanceIsa(cls, hasCxxDtor); } else { if (zone) { obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size); } else { obj = (id)calloc(1, size); } if (!obj) return nil; // Use raw pointer isa on the assumption that they might be // doing something weird with the zone or RR. obj->initIsa(cls); } if (cxxConstruct && hasCxxCtor) { obj = _objc_constructOrFree(obj, cls); } return obj; }- 我们先对几个参数分析,当前函数被调用的时候
size_t extraBytes 为0。 void *zone 指针为nil。
- 基本条件函数分析
bool hasCxxCtor = cls->hasCxxCtor(); 是否有
c++构造函数。bool hasCxxDtor = cls->hasCxxDtor(); 是否有
c++析构函数。bool fast = cls->canAllocNonpointer(); 是否能创建
nonPointer,这里的结果是true,以后我们会做相应的介绍。- size_t size = cls->instanceSize(extraBytes); 这一步比较重要,申请内存
size_t instanceSize(size_t extraBytes) { size_t size = alignedInstanceSize() + extraBytes; // CF requires all objects be at least 16 bytes. if (size < 16) size = 16; return size; } uint32_t alignedInstanceSize() { return word_align(unalignedInstanceSize()); } uint32_t unalignedInstanceSize() { assert(isRealized()); return data()->ro->instanceSize; } //WORD_MASK 7UL static inline uint32_t word_align(uint32_t x) { return (x + WORD_MASK) & ~WORD_MASK; }最后一步的就是内存对齐的算法
- 通过演算 ( 8 + 7 ) & (~7) 我们转话成2进制之后计算 值为8,同时这里也可以说明对象申请内存的时候是以8字节对齐,意思就是申请的内存大小是8的倍数。
if (size < 16) size = 16;这一步又给我们表达了申请内存小于16字节的,按照16字节返回。这里是为了后面系统开辟内存空间大小做的一致性原则的处理。
-
calloc与malloc_zone_calloc去根据申请的内存空间大小size让系统开辟内存空间给obj对象。这里涉及到另一份源码
libmalloc。我们就不展开分析了,但是我们要知道,malloc开辟内存空间的原则是按照16字节对齐的。 -
initInstanceIsa()会调用
initIsa(cls, true, hasCxxDtor);函数isa_t newisa(0); newisa.bits = ISA_MAGIC_VALUE; // isa.magic is part of ISA_MAGIC_VALUE // isa.nonpointer is part of ISA_MAGIC_VALUE newisa.has_cxx_dtor = hasCxxDtor; newisa.shiftcls = (uintptr_t)cls >> 3; isa = newisa;相应的代码简化之后就是这么几部流程,都是来初始化
isa
到此为止,alloc的分析流程就结束了。这里附上一个相应的alloc主线流程图。
二、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;
}
这里就比较简单了,直接
_objc_rootInit之后就返回了obj,说明init没做啥事,返回的是alloc出来的obj。
三、new探索
从源码入手
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
new实际上就是走了alloc流程里面的步骤,然后在+init。
四、总结
- 问下面输出的结果有什么区别?
XDPerson *p1 = [XDPerson alloc];
XDPerson *p2 = [p1 init];
XDPerson *p3 = [p1 init];
NSLog(@"p1 - %@,%p",p1, &p1);
NSLog(@"p2 - %@,%p",p2, &p2);
NSLog(@"p3 - %@,%p",p3, &p3);
答案:
我们可以分析
init实际上返回的结果就是alloc出来的对象,因此p1、p2、p3指向的是同一个内存地址,但是它们的指针地址本身就是不同的。
2019-12-29 20:09:02.971814+0800 XDTest[1809:186909] p1 - <XDPerson: 0x100f33010>,0x7ffeefbff5b8
2019-12-29 20:09:02.971978+0800 XDTest[1809:186909] p2 - <XDPerson: 0x100f33010>,0x7ffeefbff5b0
2019-12-29 20:09:02.972026+0800 XDTest[1809:186909] p3 - <XDPerson: 0x100f33010>,0x7ffeefbff5a8
init做了什么?这样设计的好处?解释一下if (self = [super init])?
init实际上就是直接返回了alloc里面创建的objc。- 这样设计可以给开发者提供更多的工厂设计方便,比如我们有时候会重写
initWith...这样的方法,让开发者更好的自定义。 self = [super init]响应继承链上父类的init方法,防止父类实现了某些特殊的方法,到了自己这里被丢弃。加上if处理,我们可以理解为一层安全的判断,防止父类在init里面直接返回nil。
学习之路,砥砺前行
不足之处可以在评论区指出