美文网首页
[iOS] Effective Objective-C ——系统

[iOS] Effective Objective-C ——系统

作者: 木小易Ying | 来源:发表于2019-10-18 10:29 被阅读0次

47. 熟悉系统框架

开发者会碰到的主要框架就是Foundation,像是NSObject、NSArray、NSDictionary等类都在其中。Foundation框架是所有Objective-C应用程序的“基础”。

还有个与Foundation相伴的框架,叫做CoreFoundation。虽然从技术上讲,CoreFoundation框架不是Objective-C框架,但它确实编写Objective-C应用程序所应熟悉的重要框架,Foundation框架中的许多功能,都可以在此框架中找到对应的C语言API。CoreFoundation与Foundation不仅名字相似,而且还有更为紧密的联系。有个功能叫做“无缝桥接”(toll-free bridging),可以把CoreFoundation中的C语言数据结构平滑转换为Foundation中的Objective-C对象,也可以反向转换。比方说,Foundation框架中的字符串是NSString,而它可以转换为CoreFoundation里与之等效的CFString对象。无缝桥接技术是用某些相当复杂的代码实现出来的,这些代码可以使运行期系统把CoreFoundation框架中的对象视为普通的Objective-C对象。

48. 多用块枚举,少用for循环

首先看下for循环dict肿么做:

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

需要新建一个array用于存key,其实不太方便。但是如果需要反向遍历,for还是蛮方便的。


下面尝试用Objective-C 1.0中的NSEnumerator来遍历:

NSEnumerator 是个抽象基类,其中只定义了两个方法,供其具体子类来实现:

-(NSArray *)allObjects;
-(id)nextObject;

其中关键的方法是nextObject,它返回枚举对象里的下个对象。每次调用该方法时,其内部的数据结构都会更新,使得下次调用方法时能返回下一个对象。等到枚举中得全部对象都已返回之后,再调用就将返回nil,这表示达到枚举末端了。

Foundation框架中内建的collection类都实现了这种遍历方式。例如,想遍历数组,可以这样写代码:

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

遍历字典:
NSDictionary *aDictionary = /* ... */;
NSEnumerator *enumerator = [aDictionary keyEnumerator];
id key;
while ((key = [enumerator nextObject]) != nil)
{
    id value = aDictionary[key];
    // Do something with 'key' and 'value'
}

而且还有反向枚举器:[anArray reverseObjectEnumerator];


Objective-C 2.0引入了快速遍历这一功能。快速遍历与使用NSEnumerator来遍历差不多,然而语法更简洁,它为for循环开设了in关键字。这个关键字大幅简化了遍历collection所需的语法,比方说要遍历数组,就可以这么写:

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

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

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
                                  objects:(id*)stackbuffer
                                    count:(NSUInteger)length;

在当前的Objective-C 语言中美最新引入的一种做法就是基于block来遍历。NSArray中定义了下面这个方法,它可以实现最基本的遍历功能:

- (void)enumerateObjectsUsingBlock:(void(^)(id object, NSUInteger idx, BOOL *stop))block;

在遍历数组及set时,每次迭代都要执行由block参数所传入的快,这个块有三个参数,分别是当前迭代所针对的对象、所针对的下标,以及指向布尔值的指针。前两个参数的含义不言而喻。而通过第三个参数所提供的机制,开发者可以终止遍历操作。例如,下面这段代码用此方法来遍历数组:

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

此方式大大胜过方式的地方在于:遍历时可以直接从block里获取更多信息。在遍历数组时,可以知道当前所针对的下标。遍历有序set(NSOrderedSet)时也一样。而在遍历字典时,无须额外编码,即可同事获取键与值,因而省去了根据给定键来获取对应值这一步。用这种方式遍历字典,可以同时得知键与值,这很可能比其他方式快很多,因为在字典内部的数据结构中,键与值本来就是存储在一起的。

另外一个好处是,能够修改block的方法名,以免进行类型转换的操作,从效果上讲,相当于把本来需要执行的类型转换操作交给block方法签名来做。比方说,要用“快速遍历法”来遍历字典。若已知字典中得对象必为字符串,则可以这样编码:

[aDictionary enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop)
 {
     // Do something with 'key' and 'obj'
 }];

之所以能如此,是因为id类型相当特殊,它可以像本例这样,为其他类型所覆写。要是原来的block签名把键与值都定义成NSObject *,那么些就不行了。此技巧出刊不甚显眼,实则相当有用。指定对象的精确类型之后,编译器就可以检测出开发者是否调用了该对象所不具备的方法,并在发现这种问题时报错。如果能够确知某collection里的对象是什么类型,那就应该使用这种方法指明其类型。

※ Options

- (void)enumerateObjectsWithOptions:(NSEnumerationOptions)options
                         usingBlock:(void(^)(id obj, NSUInteger idx, BOOL *stop))block;

- (void)enumerateKeysAndObjectsWithOptions:(NSEnumerationOptions)options
                                usingBlock: (void(^)(id key, id obj, BOOL *stop))block;

NSEnumerationOptions类型是个enum,其各种取值可用“按位或”(bitwise OR)连接,用以表明遍历方式。

typedef NS_OPTIONS(NSUInteger, NSEnumerationOptions) {
    NSEnumerationConcurrent = (1UL << 0),
    NSEnumerationReverse = (1UL << 1),
};

例如,开发者可以请求以并行方式执行各轮迭代,也就是说,如果当前系统资源状况允许,那么执行每次迭代所用的block就可以并行执行了。通过NSEnumerationConcurrent选项即可开启此功能。如果使用此选项,那么底层会通过GCD来处理并发执行事宜,具体实现时很可能会用到dispatch group。不过,到底如何来实现,不是本条索要讨论的内容。

反向遍历是通过 NSEnumerationReverse选项来实现的。要注意:只有遍历数组或有序set等有顺序的collection时,这么做才有意义。

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

NSArray 是Foundation 框架中表示数组的 Objective-C 类,而CFArray 则是 CoreFoundation 框架中的等价物。这两种创建数组的方式也许有区别,然而有项强大的功能可在这两个类型之间平滑转换,它就是 “无缝桥接”(toll-free bridging)。

使用“无缝桥接”技术,可以在定义于Foundation框架中的Objective-C类和定义于 CoreFoundation 框架中的C数据结构之间相互转换。笔者将C语言级别的API 称为数据结构,而没有称其类或对象,这是因为它们与Objective-C中的类或对象并不相同。例如,CFArray要通过CFArrayRef来引用,而这是指向struct_CFArray的指针。CFArrayGetCount这种函数则可以操作此struct,以获取数组大小。这和Objective-C 中的对应物不同,在Objective-C中,可以创建NSArray对象,并在该对象上调用count方法,以获取数组大小。

下列代码演示了简单的无缝桥接:

NSArray *anNSArray = @[@1, @2, @3, @4, @5];
CFArrayRef aCFArray = (_bridge CFArrayRef)anNSArray;
NSLog(@"Size of array = %li", CFArrayGetCount(aCFArray));
// Output: size of array = 5

转换操作中的 __bridge 告诉ARC 如何处理转换操作所涉及的 Objective-C 对象。

  • __bridge 本身的意思是:ARC 仍然具备这个 Objective-C 对象的所用权。
  • __bridge_retained 则与之相反,意味着ARC 将交出对象的所有权。若是前面那段代码改用它来实现,那么用完数组之后就要加上 CFRelease(aCFArray) 以释放其内存。
  • 反向转换可通过 __bridge_transfer 来实现。比方说,想把 CFArrayRef 转换为 NSArray *,并且想令 ARC 获得对象所有权,那么就可以采用此种转换方式。这三种转换方式称为 “桥式转换”(bridged cast)。

在CoreFoundation层面创建collection时,可以指定许多回调函数,这些函数表示此collection应如何处理其元素。然后,可运用无缝桥接技术,将其转换成具备特殊内存管理语义的Objective-C collection。

也就是改写C底层的一些回调函数,创造一个新的C语言struct,然后把它转为OC对象使用,这样就可以改变默认的行为。例如让NSMutableDictionary不要copy key。

50. 构建缓存时,选用NSCache而非NSDictionary

从因特网下载的图片应如何来缓存。首先能想到的好办法就是把内存中的图片保存到字典里,这样的话,稍后使用时就无须再次下载了。有些程序员会不假思索,直接使用 NSDictionary 来做(准确来说,是使用其可变版本),因为这个类很常用。其实,NSCache 类更好,它是 Foundation 框架专为处理这种任务而设计的。

※ NSCache 胜过 NSDictionary 之处在于:

  • 当系统资源将要耗尽时,它可以自动删减缓存。如果采用普通的字典,那么就要自己编写挂钩,在系统发出“低内存”(low memory)通知时手工删减缓存。而 NSCache 则会自动删减,由于其是 Foundation 框架的一部分,所以与开发者相比,它能在更深的层面上插入挂钩。此外,NSCache 还会先行删减 “最久未使用的”(lease recently used)对象。若想自己编写代码来为字典添加此功能,则会十分复杂。

  • NSCache 并不会“拷贝”键,而是会 “保留”它。此行为用NSDictionary 也可以实现,然而需要编写相当复杂的代码。

  • NSCache 是线程安全的。而 NSDictionary 则绝对不具备此优势。


NSDictionary是线程安全的,但是NSMutableDictionary不是,下面这段是会crash的哈:

NSMutableDictionary *dict = [NSMutableDictionary dictionary];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    for (NSInteger i = 0; i < 100; i++) {
        dict[[NSString stringWithFormat:@"%ld", (long)i]] = @(i);
    }
});

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    for (NSInteger i = 0; i < 100; i++) {
        dict[[NSString stringWithFormat:@"%ld", (long)i]] = @(i);
    }
});

NSDictionary创建了以后就不能改了,只能读所以是很安全的,但是NSMutableDictionary如果多个线程同时修改就会crash啦。

NSMutableArray是一个道理,如果多线程同时改也会crash:

NSMutableArray *muarr = [NSMutableArray array];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    for (NSInteger i = 0; i < 100; i++) {
        muarr[i] = @(i);
    }
});

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    for (NSInteger i = 0; i < 100; i++) {
        muarr[i] = @(i);
    }
});

但是NSCache是线程安全的,下面这段是不会crash滴:

NSCache *cache = [[NSCache alloc] init];
    
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    for (NSInteger i = 0; i < 100; i++) {
        [cache setObject:@(i) forKey:[NSString stringWithFormat:@"%ld", i]];
    }
});

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    for (NSInteger i = 0; i < 100; i++) {
        [cache setObject:@(i) forKey:[NSString stringWithFormat:@"%ld", i]];
    }
});

※ 缓存自动删减控制

开发者可以操控缓存删减其内容的时机。有两个与系统资源相关的尺度可供调整,其一是缓存中的对象总数,其二是所有对象的“总开销”(overall cost)。(向缓存中添加对象时,只有在能很快计算出“开销值”的情况下,才应该考虑采用这个尺度)

开发者在将对象加入缓存时,可为其指定“开销值”。当对象总数或总开销超过上限时,缓存就可能会删减其中的对象了,在可用的系统资源趋于紧张时,也会这么做。然而要注意,“可能”会删减某个对象,并不意味着“一定”会删减这个对象。删减对象时所遵照的顺序,由具体实现来定。这尤其说明:想通过调整“开销值” 来迫使缓存优先删减某对象,不是个好主意。

还有个类叫做 NSPurgeableData,和 NSCache 搭配起来用,效果很好,此类是NSMutableData 的子类,而且实现了 NSDiscardableContent 协议。如果某个对象所占的内存能够根据需要随时丢弃,那么就可以实现该协议所定义的接口。这就是说,当系统资源紧张时,可以把保存 NSPurgeableData 对象的那块内存释放掉。NSDiscardableContent 协议里定义了名为 isContentDiscarded 的方法,可用来查询相关内存是否已释放。

如果需要访问某个 NSPurgeableData 对象。可以调用其beginContentAccess方法,告诉它现在还不应丢弃自己所占据的内存。用完之后,调用endContentAccess方法,告诉它在必要时可以丢弃自己所占据的内存了。这些调用可以嵌套,所以说,它们就像递增与递减引用计数所用的方法那样。只有对象的 “引用计数”为0 时才可以丢弃。

如果将 NSPurgeableData 对象加入NSCache,那么当该对象为系统所丢弃时,也会自动从缓存中移除。通过 NSCache 的 evictsObjectsWithDiscardedContent 属性,可以开启或关闭此功能。

可用NSPurgeableData + NSCache实现如下:

#import "ViewController.h"

@interface ViewController ()
{
    NSCache *_cache;
}
@end

@implementation ViewController

- (instancetype)init
{
    self = [super init];
    if (self) {
        _cache = [[NSCache alloc] init];
        _cache.countLimit = 100;
        _cache.totalCostLimit = 5 * 1024 * 1024;
    }
    return self;
}

- (void)downloadWithURL:(NSURL *)url
{
    NSPurgeableData *cacheData = [_cache objectForKey:url];
    if (cacheData) {
        [cacheData beginContentAccess];
        
        [self useData:cacheData];
        
        [cacheData endContentAccess];
    }else{
        //network init
        //network block -->data
        {
            NSPurgeableData *purgeableData = [[NSPurgeableData alloc] initWithData:data];
            [_cache setObject:purgeableData forKey:url cost:purgeableData.length];
            
            [self useData:cacheData];
            
            [purgeableData endContentAccess];
        }
    }
}

- (void)useData:(NSPurgeableData *)data {}

@end

注意,创建好 NSPurgeableData 对象之后,其 “purge 引用计数” 会多1,所以无须再调用 beginContentAccess 了,然而其后必须调用 endContentAccess,将多出来的这个1抵消掉。

注意只有重新计算很麻烦的,例如网络、磁盘读取数据值得缓存哦。

51. 精简initialize与load的实现代码

※ load

  1. 对于加入运行期系统的类及分类,必定会调用此方法,且仅调用一次。
  2. iOS会在应用程序启动的时候调用load方法,在main函数之前调用
  3. 执行子类的load方法前,会先执行所有超类的load方法,顺序为父类->子类->分类,则先调用类里面的,再调用分类里面的
  4. 在load方法中使用其他类是不安全的,因为会调用其他类的load方法,而如果关系复杂的话,就无法判断出各个类的载入顺序,类只有初始化完成后,类实例才能进行正常使用
  5. load 方法不遵从继承规则,如果类本身没有实现load方法,那么系统就不会调用,不管父类有没有实现(跟下文的initialize有明显区别)
  6. 尽可能的精简load方法,因为整个应用程序在执行load方法时会阻塞,即,程序启动会阻塞直到所有类的load方法执行完毕,才会继续
  7. load 方法中最常用的就是方法交换method swizzling

※ initialize

  1. 对于每个类来说,在首次使用该类之前,且仅调用一次。它是由运行期系统来调用的,绝不应该通过代码直接调用。
  2. 惰性调用,只有当程序使用相关类(该类或子类)时才会调用,因此,如果某个类一直都没有使用,那么initialize方法就一直不会运行。
  3. 运行期系统会确保initialize方法是在线程安全的环境中执行,即只有执行initialize的那个线程可以操作类或类实例。其他线程都要先阻塞,等待initialize执行完。
  4. 如果类未实现initialize方法,而其超类实现了,那么会运行超类的实现代码,而且会运行两次,因为初始化子类的的时候会先初始化父类,然后会调用父类的initialize方法,而子类没有覆写initialize方法,因此会再次调用父类的实现方法,鉴于此,initialize方法实现如下:
+ (void)initialize {
  if (self == [People class]) {
    NSLog(@"%@ initialize", self);
  }
}
  1. initialize方法也需要尽量精简,只应该用来设置内部数据:比如,某个全局状态无法在编译期初始化,可以放在initialize里面。
static NSMutableArray *kSomeObjects;

@implementation People
+ (void)initialize {
  if (self == [People class]) {
    kSomeObjects = [NSMutableArray new];
  }
}

整数可以在编译期定义,然而可变数组不行,因为它是个OC对象,所以创建实例只之前必须先激活运行期系统。注意,某些OC对象可以在编译期创建,例如NSString实例。然而,创建下面这种对象会令编译器报错:

static NSMutableArray *kSomeObjects = [NSMutableArray new];
  1. initialize应该只用来设定内部数据
    如果某个类的实现代码很复杂,那么其中可能会直接或间接用到其他类。若那些类尚未初始化,则系统会迫使其初始化。然而,本类的初始化方法此时尚未运行完毕。其他类在运行其initialize方法时,有可能会依赖本类中的某些数据,而这些数据此时也许还未初始化好。

  2. 对于分类中的initialize方法,会覆盖该类的initialize方法。
    这里有个区别,如果是load的话,分类的load和本类的load不重复,都会执行,相当于分类也有自己的load哦,但initialize是覆盖的

52. 别忘了NSTimer会保留其目标对象

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

此方法创建出来的计时器,会在指定的间隔时间之后执行任务。也可以令其反复执行任务,直到开发者稍后将其手动关闭为止。target和selector表示在哪个对象上调用哪个方法。计时器会保留目标对象,等到自身失效时再释放target对象。执行完任务后,一次性计时器会失效,若repeats为YES,那么必须调用invalidate方法才能使其停止。

计时器会保留目标对象,也就是说重复执行模式的计时器,很容易引入保留环(self持有timer,timer持有self)。这个问题可以通过block解决:

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

@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();
    }
}

这个办法为何能解决“保留环”问题呢?大家马上就会明白。这段代码将计时器所应执行的任务封装成“块”,在调用计时器函数时,把它作为 userInfo 参数传进去。该参数可用来存放“不透明值”(opaque value)(不指明具体用途的值,可以理解为“万能值”),只要计时器还有效,就会一直保留着它。传入参数时要通过 copy 方法将 block 拷贝到 “堆”上,否则等到稍后要执行它的时候,该块可能已经无效了。计时器现在的 target 是 NSTimer 类对象,这是个单例,因此计时器是否会保留它,其实都无所谓。此处依然有保留环,然而因为类对象(class object)无须回收,所以不用担心。

相关文章

网友评论

      本文标题:[iOS] Effective Objective-C ——系统

      本文链接:https://www.haomeiwen.com/subject/ueqimctx.html