Effective Objective-C 2.0 第七章 系统

2019-05-23  本文已影响0人  Vergil_wj

第 47 条 熟悉系统框架

我之前这篇文章iOS 框架有介绍过,下面简单介绍下书中内容。

将一系列代码封装为动态库,并在其中放入描述其接口的头文件,这样做出来的东西就叫框架。

在 iOS 上的框架为 Cocoa Touch 框架,其实其本身并不是框架,但是里面集成了一批创建应用程序时经常会用到的框架。

我们开发者碰到的主要框架就是 FoundationUIKit两大框架。其中 Foundation 框架是 Objective-C 应用程序的“基础”。

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

遍历 collection 有四种方式。最基本的方法是for循环,其次是 NSEnumerator 遍历法及快速遍历法,最新最先进的的方式则是“block枚举法”。

1、for 循环

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

//遍历字典
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'
}

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

在遍历字典或者 Set 时,有点稍复杂,需要先获取字典中所有的键或 Set 中所有的对象,会增加额外的开销。

2、NSEnumerator

//数组
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'
}

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

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

与 for 循环相似,但在遍历数组时无法拿到下标,而我们一般开发时都会需要用到下标。

3、快速遍历法

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

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

// Set
NSSet *aSet = /* ... */;
for (id object in aSet)
{
    // Do something with 'object'
}

这种办法是简单且效率高的,然而如果在遍历字典时需要同时获取键与值,那么会多出来一步。而且,与传统for循环不同,这种遍历方式无法轻松获取当前遍历操作所针对的下标。遍历时通常会用到这个下标,比如很多算法都需要它。

4、基于 block 遍历方式

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

//字典
NSDictionary *aDictionary = /* ... */;
[aDictionary enumerateKeysAndObjectsUsingBlock:^(id key, id object, BOOL *stop)
{
    // Do something with 'key' and 'object'
    if (shouldStop)
    {
        *stop = YES;
     }
}];

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

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

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

通过无缝桥接技术,可以在Foundation框架中的OC对象与Core Foundation框架中的C语言数据之间来回转换。

NSArray *arr = @[@"1",@"2",@"3"];
CFArrayRef  aCFArray = (__bridge CFArrayRef)arr;

进行转换操作的修饰符共有3个:

OC对象 -> CF
__bridge // ARC不交出对象的所有权
__bridge_retained // ARC交出对象的所有权,手动管理内存,需要调用 CFRelease() 进行释放。
 
CF -> OC对象
__bridge_transfer // ARC获得对象的所有权,自动管理内存

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

NSCache胜过NSDictionary的之处在于:

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

+(void)load

1、对于加入运行期系统的类及分类,必定会调用此方法,且仅调用一次。

2、iOS会在应用程序启动的时候调用load方法,在main函数之前调用。

3、执行子类的load方法前,会先执行所有超类的load方法,顺序为父类->子类->分类,则先调用类里面的,再调用分类里面的。

4、在load方法中使用其他类是不安全的,因为会调用其他类的load方法,而如果关系复杂的话,就无法判断出各个类的载入顺序,类只有初始化完成后,类实例才能进行正常使用。

5、load 方法不遵从继承规则,如果类本身没有实现load方法,那么系统就不会调用,不管父类有没有实现(跟下文的initialize有明显区别)。

6、尽可能的精简load方法,因为整个应用程序在执行load方法时会阻塞,即,程序会阻塞直到所有类的load方法执行完毕,才会继续。

7、load 方法中最常用的就是方法交换method swizzling。

+(void)initialize

1、对于每个类来说,在首次使用该类之前,且仅调用一次.它是由运行期系统来调用的,绝不应该通过代码直接调用。

2、惰性调用,只有当程序使用相关类(该类或子类)时,才会调用,因此,如果某个类一直都没有使用,那么initialize方法就一直不会运行。

3、运行期系统会确保initialize方法是在线程安全的环境中执行,即,只有执行initialize的那个线程可以操作类或类实例。其他线程都要先阻塞,等待initialize执行完。

4、如果类未实现initialize方法,而其超类实现了,那么会运行超类的实现代码,而且会运行两次。

5、initialize方法也需要尽量精简,只应该用来设置内部数据:
比如,某个全局状态无法在编译期初始化,可以放在initialize里面。

6、对于分类中的initialize方法,会覆盖该类的initialize方法。

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

NSTimer 对象会保留其目标,若要释放其目标需要计时器本身失效,失效有两种方式:

使用计时器反复执行任务时,通常会造成循环引用。如果我们对代码逻辑非常清楚,则手动调用 invalidate 方法就可以了,但还有更好的方法使我们我无须关注目标对象释放问题。

我们通常的写法:

@interface EOCClass : NSObject
- (void)startPolling;
- (void)stopPolling;
@end
 
@implementation EOCClass{
    NSTimer *_poliTimer;
}
 
- (id) init{
    return [super init];
}
 
- (void)dealloc{
    [_poliTimer invalidate];
}
 
- (void)stopPolling{
    [_poliTimer invalidate];
    _poliTimer = nil;
}
 
- (void)startPolling{
    _poliTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(p_doPoll) userInfo:nil repeats:YES];
}
 
- (void)p_doPoll{
    // code

}

调用stopPolling方法或令系统将实例回收(会自动调用dealloc方法)可以使计时器失效,从而打破循环,但无法确保startPolling方法一定调用,而由于计时器保存着实例,实例永远不会被系统回收。当EOCClass实例的最后一个外部引用移走之后,实例仍然存活,而计时器对象也就不可能被系统回收,除了计时器外没有别的引用再指向这个实例,实例就永远丢失了,造成内存泄漏。

优化后的方法:

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

再修改 startPolling 方法:

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

这段代码先定义了一个弱引用指向self,然后用块捕获这个引用,这样self就不会被计时器所保留,当块开始执行时,立刻生成strong引用,保证实例在执行器继续存活。

采用这种写法之后,如果外界指向 EOCClass 实例的最后一个引用将其释放,则该实例就可为系统所回收了。回收过程中还会调用计时器的 invalidate 方法,这样的话,计时器就不会再执行任务了。此处使用 weak 引用还能令程序更加安全,因为有时开发者可能在编写 dealloc 时忘了调用计时器的 invalidate 方法,从而导致计时器再次运行,若发生此类情况,则块里的 weakSelf 会变成 nil。

要点:

上一篇下一篇

猜你喜欢

热点阅读