MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C弱引用容器(NSHashTable、NSMapTable)

2023-04-161.4k 阅读

1. 引言:理解内存管理与弱引用

在Objective - C编程中,内存管理一直是一个至关重要的话题。ARC(自动引用计数)的引入大大简化了内存管理的过程,但在某些特定场景下,我们仍然需要手动处理对象之间的引用关系,以避免内存泄漏和对象生命周期管理不当的问题。

弱引用(Weak Reference)就是一种特殊的引用类型,它不会增加对象的引用计数。当对象的强引用计数降为0,对象被释放时,指向该对象的所有弱引用会自动被设置为nil。这种特性在避免循环引用(Retain Cycle)以及管理对象生命周期方面有着重要的应用。

例如,在视图控制器(ViewController)的父子关系中,如果父视图控制器对其子视图控制器持有强引用,而子视图控制器又对父视图控制器持有强引用,就会形成循环引用,导致两个视图控制器都无法被释放。通过使用弱引用,我们可以打破这种循环,确保对象在不再被需要时能够正确地释放内存。

2. NSHashTable简介

NSHashTable是一个用于存储对象的容器类,它与NSMutableSet有一些相似之处,但NSHashTable允许使用弱引用存储对象。这意味着当存储在NSHashTable中的对象的强引用计数变为0并被释放时,NSHashTable中对应的引用会自动变为nil,从而避免了悬空指针(Dangling Pointer)的问题。

NSHashTable是一个抽象类,我们通常使用它的类方法来创建具体的实例。这些类方法允许我们指定容器的行为,例如对象的存储方式(弱引用或强引用)、内存管理选项以及容器的性能特征。

2.1 创建NSHashTable实例

下面是一些创建NSHashTable实例的常用方法:

2.1.1 创建强引用的NSHashTable

NSHashTable *strongHashTable = [NSHashTable hashTableWithOptions:NSPointerFunctionsStrongMemory];

在这个例子中,NSPointerFunctionsStrongMemory选项表示NSHashTable将对存储的对象使用强引用。这与NSMutableSet的行为类似,当对象被添加到strongHashTable中时,对象的引用计数会增加。

2.1.2 创建弱引用的NSHashTable

NSHashTable *weakHashTable = [NSHashTable weakObjectsHashTable];

[NSHashTable weakObjectsHashTable]是一个便捷方法,用于创建一个使用弱引用存储对象的NSHashTable。当对象被添加到weakHashTable中时,不会增加对象的引用计数。当对象在其他地方的强引用计数降为0并被释放时,weakHashTable中对应的引用会自动变为nil

2.2 NSHashTable的常用操作

2.2.1 添加对象

NSString *string1 = @"Hello";
NSString *string2 = @"World";

[weakHashTable addObject:string1];
[weakHashTable addObject:string2];

这里将两个字符串对象添加到weakHashTable中。由于weakHashTable使用弱引用,添加操作不会增加字符串对象的引用计数。

2.2.2 移除对象

[weakHashTable removeObject:string1];

通过removeObject:方法,可以从NSHashTable中移除指定的对象。

2.2.3 遍历NSHashTable

[weakHashTable enumerateObjectsUsingBlock:^(id  _Nonnull obj, BOOL * _Nonnull stop) {
    if (obj) {
        NSLog(@"%@", obj);
    }
}];

在遍历NSHashTable时,需要注意可能会遇到nil值,因为当对象被释放后,其在NSHashTable中的弱引用会变为nil。所以在处理对象时,需要先检查对象是否为nil

3. NSMapTable简介

NSMapTable是一个用于存储键值对(Key - Value Pairs)的容器类,类似于NSMutableDictionary。与NSMutableDictionary不同的是,NSMapTable允许对键和值分别指定不同的引用策略,包括强引用、弱引用和无主引用(Unretained Reference)。

NSMapTable同样是一个抽象类,通过类方法来创建具体的实例,这些类方法允许我们根据需求定制键和值的引用方式。

3.1 创建NSMapTable实例

3.1.1 创建强键强值的NSMapTable

NSMapTable *strongKeyStrongValueMapTable = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsStrongMemory];

在这个例子中,NSPointerFunctionsStrongMemory选项表示NSMapTable对键和值都使用强引用。这与NSMutableDictionary的默认行为类似,当键值对被添加到strongKeyStrongValueMapTable中时,键和值对象的引用计数都会增加。

3.1.2 创建弱键强值的NSMapTable

NSMapTable *weakKeyStrongValueMapTable = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsWeakMemory valueOptions:NSPointerFunctionsStrongMemory];

这里创建了一个对键使用弱引用,对值使用强引用的NSMapTable。当键对象的强引用计数降为0并被释放时,weakKeyStrongValueMapTable中对应的键引用会自动变为nil,但值对象的引用计数不受影响。

3.1.3 创建强键弱值的NSMapTable

NSMapTable *strongKeyWeakValueMapTable = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory];

此例创建的NSMapTable对键使用强引用,对值使用弱引用。当值对象在其他地方的强引用计数降为0并被释放时,strongKeyWeakValueMapTable中对应的值引用会自动变为nil

3.2 NSMapTable的常用操作

3.2.1 添加键值对

NSString *key1 = @"Key1";
NSString *value1 = @"Value1";

[weakKeyStrongValueMapTable setObject:value1 forKey:key1];

通过setObject:forKey:方法,可以向NSMapTable中添加键值对。根据NSMapTable的创建选项,键和值的引用计数会相应地变化。

3.2.2 获取值

NSString *retrievedValue = [weakKeyStrongValueMapTable objectForKey:key1];
if (retrievedValue) {
    NSLog(@"Retrieved Value: %@", retrievedValue);
}

使用objectForKey:方法可以根据键获取对应的值。同样,在处理值时需要检查是否为nil,特别是当值使用弱引用时。

3.2.3 移除键值对

[weakKeyStrongValueMapTable removeObjectForKey:key1];

通过removeObjectForKey:方法,可以从NSMapTable中移除指定键的键值对。

4. 应用场景

4.1 避免循环引用

在视图控制器的父子关系场景中,假设父视图控制器(ParentViewController)包含一个子视图控制器(ChildViewController)的数组。如果ParentViewControllerChildViewController使用强引用,而ChildViewController又对ParentViewController持有强引用,就会形成循环引用。

@interface ParentViewController : UIViewController
@property (nonatomic, strong) NSHashTable *childViewControllers;
@end

@interface ChildViewController : UIViewController
@property (nonatomic, strong) ParentViewController *parentViewController;
@end

如果使用NSHashTable来存储ChildViewController,并将NSHashTable设置为使用弱引用:

@implementation ParentViewController
- (instancetype)init {
    self = [super init];
    if (self) {
        _childViewControllers = [NSHashTable weakObjectsHashTable];
    }
    return self;
}
@end

这样,即使ChildViewControllerParentViewController持有强引用,由于ParentViewControllerChildViewController使用弱引用,不会形成循环引用,从而确保两个视图控制器在不再被需要时能够正确释放内存。

4.2 观察者模式

在观察者模式中,一个对象(被观察对象)会通知多个观察者对象状态的变化。通常,被观察对象会持有观察者对象的引用。如果使用强引用,可能会导致观察者对象在不再需要时无法被释放,因为被观察对象一直持有其强引用。

通过使用NSHashTableNSMapTable以弱引用方式存储观察者对象,可以解决这个问题。当观察者对象在其他地方的强引用计数降为0并被释放时,被观察对象中对应的弱引用会自动变为nil,避免了悬空指针问题。

@interface ObservableObject : NSObject
@property (nonatomic, strong) NSHashTable *observers;
- (void)addObserver:(id)observer;
- (void)removeObserver:(id)observer;
- (void)notifyObservers;
@end

@implementation ObservableObject
- (instancetype)init {
    self = [super init];
    if (self) {
        _observers = [NSHashTable weakObjectsHashTable];
    }
    return self;
}

- (void)addObserver:(id)observer {
    [_observers addObject:observer];
}

- (void)removeObserver:(id)observer {
    [_observers removeObject:observer];
}

- (void)notifyObservers {
    [_observers enumerateObjectsUsingBlock:^(id  _Nonnull obj, BOOL * _Nonnull stop) {
        if ([obj respondsToSelector:@selector(update)]) {
            [obj update];
        }
    }];
}
@end

4.3 缓存机制

在实现缓存机制时,有时我们希望缓存中的对象在内存紧张时能够被系统自动释放,而不会影响缓存机制的正常运行。

可以使用NSMapTable创建一个弱值的缓存。例如,在一个图片缓存系统中:

@interface ImageCache : NSObject
@property (nonatomic, strong) NSMapTable *imageCacheMapTable;
- (void)cacheImage:(UIImage *)image forKey:(NSString *)key;
- (UIImage *)retrieveImageForKey:(NSString *)key;
@end

@implementation ImageCache
- (instancetype)init {
    self = [super init];
    if (self) {
        _imageCacheMapTable = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory];
    }
    return self;
}

- (void)cacheImage:(UIImage *)image forKey:(NSString *)key {
    [_imageCacheMapTable setObject:image forKey:key];
}

- (UIImage *)retrieveImageForKey:(NSString *)key {
    return [_imageCacheMapTable objectForKey:key];
}
@end

这样,当图片对象在其他地方的强引用计数降为0并被释放时,缓存中的弱引用会变为nil,不会造成内存浪费。

5. 性能考虑

5.1 NSHashTable的性能

NSHashTable在查找和插入操作上具有较好的性能,其平均时间复杂度为O(1),类似于NSMutableSet。然而,由于NSHashTable需要额外处理弱引用,在某些情况下,性能可能会略低于NSMutableSet

当遍历NSHashTable时,由于可能存在nil值,需要额外的检查,这也会对性能产生一定的影响。在设计算法时,如果需要频繁遍历NSHashTable,并且需要处理大量数据,应该考虑这种性能开销。

5.2 NSMapTable的性能

NSMapTable的性能与NSMutableDictionary类似,在查找、插入和删除操作上,平均时间复杂度也是O(1)。同样,由于需要处理不同的引用策略,特别是弱引用,可能会带来一些额外的性能开销。

如果NSMapTable中存储的键值对数量非常大,并且对性能要求极高,应该根据实际需求仔细选择键和值的引用策略,以平衡内存管理和性能。例如,在一些对内存非常敏感但对性能要求相对较低的场景下,可以选择使用更多的弱引用;而在对性能要求极高的场景下,可能需要更多地使用强引用,同时通过其他方式解决循环引用等问题。

6. 内存管理注意事项

6.1 NSHashTable的内存管理

当使用NSHashTable以弱引用方式存储对象时,需要注意对象可能会在任何时候被释放。在访问NSHashTable中的对象时,一定要先检查对象是否为nil,否则可能会导致程序崩溃。

例如,在遍历NSHashTable时:

[weakHashTable enumerateObjectsUsingBlock:^(id  _Nonnull obj, BOOL * _Nonnull stop) {
    if (obj) {
        // 安全地处理对象
    }
}];

同时,虽然NSHashTable本身不会增加对象的引用计数,但如果在NSHashTable之外还有其他强引用指向这些对象,这些对象不会被释放。只有当所有的强引用都被移除后,对象才会被释放,NSHashTable中的弱引用才会变为nil

6.2 NSMapTable的内存管理

对于NSMapTable,根据键和值的引用策略不同,内存管理也有所不同。当使用弱引用作为键或值时,同样需要注意对象可能随时被释放的情况。

例如,在获取值时:

id value = [weakKeyStrongValueMapTable objectForKey:key];
if (value) {
    // 处理值
}

在使用NSMapTable时,要确保对键和值的引用策略选择正确,以避免内存泄漏或悬空指针问题。特别是在复杂的数据结构中,多个NSMapTable嵌套使用或者与其他容器类混合使用时,更需要仔细考虑内存管理。

7. 与其他容器类的比较

7.1 NSHashTable与NSMutableSet

NSMutableSet对存储的对象使用强引用,当对象被添加到NSMutableSet中时,对象的引用计数会增加。而NSHashTable可以选择使用弱引用存储对象,这使得NSHashTable在处理对象生命周期和避免循环引用方面具有优势。

在性能上,由于NSHashTable需要额外处理弱引用,在一些简单场景下,NSMutableSet可能会有更好的性能表现。但在需要处理对象弱引用关系的场景中,NSHashTable是更好的选择。

7.2 NSMapTable与NSMutableDictionary

NSMutableDictionary对键和值都使用强引用,而NSMapTable可以灵活地选择对键和值使用不同的引用策略,包括弱引用和无主引用。这使得NSMapTable在处理复杂的对象引用关系和避免循环引用方面具有更大的灵活性。

在性能方面,NSMapTable由于需要处理不同的引用策略,可能会有一些额外的开销。但在需要精细控制键值对引用关系的场景下,NSMapTable的优势明显。

8. 总结

NSHashTableNSMapTable是Objective - C中非常强大的容器类,它们通过支持弱引用等特性,为开发者提供了更加灵活的内存管理方式。在处理对象之间复杂的引用关系、避免循环引用以及实现一些特殊的缓存机制等方面,NSHashTableNSMapTable都有着广泛的应用。

在使用这两个容器类时,需要充分理解它们的特性和行为,特别是在内存管理和性能方面的考虑。通过合理地选择引用策略和使用方式,可以有效地提高程序的稳定性和性能,避免内存泄漏等问题。

无论是在简单的小型项目还是复杂的大型应用中,掌握NSHashTableNSMapTable的使用方法,都能为开发者在处理对象引用和内存管理时提供更多的选择和解决方案。希望通过本文的介绍,读者能够对NSHashTableNSMapTable有更深入的理解,并在实际项目中灵活运用它们。