以下所有类和对象的描述均以Objective-C为参考, cpu架构为arm64
0x0 一句话描述类和对象与内存的关系
- 类本身是一个描述, 描述里包含实例化这个类需要多大的内存, 以及内存的每个byte
是
什么内容, 这个内容的头部是一个isa, 其他内容是ivar的值或指针. - 对象是按类的描述所从内存空间里面开辟出对应大小的空间并填充isa指针(alloc), 类的初始化方法往这个空间里的byte里面存初始化的内容.
0x1 ivar
举个例子:
@interface AClass : NSObject
{
NSString *_aString;
NSInteger _aInt;
}
@end
@implementation AClass
- (instancetype)init
{
if (self = [super init]) {
_aInt = 1;
}
return self;
}
@end
这个类被编译之后变成一个描述(arm64), AClass占用24个字节, 前8个字节是isa指针, 中间八个字节是NSString的指针, 后八个字节是一个NSInteger的值.:
| isa | NSString* _aString | NSInteger _aInt |
调用[[AClass alloc] init]
在alloc的时候, 会分配24个字节的内存出来:
| 0 | 0 | 0 |
然后往前8个字节放isa的地址(mask之后的), alloc完成后的内存长这样:
| isa | 0 | 0 |
然后调用alloc出来的这个对象的init方法, init方法会把aInt的位置设为1, 而aString的位置没有做初始化, 因此还是0:
| isa | 0 | 1 |
isa指向了这个类的meta, meta里面存了父类/ivar结构/方法等内容, 后续再做详解.
0x2 ivar实操
代码贴到XCode里面, 然后XCode -> Debug ->Debug Workflow -> Always Show Disassembly
int main(int argc, char * argv[]) {
[[AClass alloc] init]; // 断点断这一行
return 0;
}
运行断点会断在这里:
通过lldb的register read指令读出当前的寄存器的值:
可以看到x8是alloc方法, x9是AClass. 断点下面两行指令会分别把AClass和alloc方法分别作为self和selector放到x0/x1寄存器作为objc_msgSend的第一个(self)和第二个参数(cmd).
把断点断在16行位置, 看看objc_msgSend调完之后x0的值:
得到的结果是:
这已经是AClass的一个对象了, 因为这里并没有调用init, 所以这个对象的状态应该是:
| isa | 0 | 0 |
通过 Shift + Command + M
看看内存0x15d602780
:
前8个byte是isa, 中8个和后8个byte都是0, 跟第一节中的描述一致.
在把断点断到第20行(第19行是调用init方法, 大家可以自己尝试), 看看init调用之后的内存内容:
前8个byte是isa, 中8个byte是0, 后8个byte的内容是数字1, 跟第一节中的描述一致.
0x3 property
@property实际上是一个编译器指令, 在编译器会根据指令后的参数自动生成ivar和ivar的getter和setter.
把示例代码做一个修改, 把ivar改为property, 再看看不同属性下自定义getter和setter的不同实现.
@interface AClass : NSObject
@property (nonatomic, strong) NSString *aString; // nonatomic, strong
@property (nonatomic, assign) NSInteger aInt; // atomic, assign
@end
main实现改为如下代码, 为了直观表示代码直接用getter和setter访问和赋值:
int main(int argc, char * argv[]) {
AClass *object =[[AClass alloc] init];
[object setAString:@"test"];
[object aString];
[object setAInt:2];
[object aInt];
return 0;
}
前置知识: 在OC发消息对应的方法的实现进行调用时, x0是调用方法的对象, x1是selector, 之后的x2/x3/x4...是传进来的参数(如果有参数). 方法调用完成后, 返回值放在x0(如果有返回值). 注意, 这段描述并不完全准确也不完整, 具体请参考苹果官方文档[1].
在用Hopper Disassembler分析下编译产物, 先来setter'-[AClass setAString:]'.
-[AClass setAString:]:
0000000100006940 stp x29, x30, [sp, #0xfffffff0]! ; Objective C Implementation defined at 0x100008cf8 (instance)
0000000100006944 mov x29, sp
0000000100006948 sub sp, sp, #0x20
// 前面是保存方法调用的现场, 用于后面恢复用
000000010000694c adrp x8, #0x100008000 ; imp___got____gxx_personality_v0
0000000100006950 add x8, x8, #0xe58 ; _OBJC_IVAR_$_AClass._aString
// 动态定位获取AClass._aString的描述地址, 放入x8
0000000100006954 stur x0, [x29, #0xfffffff8]
0000000100006958 str x1, [sp, #0x10]
000000010000695c str x2, [sp, #0x8]
// 把参数self/selector/传进来的string对象, 存到栈里
0000000100006960 ldr x0, [sp, #0x8]
0000000100006964 ldur x1, [x29, #0xfffffff8]
// 把传进来的对象从栈里捞出来放到x0, 把self从栈里捞出来放到x1
0000000100006968 ldrsw x8, [x8] ; _OBJC_IVAR_$_AClass._aString
000000010000696c add x8, x1, x8
// 从x8里把_aString的在AClass对象的偏移量捞出来, 并与x1相加, 也就是`self指针+偏移量`, 结果是一个指针
0000000100006970 str x0, [sp]
// 把传进来的对象存入栈
0000000100006974 mov x0, x8
// 把`self指针+偏移量`指针放入x0
0000000100006978 ldr x1, [sp]
// 把传进来的对象从栈里捞出来放到x1
000000010000697c bl imp___stubs__objc_storeStrong
// 把x1里传进来的对象赋值给x0, 然后强引用一次
0000000100006980 mov sp, x29
0000000100006984 ldp x29, x30, [sp], #0x10
// 恢复最前面保存的现场
0000000100006988 ret
// 返回
; endp
上面代码干的事情, 就是把要赋值的对象存一份到对象的ivar偏移量对应的位置. 同时strong属性这里会通过objc_storeStrong[2]把引用计数+1.
再来setter -[AClass aString]
:
-[AClass aString]:
0000000100006914 sub sp, sp, #0x10 ; Objective C Implementation defined at 0x100008ce0 (instance)
// 移动一下栈用来存方法里要用到的数据
0000000100006918 adrp x8, #0x100008000 ; imp___got____gxx_personality_v0
000000010000691c add x8, x8, #0xe58 ; _OBJC_IVAR_$_AClass._aString
// 动态定位获取AClass._aString的描述地址, 放入x8
0000000100006920 str x0, [sp, #0x8]
0000000100006924 str x1, [sp]
// 把self和selector存入栈
0000000100006928 ldr x0, [sp, #0x8]
// 把self从栈里捞出来
000000010000692c ldrsw x8, [x8] ; _OBJC_IVAR_$_AClass._aString
0000000100006930 add x8, x0, x8
// 从x8里把_aString的在AClass对象的偏移量捞出来, 并与x1相加, 也就是`self指针+偏移量`, 结果是一个指针
0000000100006934 ldr x0, [x8]
// 从`self指针+偏移量`指针里面的数据捞回来
0000000100006938 add sp, sp, #0x10
000000010000693c ret
// 恢复栈到调用前状态, 和返回
; endp
上面代码干的事情, 就是把内容从对象的ivar偏移量所在的位置取出来.
在来看看另一个属性, 还是先来setter-[AClass setAInt:]
:
-[AClass setAInt:]:
00000001000069b0 sub sp, sp, #0x20 ; Objective C Implementation defined at 0x100008d28 (instance)
// 移动一下栈用来存方法里要用到的数据
00000001000069b4 str x0, [sp, #0x18]
00000001000069b8 str x1, [sp, #0x10]
00000001000069bc str x2, [sp, #0x8]
// 把参数self/selector/传进来的NSInteger, 存到栈里
00000001000069c0 ldr x0, [sp, #0x18]
// 从栈里把self捞出来
00000001000069c4 adrp x1, #0x100008000 ; imp___got____gxx_personality_v0
00000001000069c8 ldrsw x1, [x1, #0xe54] ; _OBJC_IVAR_$_AClass._aInt
// 获取AClass._aInt的偏移量
00000001000069cc str x2, [x0, x1]
// 把整数塞到`self+偏移量`指针指向的内容
00000001000069d0 add sp, sp, #0x20
00000001000069d4 ret
// 恢复栈到调用前状态, 和返回
; endp
由于NSInteger本身不是对象没有引用计数等操作, 这里的代码比较简单, getter与上面的getter也类似就不做额外解析了.
strong和weak和assign的区别在于objc_storeStrong和objc_storeWeak和直接赋值.
0x4 参考
- ARM64 Function Calling Conventions :https://developer.apple.com/library/ios/documentation/Xcode/Conceptual/iPhoneOSABIReference/Articles/ARM64FunctionCallingConventions.html#//apple_ref/doc/uid/TP40013702-SW1
- objc_storeStrong: http://opensource.apple.com/source/objc4/objc4-647/runtime/NSObject.mm
- objc-msg-arm64.s: http://opensource.apple.com/source/objc4/objc4-647/runtime/Messengers.subproj/objc-msg-arm64.s