系统框架

2020-11-25  本文已影响0人  晨阳Xia

第47条 熟悉系统框架

什么叫做框架

将一系列代码封装为动态库(dynamic library),并在其中放入描述其接口的头文件,这样做出来的东西就叫框架。
在Mac OS X或iOS系统开发“带图形界面的应用程序”(graphical application)时,会用到名为Cocoa的框架。在iOS上称为Cocoa Touch。其实Cocoa本身不是框架,但是里面集成了一批创建应用程序时经常会用到的框架
开发者会碰到的主要框架就是Foundation,像是NSObject,NSArray,NSDictionary等类都在其中。Foundation框架中的类,使用NS这个前缀,此前缀是在Objective-C语言用作NeXTSETP操作系统的编程语言是首度确定的。Foundation框架真可谓所有Objective-C应用程序的基础。
还有个与Foundation相伴的框架,叫做CoreFoundation。虽然从技术上讲,CoreFoundation框架不是Objective-C框架,但它确实编写Objective-C应用程序时所应熟悉的重要框架。Foundation框架中的许多功能,都可以在此框架中找到对应的C语言API.

CoreFoundation

CoreFoundation与Foundation不仅名字相似,而且还有更为紧密的联系。有个功能叫“无缝桥接”(toll-free bridging)。可以把CoreFoundation中的C语言数据结构平滑转换为Foundation中的Objective-C对象,也可以反向转换。比如:Foundation框架中的字符串是NString,而它可以转换为CoreFoundation里与之等效的CFString对象。

CoreFoundation 的注意事项

由于ARC只负责Objective-C的对象,所以使用这些API时尤其需要注意内存管理问题。

UI框架

Mac OS X和iOS这两个平台的核心UI框架分别叫做AppKit及UIkit,他们都提供了构建在Foundation与CoreFoundation只上的Objective-C类。

要点

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

根据定义,字典和set都是“无序的”(unordered),所以无法根据特定的证书下标来直接访问其中的值。于是,就需要先获取字典里的所有键或是set里的所有对象,这两种情况下,都可以在获取到的有序数组上遍历,以便借此访问元字典及原set中的值。创建这个附加数组会有额外开销,而且还会错创建一个数组对象,他会保留collection中的所有元素的对象。当然了,释放数组时这些附加对象也要释放,可是要调用本来不需执行的方法。

使用Objective-C的NSEnumerator来遍历

API: NSArray ,NSSet,NSDictionry对象可以直接调用以下API
- (NSENumerator *)objectEnumerator;- (NSENumerator *)keyEnumerator
`- (NSEnumerator *)reverseObjectEnumrator;

基于块的遍历

// 遍历数组
- (void)enumeratorObjectUsingBlock:(void(^)(id object, NSInteger idx, BOOL *stop))block; // 遍历字典- (void)enumerateKeysAndObjectsUsingBlock:(void(^)(id key, id object, BOOL *stop))block;
// 遍历set
- (void)enumerateObjectsUsingBlock(void(^)(id object, BOOL *stop))block;- (void)enumerateObjectsWithOptions:(NSEnumerationOptions)options usingBlock:(void(^)(id obj, NSUInteger idx, BOOL *stop))block;
`- (void)enumerateKeysAndObjectsWithOptions:(NSEnumerationOptions)options usingBlock:(void(^)(id key, id object, BOOL *stop))block;

基于块的遍历的好处

  1. 遍历时可以直接从块里获取更多信息。
  2. 遍历字典时无需额外编码,即可同时获取键与值,因而省去了根据给定键获取对应值这一步。这很可能比其他方式快很多。
  3. 能够修改快的签名,以免进行类型转换操作,从效果上讲,相当于把本来需要执行的类型转换操作交给块方法签名来做。
    例子:
    NSDictionry *aDictionry = /**/
    [aDictionry enuemrateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop)(
        // Do something with 'key' and 'obj'
    )];
    
  4. NSEnumberationOptions类型是一个enum,其各种值可用“按位或”(bitwise OR)连接,用以表明遍历方式。例如,开发者可以请求并发方式执行各轮迭代,也就是说,如果资源状况允许,那么执行每次迭代所用的块就可以并行执行了。通过NSEnumerationConcurrent选项即可开启此功能。如果使用此选项,那么底层会通过GCD来处理并发执行事宜。

要点

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

要点

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

NSCatch 与 NSDictionary相比,优势在哪儿?

  1. 当系统资源将要耗尽时,他可以自动删减缓存
  2. NSCatch还会先行删减“最久未使用”(lease recently used)对象
  3. NSCatch并不会“拷贝”键,而是会“保留”它。
  4. NSCatch是线程安全的

NSPurgeableData

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

NSCatch搭配NSPurgeableData使用

如果需要访问某个NSPurgeableData对象,可以调用beginContentAccess方法,告诉他现在还不应该丢弃自己所占据的内存。用完之后,调用endContentAccess方法,告诉他在必要时可以丢弃自己所占据的内存了。这些调用可以嵌套,所以说,他们就像递增和递减引用计数所用的方法那样。
如果将NSPurgeableData对象加入NSCatch,那么当该对象为系统所丢弃时,也会自动从缓存中移除。通过NSCache的evicsObjectsWithDiscardedContent属性,可以开启或关闭此功能。

`- (void)downLoadDataWithUrl:(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:prugeableData.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
              [purgeableData endContentAccess];  
        }];
     }
}

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

要点

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

+ (void)load

对于加入运行期系统的每个类(class)和 分类(category)来说,必定会调用此方法,而且仅调用一次。当包含类或分类的程序载入系统时,就会执行此方法,而这通常就是指应用程序启动的时候,若程序是为iOS平台设计的,则肯定会在此时执行。如果分类和其所属的类都定义了load方法,则先调用类里的,在调用分类里的。
load方法的问题在于,执行该方法时,运行期系统处于“脆弱状态”(fragile state)。在执行子类的load方法之前,必定会执行所有超类的load方法。如果代码还依赖了其他程序库,那么程序库里相关的load方法也必定会先执行。然而,根据某个给定的程序库,却无法判断出其中各个类的载入顺序。因此,在load方法中使用其他类时不安全的。比方说:

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

@interface EOCClassB : NSObject
@end
@implementation EOCClassB
+ (void)load {
   NSLog(@"Loading ClassB");
   EOCClassA *object = [[EOCClassA alloc] init];
}

此处使用NSLog没问题,而且相关字符也会照常纪记录,因为Foundation框架肯定在运行load方法之前就已经载入系统了。但是在EOCClassB的load方法里使用EOCClassA却不太安全,因为无法确定在执行EOCClassB的load方法之前,EOCClassA是不是已经加载好了。可以相见:EOCClassA这个类,也许会在其load方法中执行某些重要操作,只有执行完这些操作之后,该实力才能正常使用。
有个重要的事情需要注意,那就是load方法并不像普通的方法那样,他并不遵守那套继承规则。如果某个类本身没实现load方法,那么不管其各级超类是否实现此方法,系统都不回调用。此外,分类和其所属的类里,都可能出现load方法。此时两种实现代码都会调用,类的实现要比分类先执行。

为什么要精简load里的代码

  1. 因为整个应用程序在执行load方法时都会阻塞。如果load方法中包含繁杂的代码,那么应用程序在执行期间就会变得无响应。
  2. 不要在里面等待锁,也不要调用可能会加锁的方法。总之能不做的事精就别做。实际上,凡是想通过load在类加载之前执行某些任务的,基本都做的不太对。其真正用途仅在于调试程序,比如在分类类里编写此方法,用来判断该分类是否已经正确载入系统中。也许此方法一度很有用处,但现在完全可以说:时下编写Objective-C代码时,不需要它。

+(void)initialize

+(void)load 的调用时间在 +(void)initalize之前。
对于每个类来说,该方法会在程序首次用该类之前调用,且至调用一次。它是由运行期系统来调用的,绝不应该通过代码直接调用。

+ (void)initialize 与 +(void)load的区别

  1. +(void)load 执行在 +(void)initialize之前

  2. +(void)initialize是“惰性调用的”,也就是说,只有当程序用到了相关的类时,才会调用。因此,如果某个类一直都没有用,那么其initialize方法就一直不会运行。这也就等于说应用程序无需先把每个类的initialize都执行一遍。这与load不同,对于load来说,应用程序必须阻塞并等着所有类的load都执行完,才能继续。

  3. 运行期系统在执行该方法时,是处于正常状态的,因此,从运行期系统完整度上来讲,此时可以安全使用并调用任意类中的任意方法。而且,运行期系统也能确保initialize方法一定会在“线程安全的环境”(thread-safe environment)中执行,这就是说,只有执行initialize的那个线程可以操作类或类实例。其他线程都要先阻塞,等着initialize执行完。

  4. initialize方法与其他消息一样,如果某个类为实现它,而其他超类实现了,那么就会运行超类的实现代码。

    # import <Foundation/Foundation.h>
    
    @interface EOCBaseClass : NSObject
    @end
    @implementation EOCBaseClass
    + (void)initialize {
       NSLog(@"%@ initialize",self);
    }
    @end
    
    @interface EOCSubClass : EOCBaseClass
    @ end 
    
    @implementation EOCSubClass
    @end
    

    即便EOCSubClass类没有实现initialize方法,他也会收到这条消息。由各级超类所实现的initialize也会先行调用。所以,首次使用EOCSubClass时,控制台会输出如下消息:

    EOCBaseClass initialize
    EOCSubClass initialize
    

+ (void)initialize注意

  1. 看过load与initialize方法的这些特征之后,又回到早前提过的那个主要问题上,也即是这两个方法的实现代码尽量精简。在里面设置一些状态,使本类能够正常运作就可以了,不要执行那种耗时太久或需要枷锁的任务。对于某个类来说,任何线程都有可能成为初次用到此方法的线程,并导致其初始化,如果这个线程碰巧是UI线程,那么初始化期间就会一直阻塞,导致该应用程序无响应。

  2. 开发者无法控制类的初始化时机。类在首次使用之前,肯定要初始化,但编写程序时不能令代码依赖特定的时间点,否则很危险。

  3. 如果某个类的实现代码很复杂,那么其中可能会直接或间接用到其他类。若那些类尚未初始化,则系统会迫使其初始化。然而本类初始化方法尚未运行完毕。其他类在运行其initialize方法时,有可能会依赖本类中的某些数据,而这些数据此时也许还未初始化好。例如

    #import <Foundation/Foundation.h>
    static id EOCClassAInternalData;
    @interface EOCClassB : NSObject
    @end
    
    @implementation EOCClassA
    + (void)initialize {
       if (self == [EOCClassA class]) {
           [EOCClassB doSomethingThatUsesInternalData];
           EOCClassAInternalData = [self setupInternalData];
       }
    }
    @end
    
    @implementation EOCClassB
    + (void)initialize {
       if (self == [EOCClassB class]){
          [EOCClassA doSomethingThatUsesInternalData];
          EOCClassBInternalData = [self setupInternalData];
       }
    }
    @end
    
    

    若是EOCClassA先初始化,那么EOCClassB随后也会初始化,他会在自己的初始化方法中调用EOCClassA的doSomethingThatUsesInternalData,而此时EOCClassA内部的数据还没准备好。

  4. 所以说initialize方法只应该用来设置内部数据。不应该在其中调用其他方法,即便是本类中自己的方法,也最好别调用。因为稍后可能还要给那些方法里添加更多功能,如果初始化过程中调用他们,那么还是有可能导致上面的问题。若某个全局状态无法在编译期初始化,则可以放在initialize里来做。如下:

     // EOCClass.h
     #import <Foundation/Foundation.h>
     
     @interface EOCClass : NSObject
     @end
     
     // EOCClass.m
     #import "EOCClass.h"
     static const int kInterval = 10;
     static NSMutableArray *kSomeObjects;
     
     @implementation EOCClass
     +(void)initialize {
        if (self == [EOCClass class]) {
            kSomeObjects = [[NSMutableArray alloc] init];
        }
     }
     @end
    
    

    整数可以在编译器定义,数组不可以,因为他是个Objective-C对象,所以创建实例之前必须先激活运行期系统。某些Objective-C对象也可以在编译器创建,比如NSStirng实例。然而下面这种对象慧玲编译器报错

    static NSMutableArray *kSomeojects = [[NSMutableArray alloc] init];
    

+(void)initialize的用处

  1. 初始化编译器不能创建的全局状态
  2. 除了初始化全局状态之外,如果还有其他事情要做,那么可以专门创建一个方法来执行这些操作,并要求该类的使用者必须在使用本类之前调用此方法。比如说,如果“单例类(singleton class)”在首次使用之前必须执行一些操作,那就可以采用这个办法。

要点

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

计时器要和“运行循环”相关联,运行循环到时候会出发任务。创建NSTimer是,可以将其“预先安排”在当前的运行循环中,也可以创建好,然后由开发者自己来调度。无论哪种方式,只有把计时器放在运行循环里,他才能正常出发任务。
解决方式

#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:(NSTimerInterval)interval block:(void(^)())block repeats:(BOOL)repeats {
      return [self scheduletTimerWithInterval:interfal target:self selector:@selector(eoc_blockInvoke:) userInfo:([block copy]) repeats:repeats];
}

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

使用分类的过程
// self持用NSTimer,NSTimer持由block,block持有self,但是是若引用,所以构不成环
-(void)startPolling {
       __weak EOCClass *weakSelf = self;
       _pollTimer = [NSTimer eoc_scheduleTimerWithTimeInteraval:5.0 block:^{
       [weakSelf p_doPoll];
       } repeats:yes]
}

要点

上一篇 下一篇

猜你喜欢

热点阅读