内存管理
内存管理
面试题
- 介绍一下内存的几大区域
- 使用 CDDisplayLink、NSTimer 有什么注意点
- 讲一下对 iOS 内存管理的理解
- autorelease 什么时候释放
- 方法里有局部变量,出了方法后会立即释放吗? 表现上是的
- ARC 都帮我们做了什么
- weak 指针的实现原理
CDDisplayLink、NSTimer 使用注意与处理
CDDisplayLink、NSTimer 会对 target 产生强引用,如果target 对它再产生强引用,那就会发生循环引用。
处理这个循环引用问题:
- 针对 NSTimer 可以使用 initWithBlock 的方式,直接在block 内做事情。
- CDDisplayLink 没有block 方法,可以从消息转发的机制入手,设置一个代理 NSProxy 来转发消息,断开强引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
--------- VC --------------
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MyProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest
{
NSLog(@"%s", __func__);
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self.timer invalidate];
}
--------- Proxy ------------
+ (instancetype)proxyWithTarget:(id)target
{
// NSProxy对象不需要调用init,因为它本来就没有init方法
MyProxy *proxy = [MyProxy alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
[invocation invokeWithTarget:self.target];
}
注意 应该直接使用 NSProxy 的子类做代理对象。其本身是基类,内部对于方法的处理,直接走的是消息转发阶段,不会在父类找方法实现和调用。 如果继承 NSObject 子类做代理,会执行方法查找和转发的完整步骤,效率会低
GCD Timer
NSTimer 依赖于 Runloop,如果Runloop 的任务比较繁重,可能会导致 NSTimer 不准时
GCD 的 Timer 会更加准时,它是一种专门监听系统时间定时器
原理: GCD 框架提供了一系列的接口来监听底层系统对象的活动 当监听被触发,会自动将 block 提交到指定的消息队列来处理回调 系统底层对象包括:file descriptors, Mach ports, signals, VFS nodes, etc
基本使用如下: 完整代码地址 GCD_Timer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
+ (NSString *)execTask:(void (^)(void))task start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
{
if (!task || start < 0 || (interval <= 0 && repeats)) return nil;
// 队列
dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();
// 创建定时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 设置时间
dispatch_source_set_timer(timer,
dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
interval * NSEC_PER_SEC, 0);
dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
// 定时器的唯一标识
NSString *name = [NSString stringWithFormat:@"%zd", timers_.count];
// 存放到字典中
timers_[name] = timer;
dispatch_semaphore_signal(semaphore_);
// 设置回调
dispatch_source_set_event_handler(timer, ^{
task();
if (!repeats) { // 不重复的任务
[self cancelTask:name];
}
});
// 启动定时器
dispatch_resume(timer);
return name;
}
iOS 程序的内存布局
程序装载到内存中之后,内存分布如下。
Tagged Pointer
从 64 bit 开始, iOS 引入了 Tagged Pointer 技术,用于优化 NSNumber、NSDate、NSString 等小对象的存储。
简单来说:
- Tagged Pointer 是直接保存值的指针,非对象,无 isa 指针,不指向堆空间地址,无需对其进行内存管理
- 对象值小于8个字节可以表示,则将其指针拆分为两部分,一个部分标记,一部分存值。它是一个含值的指针,并非真正的对象
- 当对象值 8 个字节无法保存,则会创建真正的对象来保存该值
看以下代码,存在什么问题,如何解决。
1
2
3
4
5
6
7
8
9
@property (nonatomic, strong) NSString *string;
dispatch_queue_t queue = dispatch_queue_create("memoryBeingFreedCase", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000000; i++) {
dispatch_async(queue, ^{
self.string = [NSString stringWithFormat:@"The num is %d", i];
// self.string = [NSString stringWithFormat:@"%d", i];
});
}
出现问题: 代码会崩溃。
因为: strong 类型的属性,在set方法内部会 release oldValue, 然后 retain newValue. 因为是多线程并发,可能会对 oldValue 进行多次 release 造成坏内存访问,崩溃。
解决方法:
- 使用线程同步技术,将并发任务改成串行
- 将属性改为 atomic 属性
- 改用 Tagged Point 技术 【上文中的注释即可】
Copy 和 mutableCopy
- copy: 不可变拷贝,产生不可变副本
- mutableCopy: 可变拷贝,产生可变副本
拷贝分为深拷贝(内容拷贝,产生新内容) 和浅拷贝(指针拷贝)。
不可变对象 copy -> 指针拷贝 -> 结果为两个指针指向同一个不可变对象 不可变对象 mutableCopy -> 内容拷贝 -> 结果为两个指针,一个可变对象,一个不可变对象
可变对象 copy -> 深拷贝 -> 结果两个指针,一个可变对象,一个copy出来的不可变对象 可变对象 mutableCopy -> 深拷贝 -> 结果是两个指针,两个可变对象
copy 关键字
copy 关键字在设置属性的时指定了其变量内存管理的方式为 copy。
下面代码会发生什么,为什么?
1
2
3
4
5
6
7
8
9
10
// .h 文件
@property (nonatomic, copy) NSMutableArray *mutableArray;
// .m 文件
NSMutableArray *array = [NSMutableArray arrayWithObjects:@1,@2,nil];
self.mutableArray = array;
[self.mutableArray removeObjectAtIndex:0];
// 会崩溃,-[__NSArrayI removeObjectAtIndex:]: unrecognized selector sent to instance 0x7fcd1bc30460
// 因为 属性指定 copy 修饰符,直接在 setter 赋值的时候将 [mutableArray copy] 成了不可变数组,虽然它的声明依旧是 NSMutableArray
自己的类实现 copy 关键字
若想令自己所写的对象具有拷贝功能,则需实现 NSCopying 协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现 NSCopying 与 NSMutableCopying 协议。
1.需声明该类遵从 NSCopying 协议 2.实现 NSCopying 协议。该协议只有一个方法:- (id)copyWithZone:(NSZone *)zone;
1
2
3
4
5
6
7
- (id)copyWithZone:(NSZone *)zone {
User *copy = [[[self class] allocWithZone:zone]
initWithName:_name
age:_age
sex:_sex];
return copy;
}
如果是对象内部有 array 这样的属性,需要考虑 array 内部的拷贝赋值问题。
MRC 与 ARC
引用计数的存储
在 iOS 中,使用引用计数来管理OC对象内存,一个新创建的 OC 对象引用计数默认为 1,当引用计数减为 0 ,OC 对象就会被销毁,释放其占用的内存空间。
在 MRC 中需要手动管理内存,调用 retain
会让 OC 对象引用计数 +1,调用 release 会让 OC 对象的引用计数 -1
内存管理的经验总结
- 当调用 alloc/new/copy/mutableCopy 方法返回了一个对象,在不需要这个对象的时候,需要调用 release/autorelease 来释放它
- 想拥有某个对象,就让它的引用计数 +1,不再想拥有某个对象就让它的计数 -1
引用计数的保存在 isa 中,在 64 位机器上经过优化,如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1; // 当19位不够保存,则在 sideTable 中保存。
uintptr_t extra_rc : 19; // 高19位为保存引用计数
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
#endif
weak 实现原理
这个可以从 weak 的作用入手。weak 修饰的对象,被销毁后其指针被赋值为 nil。所以可以从 runtime 源码看看 dealloc 方法的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
- (void)dealloc {
_objc_rootDealloc(self);
}
void
_objc_rootDealloc(id obj)
{
obj->rootDealloc();
}
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // Tagged Pointer 直接结束
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{ // 优化指针,弱引用,无关联对象,无c++析构函数,无sidetable保存引用计数
// 直接释放当前对象
free(this);
}
else {
// 进入内部释放
object_dispose((id)this);
}
}
id
object_dispose(id obj)
{
objc_destructInstance(obj);
free(obj);
return nil;
}
// dealloc方法的核心实现,内部会做判断和析构操作
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
// 判断是否有OC或C++的析构函数
bool cxx = obj->hasCxxDtor();
// 对象是否有相关联的引用
bool assoc = obj->hasAssociatedObjects();
// This order is important.
// 对当前对象进行析构
if (cxx) object_cxxDestruct(obj);
// 移除所有对象的关联,例如把weak指针置nil
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
__weak 在 runtime 中的实现
ARC 帮我们做了什么?
ARC是 LLVM + runtime 协作的结果,LLVM 编译器会自动帮我们生成 release/retain 等相关内存管理代码。runtime 在程序运行期间检测对象引用计数,释放弱引用等,帮我们管理内存。
autorelease 原理
autorelease 基于自动释放池 AutoreleasePool
autoreleasePool 的主要底层数据结构是: __AtAutoreleasePool(编译器层)、AutoreleasePoolPage(runtime源码)
调用了 autorelease 的对象最终都是通过 AutoreleasePoolPage 对象来管理的
通过 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
重写@autoreleasepool
可以看到其被重写为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 自动释放池变量C++实现
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; // 声明释放池变量
}
return 0;
}
// 实际自动释放池的大括号执行处被改写为了
__AtAutoreleasePool()
// 用户代码
~__AtAutoreleasePool()
AutoreleasePoolPage 内部结构如下:
1
2
3
4
5
6
7
8
9
10
class AutoreleasePoolPage
{
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
}
AutoreleasePoolPage 实现逻辑
- 释放池内部结构是栈,它依据线程存在
- 池(栈)中指针是自动释放obj指针,或者 POOL_BOUNDARY 指针
- 池释放时,以 POOL_BOUNDARY 为界,先释放内部的 obj
- 所有的pool,被组织成双向链表,根据需要添加/移除 pages 5.线程本地存储永远指向 hotPage: 即新存储自动释放obj 的page
调用 push 方法会将一个 POOL_BOUNDARY 入栈,并返回其存放的内存地址 调用 pop 方法时传入一个 POOL_BOUNDARY 内存地址,会从最后一个入栈的对象开始发送 release 消息,直到遇到这个 POOL_BOUNDARY id *next 指向下一个能存放 Autorelease obj 的地址
具体看一下 runtime/NSObject.mm 源码中。
对象调用 autorelease 知道后的释放时机是什么?
从源码可以看 [obj autorelease] 实际上是将 obj 放入到自动释放池。autorelease 的释放时机就是 AutoreleasePool 的释放时机。
1
2
3
4
5
[obj autorelease];
....
最终调用的是
AutoreleasePoolPage::autorelease(obj);
obj 会被加入到自动释放池
实际上 AutoreleasePool 的释放时机是和 Runloop 相关的。Runloop 内部会注册两个和 AutoreleasePool 相关的 observer,
1
2
3
4
5
// 监听 Runloop 进入,最先执行 autoreleasePush()
"<CFRunLoopObserver 0x600000ca4320 [0x7fff8062d610]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c84b28), context = <CFArray 0x6000033d12c0 [0x7fff8062d610]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fb41780b038>\n)}}",
// 监听 Runloop 休眠和退出,执行 autoreleasePop()
"<CFRunLoopObserver 0x600000ca43c0 [0x7fff8062d610]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c84b28), context = <CFArray 0x6000033d12c0 [0x7fff8062d610]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fb41780b038>\n)}}"
可以发现: 进入 Runloop 的时候最先执行 autoreleasePush() 创建并进入 pool。 Runloop 进入休眠的时候,执行 autoreleasePop() 释放当前 pool 内的对象。 Runloop 退出的时候,执行 autoreleasePop() 释放当前 pool 内的对象。
谈谈对 iOS 内存管理的理解
思路: MRC - 自己写 retain/release 时代 ARC - 编译器自动生成 RR 代码,简化开发 核心概念 AutoreleasePool 的实现原理。
###