OC对象底层内存开辟和实现(中)

最近一直有事没有更新,今天抽时间赶紧整理一下。

1、LGPerson第一次alloc为什么会先执行objc_alloc,再执行alloc?

为了验证这个问题,,我们先利用machOView工具查看一下MachO文件中的符号表,我们在符号表中搜索一下“alloc”字符串,是否有这个符号。

image.png

我们发现在符号表中并没有alloc符号,但是有objc_alloc符号

小结:说明在编译阶段 编译器就把alloc符号替换成了objc_alloc符号,那么我需要看一下编译器LLVM源码中做了哪些操作

我们使用文本查看工具全局检索一下llvm源码中的alloc关键字

image.png 上图我们看到在函数tryGenerateSpecializedMessageSend中有alloc->objc_alloc注释。

我们先解读一下注释,大概意思是说:objc在运行时提供了更快的入口点,而不是通过普通的消息发送(objc_msgSend)。这个入口点也比普通的消息发送速度更快。如果运行时确实支持所需的入口点时,这个方法就会调用并返回结果,否则返回None,就会调用自己生成一个消息发送。

分析理解:

  1. 在LLVM 编译阶段,如果方法支持更快的入口点,则会标识这个方法(alloc改成objc_alloc),则不会再走普通的消息发送,否则就会走普通的消息发送进行方法调用。
  2. 之所以所谓的“入口点”比objc_msgSend更快,是因为没有了objc_msgSend中缓存查找的过程,如:alloc会先查找缓存,没有时再调用alloc执行callAlloc方法进行内存开辟和类绑定等操作,而objc_alloc则可以直接调用callAlloc进行操作。

我们查看一下上图中标注的代码中做了什么?是否能验证我们的分析理解

首先看条件if (Sel.isUnarySelector() && Sel.getNameForSlot(0) == "alloc")是否成立。这个isUnarySelector()方法意思是否在SelectorTable中被标记(具体逻辑细节不再贴图,有兴趣可以下载源码研究),如果有标记且是"alloc"的话,就会走EmitObjCAlloc方法。 image.png 我们看到在EmitObjCAlloc方法中生成了一个objc_alloc入口点,我们看注释:“分配给定的objc对象”,由此我们可以验证以上的分析。

我们根据LLVM源码搜索的结果,查看一下tryGenerateSpecializedMessageSend函数调用过程。

检索tryGenerateSpecializedMessageSend方法调用 image.png 我们看到在GeneratePossiblySpecializedMessageSend中有个条件判断,从代码可以分析出:

如果tryGenerateSpecializedMessageSend方法返回None,这里判断为NO,就会走GenerateMessageSend方法(也就是调用者自己生成一个普通的objc_msgSend),相反就会走特殊的入口点。

总结:到此我们可以得出结论,系统针对alloc,allocWithZone等系统函数,在LLVM编译阶段在底层就进行了拦截(相当于HOOK操作)和优化(特殊的入口点)。

2、LGPerson第二次alloc为什么执行objc_alloc后,不再执行alloc(objc_msgSend调用)?

这个问题我也看了很多优秀的博客,大多的解释是第一次进来没有缓存,第二次进来有缓存了就不再通过消息发送执行alloc了(解释不够清晰和准确)。接下来我们通过代码调试来看下底层到底做了些什么?

alloc函数如果缓存的话,是应该缓存在metaClass类中,那我们来调试验证一下。

- 第一次执行[LGPerson alloc],并且在callAlloc方法打上断点(objc_alloc --> callAlloc),查看 “LGPerson的元类” 中的cache_t中的信息。

image.png 点击Continue program execution继续执行 image.png 上图中输出 “LGPerson的元类” 中的cache_t中的信息,我们可以看到此时cache中_maybeMask代码缓存的长度为0, _flags为0;

LLBD命令:

(lldb) p cls

(Class) $0 = LGPerson

(lldb) x/4gx cls // 输出LGPerson类的4个成员变量

0x1000083c0: 0x0000000100008398 0x000000010036a140 0x1000083d0: 0x00000001003623a0 0x0000000000000000

(lldb) x/4gx 0x0000000100008398 // 输出LGPerson元类的4个成员变量

0x100008398: 0x000000010036a0f0 0x000000010036a0f0

0x1000083a8: 0x00000001003623a0 0x0000000000000000

(lldb) p/x 0x100008398 + 16 // 输出LGPerson元类cache_t的地址

(long) $2 = 0x00000001000083a8

(lldb) p (cache_t *)0x00000001000083a8 // 强转

(cache_t *) $3 = 0x00000001000083a8

(lldb) p *$3 // 输出cache_t内容

(cache_t) $4 = {

  _bucketsAndMaybeMask = {

    std::__1::atomic《unsigned long》 = { // 此处《 因为尖括号冲突才这样写

      Value = 4298515360

    }

  }

   = {

     = {

      _maybeMask = {

        std::__1::atomic《unsigned int》= {

          Value = 0

        }

      }

      _flags = 0

      _occupied = 0

    }

    _originalPreoptCache = {

      std::__1::atomic<preopt_cache_t *> = {

        Value = nil

      }

    }

  }

}

我们来看fastpath(!cls->ISA()->hasCustomAWZ())函数中如何判断的(cls->ISA()获取元类) image.png

FAST_CACHE_HAS_DEFAULT_AWZ宏定义表示 是否实现alloc/allocWithZone位域标识位

我们继续下一步: image.png 继续下一步: image.png

小结:第一次执行[LGPerson alloc],第一次执行objc_alloc ->callAllocfastpath(!cls->ISA()->hasCustomAWZ())返回结果为NO,所以在callAlloc方法中会执行objc_msgSend调用alloc

我们继续执行Continue program execution

image.png

- 第一次执行[LGPerson alloc],第二次来到callAlloc方法([LGPerson alloc] -> objc_alloc -> callAlloc -> (objc_msgSend)alloc->_objc_rootAlloc->callAlloc),我看到LGPerson元类cache_t缓存中的信息

  1. _maybeMask代码缓存的长度依然为0,alloc方法并没有并缓存起来;
  2. _flags却变成了57393。

那么我们继续看下hasCustomAWZ()方法中的判断 image.png 此时我们可以根据上图中的结果可以看到,返回结果为YES,进入_objc_rootAllocWithZone方法

image.png

小结:

  1. alloc函数不会在缓存中存储,会在cache_t中的_flags中第15位来表示是否已经完成了实现。(凡是alloc第一次执行后在缓存中的说法,显然说法不够准确)
  2. 由此也产生了一个问题,这个_flags被赋值的时机是是什么时候?(后面我们来调试解答)

- 第二次执行[LGPerson alloc],(在 OC对象底层内存开辟和实现(上)中已经说明过执行流程,我们直接跳过中间步骤),我们直接看hasCustomAWZ()方法中的判断

image.png 直接进入hasCustomAWZ()方法里面的判断

image.png 返回结果为YES,表示已经实现了alloc/allocWithZone方法,所以第二次执行[LGPerson alloc]执行objc_alloc后,不再执行alloc(objc_msgSend调用),从而直接调用_objc_rootAllocWithZone方法。

总结:

  1. LGPerson第一次alloc会先执行objc_alloc,再通过objc_msgSend执行alloc,然后会在其元类中的cache_t_flags中标识已经实现alloc/allocWithZone方法。
  2. LGPerson第二次alloc执行objc_alloc后,不再执行alloc(objc_msgSend调用),因为缓存中已经标识过了。

3、NSOject第一次alloc为什么执行objc_alloc后,不再执行alloc?

因为NSOject是所有类的基类,和它的类初始化时机有关,还记得我们抛出一个问题吗, 元类cache_t_flags赋值时机是什么时候?在做这个问题中我们来做解答。

4、init和new底层做了哪些操作?

init函数底层实现,比较简单直接贴代码不再截图展示了,如下所示:

-(id) init {

return _objc_rootInit(self);

}

进入 _objc_rootInit

// C++函数

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方法返回的是对象本身
  • init可以提供自定义实现 ,通过id类型实现强转,返回需要的类型

new函数的实现如下:

+(id)new {

    return [callAlloc(self, false) init];

}

  • alloc差不多,alloc是_objc_rootAlloc->callAlloc
  • new是直接走了callAlloc的方法流程,然后走了init方法 ,new相当于是alloc + init

5、元类cache_t_flags赋值时机是什么时候?

我们还是用断点调试的方式来看一下_flags赋值的时机和几种情况,

- 第一种情况我们在类懒加载(没有实现+load)情况下看下赋值的时机

首先我们来看一下给_flags赋值的方法

vid setBit(uint16_t set) {

        __c11_atomic_fetch_or((_Atomic(uint16_t) *)&_flags, set, __ATOMIC_RELAXED);

    }

  __c11_atomic_fetch_or函数解释:是一个C++11函数,表示把set和_flags按位或结果,保存到_flags并返回

我们在[LGPerson alloc]setBit(uint16_t set)方法分别打上断点,我们先通过堆栈信息来看一下赋值的时机。 image.png 接下来我们直接看下什么时候会调用setBit方法

  • 第一次调用如下:

image.png 我们看到堆栈的调用信息为:第一次objc_alloc -> callAlloc -> objc_msgSend(alloc) -> _objc_msgSend_uncached ->lookUpImpOrForward ->realizeAndInitializeIfNeeded_locked -> realizeClassMaybeSwiftAndLeaveLocked -> realizeClassMaybeSwiftMaybeRelock -> realizeClassWithoutSwift方法中调用,代码如下:

#if FAST_CACHE_META

    if (isMeta) cls->cache.setBit(FAST_CACHE_META);

#endif

表示_flags第1位是否为元类标识,此时_flags为等于1。

  • 第二次调用如下

image.png 同样是在realizeClassWithoutSwift方法中调用,代码如下:

if (isMeta) {

        // Metaclasses do not need any features from non pointer ISA

        // This allows for a faspath for classes in objc_retain/objc_release.

        cls->setInstancesRequireRawIsa();

 }

void setInstancesRequireRawIsa() {

        cache.setBit(FAST_CACHE_REQUIRES_RAW_ISA);

 }

// class's instances requires raw isa 类的实例需要原始isa

#define FAST_CACHE_REQUIRES_RAW_ISA   (1<<13)

表示_flags第14位标识类实例(类对象)的isa是纯的isa,指向元类,不再是isa_t联合体。 此时_flags为等于8193。

  • 第三次调用如下:

image.png 还是在realizeClassWithoutSwift方法中调用,代码如下:

// Set fastInstanceSize if it wasn't set already.

cls->setInstanceSize(ro->instanceSize);

void setInstanceSize(uint32_t newSize) {

        ASSERT(isRealized());

        ASSERT(data()->flags & RW_REALIZING);

        auto ro = data()->ro();

        if (newSize != ro->instanceSize) {

            ASSERT(data()->flags & RW_COPIED_RO);

            const_cast<uint32_t *>(&ro->instanceSize) = newSize;

        }

       cache.setFastInstanceSize(newSize);

 }

void setFastInstanceSize(size_t newSize)

    {

        // Set during realization or construction only. No locking needed.

        uint16_t newBits = _flags & ~FAST_CACHE_ALLOC_MASK;

        uint16_t sizeBits;

        // Adding FAST_CACHE_ALLOC_DELTA16 allows for FAST_CACHE_ALLOC_MASK16

        // to yield the proper 16byte aligned allocation size with a single mask

        sizeBits = word_align(newSize) + FAST_CACHE_ALLOC_DELTA16;

        sizeBits &= FAST_CACHE_ALLOC_MASK;

        if (newSize <= sizeBits) {

            newBits |= sizeBits;

        }

        _flags = newBits;

    }

表示_flags第4-13位来存储对象大小,最小8字节(1000),最大为4M(1 0000 0000 0000),上图中我们看到在元类的class_ro_t中instanceStart为40,表示类对象的开始大小就是40字节。

image.png 此时_flags为等于8241, 二进制格式为:0b0010 0000 0011 0001。

小结:执行到这里其实是 类加载的过程(类懒加载到内存),包括初始化类结构,计算缓存对象空间大小,绑定父类关系等等。

在方法lookUpImpOrForward有两个判断,我可以来看下

static Class realizeAndInitializeIfNeeded_locked(id inst, Class cls, bool initialize)

{

    runtimeLock.assertLocked();

    if (slowpath(!cls->isRealized())) {

        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);

    }

    if (slowpath(initialize && !cls->isInitialized())) {

        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);

    }

    return cls;

}

  1. 第一个判断是 类是否已经完成加载;
  2. 第二个判断是 类是否完成类初始化;

所以上面的整个过程都是在第一个判断条件中执行的。接下来我们继续看类初始化过程:

  • 第四-五次调用如下:

image.png

image.png

此时的堆栈调用变为:第一次objc_alloc -> callAlloc -> objc_msgSend(alloc) -> _objc_msgSend_uncached ->lookUpImpOrForward ->initializeAndLeaveLocked -> initializeAndMaybeRelock -> initializeNonMetaClass -> lockAndFinishInitializing-> _finishInitializing->objc_class::setInitialized方法中调用,代码如下

void objc_class::setInitialized() {

    Class metacls;

    Class cls;

    ASSERT(!isMetaClass());

    cls = (Class)this;

    metacls = cls->ISA();

    mutex_locker_t lock(runtimeLock);

    // Special cases:

    // - NSObject AWZ  class methods are default.

    // - NSObject RR   class and instance methods are default.

    // - NSObject Core class and instance methods are default.

   objc::AWZScanner::scanInitializedClass(cls, metacls);

   objc::RRScanner::scanInitializedClass(cls, metacls);

  objc::CoreScanner::scanInitializedClass(cls, metacls);

#if CONFIG_USE_PREOPT_CACHES

    cls->cache.maybeConvertToPreoptimized();

    metacls->cache.maybeConvertToPreoptimized();

#endif

    if (PrintInitializing) {

        _objc_inform("INITIALIZE: thread %p: setInitialized(%s)",

                     objc_thread_self(), cls->nameForLogging());

    }

    // Update the +initialize flags. 更新+initialize标识

    metacls->changeInfo(RW_INITIALIZED, RW_INITIALIZING);

}

注意:上述标颜色的代码是C++的模板调函数调用,会一次调用2次cache.setBit方法,调用代码依次是

void setHasDefaultAWZ() {

      cache.setBit(FAST_CACHE_HAS_DEFAULT_AWZ);

 }

void setHasDefaultRR() {

      bits.setBits(FAST_HAS_DEFAULT_RR);

}

void setHasDefaultCore() {

    return cache.setBit(FAST_CACHE_HAS_DEFAULT_CORE);

}

  • FAST_CACHE_HAS_DEFAULT_AWZ标识在cache_t_flags赋值时机有什么不一样.的15位是否是有实现默认的alloc/allocWithZone方法;
  • FAST_CACHE_HAS_DEFAULT_CORE标识在 cache_t_flags的16位是否有默认的类或超类new/self/class/respondsToSelector/iskindof方法实现;
  • FAST_HAS_DEFAULT_RR是在class_data_bit_t的bit中来标识,表是否有默认的类或超类的retain/release/autorelease/retainCount/_tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference等方法实现;

总结:这部分是类初始化的过程,在类第一次使用的时候(也就是第一次objc_alloc->callAlloc 方法中用objc_msgSend执行alloc时)会依次执行类加载和类初始化的操作,会调用+initialize方法;

initializeNonMetaClass方法中会递归判断父类是否实现初始化,所以会先执行父类的+initialize`方法;

这也就解释了NSOject第一次alloc为什么执行objc_alloc后,不再执行alloc。是因为在初始化其他的类的时候,NSObject已经被初始化。

metacls->changeInfo(RW_INITIALIZED, RW_INITIALIZING); 当类初始化完后会更行类的标识状态,在class_rw_tflags的第30位来标识,宏定义状态RW_INITIALIZED (1<<29)。

第二种情况我们在类非懒加载(实现+load)情况下看下赋值的时机

  • 那么我们在LGPerson中先实现+load方法,然后运行看下cache_t_flags赋值时机有什么不一样。

第一次赋值时机: image.png 第二次赋值时机: image.png 第三次赋值时机: image.png 以上我们可以看出类的加载出现在了dyld动态链接阶段,会执行+load方法;但此时并不会初始化类(不会执行+initialize方法)。此时的cache_t_flags的值为8241,标识已经完成加载。

在我们继续执行到第一次objc_alloc时: image.png 我们看到在lookUpImpOrForward方法中直接走了类的初始化,会调用执行+initialize方法;

总结:

  • 在非懒加载类中,类的加载会提前到动态链接阶段(dyld);
  • 在懒加载类中,类的加载会在第一次类的使用时;
  • 类的初始化都是在第一次类的使用时才会被初始化。

猜你喜欢

转载自juejin.im/post/7109868279929241636