学习目标

  • 深入理解ARC(Automatic Reference Counting)的工作原理。
  • 掌握 __weak__unsafe_unretained 的区别与使用场景。
  • 理解 __block__strong 在Block中的作用。
  • 了解非ARC环境下的手动内存管理(MRC)概念。
  • 掌握如何分析和解决内存泄漏问题。

学习内容

在Objective-C中,内存管理是开发高质量、稳定应用的关键。虽然ARC极大地简化了内存管理,但理解其底层原理和高级概念仍然至关重要,尤其是在处理循环引用、多线程以及与C/C++代码交互时。

1. ARC(Automatic Reference Counting)深入

ARC是LLVM编译器和Objective-C运行时共同协作的结果。它在编译时自动插入 retainreleaseautorelease 调用,以确保对象在不再被需要时被释放。开发者无需手动管理引用计数,但仍需理解引用类型。

1.1 引用类型修饰符
  • __strong (默认):强引用,持有对象,增加引用计数。当对象没有强引用时,会被释放。
  • __weak:弱引用,不持有对象,不增加引用计数。当对象被释放时,弱引用会自动置为 nil,避免野指针。
  • __unsafe_unretained:不安全弱引用,不持有对象,不增加引用计数。当对象被释放时,它不会自动置为 nil,可能导致野指针(Dangling Pointer)。通常在以下情况使用:
    • 性能敏感的场景,因为 __weak 需要运行时维护一张弱引用表。
    • 兼容旧版操作系统(iOS 4及以下),因为 __weak 是iOS 5引入的。
    • 引用对象生命周期明确短于当前对象,且不会出现循环引用的情况。
  • __autoreleasing:用于修饰通过引用传递的参数,表示该参数在方法返回时会被自动释放池管理。

示例:__weak vs __unsafe_unretained

@interface MyObject : NSObject
@property (nonatomic, strong) NSString *name;
@end

@implementation MyObject
- (void)dealloc {
    NSLog(@"MyObject %@ deallocated", self.name);
}
@end

void testWeakAndUnsafeUnretained() {
    MyObject *obj = [[MyObject alloc] init];
    obj.name = @"TestObject";

    __weak MyObject *weakObj = obj;
    __unsafe_unretained MyObject *unsafeObj = obj;

    NSLog(@"Before obj = nil:");
    NSLog(@"weakObj: %@", weakObj); // TestObject
    NSLog(@"unsafeObj: %@", unsafeObj); // TestObject

    obj = nil; // TestObject deallocated

    NSLog(@"After obj = nil:");
    NSLog(@"weakObj: %@", weakObj); // (null) - 自动置为nil
    // NSLog(@"unsafeObj: %@", unsafeObj); // EXC_BAD_ACCESS - 野指针,访问已释放内存
    // 实际开发中,不应在对象释放后访问__unsafe_unretained修饰的变量
}

// 调用
// testWeakAndUnsafeUnretained();

2. Block中的内存管理

Block在捕获外部变量时,会根据变量的类型和修饰符进行不同的内存管理。

2.1 捕获变量的默认行为
  • 局部变量(基本数据类型):Block会捕获其值,并在Block内部创建一个副本。即使外部变量改变,Block内部的值也不会改变。
  • 局部变量(对象类型):Block会对其进行强引用(__strong),增加引用计数。这可能导致循环引用。
  • 全局变量/静态变量:Block直接引用,不进行捕获。
2.2 解决Block循环引用:__weak__strong

当Block内部引用了Block外部的对象,而该对象又强引用了Block时,就会形成循环引用。通常使用 __weak__unsafe_unretained 来打破循环。

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) void (^greetBlock)(void);
@end

@implementation Person
- (instancetype)init {
    self = [super init];
    if (self) {
        // 循环引用示例:self 强引用 greetBlock,greetBlock 强引用 self
        // self.greetBlock = ^{ NSLog(@"Hello, I'm %@", self.name); };

        // 解决循环引用方法一:使用 __weak
        __weak typeof(self) weakSelf = self;
        self.greetBlock = ^{ NSLog(@"Hello, I'm %@", weakSelf.name); };

        // 解决循环引用方法二:__weak 和 __strong 配合使用,防止 weakSelf 在 Block 执行过程中被释放
        // __weak typeof(self) weakSelf = self;
        // self.greetBlock = ^{
        //     __strong typeof(weakSelf) strongSelf = weakSelf;
        //     if (strongSelf) {
        //         NSLog(@"Hello, I'm %@", strongSelf.name);
        //     }
        // };
    }
    return self;
}

- (void)dealloc {
    NSLog(@"Person %@ deallocated", self.name);
}
@end

void testBlockRetainCycle() {
    Person *p = [[Person alloc] init];
    p.name = @"Alice";
    p.greetBlock();
    // p = nil; // 如果有循环引用,这里不会调用 dealloc
}

// 调用
// testBlockRetainCycle();
2.3 __block 修饰符

__block 用于修饰局部变量,使其在Block内部可以被修改。对于对象类型,__block 变量在Block内部不会被强引用,而是以指针的形式捕获。如果Block被拷贝到堆上,__block 变量也会被拷贝到堆上。

void testBlockVariableModification() {
    __block int counter = 0;
    void (^incrementBlock)(void) = ^{
        counter++; // 可以修改
        NSLog(@"Counter: %d", counter);
    };

    incrementBlock(); // Counter: 1
    incrementBlock(); // Counter: 2

    __block MyObject *obj = [[MyObject alloc] init];
    obj.name = @"Original";

    void (^changeObjectBlock)(void) = ^{
        obj = [[MyObject alloc] init]; // 可以修改obj指向的对象
        obj.name = @"Changed";
        NSLog(@"Object in block: %@", obj.name);
    };

    changeObjectBlock(); // Original deallocated, Object in block: Changed
    NSLog(@"Object outside block: %@", obj.name); // Changed
}

// 调用
// testBlockVariableModification();

3. 手动内存管理(MRC)概念回顾

在ARC出现之前,Objective-C使用手动引用计数(Manual Reference Counting, MRC)。理解MRC有助于理解ARC的底层机制。

  • 核心原则:谁 allocnewcopymutableCopy,谁 release
  • retain:增加对象的引用计数。
  • release:减少对象的引用计数。当引用计数为0时,对象被释放。
  • autorelease:将对象放入当前线程的自动释放池中,在自动释放池销毁时统一 release
  • dealloc:对象被释放前调用的方法,用于清理资源。在MRC中,需要在这里调用 [super dealloc]

示例(MRC环境,仅作概念理解):

// 假设在MRC环境下
// MyObject *obj = [[MyObject alloc] init]; // 引用计数为1
// [obj retain]; // 引用计数为2
// [obj release]; // 引用计数为1
// [obj release]; // 引用计数为0,对象被释放

// NSString *str = [[[NSString alloc] initWithFormat:@"Hello"] autorelease];
// str 会在当前自动释放池结束时被释放

4. 内存泄漏分析与解决

内存泄漏是指程序中已分配的内存,在不再需要时未能被释放,导致内存占用持续增长,最终可能耗尽系统资源。

4.1 常见内存泄漏场景
  • 循环引用:两个或多个对象相互强引用,导致它们的引用计数永远不会降为0。
  • Block循环引用:如上所述,Block和其捕获的对象相互强引用。
  • NSTimer未失效:如果 NSTimer 被一个对象强引用,而 NSTimer 又强引用了该对象(通常通过 target),则可能导致循环引用。需要手动调用 invalidate
  • Delegate未设置为 weak:如果Delegate属性被声明为 strong,而Delegate对象又强引用了其代理,则会形成循环引用。
  • C/C++内存泄漏:Objective-C对象与C/C++对象混合使用时,C/C++部分内存未正确释放。
  • 大量临时对象:在循环中创建大量临时对象,如果不在 autoreleasepool 中及时释放,可能导致内存峰值过高。
4.2 内存泄漏检测工具
  • Xcode Instruments
    • Leaks:专门用于检测内存泄漏,可以显示泄漏对象的类型、大小和调用栈。
    • Allocations:用于分析内存分配情况,可以查看对象的生命周期、内存增长趋势。
  • 静态分析器:Xcode内置的静态分析器可以在编译时发现潜在的内存管理问题。
4.3 解决策略
  • 打破循环引用:使用 __weak__unsafe_unretained 关键字。
  • 及时释放资源:对于 NSTimer、通知观察者等,在对象 dealloc 时及时移除或失效。
  • 正确使用 autoreleasepool:在大量循环创建临时对象时,手动创建 autoreleasepool 来及时释放内存。
// 示例:在循环中创建大量临时对象时使用 @autoreleasepool
- (void)processLargeDataArray:(NSArray *)dataArray {
    for (id data in dataArray) {
        @autoreleasepool {
            // 在这里创建的临时对象会在每次循环结束时被释放
            NSString *tempString = [NSString stringWithFormat:@"Processing: %@", data];
            // ... 对 tempString 进行操作
        }
    }
}

练习

  1. 创建一个 ViewController 和一个 DataManager 类。让 ViewController 强引用 DataManagerDataManager 内部有一个Block,该Block需要引用 ViewController 的某个属性。设计代码以避免循环引用。
  2. 解释 __weak__unsafe_unretained 的主要区别,并说明在什么情况下会优先选择 __unsafe_unretained
  3. 编写一个程序,模拟一个 NSTimer 导致的循环引用,并尝试使用 invalidate 方法解决它。
  4. 使用Xcode Instruments的Leaks工具检测你编写的代码是否存在内存泄漏,并尝试修复。
  5. 解释 __block 关键字在Block中对基本类型和对象类型变量的影响。

总结

今天我们深入探讨了Objective-C的内存管理高级话题,包括ARC的底层机制、__weak__unsafe_unretained 的区别、Block中的内存管理以及如何解决循环引用。我们还回顾了MRC的基本概念,并学习了如何使用Xcode工具分析和解决内存泄漏。掌握这些高级内存管理知识对于编写健壮、高效的Objective-C应用程序至关重要。明天我们将学习多线程与并发编程。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐