iOS - 探索底层alloc流程

341 阅读6分钟

前言

在iOS中,我们要对一个对象进行使用,通常都会调用[[NSObject alloc] init]方法来对对象进行初始化,那么这里的alloc和init到底是用来干什么的呢?带着这个疑问,首先我们来看一段代码

    DMPerson *p1 = [DMPerson alloc];
    DMPerson *p2 = [p1 init];
    DMPerson *p3 = [p1 init];
        
    NSLog(@"%@-%p-%p",p1,p1,&p1);
    NSLog(@"%@-%p-%p",p2,p2,&p2);
    NSLog(@"%@-%p-%p",p3,p3,&p3);

这段代码的运行结果如下:

   DMTest[12144:265363] <DMPerson: 0x10540ccc0>-0x10540ccc0-0x16fdff378
   DMTest[12144:265363] <DMPerson: 0x10540ccc0>-0x10540ccc0-0x16fdff370
   DMTest[12144:265363] <DMPerson: 0x10540ccc0>-0x10540ccc0-0x16fdff368

结论: 可以看到,打印的p1,p2,p3的三个对象地址都是同一个,但是他们的指针地址是不同的。 我们可以先粗略的这样认为,alloc的功能是开辟了一个内存空间,而init不具有开辟内存空间的功能,只是实例化了一个类对象。他们在内存中的关系可以用下面这张图来表示

image.png

那么具体是不是这样的呢,我们开始着手来探索alloc的流程

准备

为了研究iOS的底层,在网上寻找到了可以进行编译的iOS底层代码,可以通过GitHub - LGCooci/objc4_debug地址去下载。由于我系统和Xcode没有升级到最新版本,不确定最新版本是否可以编译这个代码,下面贴出我能够运行的配置:

  • Mac os 11.0.1
  • Xcode 12.3
  • obje4 - 818.2 下载好代码后,直接run代码,可能会出现一些报错,可以参考iOS_objc4 源码编译调试里面的错误信息进行解决。运行成功后
  • 在Target界面,添加一个新的Taget:DMTest image.png
  • 绑定二进制依赖关系 image.png
  • 运行代码,可以进行调试 image.png

alloc源码流程

我们在上面已经准备好了可以编译的苹果开源源码,下面我们开始进行alloc的探索,首先准备好定义好的对象,代码就如文章一开始贴出来的那样

image.png

1.alloc和_objc_rootAlloc

alloc的上层代码是

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

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

这里有一个坑点,因为我们调用的是DMPerson的alloc方法,实际上他不会直接走到+(id)alloc方法中,而是先会走到objc_alloc方法中,在这个方法中调用callAlloc方法,最后通过方法查找,找到父类NSObjectalloc方法,然后才进入+(id)alloc

2.callAlloc

找到第一个核心方法callAlloc,代码如下

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__ //判断OC2.0版本,在很早之前苹果已经把代码切换到了OC2.0因此基本上一定会走这里
  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));
}

3. _class_createInstanceFromZone

因为callAlloc方法中存在分支,因此通过断点才能发现走哪,最后断点发现走的是_objc_rootAllocWithZone方法,再往后调用了_class_createInstanceFromZone方法,这个方法是整个alloc方法的核心

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;
  
 //instanceSize方法计算需要申请的内存大小
  size = cls->instanceSize(extraBytes);
  if (outAllocatedSize) *outAllocatedSize = size;

  id obj;
  if (zone) {
    obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
  } else {
  //calloc方法申请内存空间
    obj = (id)calloc(1, size);
  }
  if (slowpath(!obj)) {
    if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
      return _objc_callBadAllocHandler(cls);
    }
    return nil;
  }

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

4._class_createInstanceFromZone中的重要方法

_class_createInstanceFromZone方法中,最重要的三个方法分别是:

cls->instanceSize

这个方法是为了确定需要开辟的内存的大小,跟进去代码可以看到

    inline size_t instanceSize(size_t extraBytes) const {
       //快速计算内存大小
       if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
           return cache.fastInstanceSize(extraBytes);
       }
       //计算需要开辟的内存大小,extraBytes为额外的内存大小一般为0
       size_t size = alignedInstanceSize() + extraBytes;
       // CF requires all objects be at least 16 bytes.
       //最小返回16字节
       if (size < 16) size = 16;
       return size;
   }

在创建我们自己的类的时候,因为没有实现alloc方法,会调用方法查找的流程,其中会调用unalignedInstanceSize来确定类内部的大小,这里会调用alignedInstanceSize()采用8字节对齐

    // Class's ivar size rounded up to a pointer-size boundary.
   uint32_t alignedInstanceSize() const {
       //返回需要开辟的内存大小
       return word_align(unalignedInstanceSize());
   }
   
   // May be unaligned depending on class's ivars.
   //这个大小只跟成员变量的数量有关
   uint32_t unalignedInstanceSize() const {
       ASSERT(isRealized());
       return data()->ro()->instanceSize;
   }

可以看出需要开辟的内存空间大小实际上只跟这个类拥有的成员变量的数量有关,而一个新创建出来的类,没有任何的属性或者成员变量的话,实际上他已经包含了一个isa指针的成员变量,因此他会返回8。

当调用cls->instanceSize时而当走到都会走快速流程cache.fastInstanceSize, 而我们以align16(14)为例,来研究下align16方法这个字节对齐算法

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

//align16(14)14 + 15) & ~ 15 实际上就是 29 & ~15

29的二进制 : 0001 1101
15的二进制 : 0000 1111
~15的二进制 : 1111 0000

29 & ~150001 0000 = 32

(id)calloc

这个方法是通过cls->instanceSize方法计算出需要开辟的内存大小后,向系统申请一片该大小的内存空间。通过断点打印进行分析

image.png 发现在调用calloc之前,obj指向的是一片脏地址,打印出来的东西并不确定是什么,而调用之后,则直接指向了一片我们申请的内存空间地址0x00000001050082f0

obj->initInstanceIsa

这个方法是将calloc申请的内存地址,与我们的类绑定起来,通过断点分析可以发现

image.png 在调用initInstanceIsa方法后,我们的objDMPerson关联起来了,这与我们平时使用po命令打印出来的某个类对象是一样的结果。

流程图

最后将整个alloc方法执行的流程图放出来 未命名文件 (2).png

总结与疑问

从整个流程我们可以很清楚的看到,alloc方法实际上就是我们的类向系统申请一片内存空间的方法。 而最后还有一点疑问就是在计算内存大小的时候,走快速方法使用的是16字节对齐,而非快速方法使用的是8字节对齐,最终不小于16字节。

image.png 这个疑问,笔者后续会继续探索,如果有知道答案的朋友,也欢迎在下面留言。

后续探索

上面留下的一个疑问,在后续探索中似乎有一些结果,下面补充进来。 由于我们创建的DMPerson类,没有实现alloc方法,因此系统会走慢速方法查找,在慢速方法查找的过程中,会系统会调用realizeAndInitializeIfNeeded_locked方法

image.png 而在这个方法中,会执行setInstanceSize方法,设置cache

image.png 因为我在DMPerson里面添加了2个属性,因此在8字节对齐的情况下,size是24没有问题。 而后在调用[NSObject alloc]的时候,调用cls->instanceSize方法直接会走快速流程读取缓存cache.fastInstanceSize(extraBytes),在对象层面会直接走16字节对齐,因此最后会返回32.

最后,在类的内部层面,采用的是8字节对齐,而对于整个对象来说,采用的是16字节对齐。