40、实例变量的内存管理(ARC)与常见问题处理

实例变量的内存管理(ARC)与常见问题处理

在编程中,内存管理是一个至关重要的环节。自动引用计数(ARC)的出现,极大地简化了内存管理的工作。下面将详细介绍ARC下实例变量的内存管理、保留循环和弱引用,以及一些不常见的内存管理情况。

ARC下实例变量的内存管理

当使用ARC时,它会自动管理实例变量的内存,开发者通常不需要(并且在大多数情况下也不能)手动进行内存管理。ARC会按照一定的规则处理实例变量的赋值操作。

直接赋值

默认情况下,ARC处理实例变量赋值的方式与处理其他变量类似。当给实例变量赋值时,它会创建一个临时变量,保留赋值的值,释放实例变量的当前值,然后进行赋值。例如:

self->_theData = d;

ARC实际上会按照保留新值、释放旧值的规则,执行类似以下的操作:

// 假想场景:赋值时保留,释放前一个值
id temp = self->_theData;
self->_theData = d;
[self->_theData retain];
[temp release];

这与手动编写的正式设置方法的逻辑基本相同。

正式设置方法

在ARC下,简单的设置方法可能只包含直接赋值,因为ARC会正确处理所有相关的手动内存管理。例如:

- (void) setTheData: (NSMutableArray*) value {
    self->_theData = value;
}

当对象销毁时,ARC会释放其保留的实例变量的值,因此开发者无需在 dealloc 方法中手动释放实例变量,也不需要调用 super 。不过,在ARC下,开发者仍可能需要实现 dealloc 方法来处理其他事务,如取消通知注册。

如果想手动释放实例变量的值,可以将实例变量设置为 nil (可能通过设置方法)。当将变量置为 nil 时,ARC会默认释放其现有值。

初始化方法

在ARC下,涉及设置对象实例变量值的初始化方法的代码与非ARC下基本相同,只是不需要(也不能)使用 retain 。例如:

// 示例12 - 7. 在ARC下保留实例变量的简单初始化方法
- (id) initWithName: (NSString*) s {
    self = [super init];
    if (self) {
        self->_name = s;
    }
    return self;
}

使用 copy 方法的初始化方法在ARC下也保持不变,ARC能够理解如何管理以 copy 开头的方法返回的对象的内存。例如:

// 示例12 - 8. 在ARC下复制实例变量的简单初始化方法
- (id) initWithName: (NSString*) s {
    self = [super init];
    if (self) {
        self->_name = [s copy];
    }
    return self;
}
保留循环和弱引用

ARC的行为是自动且机械的,它不了解应用中对象之间的逻辑关系。有时,开发者需要给ARC提供额外的指令,以防止它做出有害的操作,其中之一就是保留循环。

保留循环的概念

保留循环是指对象A和对象B相互保留的情况。如果这种情况持续存在,会导致两个对象都泄漏,因为它们的引用计数都无法减为零。例如,在订单和商品系统中,订单需要知道其包含的商品,商品可能需要知道所属的订单,若订单保留其商品,商品也保留其订单,就会形成保留循环。

以下是一个简单的示例,展示了保留循环的问题:

@implementation MyClass {
    id _thing;
}
- (void) setThing: (id) what {
    self->_thing = what;
}
-(void)dealloc {
    NSLog(@"%@", @"dealloc");
}
@end

MyClass* m1 = [MyClass new];
MyClass* m2 = [MyClass new];
m1.thing = m2;
m2.thing = m1;

在这个示例中, m1 m2 相互保留,即使自动指针变量 m1 m2 超出作用域并被销毁, dealloc 方法也不会被调用,两个 MyClass 对象会泄漏。

弱引用的使用

为了防止实例变量保留赋值给它的对象,可以将实例变量声明为弱引用。可以在实例变量的声明中使用 __weak 限定符:

@implementation MyClass {
    __weak id _thing;
}

这样就不会形成保留循环。在上述示例中,当代码执行完毕后,两个 MyClass 对象会正常销毁,因为ARC会在自动变量 m1 m2 超出作用域时发送释放消息。

在ARC中,未显式声明为弱引用的引用是强引用。实际上有 __strong 限定符,但通常不需要使用,因为它是默认的。此外,还有 __unsafe_unretained __autoreleasing 这两个很少使用的限定符。

弱引用的实际应用

在实际开发中,弱引用最常用于连接对象与其委托。委托是一个独立的实体,对象通常不需要拥有其委托的所有权。因此,大多数委托应该声明为弱引用。例如,在Xcode的Utility Application项目模板创建的项目中,会看到以下代码:

@property (weak, nonatomic) id <FlipsideViewControllerDelegate> delegate;

这里的 weak 关键字相当于将 _delegate 实例变量声明为 __weak

ARC弱引用与非ARC弱引用的区别

在非ARC代码中,通过在赋值时不保留引用,可以防止保留循环,但这种引用只是简单地不进行内存管理,被称为非ARC弱引用。非ARC弱引用存在变成悬空指针的风险,当它指向的实例被释放并销毁时,可能会导致程序崩溃。而ARC弱引用在实例的引用计数达到零并即将消失时,会自动将其设置为 nil ,避免了悬空指针的问题。

然而,Cocoa的大部分代码不使用ARC,其内置类中保持弱引用的属性是非ARC弱引用,使用 assign 关键字声明。例如, UINavigationController delegate 属性声明如下:

@property(nonatomic, assign) id<UINavigationControllerDelegate> delegate

即使开发者的代码使用ARC,由于Cocoa代码不使用ARC,仍可能出现内存管理错误。如果引用的对象已经销毁,向悬空指针发送消息会导致应用崩溃。为了避免这种情况,当对象即将销毁时,开发者有责任将引用设置为 nil 或其他对象。

不常见的内存管理情况

除了上述常见情况,还有一些不常见的内存管理情况需要注意。

NSNotificationCenter的内存管理

NSNotificationCenter 在内存管理方面有一些特殊之处。

  • 使用 addObserver:selector:name:object: 注册 :当使用该方法注册时,传递给通知中心的对象引用是一个非ARC弱引用。为了避免对象销毁后通知中心向悬空指针发送通知,必须在对象销毁前取消注册。
  • 使用 addObserverForName:object:queue:usingBlock: 注册 :这种情况下的内存管理较为复杂,尤其是在ARC下。具体问题如下:
    • 调用 addObserverForName:object:queue:usingBlock: 返回的观察者令牌会被通知中心保留,直到取消注册。
    • 观察者令牌可能通过块保留 self 。在取消注册之前,通知中心会保留 self ,导致内存泄漏。并且由于注册状态下 dealloc 方法不会被调用,不能在 dealloc 中取消注册。
    • 如果同时保留观察者令牌,且观察者令牌保留 self ,会形成保留循环。

以下是一个注册通知并将观察者令牌赋值给实例变量的示例:

self->_observer = [[NSNotificationCenter defaultCenter]
    addObserverForName:@"heyho"
    object:nil queue:nil usingBlock:^(NSNotification *n) {
        NSLog(@"%@", self);
    }];

原本打算在 dealloc 中取消注册,但由于存在保留循环, dealloc 方法不会被调用:

- (void) dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self->_observer];
}

为了打破保留循环,有两种方法:
1. 释放观察者对象 :在取消注册时释放 _observer 对象。可以在 viewDidDisappear: 方法中进行操作:

- (void) viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    [[NSNotificationCenter defaultCenter] removeObserver:self.observer];
    self->_observer = nil; // 释放观察者
}

当观察者取消注册时,通知中心会释放它,同时手动释放后,观察者会销毁并释放 self dealloc 方法会被调用。如果将 _observer 实例变量标记为 __weak ,可以省略最后一行代码。
2. 避免块保留 self :通过“弱 - 强舞蹈”技术,避免在块中直接引用 self 。示例代码如下:

// 示例12 - 9. 弱 - 强舞蹈防止块保留self
__weak MyClass* wself = self; 
self->_observer = [[NSNotificationCenter defaultCenter]
    addObserverForName:@"heyho"
    object:nil queue:nil usingBlock:^(NSNotification *n) {
        MyClass* sself = wself; 
        if (sself) {
            // 自由引用sself,但永远不要引用self 
        }
    }];

“弱 - 强舞蹈”的步骤如下:
1. 在块外部创建一个对 self 的局部弱引用,使块能够访问该引用。
2. 在块内部,将弱引用赋值给一个正常的强引用。由于弱引用不稳定,可能在代码执行过程中变为 nil ,赋值给强引用可以解决这个问题。
3. 在块内部使用强引用代替对 self 的引用,并在操作前进行 nil 检查。

NSTimer的内存管理

NSTimer 也有特殊的内存管理规则。 NSTimer 类文档指出,运行循环会保留其定时器,并且 repeating 定时器会保留目标对象,直到定时器失效。如果在 repeating 定时器未失效时,目标对象无法销毁,且不能在 dealloc 中使定时器失效。因此,需要找到其他合适的时机发送 invalidate 消息。

可以使用基于GCD的块式定时器作为替代。定时器“对象”是 dispatch_source_t ,通常作为实例变量保留(ARC会管理其内存)。定时器在“恢复”后会重复触发,通过将实例变量置为 nil 来停止触发。但同样需要注意防止定时器的块保留 self ,避免保留循环。以下是一个典型的示例代码:

@implementation MyClass {
    dispatch_source_t _timer; // ARC会管理这个伪对象
}
- (void)doStart:(id)sender {
    self->_timer = dispatch_source_create(
        DISPATCH_SOURCE_TYPE_TIMER,0,0,dispatch_get_main_queue());
    dispatch_source_set_timer(
        self->_timer, dispatch_walltime(nil, 0),
        1 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC
        );
    __weak id wself = self;
    dispatch_source_set_event_handler(self->_timer, ^{
        MyClass* sself = wself;
        if (sself) {
            [sself dummy:nil]; // 防止保留循环
        }
    });
    dispatch_resume(self->_timer);
}
- (void)doStop:(id)sender {
    self->_timer = nil;
}
- (void) dummy: (id) dummy {
    NSLog(@"timer fired");
}
- (void) dealloc {
    [self doStop:nil];
}
@end
其他特殊情况

其他Cocoa对象的不寻常内存管理行为通常会在文档中明确说明。例如,在释放 UIWebView 实例之前,必须先将其 delegate 属性设置为 nil CAAnimation 对象会保留其委托。

此外,有些情况下文档可能没有明确警告特殊的内存管理问题,但ARC会警告由于在块中使用自引用可能导致的保留循环。例如, UIPageViewController setViewControllers:direction:animated:completion: 方法的完成处理程序,如果块中的代码引用了同一个 UIPageViewController 实例,编译器会发出警告。可以使用“弱 - 强舞蹈”技术来避免保留循环。

综上所述,在使用ARC进行内存管理时,虽然它大大简化了开发者的工作,但仍需要注意保留循环和一些特殊对象的内存管理情况,以确保应用的稳定性和性能。通过合理使用弱引用和“弱 - 强舞蹈”技术,可以有效避免内存泄漏和悬空指针问题。

实例变量的内存管理(ARC)与常见问题处理

内存管理问题总结与应对策略

在前面的内容中,我们详细探讨了ARC下实例变量的内存管理、保留循环和弱引用,以及一些不常见的内存管理情况。下面我们对这些问题进行总结,并给出相应的应对策略。

问题类型 问题描述 应对策略
保留循环 对象之间相互保留,导致引用计数无法减为零,对象泄漏 使用弱引用( __weak )避免相互保留;使用“弱 - 强舞蹈”技术防止块保留 self
非ARC弱引用悬空指针 非ARC代码中,引用在对象销毁后可能变成悬空指针,导致程序崩溃 在对象销毁前,将引用设置为 nil 或其他对象
NSNotificationCenter内存管理 使用 addObserverForName:object:queue:usingBlock: 注册时可能出现保留循环和悬空指针问题 取消注册观察者令牌;使用“弱 - 强舞蹈”技术避免块保留 self
NSTimer内存管理 repeating 定时器会保留目标对象,导致目标对象无法销毁 找到合适的时机发送 invalidate 消息;使用基于GCD的块式定时器并防止块保留 self
内存管理流程图

下面是一个简单的流程图,展示了在ARC下处理常见内存管理问题的一般流程:

graph TD;
    A[开始] --> B{是否存在保留循环风险};
    B -- 是 --> C[使用弱引用或“弱 - 强舞蹈”技术];
    B -- 否 --> D{是否使用NSNotificationCenter或NSTimer};
    D -- 是 --> E{是否使用addObserverForName:object:queue:usingBlock:或repeating定时器};
    E -- 是 --> F[取消注册或发送invalidate消息并防止块保留self];
    E -- 否 --> G[正常处理];
    D -- 否 --> G[正常处理];
    C --> G;
    F --> G;
    G --> H[结束];
代码示例总结

为了更好地理解和应用上述内存管理技术,下面对前面提到的代码示例进行总结。

直接赋值与正式设置方法
// 直接赋值
self->_theData = d;

// 正式设置方法
- (void) setTheData: (NSMutableArray*) value {
    self->_theData = value;
}
初始化方法
// 保留实例变量的初始化方法
- (id) initWithName: (NSString*) s {
    self = [super init];
    if (self) {
        self->_name = s;
    }
    return self;
}

// 复制实例变量的初始化方法
- (id) initWithName: (NSString*) s {
    self = [super init];
    if (self) {
        self->_name = [s copy];
    }
    return self;
}
弱引用的使用
@implementation MyClass {
    __weak id _thing;
}
“弱 - 强舞蹈”技术
__weak MyClass* wself = self; 
self->_observer = [[NSNotificationCenter defaultCenter]
    addObserverForName:@"heyho"
    object:nil queue:nil usingBlock:^(NSNotification *n) {
        MyClass* sself = wself; 
        if (sself) {
            // 自由引用sself,但永远不要引用self 
        }
    }];
基于GCD的块式定时器
@implementation MyClass {
    dispatch_source_t _timer; // ARC会管理这个伪对象
}
- (void)doStart:(id)sender {
    self->_timer = dispatch_source_create(
        DISPATCH_SOURCE_TYPE_TIMER,0,0,dispatch_get_main_queue());
    dispatch_source_set_timer(
        self->_timer, dispatch_walltime(nil, 0),
        1 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC
        );
    __weak id wself = self;
    dispatch_source_set_event_handler(self->_timer, ^{
        MyClass* sself = wself;
        if (sself) {
            [sself dummy:nil]; // 防止保留循环
        }
    });
    dispatch_resume(self->_timer);
}
- (void)doStop:(id)sender {
    self->_timer = nil;
}
- (void) dummy: (id) dummy {
    NSLog(@"timer fired");
}
- (void) dealloc {
    [self doStop:nil];
}
@end
实际开发中的注意事项

在实际开发中,为了确保内存管理的正确性,我们需要注意以下几点:

  1. 代码审查 :在编写代码时,仔细审查是否存在保留循环的风险。特别是在使用块和委托时,要确保不会出现相互保留的情况。
  2. 文档阅读 :对于Cocoa框架中的对象,要仔细阅读其文档,了解其特殊的内存管理行为。例如, UIWebView CAAnimation 等对象的内存管理需要特别注意。
  3. 测试与调试 :使用内存分析工具(如Instruments)进行测试和调试,及时发现和解决内存泄漏问题。同时,在开发过程中可以开启僵尸对象模式,帮助定位悬空指针问题。
  4. 遵循最佳实践 :遵循内存管理的最佳实践,如合理使用弱引用和“弱 - 强舞蹈”技术,确保代码的健壮性和可维护性。
总结

ARC的出现大大简化了内存管理的工作,但开发者仍然需要了解内存管理的基本原理和常见问题,以避免内存泄漏和悬空指针等问题。通过合理使用弱引用、“弱 - 强舞蹈”技术,以及注意特殊对象的内存管理行为,我们可以确保应用的稳定性和性能。在实际开发中,要养成良好的代码习惯,进行代码审查和测试,及时发现和解决内存管理问题。希望本文能够帮助开发者更好地理解和应用ARC进行内存管理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值