alloc初探

619 阅读3分钟

一、alloc的作用

作为oc语言开发者,大家都知道如何创建一个实例对象,没错就是alloc。那么alloc在底层是如何调用的呢?我们一起来探究一下。 代码搞起

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

打印结果

p : <LGPersion: 0x6000006c0160>---0x6000006c0160----0x7ffee9ae3c58
p1: <LGPersion: 0x6000006c0160>---0x6000006c0160----0x7ffee9ae3c50
p2: <LGPersion: 0x6000006c0160>---0x6000006c0160----0x7ffee9ae3c48

通过上述代码和打印结果可以明显看出:p,p1,p2三者的对象地址是一样的,但是指针地址不一样,所以p,p1,p2三个对象指向的是同一个内存空间,在内存中的表现如图所示:

未命名文件.png

结论:alloc开辟了对象的内存空间,init并没有对空间有操作

二、探究alloc的方法

然而alloc是如何开辟内存的呢?我们该如何去探究呢?接下来我们用3种方法去探索:

1、setp into + 符号断点

先在alloc之前加个断点,然后按住control 点击setp into

截屏2021-06-06 下午10.02.58.png 进入

截屏2021-06-06 下午10.05.02.png 然后加入符号断点objc_alloc

截屏2021-06-06 下午10.07.34.png 放开断点进入libobjc.A.dylib objc_alloc:

截屏2021-06-06 下午10.07.44.png

2、汇编

汇编模式:debug -> debug Workflow ->Always Show Disassembly 具体使用:进入断点后按照上述步骤即可进入汇编代码。如图:

1、断点

截屏2021-06-06 下午9.25.14.png

2、进入汇编

截屏2021-06-06 下午9.12.02.png 汇编:

截屏2021-06-06 下午9.26.47.png

3、step into

截屏2021-06-06 下午9.28.17.png 按住control 点击step into 进入alloc

截屏2021-06-06 下午9.28.37.png

3、直接符号断点

我们都知道这个方法是alloc 所以直接下符号断点,流程如下: 先在alloc前打断点断点之后如图

截屏2021-06-06 下午9.42.17.png

截屏2021-06-06 下午9.42.35.png 加入符号之后直接放开当前断点

放开.jpg 然后如图所示

截屏2021-06-06 下午9.41.12.png

三、alloc源码跟踪

打开Source Browser 搜索objc,如图:

截屏2021-06-06 下午10.20.52.png

下载objc4-818.2.tar.gz解压

command + 左键alloc

直接跳转到

截屏2021-06-06 下午10.27.40.png

继续 command

截屏2021-06-06 下午10.28.30.png 继续, 截屏2021-06-06 下午10.30.32.png 进入_objc_rootAllocWithZone

截屏2021-06-22 上午10.17.17.png 继续进入_class_createInstanceFromZone (核心代码) 核心.jpg

如图所示(已经标出主要部分)

cls->instanceSize(extraBytes) 计算大小

calloc(1, size) 开辟空间

initInstanceIsa(cls, hasCxxDtor) 将开辟的空间跟类关联起来

细细一品确实是核心东西 ,那么下面开始挨个探究。

1、计算大小

我们首先进入instanceSize 如图

大小计算.jpg 有个判断先,这里判是否有缓存 有的话直接获取返回(节省时间)

进入cache.fastInstanceSize

截屏2021-06-06 下午11.15.55.png

command 查看FAST_CACHE_ALLOC_MASK 值为0x1ff8

如图点断所示,我们需要进入align16

截屏2021-06-06 下午11.21.08.png

梳理一下这个算式

设x = 9 
则算式为 (9 + 15 & ~ 15
二进制计算
x = 0000 1001
15  0000 1111
9 + 15  0001 1000
~15 :1111 0000
(9 + 15 & ~ 15  0001 0000 结果为16

设x = 17
则算式为 (17 + 15 & ~ 15
二进制计算
x = 0001 0001
15  0000 1111
17 + 15  0010 0000
~15 :1111 0000
(17 + 15 & ~ 15  0010 0000 32

所以 align16 是一个对齐方法,即返回16的整数倍,如果不足16,返回16,如果x>16*n && x<16 * (n + 1)则返回 16*(n +1),等同于>> 4 <<4,该方法叫做字节对齐(简单论证不多举例了),字节对齐的意义是什么?

提高cpu读取速度:以固定的长度读取数据,会大幅度提高cup工作效率

接下来查看 普通方法 command 进入alignedInstanceSize

截屏2021-06-06 下午11.51.27.png 继续进入unalignedInstanceSize(没有对齐的InstanceSize

截屏2021-06-06 下午11.54.35.png

所以此方法返回的是对象的大小(未对齐之前的),那么对象的大小如何判断的呢?我们细想一下,对象里面有什么?属性、方法、和协议等;但是对象的大小只取决于成员变量。该方法返回的是data中的roinstanceSize,即实例变量的大小。该方法的注释中也提到 May be unaligned depending on class's ivars.成员变量的大小。

然后 word_align

截屏2021-06-07 上午12.04.36.png WORD_MASK 为7 所以该方法是8字节对齐方法。

最后返回到 instanceSize 中返回

2、开辟空间

截屏2021-06-07 上午12.21.05.png 此图断点为刚声明,虽然有地址,却是脏地址,不可用的。

截屏2021-06-07 上午12.21.17.png 截屏2021-06-07 上午12.13.53.png

越过开辟的方法,之后发现地址变了,但是po一下obj发现只是一个地址,跟平时的对象不太一样

截屏2021-06-07 上午12.23.11.png 因为接下来的步骤还没走,class绑定

3、绑定

进入 initInstanceIsa

截屏2021-06-07 上午12.25.39.png

进入 initIsa

截屏2021-06-07 上午12.34.24.png

完事了,我们回到 _class_createInstanceFromZone 并po,发现obj正常了,跟平时的一样,说明我的alloc走完了。

截屏2021-06-07 上午12.36.04.png

4、总结

通过以上的探索在下绘制了,简单的alloc流程图:

未命名文件-2.png

5、修改

callAlloc中,我们发现有3个return,我们都加上断点:

截屏2021-06-22 上午9.53.31.png 然后关闭断点,运行程序进入到 alloc前开启断点:

截屏2021-06-22 上午9.57.23.png 直接走到了callAlloc,没有进入到alloc中?这是什么情况?我们再细看return的东西objc_msgSend消息发送,参数是clsalloc,此刻发送的是alloc方法,到底发生了什么事情?经过大神提醒和llvm查找,发现在调用alloc方法时候llvm进行了方法交换,代码太长截图不方便直接上代码:

/// The ObjC runtime may provide entrypoints that are likely to be faster
/// than an ordinary message send of the appropriate selector.
///
/// The entrypoints are guaranteed to be equivalent to just sending the
/// corresponding message.  If the entrypoint is implemented naively as just a
/// message send, using it is a trade-off: it sacrifices a few cycles of
/// overhead to save a small amount of code.  However, it's possible for
/// runtimes to detect and special-case classes that use "standard"
/// behavior; if that's dynamically a large proportion of all objects, using
/// the entrypoint will also be faster than using a message send.
///
/// If the runtime does support a required entrypoint, then this method will
/// generate a call and return the resulting value.  Otherwise it will return
/// None and the caller can generate a msgSend instead.
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) )
          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;
}

再细点:

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

继续精简:

if (Sel.isUnarySelector() && Sel.getNameForSlot(0) )
                    return CGF.EmitObjCAlloc(Receiver, CGF.ConvertType(ResultType));

在这里将alloc返回成了calloc,所以才会直接进入到calloc方法中,经过一系列判断再回到alloc中,所以修正后的alloc如图所示:

alloc流程图.png