iOS 内存管理

334 阅读8分钟

ARC

init 创建

  1. 无强指针指向
- (void)test {
    [[MyObject alloc] init];
}
movq MyObject
callq objc_alloc_init
callq objc_release
  1. 有强指针指向
- (void)test {
    MyObject *obj = [[MyObject alloc] init];
}
movq MyObject
callq objc_alloc_init
callq objc_storeStrong

类方法创建

  1. 类创建方法无强指针指向
- (void)test {
    [MyObject obj];
}
movq MyObject
callq objc_msgSend
callq objc_unsafeClaimAutoreleasedReturnValue
  1. 类创建方法有强指针指向
- (void)test {
    MyObject *obj = [MyObject obj];
}
movq MyObject
callq objc_msgSend
callq objc_retainAutoreleasedReturnValue
callq objc_storeStrong

调用方法

  1. 有无强指针相同,仅最后一句是调用 release 还是 stroeStrong
- (void)test {
    [[[MyObject alloc] init] description];
}
movq MyObject
callq objc_alloc_init
callq objc_msgSend
callq objc_unsafeClaimAutoreleasedReturnValue
callq objc_release

方法附带返回值

  1. 无强指针指向
- (MyObject *)test {
    return [[MyObject alloc] init];
}
movq MyObject
callq objc_alloc_init
jmp objc_autoreleaseReturnValue
  1. 有强指针指向
- (MyObject *)test {
    MyObject *obj = [[MyObject alloc] init];
    return obj;
}
movq MyObject
callq objc_alloc_init
callq objc_retain
callq objc_storeStrong
jmp objc_autoreleaseReturnValue

方法附带参数

- (void)test:(NSObject *)obj {
}
callq objc_storeStrong
callq objc_storeStrong

弱引用

  1. 仅声明
- (void)test {
    __weak typeof(self) weakSelf = self;
}
callq objc_initWeak
callq obj_destroyWeak
  1. 调用 weakSelf
- (void)test {
    __weak typeof(self) weakSelf = self;
    [weakSelf description];
}
callq objc_initWeak
// 如果多次调用 weakSelf,下面会重复多次
===========================
callq objc_loadWeakRetained
callq objc_msgSend
callq objc_unsafeClaimAutoreleasedReturnValue
callq objc_release
===========================
callq objc_destroyWeak
callq objc_destroyWeak

block

  1. 声明一个强引用的 block
- (void)test {
    void (^block)(void) = ^{
    };
}
callq objc_retainBlock
callq objc_storeStrong
  1. 声明一个弱引用的 block 同声明一个普通的弱引用的对象

  2. 捕获 self

- (void)test {
    void (^block)(void) = ^{
        [self description];
    };
    block();
}
callq objc_msgSend
callq objc_unsafeClaimAutoreleasedReturnValue

block 作为属性

  1. 相互持有导致相互引用
- (void)test {
    // 这里不能调用 self.block,不然汇编会有很污染
    _block = ^{
        [self description];
    };
    _block();
}
leaq __block_descriptor_40_e8_32s_e5_v8
leaq _block_invoke
movq _NSConcreteStackBlock
callq objc_retain
callq objc_retainBlock
callq objc_release
callq objc_storeStrong
  1. __weak 解决循环引用
- (void)test {
    __weak typeof(self) _self = self;
    _block = ^{
        [_self description];
    };
    _block();
}
// 多次调用 _self 会导致下面四个方法执行多次
==============================
callq objc_loadWeakRetained
callq objc_msgSend 'description'
callq objc_unsafeClaimAutoreleasedReturnValue
callq objc_release
==============================
  1. __strong 解决 block 执行过程中提前释放(不能保证捕获的 weakself 有值,下面有例子)
- (void)test {
    __weak typeof(self) _self = self;
    _block = ^{
        __strong typeof(self) self = _self;
        [self description];
    };
    _block();
}
callq objc_loadWeakRetained
// 多次调用 self 的话下面这两个方法会执行多次
==============================
callq objc_msgSend 'description'
callq objc_unsafeClaimAutoreleasedReturnValue
==============================
callq objc_storeStrong

这里也有一个点就是 block 内用 strong 指针指向 weak 后,objc_loadWeakRetained/objc_release 方法就不会多次调用了,节约了性能。

不会导致循环引用的 block

只要不相互持有的都不会导致循环引用。(或者在某一时机被主动破开)

  • self 不持有 blockblock 持有 self,如 block 中的例子3
  • UIView.animated
  • UIAlertAction
  • GCD
  • 其他等等

strong 捕获的 weak 被提前释放

- (void)test {
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"test--: %@", self);
    });
    __weak typeof(self) _self = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        __strong id self = _self;
        NSLog(@"test dance: %@", self);
    });
}
// 打印结果
2022-01-10 17:12:37.256066+0800 iOSMemoryManagement[42640:1493873] test--: <TestBlock: 0x600002918300>
2022-01-10 17:12:37.256253+0800 iOSMemoryManagement[42640:1493873] TestBlock dealloc <_NSMainThread: 0x600003e08140>{number = 1, name = main}
2022-01-10 17:12:38.344510+0800 iOSMemoryManagement[42640:1493873] test dance: (null)

这里很明显能看到如果直接捕获 self,是可以打印出来的,而用 strong weak dance 捕获的值被释放了。

原因:(下面的解释还要验证,仅为猜测)

  1. 第一个打印对象的原因,由于 block 持有 self 会导致 self 引用计数 +1,block 执行完成后,会调用 block_release,然后 self 引用计数 -1,此时引时计数归 0,self 即被回收。
  2. 第二个打印为空的原因,由于 strong 捕获的是 weakself,并不会导致 self 引用计数增加,故当 self 被释放时,weakself 还是会被置 nil,所以 strong 的指向也是 nil

strong 防止 block 执行过程中被释放

B 页面立刻执行两个延时函数

- (void)test {
    __weak typeof(self) _self = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        __strong typeof(self) self = _self; // 1
        if (!self) return;
        NSLog(@"1 %@", _self);
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"2 %@", self);
        });
    });
}

如果注释 1 关闭,那么打印 1 后,2 有可能无法打印。如果注释 1 打开,如果可以打印 1,那么 2 是无法被打印的。

weak 可以防止由 block 捕获 self 可能延长的生命周期

在网络请求中尽管不会导致循环引用,但是弱引用也是有必要的,以防网络请求较慢时,发生 self 被延迟释放的问题。

循环引用

定时器(Timer)

  • iOS 10以前,OC 可以使用 NSObject/NSProxyswift 中仅可以使用 NSObject。(由于太简单,不写了)。
  • iOS 10之后,可以使用 block 的弱引用方式。
    • 如果不强持有 timer,那么就算捕获 self,仍然不会发生循环引用。(加进 RunLoop 中也算持有)
    • 如果强持有 timer,而不捕获 self 也不会导致循环引用。
    • 虽然 self 不被强持有,可以被正常释放,但是仍然不能忘记,要在 self deinit 的时候,把 timer invalidate并置 nil

定时器使用

image.png

定时器的闭包实现(YYKit)

+ (void)_yy_ExecBlock:(NSTimer *)timer {
    if ([timer userInfo]) {
        void (^block)(NSTimer *timer) = (void (^)(NSTimer *timer))[timer userInfo];
        block(timer);
    }
}

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds block:(void (^)(NSTimer *timer))block repeats:(BOOL)repeats {
    return [NSTimer scheduledTimerWithTimeInterval:seconds target:self selector:@selector(_yy_ExecBlock:) userInfo:[block copy] repeats:repeats];
}

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)seconds block:(void (^)(NSTimer *timer))block repeats:(BOOL)repeats {
    return [NSTimer timerWithTimeInterval:seconds target:self selector:@selector(_yy_ExecBlock:) userInfo:[block copy] repeats:repeats];
}

notification

  • selector 方式的通知现在不需要主动移除,既不会导致循环引用,也不会导致 selector 执行
  • usingBlock 这种方式使用 [weak self] 是必须的,否则会出现内存泄露。而且还需要主动调用 removeObserve:token,不然会导致 block 仍然会执行。(我觉得这个是对之前改变的一些补充,在以前在大部分情况下,我们都需要主动去调 removeObserve:self 来破除循环引用,现在第一种方式不能使用了,但是如果真的不想释放,还有这种方式可以使用)
let center = NSNotificationCenter.defaultCenter()
let mainQueue = NSOperationQueue.mainQueue()
self.localeChangeObserver = center.addObserverForName(NSCurrentLocaleDidChangeNotification, object: nil, queue: mainQueue) { (note) in
    print("The user's locale changed to: (NSLocale.currentLocale().localeIdentifier)")
}

Tip

To avoid a retain cycle, use a weak reference to self inside the block when selfcontains the observer as a strong reference.

Unregister an observer to stop receiving notifications. To unregister an observer, use removeObserver(_:) or removeObserver(_:name:object:) with the most specific detail possible. For example, if you used a name and object to register the observer, use the name and object to remove it.

You must invoke removeObserver(_:) or removeObserver(_:name:object:) before the system deallocates any object that addObserver(forName:object:queue:using:)specifies.

官方文档说的极其清楚,如果不想循环引用的话就用 [weak self] 吧,而且在你释放的时候必须执行 removeObserver 方法。

网络请求

  • 这种情况下,如果不弱引用,必定造成内存泄露
URLSession(configuration: URLSessionConfiguration.default)
.dataTask(with: URL(string: "https://www.baidu.com")!) { data, respose, error in
    print(self, error)
}
  • 这种情况下,block 执行完成后,self 就会被 release(引用计数-1,并不一定会被释放,如果这个控制器本身没有 pop 的情况下,但也有一个问题,就是这段代码在控制器 pop 的后还是会触发)
URLSession(configuration: URLSessionConfiguration.default)
.dataTask(with: URL(string: "https://www.baidu.com")!) { data, respose, error in
     print(self, error)
}.resume()
  • 使用 Alamofire 还是会有方法2 的问题 所以我们在执行网络请求的时候 weak self 是必须的,而大部分情况下,strong self 也是不能加的,因为在 block 执行过程中 self 被释放了,那么后面的代码不再执行恰恰是我们期望的。(那我们还需要判断 weakself 还存在吗? 因为好像每一句代码执行前他都有可能被释放掉了,我感觉在这种情况下就不要判断了)。

block

  • self 作为参数传入时不会造成循环引用
- (void)test {
    TestBlock *sblock = [[TestBlock alloc] init];
    sblock.block = ^(TestBlock * **_Nonnull** obj) {
        // 如果这里捕获 sblock 的话,那么会出现循环引用的问题
        NSLog(@"%@", obj);
    };
    sblock.block(sblock);
}
  • __weak 破除循环引用
  • 不持有 block 破除循环引用
  • 执行完成后将 block 置为 nil 破除循环引用。
- (void)test {
    TestBlock *sblock = [[TestBlock alloc] init];
    sblock.block = ^ {
        NSLog(@"%@", sblock);
    };
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        sblock.block = **nil**;
    });
    sblock.block();
}

封装传参解除循环循环引用

class Delegate {
    var temp: (() -> Void)?
    
    func delegate<T: AnyObject>(_ target: T, block: @escaping (T) -> Void) {
        self.temp = { [weak target] in
            guard let target = target else { return }
            block(target)
        }
    }
    
    func call() {
        temp?()
    }
    
    func callAsFunction() {
        temp?()
    }
}

这个在 Kingfisher 三方库被使用到了,

内存优化

  1. 使用 autoreleasepool 降低内存峰值(解决内存抖动)

内存泄露检测

Profile->Leaks

Zombie Objects

  • 原理:

相关 api 解析

autorelease

数据结构

以栈为节点的双向链表。

weak

数据结果

NSHashMap

MRC/CoreFoundation

Bridge

  • __bridge 直接转换成 Core 相关 API,内存不管
  • __bridge_retain 转换成 Core 相关 API,引用计数 +1
  • __bridge_transfer 转换成 Core 相关 API,引用计数 -1,CFAutoRelease,会加进自动释放池
+ (NSString *)stringWithUUID {
    CFUUIDRef uuid = CFUUIDCreate(NULL);
    CFStringRef string = CFUUIDCreateString(NULL, uuid);
    CFRelease(uuid);
    return (__bridge_transfer NSString *)string;
    // 这种写法也可以,上面的 YYKit 的写法
    return CFAutoRelease(string);
}
  • CF_RETURNS_RETAINED,这个时候外部调用的时候需要主动调用 CFRelease,当方法名中没有 copycreate,那么需要标记一下。
  • CF_RETURNS_RETAINED,同理,如果方法中有 copycreate,但不需要主动调 CFRelease 也要标记一下。其实就是为了在我们静态检查的时候,编译器不要太笨蛋的误报了。
- (CTFontRef)CTFontRef CF_RETURNS_RETAINED {
    CTFontRef font = CTFontCreateWithName((__bridge CFStringRef)self.fontName, self.pointSize, NULL);
    return font;
}

Swift 和 CoreFoundation 框架交互

上述的代码直接被 struct UUID取代了。

UUID().uuidString

从这里还想吐槽一句,就是很多 oc 难用且较常用的 apiswift 也是做了封装的。

Unmanaged

  • takeRetainedValue() -> Instance
  • takeUnretainedValue() -> Instance
  • passRetained(_ value: Instance) -> Unmanaged
  • passUnretained(_ value: Instance) -> Unmanaged
  • ...

总结

  1. block 作用属性的时候要万分注意是否会产生循环引用(无论是自定义组件还是单例中)。
  2. GCD网络请求 中的 block,虽然不会造成循环引用,我们仍然需要根据实际情况判断是否需要使用 weak self(绝大部分情况需要),因为这里会发生线程或者单例持有 block 导致 block 无法及时释放,从而导致 self 无法被释放,他们的 self 被释放发生在 block 执行完成之后。(但是大部分情况,例如页面退出后,实际上是无需再执行后面的代码,这时仍然需要使用弱引用的)
  3. 使用 strong self 可以保证 block 在执行过程中 self 不被释放。(不保证捕获的 weak self 不为 nil)(所以在这里提前判断一下 strong selfnil 就十分有必要的,判断成功了,那么 block 后面所有的 self 不用考虑了,全部不为空),外部只要使用了 weak self,如果不想后面的代码执行,一定要判断一下是否为 nil
  4. 本质就是引用计数,alloc 系列方法、retainrelease 配合计数,计数归零后 dealloc

参考资料