iOS之alloc底层实现分析02(未完待续)

394 阅读5分钟

在上一篇文章中,讲述了alloc内部的方法执行流程。这边再次做一个总结:
(PS:本文章是基于64位系统下进行的阐述,所有的类型所占字节请按照64位计算。)

准备资料:

LLVM源码:github.com/apple/llvm-…

内存对齐原则 内存对齐原则.png

ascall码对照表 ascall码对照表

我们正式开始吧,先打开汇编调试,我们可以看到alloc内部首先执行的是objc_alloc方法(图01、图02)。

01.png

02.png

接下来全局搜索objc_alloc、alloc方法,找到并打上断点(图03、图04)

03.png

04.png

为了不受到干扰,先关掉最开始的汇编调试,把objc_alloc、alloc方法的两个断点先取消,等执行到alloc这一行再打开两个断点。
LGPerson *p = [LGPerson alloc] ;

接下来一步步通过断点去追踪,我们可以得到这样一个调用流程:

05.png

06.png

07.png

08.png

09.png

10.png

流程图如下

graph TD
alloc --> objc_alloc --> callAlloc --> objc_msgSend,alloc --> _objc_rootAlloc --> callAlloc --> _objc_rootAllocWithZone --> _class_createInstanceFromZone --> 返回结果

我们可以看到alloc执行了两次,这是因为系统对alloc方法做了hook处理。 感兴趣的小伙伴,可以先下载LLVM的源码,全局搜索"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);
}

里面有两个方法:
1、tryGenerateSpecializedMessageSend(特殊方法)
2、GenerateMessageSend(普通方法)
当满足if条件时,执行第1个方法,否则就执行第2个。
当第1个方法执行完,下次再进GeneratePossiblySpecializedMessageSend方法,里面的if判断就不成立,那就会走第2个方法,也就是普通方法。
这就是有些特殊方法(例如alloc)会走两次的原因。

点击进入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()) {
        // 
        if (Sel.isUnarySelector() && Sel.getNameForSlot(0) == "alloc")
          
            return CGF.EmitObjCAlloc(Receiver, CGF.ConvertType(ResultType));
       // ...
    }
    break;

  case OMF_autorelease:
     // ...
    break;

  case OMF_retain:
    // ...
    break;

  case OMF_release:
    // ...
    break;

  default:
    break;
  }
  return None;
}

点击进入EmitObjCAlloc方法

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

由此我们知道,LLVM对我们的系统alloc方法进行了优化,将SEL(alloc)对应的IMP改成了objc_alloc的IMP。

内存对齐原则

类型对应所占字节大小如下:

11.jpeg

打印内存大小有3种方法:sizeof、class_getInstanceSize、malloc_size。

  • sizeof类型占用的内存大小。 参数可以传基本数据类型、对象、指针。如果传入的是NSObject类型,我们知道NSObject本身就是一个结构体指针,它占用大小就是8个字节。
  • class_getInstanceSize:本质上就是成员变量占用内存的总和大小,8字节对齐原则。如果对象继承自NSObject且没有自定义其它属性,那大小就是8,如果有自定义属性类型,可以参考类型对应所占字节大小计算大小。
  • malloc_size:系统实际分配空间大小,16字节对齐。

接下来我们研究一下影响对象内存大小的因素

LGPerson类如下:

@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, copy) NSString *hobby;
@property (nonatomic, assign) int age;
@property (nonatomic) double height;

@property (nonatomic) char c1;
@property (nonatomic) char c2;

+ (void)sayNB;
@end

然后我们打印一下这个类在内存中的实际大小。

12.png 类型对应所占字节大小已经给大家准备了,可以对着表格计算大小。
问题来了,8 + 8 + 8 + 4 + 8 + 1 + 1 = 38,那么为什么控制台打印的是48呢?因为上面少算了一个isa。然后又有同学问了,就算38+8也不等于48啊,小编你是不是欺负我数学没及格过。
说到这里,我们不得不考虑一个知识点:内存对齐。
对于属性来说,是8字节对齐。
对于一个对象来说,是16字节对齐。
class_getInstanceSize本质上打印的是,实例对象所有属性的大小总和,要8个字节对齐。

那么影响对象内存大小的因素除了属性,成员变量、实例方法、静态方法是否会影响呢?我们来一一测试:

1、添加成员变量 13.png
2、添加类方法、实例方法 14.png
3、添加协议 15.png
4、添加block 16.png 因此我们可以得出结论,影响对象所占内存大小的有属性、成员变量、代理、block。

接下来我们给LGPerson的实例对象person赋值:

17.png

控制台打印:x/8gx person输出

18.png


19.png

接下来我们研究一下结构体内存对齐

内存对齐原则.png

struct LGStruct1 {
    double a;       // 8    [0 1 2 3 4 5 6 7]
    char b;         // 1    [8]
    int c;          // 4    (9 10 11 [12 13 14 15]
    short d;        // 2    [16 17]                计算结果:24
}struct1;

struct LGStruct2 {
    double a;       // 8    [0 7]
    int b;          // 4    [8 9 10 11]
    char c;         // 1    [12]
    short d;        // 2    (13 [14 15]             计算结果:16
}struct2;

// 家庭作业 : 结构体内存对齐
struct LGStruct3 {
    double a;       //8             [0 1 2 3 4 5 6 7]
    int b;         //4              [8 9 10 11]
    char c;        //1              [12]
    short d;       //2              (13 [14 15]
    int e;         //4              [16 17 18 19]
    struct LGStruct1 str; //24      (20 21 22 23 [24 25 ...47]  计算结果:48
}struct3;

就拿struct1来举例,

  • 根据规则第一条,第一个属性要从offset为0的位置开始放,占8个字节,所以是[0,7]
  • 接下来从第8个位置开始存放,根据规则第一条,当前位置序号必须是当前所放属性所占大小的整数倍,也就是说序号要满足是1的整数倍才能开始存放,很显然是成立的,所以是[8]
  • 接下来从第9位置开始存储,9明显不符合第一条规定,继续找。。。得到存放位置[12,15];
  • 接下来从第16位置开始存储...得到存放位置[16,17];
  • 17并不满足规则第三条,必须是内部最大数成员的整数倍,得到结果24 (解释一下啊:[0,7]指的是所占区域为0~7这八个位置。)

接下来运行一下代码,验证推断的结果:

20.png

如上图所示,我们的计算结果是正确的。