iOS 底层探索篇 —— alloc、init、new的探索

1,157 阅读9分钟

一、alloc探索

我们写代码的时候总是会alloc,如下,我们知道它是在为对象开辟内存空间,那么具体的开辟流程是怎么样的呢,接下来我们开始对其研究。

XDPerson *person = [XDPerson alloc];

1 准备工作

  1. 下断点调试。
  • 打开Debug->Debug Workflow->Always Show Disassembly
  • 真机运行。

我们可以看到汇编代码,并且在汇编文件的顶部看到libobjc.A.dylib objc_alloc

  1. objc源码 + 配置流程
  • 这里对源码做一下说明,它分为两个版本 legecy->objc1modern->objc2
  • 我们现在使用的是objc2版本。
  1. llvm源码,源码比较大,可以自行下载查看。

2 探索过程

源码里面写代码来探索,同样是这么一行代码。

XDPerson *person = [XDPerson alloc];
  • 现象: 我们断点在alloc这行,运行起来之后可以进入到源码里面。发现直接进入到alloc类方法,下一步就是_objc_rootAlloc这个函数。

  • 这个时候我们有疑问,不是应该alloc类方法之后的下一步是objc_alloc吗?

    没错,这个过程是编译器直接给我们优化了,也就间接了说明了objc4源码并没有真正的开源,只做到了部分的开源,这里分为两个阶段,编译阶段llvm源码 + 运行阶段objc源码。

2.1 源码llvm探索

llvm很友好,它提供了一系列的测试代码,可以供我们去理性理解分析,接下来开始我们的探索之路。

  1. 测试代码: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的流程。

  2. 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;
    }
    

    看到这段代码,我们好熟悉呀,allocautoreleaseretainrelease,这里其实就是编译阶段符号绑定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_allocobjc4源码里面会最终调用[cls alloc],它是一个没有返回值的函数,虽然也是alloc特殊函数名称,但是在我们追踪到源头的位置if条件里面没有成立,于是就直接走了通用消息查找流程。

笔者只对llvm做了一下简单的流程分析,里面还有很多小细节,可以去探索发现。

2.2 源码objc探索

下面我们开始对我们感兴趣的objc源码进行分析。有了前面的llvm的过程,我们就可以直接进入源码查看alloc的流程了。在下面的分析中,我们分为两块,alloc主线流程 + 具体函数分支流程。

alloc主线流程

  1. alloc的入口

    + (id)alloc {
        return _objc_rootAlloc(self);
    }
    
    • 一个简单的有OC方法进入到C函数里面。
  2. _objc_rootAlloc分析

    id
    _objc_rootAlloc(Class cls)
    {
        return callAlloc(cls, false/*a*/, true/*allocWithZone*/);
    }
    
    • 一个调用流程,调用callAlloc,入参cls类,false->checkNiltrue->allocWithZone
  3. 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的主线流程我们大致了解了。

具体函数分支流程

下面我们针对具体函数做分析

  1. cls->ISA()->hasCustomAWZ()

    • cls->ISA()通过对象的isa的值 通过一个&运算isa.bits & ISA_MASK找到当前类的元类。
    • hasCustomAWZ()判断元类里面是否有自定义的allocWithZone,这个与我们在类里面写的allocWithZone是不同。
  2. cls->canAllocFast()

    • 这里我们可以在源码里面看到最终直接返回的值是false,这段部分涉及到objc_class结构体里面的相关解释,这里就不做展开说明了。
  3. [cls allocWithZone:nil]

    • 调用_objc_rootAllocWithZone
    • 我们已经了解到目前的是objc2版本,直接进入class_createInstance(cls, o)
  4. 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;
    }
    

    最后一步的就是内存对齐的算法

    1. 通过演算 ( 8 + 7 ) & (~7) 我们转话成2进制之后计算 值为8,同时这里也可以说明对象申请内存的时候是以8字节对齐,意思就是申请的内存大小是8的倍数。
    2. if (size < 16) size = 16;这一步又给我们表达了申请内存小于16字节的,按照16字节返回。这里是为了后面系统开辟内存空间大小做的一致性原则的处理。
    • callocmalloc_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

四、总结

  1. 问下面输出的结果有什么区别?
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出来的对象,因此p1p2p3指向的是同一个内存地址,但是它们的指针地址本身就是不同的。

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
  1. init做了什么?这样设计的好处?解释一下if (self = [super init])?
  • init实际上就是直接返回了alloc里面创建的objc
  • 这样设计可以给开发者提供更多的工厂设计方便,比如我们有时候会重写initWith...这样的方法,让开发者更好的自定义。
  • self = [super init]响应继承链上父类的init方法,防止父类实现了某些特殊的方法,到了自己这里被丢弃。加上if处理,我们可以理解为一层安全的判断,防止父类在init里面直接返回nil

学习之路,砥砺前行

不足之处可以在评论区指出