描述
最早被人问及 iOS
方法内的 局部变量
是在什么时机释放的时候, 肤浅的认为: 可能编译器给每个方法都加了 自动释放池
?
后来随着成长, 发现事实并没有自己当初想象的那么简单. 虽然网上有很多从源码方面总结 局部变量
内存相关的文章, 但是都是从源码的角度来分析的, 今天正好有空, 就从更深入的地方来探索一番, 整理一下思路.
今天的主角就是下面这段代码:
1
2
3
4
5
- (void)memoryReleaseTest {
NSObject *obj = nil;
obj = [[NSObject alloc] init];
NSObject *p = obj;
}
正文
准备工作
- 在
memoryReleaseTest
方法的第一行打断点 - 在
Xcode
中选择Debug
->Debug Workflow
->Always Show Disassembly
并勾选 - 在
viewDidLoad
中调用memoryReleaseTest
(保证方法被调用即可)
然后在运行之后就能看到如下 arm64
汇编代码 (真机运行).
Demo`-[ViewController memoryReleaseTest]:
0x104d550a8 <+0>: sub sp, sp, #0x30 ; =0x30
0x104d550ac <+4>: stp x29, x30, [sp, #0x20]
0x104d550b0 <+8>: add x29, sp, #0x20 ; =0x20
0x104d550b4 <+12>: adrp x8, 8
0x104d550b8 <+16>: add x8, x8, #0xa00 ; =0xa00
0x104d550bc <+20>: stur x0, [x29, #-0x8]
0x104d550c0 <+24>: str x1, [sp, #0x10]
0x104d550c4 <+28>: mov x0, #0x0
0x104d550c8 <+32>: str x0, [sp, #0x8]
0x104d550cc <+36>: ldr x8, [x8]
0x104d550d0 <+40>: mov x0, x8
0x104d550d4 <+44>: bl 0x104d561d0 ; symbol stub for: objc_alloc
0x104d550d8 <+48>: adrp x8, 8
0x104d550dc <+52>: add x8, x8, #0x9a8 ; =0x9a8
0x104d550e0 <+56>: ldr x1, [x8]
0x104d550e4 <+60>: bl 0x104d56200 ; symbol stub for: objc_msgSend
0x104d550e8 <+64>: ldr x8, [sp, #0x8]
0x104d550ec <+68>: str x0, [sp, #0x8]
0x104d550f0 <+72>: mov x0, x8
0x104d550f4 <+76>: bl 0x104d56224 ; symbol stub for: objc_release
0x104d550f8 <+80>: ldr x8, [sp, #0x8]
0x104d550fc <+84>: mov x0, x8
0x104d55100 <+88>: bl 0x104d56230 ; symbol stub for: objc_retain
0x104d55104 <+92>: str x0, [sp]
0x104d55108 <+96>: mov x8, sp
0x104d5510c <+100>: mov x0, x8
0x104d55110 <+104>: mov x8, #0x0
0x104d55114 <+108>: mov x1, x8
0x104d55118 <+112>: bl 0x104d56248 ; symbol stub for: objc_storeStrong
0x104d5511c <+116>: add x8, sp, #0x8 ; =0x8
0x104d55120 <+120>: mov x0, x8
0x104d55124 <+124>: mov x8, #0x0
0x104d55128 <+128>: mov x1, x8
0x104d5512c <+132>: bl 0x104d56248 ; symbol stub for: objc_storeStrong
0x104d55130 <+136>: ldp x29, x30, [sp, #0x20]
0x104d55134 <+140>: add sp, sp, #0x30 ; =0x30
0x104d55138 <+144>: ret
这段代码如果看不懂 arm64
汇编也没关系, 我们只挑能看懂的看即可, 影响不大.
省略掉栈空间操作相关指令后, 我们需要分析的指令代码如下:
0x000000000 <+44>: bl 0x104d561d0 ; symbol stub for: objc_alloc
0x000000001 <+60>: bl 0x104d56200 ; symbol stub for: objc_msgSend
0x000000002 <+76>: bl 0x104d56224 ; symbol stub for: objc_release
0x000000003 <+88>: bl 0x104d56230 ; symbol stub for: objc_retain
0x000000004 <+112>: bl 0x104d56248 ; symbol stub for: objc_storeStrong
0x000000005 <+132>: bl 0x104d56248 ; symbol stub for: objc_storeStrong
0x000000006 <+144>: ret
省略掉的代码都是栈区
栈空间拉伸
,栈平衡
,寄存器存储
等指令, 忽略也不影响理解. 为了方便理解, 把指令地址也替换为了0x000000000 ~ 0x000000006
这样的序号.
解释
bl
是跳转指令, 可以简单理解为方法调用.ret
指令可以简单的理解为代码中的return
, 函数执行到这个地方就结束了.
分析指令
0x000000000 指令分析
0x000000000 <+44>: bl 0x104d561d0 ; symbol stub for: objc_alloc
这条指令根据符号备注可以知道是调用了 objc_alloc
方法.
在控制台执行如下指令:
提示 因为汇编层面不存在
实参
形参
的概念, 函数传参是直接由x0, x1, x2 ...
来存储传入的第0个, 第1个, 第2个 ...
参数. 当然, 当一个函数有多个参数的时候, 会有其他传参方法, 此处不做解释.上图读取
x0
寄存器目的是为了获取objc_alloc
传入的参数. 由函数名我们就可以知道他是为一个对象分配一段内存空间.
可以得知 x0
寄存器内存储的地址对应的内容为 NSObject
. 也就是对应了如下代码:
1
[NSObject alloc]
0x000000001 指令分析
0x000000001 <+60>: bl 0x104d56200 ; symbol stub for: objc_msgSend
这条指令, 了解过 runtime
的应该对 objc_msgSend
都不陌生.
在控制台执行如下指令
由 x0
, x1
寄存器内容, 可以了解到 objc_msgSend
传入的内容为: NSObject
实例对象的地址 和 init
字符串 (SEL
类型的本质就是 C 字符串
).
x1
寄存器存储的内存地址为 0x00000001c91b2a3b
, 在该地址之后紧接着的 4 个字节里面存储的内容为: 69 6e 69 74
实际上是 i n i t
4个字符的 ASCII
码的 16
进制.
到此为止 0x000000000
和 0x000000001
2 条指令就是我们的代码:
1
[[NSObject alloc] init]
提示
objc_msgSend
方法类型是这样的:objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
po
为打印对象,x
读取内存, 均为lldb
指令, 可以另作了解
0x000000002 指令分析
0x000000002 <+76>: bl 0x104d56224 ; symbol stub for: objc_release
这条指令是调用了 objc_release
这个方法. 我们差看一下这条指令释放的是哪个对象:
在这个地方给 objc_release
函数传入的地址为 0x0000000000000000
, 也就是传入的是 nil
.
这个地方之所以有这么一条指令其实是因为我们的代码:
1
NSObject *obj = nil;
obj
指针初始化时候赋值为 nil
, 因为在把其他对象的指针赋值给 obj
的时候, 就要对应的对原来 obj
所指向的内存地址的对象执行一次 release
操作.
0x000000003 指令分析
0x000000003 <+88>: bl 0x104d56230 ; symbol stub for: objc_retain
这个地方调用了 objc_retain
函数对某个对象执行了 retain
操作, 我猜应该是我们上面初始化的 NSObject
对象.
打印一下 x0
寄存器的内容:
如猜测得一样, 这个对 NSObject
对象执行了 release
操作. 对应了我们的赋值操作:
1
NSObject *p = obj;
也正好印证了: 在 ARC
下, 指针默认是 __storng
类型, 赋值时候, 对应的对象会被 retain
一次.
0x000000004 & 0x000000005 指令分析
这 2 条指令一模一样, 就放在一起分析了
0x000000004 <+112>: bl 0x104d56248 ; symbol stub for: objc_storeStrong
0x000000005 <+132>: bl 0x104d56248 ; symbol stub for: objc_storeStrong
在这个方法将要结束的地方调用了 2 次 objc_storeStrong
函数.
objc_storeStrong
函数的实现如下:
1
2
3
4
5
6
7
8
9
void objc_storeStrong(id *location, id obj) {
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}
函数有点像我们在 MRC
下的 Setter
方法.
查看一下这 2 次调用传入的参数是什么内容:
这个地方略为特殊, objc_storeStrong
的第一个参数 location
为 id *
类型, 也就是 二级指针
.
因此 x0
寄存器读取到的内存地址 0x000000016f08dc30
里面放的内容才是传入参数的本体.
本次测试的机器为 XR
为 64
位架构的 CPU, 因此指针所占用的内存空间为 8
个字节, 如图种红框所示. 所以我们需要的对象的真实地址为 0x0280d08000
(省略掉了头部的 0
).
因此得知传入的第一个参数的本体实际上就是 NSObject
对象.
x1
寄存器内容为 0x0000000000000000
, 也就是第二个参数是 nil
.
因此在 objc_storeStrong
内部 NSObject
对象会被 release
一次.
总结 此处调用了 2 次
objc_storeStrong
函数,NSObject
就被release
2次, 对应了alloc
和p
指针赋值之后的retain
操作. 随着obj
和p
指针被置为nil
,NSObject
对象的生命周期也到此结束了.
0x000000006 指令分析
0x000000006 <+144>: ret
函数返回指令, 指令跳转到 PC
寄存器所指向的指令地址. 至此 memoryReleaseTest
方法执行结束. 进入到 viewDidLoad
方法内的下一句代码的执行流程.
总结
在 ARC
环境下, 方法在将要执行结束的时候, 局部变量的指针都会被置为 nil
, 此时在 objc_storeStrong
函数内部, 被局部变量强引用的对象会被执行 release
操作. 最终对象是否会被释放还是要取决于是否依旧有其他指针强引用 (比如: 全局变量/属性 等).
在 MRC
环境下, 汇编代码就干净的多, obj
和 p
引用 NSObject
对象时候不会执行 retain
操作, 在方法执行结束的时候也不会因为被置为 nil
而对 NSObject
执行 release
操作 (况且也不会被自动置为 nil
). 因此在 MRC
下, 如果开发者不主动对对象执行 release/autorelease
操作, 对象就会一直存在于内存中. 并随着方法的结束, 局部变量的指针被释放, 对象就再也无法得到释放, 发生内存泄露.
附加内容
顺便附上 MRC
的汇编:
Demo `-[ViewController memoryReleaseTest]:
0x1007f5268 <+0>: sub sp, sp, #0x30 ; =0x30
0x1007f526c <+4>: stp x29, x30, [sp, #0x20]
0x1007f5270 <+8>: add x29, sp, #0x20 ; =0x20
0x1007f5274 <+12>: adrp x8, 8
0x1007f5278 <+16>: add x8, x8, #0x9f8 ; =0x9f8
0x1007f527c <+20>: stur x0, [x29, #-0x8]
0x1007f5280 <+24>: str x1, [sp, #0x10]
0x1007f5284 <+28>: mov x0, #0x0
0x1007f5288 <+32>: str x0, [sp, #0x8]
0x1007f528c <+36>: ldr x8, [x8]
0x1007f5290 <+40>: mov x0, x8
0x1007f5294 <+44>: bl 0x1007f61ac ; symbol stub for: objc_alloc_init
0x1007f5298 <+48>: str x0, [sp, #0x8]
0x1007f529c <+52>: ldr x8, [sp, #0x8]
0x1007f52a0 <+56>: str x8, [sp]
0x1007f52a4 <+60>: ldp x29, x30, [sp, #0x20]
0x1007f52a8 <+64>: add sp, sp, #0x30 ; =0x30
0x1007f52ac <+68>: ret