在上一篇文章中,讲述了alloc内部的方法执行流程。这边再次做一个总结:
(PS:本文章是基于64位系统下进行的阐述,所有的类型所占字节请按照64位计算。)
准备资料:
LLVM源码:github.com/apple/llvm-…
内存对齐原则
ascall码对照表
我们正式开始吧,先打开汇编调试,我们可以看到alloc内部首先执行的是objc_alloc方法(图01、图02)。
接下来全局搜索objc_alloc、alloc方法,找到并打上断点(图03、图04)
为了不受到干扰,先关掉最开始的汇编调试,把objc_alloc、alloc方法的两个断点先取消,等执行到alloc这一行再打开两个断点。
LGPerson *p = [LGPerson alloc] ;
接下来一步步通过断点去追踪,我们可以得到这样一个调用流程:
流程图如下
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。
内存对齐原则
类型对应所占字节大小如下:
打印内存大小有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
然后我们打印一下这个类在内存中的实际大小。
类型对应所占字节大小已经给大家准备了,可以对着表格计算大小。
问题来了,8 + 8 + 8 + 4 + 8 + 1 + 1 = 38,那么为什么控制台打印的是48呢?因为上面少算了一个isa。然后又有同学问了,就算38+8也不等于48啊,小编你是不是欺负我数学没及格过。
说到这里,我们不得不考虑一个知识点:内存对齐。
对于属性来说,是8字节对齐。
对于一个对象来说,是16字节对齐。
class_getInstanceSize本质上打印的是,实例对象所有属性的大小总和,要8个字节对齐。
那么影响对象内存大小的因素除了属性,成员变量、实例方法、静态方法是否会影响呢?我们来一一测试:
1、添加成员变量
2、添加类方法、实例方法
3、添加协议
4、添加block
因此我们可以得出结论,影响对象所占内存大小的有属性、成员变量、代理、block。
接下来我们给LGPerson的实例对象person赋值:
控制台打印:x/8gx person输出
接下来我们研究一下结构体内存对齐
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这八个位置。)
接下来运行一下代码,验证推断的结果:
如上图所示,我们的计算结果是正确的。