实例变量的内存管理(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
实际开发中的注意事项
在实际开发中,为了确保内存管理的正确性,我们需要注意以下几点:
- 代码审查 :在编写代码时,仔细审查是否存在保留循环的风险。特别是在使用块和委托时,要确保不会出现相互保留的情况。
-
文档阅读
:对于Cocoa框架中的对象,要仔细阅读其文档,了解其特殊的内存管理行为。例如,
UIWebView和CAAnimation等对象的内存管理需要特别注意。 - 测试与调试 :使用内存分析工具(如Instruments)进行测试和调试,及时发现和解决内存泄漏问题。同时,在开发过程中可以开启僵尸对象模式,帮助定位悬空指针问题。
- 遵循最佳实践 :遵循内存管理的最佳实践,如合理使用弱引用和“弱 - 强舞蹈”技术,确保代码的健壮性和可维护性。
总结
ARC的出现大大简化了内存管理的工作,但开发者仍然需要了解内存管理的基本原理和常见问题,以避免内存泄漏和悬空指针等问题。通过合理使用弱引用、“弱 - 强舞蹈”技术,以及注意特殊对象的内存管理行为,我们可以确保应用的稳定性和性能。在实际开发中,要养成良好的代码习惯,进行代码审查和测试,及时发现和解决内存管理问题。希望本文能够帮助开发者更好地理解和应用ARC进行内存管理。
超级会员免费看
11

被折叠的 条评论
为什么被折叠?



