iOS进阶— 对象alloc流程

iOS进阶— 对象alloc流程

IOS小彩虹2021-08-24 10:15:55500A+A-

前言

作为一名iOS开发人员,平时最熟悉的莫过于对象的创建和初始化方法 allocinit 了。但是最近遇到的一道题,却让我觉得它们就像最熟悉的陌生人,想要一探究竟。 Snip20210606_1.png

按照我的猜想,p1、p2、p3变量地址不同,指向地址也不相同。但实际的执行结果出乎我的意料。

Snip20210606_3.png

p1、p2、p3打印的对象及指向地址完全相同,所不同的仅仅是指针变量本身的地址。为什么会有这样的结果呢,由此不妨先猜想,实际上 alloc 才会发生对象的内存开辟,init 并不会影响内存。下面我们就一起探索一下,来验证这个猜想。

一、alloc及init探索

1.1 定位源码

首先我们打开 XCocde 的汇编调试,Debug --> Debug overflow --> Always show Disassembly,在 allocinit (即13行 和 14行)分别打上断点,通过单步执行,我们发现这两个方法都指向同一个库libobjc。

Snip20210607_4.png

Snip20210607_5.png

虽然iOS系统并没有开源,但是苹果也提供了一些部分源码开源,以供开发者学习研究,我们可以通过苹果开源网站 opensource 来找到并下载源码,本次我们探索的是 macOS 11.2下的 objc 818 源码,可以通过如下地址下载和配置。

源码地址: 源码地址

源码配置可参考大神的文章:源码调试配置

1.2 验证猜想

在 BPPerson 中,我们并未定义 allocinit 方法,因此可以推测这两个方法在父类中,而且通过汇编调试结果看,也指向了父类 NSObject ,因此我们可以在源码中搜索这两个方法,查看其源码实现。

打开源码工程,全局搜索 allocinit ,我们可以在 NSObject.mm 找到这两个方法的定义如下:

+ (id)alloc {
    return _objc_rootAlloc(self);
}

id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

+ (id)init {
    return (id)self;
}

- (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;
}

从源码可以看出,init 方法最终执行 _objc_rootInit 函数,且返回了对象本身,而 alloc 方法最终执行了 callAlloc 方法,在此方法中进行了对象的开辟。由此执行结果和源码实现都可以验证我们的猜想。

init 方法返回自身,为何还要实现并调用呢? 原因是这样实现可以让子类重写该方法,在这个方法中作自己需要的操作,同时也方便进行方法的扩展。所以init方法还是有必要实现并调用的。

二、alloc 流程

验证了 allocinit 的方法实现,我们还可以来探索一下 alloc 方法的实现流程,具体看下在对象开辟过程中都做了哪些事情。

2.1 alloc的符号绑定

2.1.1 符号绑定的汇编验证

在上面的探索中,当调用 [BPPerson alloc] 时,按照我们的经验,此时会进入 NSObject+ (id)alloc 方法,因为这仅仅是类方法的简单调用。但在实际调试中,当断点执行到 [BPPerson alloc] 时,进行汇编的单步调试 (ctrl + 控制台单步执行按钮) 时,却发现下一步走到了如下地方

Snip20210607_8.png

Snip20210607_7.png

我们发现,代码并没有直接走到 + (id)alloc 方法,而是到了 objc_alloc 中,对即将跳转的地址 0x0000000100003eb6 进行读汇编,可以发现这里这里最终会调用 dyld 的 dyld_stub_binder 函数,也就是说这里可能是在进行一个符号的绑定。

2.1.2 Mach-O 文件查看

通过查看Mach-O 文件的 lazy-symbolSymbol table 我们可以发现,在编译完成后,并未生成 alloc 的符号,而是只有 _objc_alloc 的符号。

Snip20210607_9.png Snip20210607_10.png

新增补充:在调用 [BPPerson alloc] 时,本应该在 Mach-O 中生成 alloc 符号,但是此时却只有 _objc_alloc ,这很可能是一个底层的 C++ 函数,而不是调用的 NSObject 的 alloc 方法,这是什么原因呢?我们后面继续探索。

2.1.3 符号绑定源码验证

通过汇编和Mach-O的探索,我们可以得到两个结果:

  • 1、在调用 alloc 后,没有马上进入 NSObject 的 alloc 方法,这一点通过汇编可以得到验证
  • 2、我们的 Mach-O 在编译完成后即会生成,也就是说在编译阶段没有生成 alloc 的符号,只有一个 _objc_alloc 的 符号

由这两个结果,我们可以猜测系统对于 alloc 是否会有一些特殊的处理,由于与 alloc 相关的都指向了 objc_alloc,我们就在源码中搜索一下,看看 objc_alloc 到底是什么?全局搜索 objc_alloc 后,我们可以在 NSObject.mm 中发现 objc_alloc 的函数实现,这一点我们在后面章节分析;更为关键的是我们可以在 objc-runtime-new.mm 中发现一个函数 fixupMessageRef ,在这个函数中对 alloc 的 IMP 做了一次变换,其源码入下:

/***********************************************************************
* fixupMessageRef
* Repairs an old vtable dispatch call site. 
* vtable dispatch itself is not supported.
**********************************************************************/
static void 
fixupMessageRef(message_ref_t *msg)
{    
    msg->sel = sel_registerName((const char *)msg->sel);

    if (msg->imp == &objc_msgSend_fixup) { 
        if (msg->sel == @selector(alloc)) {
            msg->imp = (IMP)&objc_alloc;  // alloc 的 IMP 被修改为 objc_alloc 的函数地址
        } else if (msg->sel == @selector(allocWithZone:)) {
            msg->imp = (IMP)&objc_allocWithZone;
        } else if (msg->sel == @selector(retain)) {
            msg->imp = (IMP)&objc_retain;
        } else if (msg->sel == @selector(release)) {
            msg->imp = (IMP)&objc_release;
        } else if (msg->sel == @selector(autorelease)) {
            msg->imp = (IMP)&objc_autorelease;
        } else {
            msg->imp = &objc_msgSend_fixedup;
        }
    } 
    else if (msg->imp == &objc_msgSendSuper2_fixup) { 
        msg->imp = &objc_msgSendSuper2_fixedup;
    } 
    else if (msg->imp == &objc_msgSend_stret_fixup) { 
        msg->imp = &objc_msgSend_stret_fixedup;
    } 
    else if (msg->imp == &objc_msgSendSuper2_stret_fixup) { 
        msg->imp = &objc_msgSendSuper2_stret_fixedup;
    } 
#if defined(__i386__)  ||  defined(__x86_64__)
    else if (msg->imp == &objc_msgSend_fpret_fixup) { 
        msg->imp = &objc_msgSend_fpret_fixedup;
    } 
#endif
#if defined(__x86_64__)
    else if (msg->imp == &objc_msgSend_fp2ret_fixup) { 
        msg->imp = &objc_msgSend_fp2ret_fixedup;
    } 
#endif
}

在这段代码中我们可以发现,当调用 alloc 方法,即 msg->sel == @selector(alloc) 下时,实际会将IMP 重新赋值为 objc_alloc,因此,当我们调用 alloc 会先去调用 objc_alloc

2.1.4 llvm源码验证

在上一小节的 fixupMessageRef 函数中,我们发现不止 alloc,还有很多其它的系统方法,如 allocWithZone:、retain等等,那这个函数又是干什么的呢?我们继续在 objc源码中全局搜索 fixupMessageRef,最终发现只在 _read_images 函数中调用了fixupMessageRef,由于该方法代码较长,这里只展示关于 fixupMessageRef 的部分:

/***********************************************************************
* _read_images
* Perform initial processing of the headers in the linked 
* list beginning with headerList. 
*
* Called by: map_images_nolock
*
* Locking: runtimeLock acquired by map_images
**********************************************************************/
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
    header_info *hi;
    uint32_t hIndex;
    size_t count;
    size_t i;
    Class *resolvedFutureClasses = nil;
    size_t resolvedFutureClassCount = 0;
    static bool doneOnce;
    bool launchTime = NO;
    TimeLogger ts(PrintImageTimes);
    
/**
... 其它代码
*/
    
#if SUPPORT_FIXUP
    // Fix up old objc_msgSend_fixup call sites
    for (EACH_HEADER) {
        message_ref_t *refs = _getObjc2MessageRefs(hi, &count);
        if (count == 0) continue;

        if (PrintVtables) {
            _objc_inform("VTABLES: repairing %zu unsupported vtable dispatch "
                         "call sites in %s", count, hi->fname());
        }
        for (i = 0; i < count; i++) {
            fixupMessageRef(refs+i);
        }
    }

    ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");
#endif

/**
... 其它代码
*/
}

_read_images是在 dyld加载流程 之后执行,而 Mach-O 在 dyld动态链接 之前就已经生成,所以 _objc_alloc 符号,可能在编译时就已经生成,因此我们再看一下 llvm 的源码,下载一份 llvm源码,在 VSCode 打开(不用XCode,是因为它比较慢),搜索 objc_alloc,最终找到如下代码:

/// Allocate the given objc object.
///   call i8* \@objc_alloc(i8* %value)
llvm::Value *CodeGenFunction::EmitObjCAlloc(llvm::Value *value,
                                            llvm::Type *resultType) {
  return emitObjCValueOperation(*this, value, resultType,
                                CGM.getObjCEntrypoints().objc_alloc,
                                "objc_alloc");
}

搜索一下 EmitObjCAlloc 的调用,会找到 tryGenerateSpecializedMessageSend

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;

  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分支暂时省略
 */
  return None;
}

继续搜索 tryGenerateSpecializedMessageSend,可以找到 GeneratePossiblySpecializedMessageSend,很显然这个函数是关于消息发送的方法,其源码如下:

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);
}

简单分析下 GeneratePossiblySpecializedMessageSend 函数:

  • 在条件判断中调用了 tryGenerateSpecializedMessageSend 函数,根据这函数的名称可以推测其作用时对一些特殊的消息发送作处理,进入函数可以看到这些特殊的函数是指 alloc、allocWithZone等。

  • 猜测是苹果对这些系统方法做了一次插桩,可能是为了对这些方法进行监测,不过这一点我无从考证,仅仅是猜测,如果不对也欢迎大家指正。

  • 如果不满足条件判断,则走正常的消息发送。这个条件包括 1、不是系统的方法;2、系统方法,但已经进行过插桩操作。

下面回归到正常的流程分析,在 tryGenerateSpecializedMessageSend 中会调用 EmitObjCAlloc,改变了 alloc 的调用流程。这两步操作也说明在调用 alloc 时,会先调用 objc_alloc。下面就对 objc_alloc 进行进一步探索。

2.2 alloc流程分析

2.2.1 关键函数定位

既然会先调用 objc_alloc,那我们就看下 objc_alloc 函数做了哪些事情。在源码中搜索 objc_alloc,可以找到如下的代码

// Calls [cls alloc].
id
objc_alloc(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}

对比 alloc 的源码,我们可以发现两个函数最终都会调用 callAlloc,因此,我们进入关键函数 callAlloc 中看一下,其源码如下:

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
    if (slowpath(checkNil && !cls)) return nil;
    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));
}

我们分别在 alloc、 objc_alloc、 callAlloc 打上断点,通过断点调试源码我们发现一个调用顺序 objc_alloc -> callAlloc -> alloc -> _objc_rootAlloc -> callAlloc

  • 在这个流程中 callAlloc 被调用了两次,第一次在调用 objc_alloc 之后进入,之后通过 objc_msgSend 消息发送,调用了 + alloc 方法
  • 在调用 + alloc 后,进入 callAlloc 则会走进 _objc_rootAllocWithZone 函数中。

接下来我们就可以进入 _objc_rootAllocWithZone,进入其中查看对象创建的流程。

Tips: slowpath() 和 fastpath() 是编译器优化,分别表示 条件很小可能执行 和 条件很大可能执行。因此,编译器在加载指令时,遇到slowpath时,可以暂不加载,等到条件触发了再加载。从而优化指令加载时机,提高效率。

2.2.2 _objc_rootAllocWithZone函数分析

_objc_rootAllocWithZone 函数中只调用 _class_createInstanceFromZone 函数,继续跟进去可以发现这就是对象创建的函数

/***********************************************************************
* class_createInstance
* fixme
* Locking: none
*
* Note: this function has been carefully written so that the fastpath
* takes no branch.
**********************************************************************/
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;
    }

    // isa 相关
    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);
}

这个函数中的条件判断比较多,但主要的流程可以按照 获取对象大小、创建对象、初始化isa、返回对象 来分析。创建对象和isa的初始化相关的方法,如 calloc、initIsa 等本篇文章暂不探索,本次先看一下获取内存大小及内存对齐的部分。

2.2.3 内存对齐

在开辟对象前,先要获取对象的大小,即 size = cls->instanceSize(extraBytes); 这段代码,跟进去可以看到

inline size_t instanceSize(size_t extraBytes) const {
    if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
        return cache.fastInstanceSize(extraBytes);
    }

    size_t size = alignedInstanceSize() + extraBytes;
    // CF requires all objects be at least 16 bytes.
    if (size < 16) size = 16;  
    return size;
}

如果有缓存则进入 fastInstanceSize,否则调用 alignedInstanceSize,并且对象大小最小不低于16。

在获取大小时,会进行内存对齐,进入这两个方法会发现 word_alignalign16 的调用。

static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}
static inline size_t word_align(size_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}
static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}

#ifdef __LP64__
#   define WORD_SHIFT 3UL
#   define WORD_MASK 7UL
#   define WORD_BITS 64
#else
#   define WORD_SHIFT 2UL
#   define WORD_MASK 3UL
#   define WORD_BITS 32
#endif

通过源码可以看出,内存对齐在64位机器上是以 8 字节对齐,在32位机器以 4 字节对齐。

Q1: 为什么要进行内存对齐?

  • 假定一个对象分别包含 int、double、指针类型的成员变量,CPU在读取内存时,分别按照各自类型的大小去读,则每读去一个变量就要重新计算要读取多大的空间,这样势必会影响效率
  • 如果每次都读取定长的空间,则不需要再进行计算。即以空间换时间,可以提升效率。
  • 虽然以8字节对齐会浪费一定的空间,但是系统也会进行优化,比如一个int 和 char类型变量,两者加起来也不足8字节,则适当时可以两则合并在同一个 8 字节空间中存储。

Q2: 为什么是8字节对齐?

  • 在基本类型中最长即为8字节,如果是结构体则可以存8字节,不够时再开辟8个字节。

三、总结

1、alloc流程图总结

本文主要总结了 alloc 的大致流程以及内存对齐相关知识,最后再来总结 alloc 的流程图如下:

alloc 流程.png

本篇文章中还有部分内容没有探究到,关于对象创建和isa初始化会在之后的文章中总结。对于文章中不正确的地方还欢迎大家指正。

2、文章补充与修改

在 objc_alloc 的调用部分,之前的理解不正确,在此对 2.1.3 做了修改和增补,增加 2.1.4 节关于 llvm 源码部分的探究,不过由于对 llvm 源码理解不够,部分过程不太清晰,如果有误,依然欢迎大家指正。

3、文章参考

iOS 底层 - OC 对象的创建流程

关于alloc初探中alloc进入objc_alloc的原因

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1
本网站由 提供CDN/云存储服务

联系我们