《Effective Objective-C 2.0》读书笔记---第七章


前言:


虽说不使用系统框架也能编写OC代码,但几乎没人这么做,即便是NSObject这个标准的根类,也属于Foundation框架,而非语言本身。若不使用Foundation,就必须自己编写根类,同时还要自己来写collection、事件循环、以及其他会用到的类。


第47条:熟悉系统框架


用户升级操作系统后,你所开发的应用程序也可以使用最新版的系统库了。将一系列代码封装为动态库,并在其中放入描述接口的头文件,这样做出来的东西就叫框架。iOS系统框架都使用动态库。iOS程序不允许在其中包含动态库,因此第三方框架都要使用静态库。


Cocoa框架(iOS上称为Cocoa Touch)本身不是框架,只是集成了一批创建应用程序会用到的框架。


“无缝桥接”(toll-free bridging),可以把CoreFoundation中的C语言数据结构平滑转换为Foundation中的OC对象,也可以方向转换,这里参考《理解__bridge,__bridge_transfer和__bridge_retained》无缝桥接技术是用某些相当复杂的代码实现出来的,这些代码可以使运行期系统把CoreFoundation框架中的对象视为普通的Objective-C对象。


用C语言来实现API的好处是,可以绕过Objective-C的运行期系统,从而提升执行速度。当然,由于ARC只负责Objective-C的对象,所以使用这些API时尤其要注意内存管理问题。


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


collection也就是我们常用的NSArray、NSDictionary、NSSet类型


有以下4种遍历方式:


第一种:for循环


第二种:NSEnumerator遍历


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

- (NSArray*)allObjects;

- (nullable ObjectType)nextObject;


使用如下:

//Array

    NSArray *anArray =/* ... */;

    NSEnumerator *enumerator = [anArray objectEnumerator];//反向遍历用reverseObjectEnumerator

    id object;

    while ((object = [enumerator nextObject]) != nil)

    {

        // Do something with 'object'

    }


// Dictionary

    NSDictionary *aDictionary =/* ... */;

    NSEnumerator *enumerator = [aDictionary keyEnumerator];

    id key;

    while ((key = [enumerator nextObject]) != nil)

    {

        id value = aDictionary[key];

        // Do something with 'key' and 'value'

    }


第三种:快速枚举


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

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state

                                  objects:(id*)stackbuffer

                                    count:(NSUInteger)length;



第四种:基于块的遍历方式


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

- (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)连接,用以表明遍历方式。例如,开发者可以请求以冰法方式执行各轮迭代,也就是说,如果当前系统资源状况允许,那么执行每次迭代所用的block就可以并行执行了。通过NSEnumerationConcurrent选项即可开启此功能。如果使用此选项,那么底层会通过GCD来处理冰法执行事宜,具体实现时很可能会用到dispatch group。


扩展:

他们的效率测试

我枚举相同的数组并打印,所用的时间如下:

0.00382315 s  —->方法一

0.0019234 s  —->方法二

0.00197317 s  —->方法三

0.00178474 s  —->方法四


可见使用块的优势所在


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


这里参考《理解__bridge,__bridge_transfer和__bridge_retained》


这里还有个知识点就是通过桥接技术使NSDictionary不拷贝键,实现如下:


#import <Foundation/Foundation.h>

#import <CoreFoundation/CoreFoundation.h>


const void* EOCRetainCallback(CFAllocatorRef allocator,

                              const void *value)

{

    return value;

}


void EOCReleaseCallback(CFAllocatorRef allocator,

                        const void *value)

{

    CFRelease(value);

}


CFDictionaryKeyCallBacks keyCallbacks = {

    0,

    EOCRetainCallback,

    EOCReleaseCallback,

    NULL,

    CFEqual,

    CFHash

};


CFDictionaryValueCallBacks valueCallbacks = {

    0,

    EOCRetainCallback,

    EOCReleaseCallback,

    NULL,

    CFEqual

};


CFMutableDictionaryRef aCFDictionary =

        CFDictionaryCreateMutable(NULL,

                              0,

                              &keyCallbacks,

                              &valueCallbacks);


NSMutableDictionary *anNSDictinary =

    (__bridge_transfer NSMutableDictionary *)aCFDictionary;


在CoreFoundation层创建字典,修改内存管理语义,对键执行“保留”而非“拷贝”操作。


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


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


NSCache并不会“拷贝”键,而是会“保留”它。此行为用NSDictionary也可以实现,然而需要编写相当复杂的代码。(第49条)NSCache对象不拷贝键的原因在于:很多时候,键都是由不支持拷贝操作的对象来充当的。另外,NSCache是线程安全的。而NSDictionary则绝对不具备此优势。(备注:本人觉得这里的NSDictionary应该是说的可变的NSMutableDictionary,因为NSDictionary是线程安全的,而且不可变


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


向缓存中添加对象时,只有在能很快计算出”开销值“的情况下,才应该考虑采用这个尺度。若计算过程很复杂,那么照这种方式来使用缓存就达不到最佳效果了。


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


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


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


使用如下:

-(void)downloadDataForURL:(NSURL *)url{
    NSPurgeableData *cachedData = [_cache objectForKey:url];
    if(cachedData){
        //Stop the data being purged
        [cachedData beginContentAccess];

        //Use the cached data
        [self useData:cachedData];

        //Mark that the data may be purged again
        [cachedData 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:data.length];//这里应该要调用这个有cost的方法,totalCostLimit才会有效,因为它不会自己去计算缓存大小

            //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
            [purgeableData endContentAccess];
        }];
    }
}

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


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


+ (void)load;

对于加入运行期系统中的每个类及分类来说,必定会调用此方法,而且仅调用一次。不管你的程序有没有用到该类。在main前调用。先调用类里的,再调用分类里的。


在执行子类的load方法之前,必定会先执行所有超类的load方法,而如果代码还依赖了其他程序库,那么程序库里相关类的load方法也必定会先执行。然而,根据某个给定的程序库,却无法判断出其中各个类的载入顺序。因此,在load方法中使用其他类是不安全的。


load并不遵从继承规则,如果某个类本身没实现load方法,不会继而去查找并调用超类的。超类的load方法会在加载超类信息时调用(如果实现了的话)。


而且load方法务必实现得精简一些,也就是要尽量减少其所执行的操作,因为整个应用程序在执行load方法时都会阻塞。如果load方法中包含繁杂的代码,那么应用程序在执行期间就会变得无响应。不要在里面等待锁,也不要调用可能会加锁的方法。总之,能不做的事情就别做。


+ (void)initialize;


对于每个类来说,该方法会在程序首次用该类之前调用,且只调用一次。它是由运行期系统来调用的,绝不应该通过代码直接调用。如果整个程序都没有用到该类,自然不会调用这个方法了。在mian启动后调用。


initialize方法与其他消息一样,如果某个类未实现它,而其超类实现了,那么就会运行超类的实现代码。因此,通常这样来实现

+(void)initialize

{

    if(self = [EOCBaseClass class])

    {

        NSLog(@"%@ initialized",self);

    }

}


运行期系统也能保证initialize方法一定会在”线程安全的环境“中执行,这就是说,只有执行initialize的那个线程可以操作类或类实例。其他线程都要先阻塞,等着initialize执行完。


load与initialize方法都应该实现得精简一些,这有助于保持应用程序的响应能力,也能减少引入”依赖环“(interdependency cycle)的几率。


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


NSTimer对象会保留其目标,直到计时器本身失效为止,调用invalidate方法可令计时器失效,另外,一次性的计时器在触发完任务之后也会失效。开发者若将计时器设置成重复执行模式,那么必须自己调用invalidate方法,才能令其停止。


由于计时器会保留其目标对象,所以设置成重复执行模式的那种计时器,很容易引入“保留环”。解决的方法如下:

#import <Foundation/Foundation.h>
@interface NSTimer(EOCBlockSupport)
+(NSTimer*) eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats;
@end
@implementation NSTimer(EOCBlockSupport)
+(NSTimer*) eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats{
    return [self scheduledTimerWithTimerInterval: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

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

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

猜你喜欢

转载自blog.csdn.net/junjun150013652/article/details/53843253