0%

《Effective Objective-C 2.0》7. 系统框架

第 7 章 系统框架

第 47 条:熟悉系统框架

框架:将一系列代码封装为动态库(dynamic library),并在其中放入描述其接口的头文件。

静态库:有时为 iOS 平台构建的第三方框架所使用的是静态库(static library),这是因为 iOS 应用程序不允许在其中包含动态库。这些东西严格来讲并不是真正的框架,然而也经常视为框架。不过,所有 iOS 平台的系统框架仍然使用动态库。

  • Cocoa:在为 Mac OS X 或 iOS 系统开发 “带图形界面的应用程序”(graphical application) 时,会用到名为 Cocoa 的框架,在 iOS 上称为 Cocoa Touch

  • Foundation:它是所有 Objectice-C 应用程序的基础。Foundation 框架中的类使用 NS 前缀。

  • CoreFoundation:从技术上讲,CoreFoundation 框架不是 Objective-C 框架,但它却是编写 Objectice-C 应用程序时所应熟悉的重要框架, Foundation 框架中的许多功能,都可以在此框架中找到对应的 C 语言 API。

    无缝桥接(toll- free bridging ) 功能:

    CoreFoundation 中的 C 语言数据结构平滑转换为 Foundation 中的 Objective-C 对象,也可以反向转换。

    无缝桥接技术所实现的代码可以使运行期系统把 CoreFoundation 框架中的对象视为普通的 Objective-C 对象。

    例如:NSString <—> CFString

  • CFNetwork:此框架提供了 C 语言级别的网络通信能力,它将 BSD 套接字(BSD socket)抽象成易于使用的网络接口。而 Foundation 则将该框架里的部分内容封装为 Objective-C 语言的接口,以便进行网络通信,例如可以用 NSURLConnection 从 URL 中下载数据。

  • CoreAudio:该框架所提供的 C 语言 API 可用来操作设备上的音频硬件。这个框架属于比较难用的那种,因为音频处理本身就很复杂。所幸由这套 API 可以抽象出另外一 套 Objective-C 式 API,用后者来处理音频问题会更简单些。

  • AVFoundation:此框架所提供的 Objective-C 对象可用来回放并录制音频及视频,比如能够在 UI 视图类里播放视频。

  • CoreData:此框架所提供的 Objective-C 接口可将对象放人数据库,便于持久保存。 CoreData 会处理数据的获取及存储事宜,而且可以跨越 Mac OS X 及 iOS 平台。

  • CoreText:此框架提供的 C 语言接口可以高效执行文字排版及渲染操作。

Objective-C 编程的重要特性:经常需要使用底层的 C 语言级 API。用 C 语言来实现 API 的好处是,可以绕过 Objective-C 的运行期系统,从而提升执行速度。

读者可能会编写使用 UI 框架的 Mac OS X 或 iOS 应用程序。这两个平台的核心 UI 框架分别叫做 AppKitUIKit , 它们都提供了构建在 FoundationCoreFoundation 之上的 Objective-C 类。框架里含有 UI 元素,也含有粘合机制,令开发者可将所有相关内容组装为应用程序。在这些主要的 UI 框架之下,是 CoreAnimationCoreGraphics 框架。

  • CoreAnimation 是用 Objective-C 语言写成的,它提供了一些工具,而 UI 框架则用这些工具来渲染图形并播放动画。开发者编程时可能从来不会深入到这种级别,不过知道该框架总是好的。CoreAnimation 本身并不是框架,它是 QuartzCore 框架的一部分。然而在框架的国度里,CoreAnimation 仍应算作 “一等公民”(first-class citizen)。
  • CoreGraphics 框架以 C 语言写成,其中提供了 2D 渲染所必备的数据结构与函数。例如,其中定义了 CGPoint、CGSize、CGRect 等数据结构,而 UllKit 框架中的 UlView 类在确定视图控件之间的相对位置时,这些数据结构都要用到。

还有很多框架构建在 UI 框架之上,比方说 MapKit 框架,它可以为 iOS 程序提供地图功能。又比如 Social 框架,它为 Mac OS X 及 iOS 程序提供了社交网络(social networking) 功能。开发者通常会将这些框架与操作系统平台所对应的核心 UI 框架结合起来使用。

总的来说,许多框架都是安装 Mac OS X 与 iOS 系统时的标准配置。所以,在打算编写新的工具类之前,最好在系统框架里搜一下,通常都有写好的类可供直接使用。

要点

  • 许多系统框架都可以直接使用。其中最重要的是 FoundationCoreFoundation,这两个框架提供了构建应用程序所需的许多核心功能。
  • 很多常见任务都能用框架来做,例如音频与视频处理、网络通信、数据管理等。
  • 请记住,用纯 C 写成的框架与用 Objective-C 写成的一样重要,若想成为优秀的 Objective-C 开发者,应该掌握 C 语言的核心概念

第 48 条:多用块枚举,少用 for 循环

实现枚举的方法:

  1. 标准的 C 语言循环;
  2. Objective-C 1.0 的 NSEnumerator 方法;
  3. Objective-C 2.0 的快速遍历法(fast enumeration)。
  4. 基于块的遍历方式。

for 循环

/** NSArray */
NSArray *array;
for (int i = 0; i < array.count; i++) {
    id object = array[i];
    // Do something with 'object'
}

/** NSDictionary */
NSDictionary *dictionary;
NSArray *keys = [dictionary allKeys];
for (int i = 0; i < keys.count; i++) {
    id key = keys[i];
    id value = dictionary[key];
    // Do something with 'key' and 'value'
}

/** NSSet */
NSSet *set;
NSArray *objects = [set allObjects];
for (int i = 0; i < objects.count; i++) {
    id object = object[i];
    // Do something with 'object'
}

根据定义,字典与 set 都是 “无序的”(imoniered),所以无法根据特定的整数下标来直接访问其中的值。于是,就需要先获取字典里的所有键或是 set 里的所有对象,这两种情况下,都可以在获取到的有序数组上遍历,以便借此访问原字典及原 set 中的值。创建这个附加数组会有额外开销,而且还会多创建一个数组对象,它会保留 collection 中的所有元素对象。

使用 Objective-C 1.0 的 NSEnumerator 遍历

NSEnumerator.h

typedef struct {
    unsigned long state;
    id __unsafe_unretained _Nullable * _Nullable itemsPtr;
    unsigned long * _Nullable mutationsPtr;
    unsigned long extra[5];
} NSFastEnumerationState;

@protocol NSFastEnumeration

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len;

@end

@interface NSEnumerator<ObjectType> : NSObject <NSFastEnumeration>

//❇️ 返回枚举里的下个对象。每次调用该方法时,其内部数据结构都会更新,使得下次调用方法时能返回下个对象。等到枚举中的全部对象都已返回之后,再调用就将返回nil,这表示达到枚举末端了。
- (nullable ObjectType)nextObject;

@end

@interface NSEnumerator<ObjectType> (NSExtendedEnumerator)

@property (readonly, copy) NSArray<ObjectType> *allObjects;

@end

使用示例:

/** NSArray */
NSArray *array;
NSEnumerator *enumerator = [array objectEnumerator];
id object;
while ((object = [enumerator nextObject]) != nil) {
    // Do something with 'object'
}

/** NSDictionary */
NSDictionary *dictionary;
NSEnumerator *enumerator = [dictionary keyEnumerator];
id key;
while ((key = [enumerator nextObject]) != nil) {
    id value = dictionary[key];
    // Do something with 'key' and 'value'
}

/** NSSet */
NSSet *set;
NSEnumerator *enumerator = [set objectEnumerator];
id object;
while ((object = [enumerator nextObject]) != nil) {
    // Do something with 'object'
}
  • 优点:不论遍历哪种 collection,都可以采用这套相似的语法。

  • NSEnumerator 还有多种 “枚举器”(enumerator) 可供使用,如反向遍历数组:

    /** NSArray */
    NSArray *array;
    // reverseObjectEnumerator:反向遍历数组
    NSEnumerator *enumerator = [array reverseObjectEnumerator];
    id object;
    while ((object = [enumerator nextObject]) != nil) {
      // Do something with 'object'
    }

快速遍历

/** NSArray */
NSArray *array;
for (id object in array) {
    // Do something with 'object'
}

/** NSDictionary */
NSDictionary *dictionary;
for (id key in dictionary) {
    id value = dictionary[key];
    // Do something with 'key' and 'value'
}

/** NSSet */
NSSet *set;
for (id object in set) {
    // Do something with 'object'
}

如果某个类的对象支持快速遍历,那么就可以宣称自己遵从名为 NSFastEmimeraticm 的协议,从而令开发者可以采用此语法来迭代该对象。

该方法允许类实例同时返回多个对象,使得循环遍历操作更为髙效。

@protocol NSFastEnumeration

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
                                  objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer
                                    count:(NSUInteger)len;

@end

基于块的遍历方式

方法列表:

/** NSArray */
- (void)enumerateObjectsUsingBlock:(void (NS_NOESCAPE ^)(ObjectType obj, NSUInteger idx, BOOL *stop))block;
- (void)enumerateObjectsWithOptions:(NSEnumerationOptions)opts usingBlock:(void (NS_NOESCAPE ^)(ObjectType obj, NSUInteger idx, BOOL *stop))block;
- (void)enumerateObjectsAtIndexes:(NSIndexSet *)s options:(NSEnumerationOptions)opts usingBlock:(void (NS_NOESCAPE ^)(ObjectType obj, NSUInteger idx, BOOL *stop))block;

/** NSDictionary */
- (void)enumerateKeysAndObjectsUsingBlock:(void (NS_NOESCAPE ^)(KeyType key, ObjectType obj, BOOL *stop))block;
- (void)enumerateKeysAndObjectsWithOptions:(NSEnumerationOptions)opts usingBlock:(void (NS_NOESCAPE ^)(KeyType key, ObjectType obj, BOOL *stop))block;

/** NSSet */
- (void)enumerateObjectsUsingBlock:(void (NS_NOESCAPE ^)(ObjectType obj, BOOL *stop))block;
- (void)enumerateObjectsWithOptions:(NSEnumerationOptions)opts usingBlock:(void (NS_NOESCAPE ^)(ObjectType obj, BOOL *stop))block;

示例代码:

/** NSArray */
NSArray *array;
[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    // Do something with 'object'
    if (shouldStop) {
        *stop = YES;
    }
}];

/** NSDictionary */
NSDictionary *dictionary;
[dictionary enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
    // Do something with 'key' and 'value'
    if (shouldStop) {
        *stop = YES;
    }
}];

/** NSSet */
NSSet *set;
[set enumerateObjectsUsingBlock:^(id  _Nonnull obj, BOOL * _Nonnull stop) {
    // Do something with 'object'
    if (shouldStop) {
        *stop = YES;
    }
}];

此方式的优点:

  1. 遍历时可以直接从块里获取更多信息。

  2. 能够修改块的方法签名,以免进行类型转换操作,从效果上讲,相当于把本来需要执行的类型转换操作交给块方法签名来做。

    <!–hexoPostRenderEscape:

    // for-in 遍历:
    NSDictionary *dictionary;
    for (NSString *key in dictionary) {
    NSString *object = (NSString *)dictionary[key];
    // Do something with 'key' and 'value'
    }

// 基于块的遍历,可以在块方法签名中直接转换数据类型:
[dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) {
// Do something with 'key' and 'value'
}];
:hexoPostRenderEscape–>

  1. 反向遍历:设置 NSEnumerationOptions 选项为 NSEnumerationReverse

  2. 并行执行:设置 NSEnumerationOptions 选项为 NSEnumerationConcurrent

要点

  • 遍历 collection 有 4 种方式。最基本的办法是 for 循环,其次是 NSEnumerator 遍历方法及快速遍历法,最新、最先进的方式则是 块枚举法
  • 块枚举法 本身就能通过 GCD 来并发执行遍历操作,无需另行编写代码。而采用其他遍历方式则无法轻易实现这一点。
  • 若提前知道待遍历的 collection 含有何种对象,则应修改块签名,指出对象的具体类型。

第 49 条:对自定义其内存管理语义的 collection 使用无缝桥接

无缝桥接:可以在定义于 Foundation 框架中的 Objective-C 类和定义于 CoreFoundation 框架中的 C 数据结构之间互相转换。

// NSArray → CFArray
NSArray *anNSArray = @[@1, @2, @3, @4, @5];
// CFArray 需要通过 CFArrayRef 来引用,
// CFArrayRef 是指向 struct__CFArray 的指针
CFArrayRef aCFArray = (__bridge CFArrayRef)anNSArray;
NSLog(@"Size of array = %li", CFArrayGetCount(aCFArray));

桥式转换

  • __bridge 告诉 ARC (参见第 30 条)如何处理转换所涉及的 Objective-C 对象。
  • __bridge 本身的意思是:ARC 仍然具备这个 Objective-C 对象的所有权。而 __bridge_ retained 则与之相反,意味着 ARC 将交出对象的所有权。
  • 反向转换可通过 __bridge_transfer 来实现。

需要使用无缝桥接的原因:

  • Foundation 框架中的 Objective-C 类所具备的某些功能,是 CoreFoundation 框架中的 C 语言数据结构所不具备的。

  • 在使用 Foundation 框架中的字典对象时会遇到一个大问题,那就是其键的内存管理语义为 “拷贝”,而值的语义却是 “保留”。除非使用强大的无缝桥接技术,否则无法改变其语义。

    创建 CFMutableDictionary 时,可以通过下列方法来指定键和值的内存管理语义:

CFMutableDictionaryRef CFDictionaryCreateMutable(
    CFAllocatorRef allocator,
    CFIndex capacity,
    const CFDictionaryKeyCallBacks *keyCallBacks,
    const CFDictionaryValueCallBacks *valueCallBacks
);
  • CFAllocatorRef:将要使用的内存分配器(allocator) 。CoreFoundation 对象里的数据结构需要占用内存,而分配器负责分配及回收这些内存。开发者通常为这个参数传人 NULL,表示采用默认的分配器。

  • CFIndex:定义字典的初始大小。它并不会限制字典的最大容景,只是向分配器提示一开始应该分配多少内存。假如要创建的字典含有 10 个对象,那就向该参数传入 10。

  • 最后两个参数值得注意。它们定义了许多回调函数,用于指示字典中的键和值在遇到各种事件时应该执行何种操作。这两个参数都是指向结构体的指针,二者所对应的结构体如下:

    <!–hexoPostRenderEscape:

    typedef struct {
    CFIndex version;
    CFDictionaryRetainCallBack retain;
    CFDictionaryReleaseCallBack release;
    CFDictionaryCopyDescriptionCallBack copyDescription;
    CFDictionaryEqualCallBack equal;
    CFDictionaryHashCallBack hash;
    } CFDictionaryKeyCallBacks;

typedef struct {
CFIndex version;
CFDictionaryRetainCallBack retain;
CFDictionaryReleaseCallBack release;
CFDictionaryCopyDescriptionCallBack copyDescription;
CFDictionaryEqualCallBack equal;
} CFDictionaryValueCallBacks;

/*

  • version参数目前应设为0。当前编程时总是取这个值,不过将来苹果公司也许会修改此结构体,所以

  • 要预留该值以表示版本号。这个参数可以用于检测新版与旧版数据结构之间是否兼容。

  • 结构体中的其余成员都是函数指针,它们定义了当各种事件发生时应该采用哪个函数来执行相关任务。

  • /:hexoPostRenderEscape–>

    底层实现有点复杂,暂缓研究😂

要点

  • 通过无缝桥接技术,可以在 Foundation 框架中的 Objective-C 对象与 CoreFoundation 框架中的 C 语言数据结构之前来回转换。
  • CoreFoundation 层面创建 collection 时,可以指定许多回调函数,这些函数表示此 collection 应如何处理其元素。然后,可运用无缝桥接技术,将其转换成具备特殊内存管理语义的 Objective-C collection。

第 50 条:构建缓存时选用 NSCache 而非 NSDictionary

  • NSCache 胜过 NSDictionary 之处在于,当系统资源将要耗尽时,它可以自动删减缓存。
  • NSCache 还会先行删减 最久未使用的(lease recently used) 对象
  • NSCache 并不会 “拷贝” 键,而是会 “保留” 它。原因在于,很多时候,键都是由不支持拷贝操作的对象来充当的。
  • NSCache 是线程安全的。在开发者自己不编写加锁代码的前提下, 多个线程便可以同时访问 NSCache。对缓存来说,线程安全通常很重要,因为开发者可能要在某个线程中读取数据,此时如果发现缓存里找不到指定的键,那么就要下载该键所对应的数据了。而下载完数据之后所要执行的回调函数,有可能会放在背景线程中运行,这样的话,就等于是用另外一个线程来写入缓存了。
  • 开发者可以操控缓存删减其内容的时机。有两个与系统资源相关的尺度可供调整,其一是缓存中的对象总数,其二是所有对象的 “总开销”(overall cost)。

示例代码:

// EOCNetworkFetcher.h
#import <Foundation/Foundation.h>

// Network fetcher class
typedef void(^EOCNetworkFetcherCompletionHandler)
                                (NSData *data);

@interface EOCNetworkFetcher : NSObject
@property (nonatomic, strong, readonly) NSURL *url;
- (instancetype)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler:
            (EOCNetworkFetcherCompletionHandler)completion;
@end

// EOCNetworkFetcher.m
#import "EOCNetworkFetcher.h"

@implementation EOCNetworkFetcher {
    NSCache *_cache;
}

- (instancetype)init {
    if (self = [super init]) {
        _cache = [NSCache new];

        // Cache a maximu of 100 URLs
        // 可缓存的总对象数目上限
        _cache.countLimit = 100;

        /**
         * The size in bytes of data is used as the cost,
         * 开销值 以 字节 为单位
         * so this sets a cost limit of 5MB
         * 因此需要将 MB 换算为 字节
         */
        _cache.totalCostLimit = 5 * 1024 * 1024;
    }
    return self;
}

- (instancetype)initWithURL:(NSURL *)url {
    if (self = [super init]) {
        _url = url;
    }
    return self;
}

- (void)downloadDataForURL:(NSURL *)url {
    NSData *cacheData = [_cache objectForKey:url];
    if (cacheData) {
        // Cache hit
        [self useData:cacheData];
    }else {
        // Cache miss,缓存中没有访问者所需的数据
        EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
        [fetcher startWithCompletionHandler:^(NSData *data) {
            [self useData:data];
        }];
    }
}
  • NSPurgeableData 类,NSMutableData 的子类。如果某个对象所占的内存能够根据需要随时丢弃,那么就可以实现 NSDiscardableContent 协议所定义的接口。

    - (void)downloadDataForURL:(NSURL *)url {
      NSPurgeableData *cacheData = [_cache objectForKey:url];
      if (cacheData) {
          // Stop the data being purged
          [cacheData beginContentAccess];
    
          // Use the cached data
          [self useData:cacheData];
    
          // Mark that the data may be purged again
          [cacheData endContentAccess];
      }else {
          // Cache miss
          EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
          [fetcher startWithCompletionHandler:^(NSData *data) {
              NSPurgeableData *purgeableData = [NSPurgeableData dataWithData:data];
              [_cache setObject:purgeableData
                         forKey:url
                           cost:purgeableData.length];
    
              // Don't need to beginContentAccess as it begins
              // with access already marked
    
              // Use the retrieved data
              [self useData:data];
    
              // Mark that the data may be purged now
              [cacheData endContentAccess];
          }];
      }
    }

要点

  • 实现缓存时应选用 NSCache 而非 NSDictionary 对象。因为 NSCache 可以提供优雅的自动删减功能,而且是线程安全的,此外,它与字典不同,并不会拷贝键。
  • 可以给 NSCache 对象设置上限,用以限制缓存中的对象总个数及总成本,而这些尺度则定义了缓存删减其中对象的时机。但是绝对不要把这些尺度当成可靠的硬限制,他们仅对 NSCache 起指导作用。
  • NSPurgeableDataNSCache 搭配使用,可实现自动清除数据的功能,也就是说,当 NSPurgeableData 对象所占内存为系统丢弃时,该对象自身也会从缓存中移除。
  • 如果缓存使用得当。那么应用程序的响应速度就能提高。只有那种重新计算起来很费事的数据,才值得放入缓存,比如那些需要从网络获取或从磁盘读取的数据。

第 51 条:精简 initialize 与 load 的实现代码

load 方法

+ (void)load;
  • 对于加入运行期系统中的每个类(class) 及分类(category) 来说,必定会调用此方法,而且仅调用一次。当包含类或分类的程序库载入系统时,就会执行此方法,而这通常就是指应用程序启动的时候,若程序是为 iOS 平台设计的,则肯定会在此时执行。
  • 如果分类和其所属的类都定义了 load 方法,则先调用类里的,再调用分类里的。
  • load 方法中使用其他类是不安全的。
  • 不遵循继承规则。如果某个类本身没实现 load 方法,那么不管其各级超类是否实现此方法,系统都不会调用。此外,分类和其所属的类里,都可能出现 load 方法。此时两种实现代码都会调用,类的实现要比分类的实现先执行。
  • 而且 load 方法务必实现得精简一些,也就是要尽量减少其所执行的操作,因为整个应用程序在执行 load 方法时都会阻塞。应用程序必须阻塞并等着所有类的 load 都执行完,才能继续。
  • load 方法真正用途仅在于调试程序,比如可以在分类里编写此方法,用来判断该分类是否已经正确载入系统中。也许此方法一度很有用处,但现在完全可以说:时下编写 Objective-C 代码时,不需要用它。

initialize 方法

+ (void)initialize;
  • 对于每个类来说,该方法会在程序首次用该类之前调用,且只调用一次。它是由运行期系统来调用的,绝不应该通过代码直接调用。
  • 它是 “惰性调用的”,也就是说,只有当程序用到了相关的类时,才会调用。
  • 运行期系统在执行该方法时,是处于正常状态的。而且,运行期系统也能确保 initialize 方法一定会在线程安全的环境中执行。
  • 遵循继承规则。initialize 方法与其他消息一样,如果某个类未实现它,而其超类实现了,那么就会运行超类的实现代码。

总结

  • 这两个方法的实现代码要尽量精简。在里面设置一些状态,使本类能够正常运作就可以了,不要执行耗时太久或需要加锁的任务。
  • 开发者无法控制类的初始化时机。
  • 如果某个类的实现代码很复杂,那么其中可能会直接或间接用到其他类。
  • initialize 方法只应该用来设置内部数据。若某个全局状态无法在编译期初始化,则可以放在 initialize 里来做。

要点

  • 在加载阶段,如果实现了 load 方法,那么系统就会调用它。分类里也可以定义此方法,类的 load 方法要比分类中的先调用。与其他方法不同,load 方法不参与覆写机制。
  • 首次使用某个类之前,系统会向其发送 initialize 消息。由于此方法遵从普通的覆写规则,所以通常应该在里面判断当前要初始化的是哪个类。
  • loadinitialize 方法都应该实现的精简一些,这有助于保持应用程序的响应能力,也能减少引入依赖环的几率。
  • 无法在编译期设定的全局常量,可以放在 initialize 方法里初始化。

第 52 条:别忘了 NSTimer 会保留其目标对象

  • Foundation 框架中有个类叫做 NSTimer ,开发者可以指定绝对的日期与时间,以便到时执行任务,也可以指定执行任务的相对延迟时间。计时器还可以重复运行任务,有个与之相关联的 “间隔值”(interval) 可用来指定任务的触发频率。
  • 只有把计时器放在运行循环里,它才能正常触发任务。
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti
                                     target:(id)aTarget
                                   selector:(SEL)aSelector
                                   userInfo:(nullable id)userInfo
                                    repeats:(BOOL)yesOrNo;
  • 用此方法创建出来的计时器,会在指定的间隔时间之后执行任务。也可以令其反复执行任务,直到开发者稍后将其手动关闭为止。target 与 selector 参数表示计时器将在哪个对象上调用哪个方法。计时器会保留其目标对象,等到自身 “失效” 时再释放此对象。调用 invalidate 方法可令计时器失效;执行完相关任务之后,一次性的计时器也会失效。开发者若将计时器设置成重复执行模式,那么必须自己调用 invalidate 方法,才能令其停止。

  • 由于计时器会保留其目标对象,所以反复执行任务通常会导致应用程序出问题。也就是说,设置成重复执行模式的那种计时器,很容易引人 “保留环”。

    <!–hexoPostRenderEscape:

    //  EOCClass.h
    #import <Foundation/Foundation.h>

@interface EOCClass : NSObject

  • (void)startPolling;
  • (void)stopPolling;
    @end

// EOCClass.m
#import "EOCClass.h"

@implementation EOCClass {
// ❇️ EOCClass → _pollTimer
NSTimer *_pollTimer;
}

  • (instancetype)init {
    return [super init];
    }

  • (void)dealloc {
    [_pollTimer invalidate];
    }

  • (void)startPolling {
    // ❇️ _pollTimer → EOCClass
    _pollTimer =
    [NSTimer scheduledTimerWithTimeInterval:5.0

                                   target:self
                                 selector:@selector(p_doPoll)
                                 userInfo:nil
                                  repeats:YES];

    }

  • (void)stopPolling {
    [_pollTimer invalidate];
    _pollTimer = nil;
    }

  • (void)p_doPoll {
    // Poll the resource
    }

@end:hexoPostRenderEscape–>

解决引用循环方法:

//  NSTimer+EOCBlocksSupport.h
#import <Foundation/Foundation.h>

@interface NSTimer (EOCBlocksSupport)

+ (NSTimer *)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                          block:(void(^)())block
                                        repeats:(BOOL)repeats;
@end

//  NSTimer+EOCBlocksSupport.m
#import "NSTimer+EOCBlocksSupport.h"

@implementation NSTimer (EOCBlocksSupport)

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

+ (void)eoc_blockInvoke:(NSTimer *)timer {
    void (^block)() = timer.userInfo;
    if (block) {
        block();
    }
}

@end

将计时器所应执行的任务封装成 block 。在调用计时器函数时,把它作为 userlnfo 参数传进去。该参数可用来存放不透明值 (opaque value),只要计时器还有效,就会一直保留着它。传人参数时要通过 copy 方法将 block 拷贝到 “堆” 上(参见第 37 条),否则等到稍后要执行它的时候,该块可能已经无效了。计时器现在的 target 是 NSTimer 类对象,这是个单例,因此计时器是否会保留它,其实都无所谓。此处依然有保留环,然而因为类对象(classobject) 无须回收,所以不用担心。

优化:

- (void)startPolling {
    __weak EOCClass *weakSelf = self;
    _pollTimer =
    [NSTimer eoc_scheduledTimerWithTimeInterval:5.0
                                          block:^{
                                              EOCClass *strongSelf = weakSelf;
                                              [strongSelf p_doPoll];
                                          }
                                        repeats:YES];

}

iOS 10.0 中已经引入了该方法

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

要点

  • NSTimer 对象会保留其目标,直到计时器本身失效为止,调用 invalidate 方法可令计时器失效,另外,一次性的计时器在触发完任务之后也会失效。
  • 反复执行任务的计时器,很容易引入保留环,如果这种计时器的目标对象又保留了计时器本身,那肯定会导致循环引用。这种循环引用,可能是直接发生的,也可能是通过对象图里的其他对象间接发生的。
  • 可以扩充 NSTimer 的功能,用块来打破循环引用。不过,除非 NSTimer 将来在公共接口里提供此功能,否则必须创建分类,将相关实现代码加入其中。

欢迎关注我的其它发布渠道