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

1、alloc探索

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

XDPerson *person = [XDPerson alloc];
复制代码

1 准备工做

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

咱们能够看到汇编代码,而且在汇编文件的顶部看到libobjc.A.dylib objc_allocgit

  1. objc源码 + 配置流程
  • 这里对源码作一下说明,它分为两个版本 legecy->objc1modern->objc2
  • 咱们如今使用的是objc2版本。
  1. llvm源码,源码比较大,能够自行下载查看。

2 探索过程

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

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

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

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

2.1 源码llvm探索

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

  1. 测试代码:test_alloc_class_ptr搜索app

    // 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主线流程图。

2、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

3、new探索

从源码入手

+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}
复制代码

new实际上就是走了alloc流程里面的步骤,而后在+init

4、总结

  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
相关文章
相关标签/搜索