美文网首页
编写高质量iOS与OSX代码的52个有效方法-第七章-系统框架

编写高质量iOS与OSX代码的52个有效方法-第七章-系统框架

作者: 竹与豆 | 来源:发表于2018-07-27 17:23 被阅读18次

    47、熟悉系统框架

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

    图形界面用到名为Cocoa的框架。

    常用到的主要框架就是Foundation,框架中的类使用NS前缀。是所有OC应用程序的基础。不仅提供了collection等基础核心功能,还提供字符串处理这种复杂功能。

    CoreFoundation不是OC框架,但却是辨析OC应用程序的重要框架,Foundation中的许多功能,都可以在此框架中找到对应的C语言API。它与Foundation有着紧密的联系。

    无缝桥接(toll-free bridging),可以把CoreFoundation中的C语言数据结构平滑转换为Foundation的OC对象,也可以发转换。比如说,Foundation中的NSString,可以转换为CoreFoundation中与之等效的CFString对象。无缝桥接技术,可以试运行期系统把CoreFoundation框架中的对象视为普通的OC对象。

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

    CoreAudio 提供C语言API可用来操作设备上的音频硬件。同时也抽象出一套OC的API。

    AVFoundation 提供OC对象可用来处理视频及音频。

    CoreData 提供OC接口可将对象放入数据库,便于持久保存。CoreData可以处理数据的获取及存储。

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

    OC变成的一项重要特点:经常使用底层C语言级别API。用C语言来实现API的好处是可以绕过OC的运行期系统,从而提升执行速度。但ARC只负责OC对象,所以用这些API要注意内存管理问题。若想使用这种框架,一定的熟悉C语言基础。

    UIKit 提供了构架在Foundation和CoreFoundation之上的OC类,框架含有UI元素,也含有粘合机制,领开发者可以将所有相关内容组装成应用程序。在这些主要UI框架之下,是CoreAnimation和CoreGraphics框架。

    CoreAnimation是OC语言写成的,提供一些工具,UI框架则用这些工具来渲染图形并播放动画。CoreAnimation本身不是框架,是QuartzCore框架的一部分。

    CoreGraphics框架以C语言写成,提供了2D渲染所必备的数据结构与函数。如定义了CGPoint,CGSize,CGRect等数据结构。

    MapKit框架,提供地图功能。

    Social框架,提供社交网络功能。


    • 许多系统框架可以直接使用,其中最重要的是Foundation和CoreFoundation,这两个框架提供了构建应用程序所需的许多核心功能。
    • 很多常见任务都能用框架来做,如音频与视频处理、网络通信、数据管理等。
    • 纯C编写的框架和用OC编写的一样重要,要掌握C语言的核心概念!!!

    多用块枚举,少用for循环

    for循环

    - (void)forArray:(NSArray *)array {
        for (int i = 0; i < array.count; i ++) {
            NSString *str = array[i];
            NSLog(@"obj:%@",str);
        }
        
    }
    - (void)forDictionary:(NSDictionary *)dict {
        NSArray *keys = [dict allKeys];
        for (int i = 0; i < keys.count; i ++) {
            NSString *key = keys[i];
            NSString *value = dict[key];
            NSLog(@"key:%@  -- value:%@",key,value);
        }
    }
    - (void)forSet:(NSSet *)set {
        NSArray *array = [set allObjects];
        for (int i = 0; i < array.count; i ++) {
            NSString *str = array[i];
            NSLog(@"obj:%@",str);
        }
    }
    

    根据定义字典有set都是无需的,所以无法根据特定的整数下标来直接访问其中的值。需要先获取字典中的键或是set里的所有对象,然后在获取到的有序数组上遍历,借此访问字典及原set中的值。

    for循环也可以实现反向遍历,计数器的值从元素个数减1开始,每次迭代减1,直到0为止。执行反向遍历时,for循环会比其他方式简单许多。

    NSEnumerator遍历

    NSEnumerator是个抽象基类,定义两个方法,供其子类实现。

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

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

    Foundation中的collection都实现了这种遍历方法。

    - (void)forArray:(NSArray *)array {
        NSLog(@">>>>>>>> Array <<<<<<<<<<");
        
        NSEnumerator *enumerator = [array objectEnumerator];
        NSString *string;
        while ((string = [enumerator nextObject]) != nil) {
            NSLog(@"obj:%@",string);
        }
        
    }
    - (void)forDictionary:(NSDictionary *)dict {
        NSLog(@">>>>>>>> Dictionary <<<<<<<<<<");
        
        NSEnumerator *enumerator = [dict keyEnumerator];
        NSString *key;
        while ((key = [enumerator nextObject]) != nil) {
            NSString *value = dict[key];
            NSLog(@"key:%@  -- value:%@",key,value);
        }
    }
    - (void)forSet:(NSSet *)set {
        NSLog(@">>>>>>>> Set <<<<<<<<<<");
        
        NSEnumerator *enumerator = [set objectEnumerator];
        NSString *string;
        while ((string = [enumerator nextObject]) != nil) {
            NSLog(@"set:%@",string);
        }
    }
    

    NSEnumerator有多种枚举器可供使用。比如说,有反向遍历数组所用的的枚举器。用它遍历,则反方向迭代collection中的元素。

    NSEnumerator *reEnumerator = [array reverseObjectEnumerator];
    NSString *string1;
    while ((string1 = [reEnumerator nextObject]) != nil) {
        NSLog(@"obj:%@",string1);
    }    
    

    快速遍历

    for...in,大幅简化了遍历collection所需的语法。

    - (void)forArray:(NSArray *)array {
        NSLog(@">>>>>>>> Array <<<<<<<<<<");
        for (NSString *string in array) {
            NSLog(@"%@",string);
        }
    }
    - (void)forDictionary:(NSDictionary *)dict {
        NSLog(@">>>>>>>> Dictionary <<<<<<<<<<");
        
        for (NSString *key in dict) {
            NSString *value = dict[key];
            NSLog(@"key:%@ -- value:%@",key,value);
        }
    }
    - (void)forSet:(NSSet *)set {
        NSLog(@">>>>>>>> Set <<<<<<<<<<");
        
        for (NSString *string in set) {
            NSLog(@"set:%@",string);
        }
    }
    

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

    NSEnumerator对象也实现了NSFastEnumeration协议,所以可以用来执行反向遍历

    NSEnumerator *enumerator = [array reverseObjectEnumerator];
    for (NSString *string in enumerator) {
        NSLog(@"%@",string);
    }
    

    与传统for循环不同,这种遍历方式无法轻松获取当前遍历操作所针对的下标。遍历时经常会用到这个下标,比如很多算法都需要它。

    基于块的遍历方式

    基于块遍历,NSArray定义了相关方法,下面可以实现最基本的遍历功能:

    - (void)enumerateObjectsUsingBlock:(void (NS_NOESCAPE ^)(ObjectType obj, NSUInteger idx, BOOL *stop))block
    

    除此之外,还有一系列类似的方法,可以接受各种选项,以及控制遍历操作。

    遍历数组即set时,每次迭代都要执行由block参数传入的块,这个快有三个参数,分别是当前迭代所针对的对象、所针对的下标,以及指向布尔值的指针。第三个参数所提供的,开发者可以终止遍历操作。

    
    - (void)forArray:(NSArray *)array {
        NSLog(@">>>>>>>> Array <<<<<<<<<<");
        [array enumerateObjectsUsingBlock:^(NSString *string, NSUInteger idx, BOOL * _Nonnull stop) {
            NSLog(@"string :%@",string);
            if (idx == 3) {
                *stop = YES;
            }
        }];
    }
    - (void)forDictionary:(NSDictionary *)dict {
        NSLog(@">>>>>>>> Dictionary <<<<<<<<<<");
        
        [dict enumerateKeysAndObjectsUsingBlock:^(NSString * key, NSString * obj, BOOL * _Nonnull stop) {
            NSLog(@"key:%@ -- value:%@",key,obj);
            if ([key isEqualToString:@"8"]) {
                *stop = YES;
            }
        }];
    }
    - (void)forSet:(NSSet *)set {
        NSLog(@">>>>>>>> Set <<<<<<<<<<");
        [set enumerateObjectsUsingBlock:^(NSString * obj, BOOL * _Nonnull stop) {
            NSLog(@"%@",obj);
            if ([obj isEqualToString:@"10002"]) {
                * stop = YES;
            }
        }];
    }
    

    打印结果

    >>>>>>>> Array <<<<<<<<<<
    string :10000
    string :10001
    string :10002
    string :10003
    string :10004
    string :10005
    string :10006
    >>>>>>>> Dictionary <<<<<<<<<<
    key:7 -- value:10007
    key:3 -- value:10003
    key:8 -- value:10008
    >>>>>>>> Set <<<<<<<<<<
    10008
    10001
    10003
    10005
    10007
    10009
    10000
    10002
    // 字典和set是无序的,结果中断结果不同。
    

    这种方法遍历时能直接从块中获取更多信息。另外一个可以修改块方法签名。比如将id _Nonnull修改成已知类型NSString *。之所以能如此,因为id类型相当特殊,它可以为其他类型所覆写。

    用此方法可以执行反向遍历,可向其传入选项掩码(option mask)

    - (void)enumerateObjectsWithOptions:(NSEnumerationOptions)opts usingBlock:(void (NS_NOESCAPE ^)(ObjectType obj, NSUInteger idx, BOOL *stop))block;
    
    - (void)enumerateKeysAndObjectsWithOptions:(NSEnumerationOptions)opts usingBlock:(void (NS_NOESCAPE ^)(KeyType key, ObjectType obj, BOOL *stop))block;
    

    反向遍历只有对数组或对有序set等有序的collection时,才有意义。

    [array enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(NSString * obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSLog(@"%@",obj);
    }];
    

    NSEnumerationOptions类型是个enum,其各种取值可用按位或连接,用以表明遍历方式。例如可以请求以并发方式执行个轮迭代,也就是说,如果当前系统资源状况允许,那么执行每次迭代所用的块就可以并行执行了。通过NSEnumerationConcurrent选项即可开启此功能。底层会通过GCD处理并发执行事宜。


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

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

    使用无缝桥接技术,可以再定义于Foundation框架中的OC类和定义与CoreFoundation框架中的C数据结构之间互相转换。

    CFArrayRef aCFArray = (__bridge CFArrayRef)array;
    NSLog(@"size of CFArrat :%li",CFArrayGetCount(aCFArray));
    

    转换操作中的__bridge告诉ARC如何处理转换所涉及的OC对象,__bridge本身的意思是:ARC仍然具备这个OC对象的所有权。__bridge_retained与之相反,意味着ARC将交出对象的所有权。若是前面那段代码改用它来实现,那么用完数组之后就要加上CFRelease(aCFArray);以释放内存。

    CFArrayRef aCFArray = (__bridge_retained CFArrayRef)array;
    NSLog(@"size of CFArrat :%li",CFArrayGetCount(aCFArray));
    CFRelease(aCFArray);
    

    反向转换通过__bridge_transfer

    CFArrayRef aCFArray = (__bridge_retained CFArrayRef)array;
    NSLog(@"size of CFArrat :%li",CFArrayGetCount(aCFArray));
    NSArray *sArray = (__bridge_transfer NSArray*)aCFArray;
    

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

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

    缓存图片,用NSCache更好,这是Foundation框架专门为处理这种任务而设计的。

    NSCache胜过NSDictionary之处在于,当系统资源将要耗尽时,它可以自动删减缓存。如采用普通字典,就要自己编写挂钩,在系统发出低内存通知时手动删减缓存。NSCache能自动删减,由于其实Foundation框架的一部分,他能在更深的层面上插入挂钩。此外,NSCache还会先行删减最久未使用的(lease recently used)对象。

    NSCache不会拷贝键,而是会保留。NSCache不自动拷贝键,所以,在键不支持拷贝操作的情况下,该类用起来比字典更方便。此外,NSCache是线程安全的,在开发者不编写加锁代码的前提下,多个线程便可以同时访问NSCache。而字典绝对不具备此优势。

    对缓存来说,线程安全通常很重要。

    开发者可以操控缓存删减内容的时机,有两个系统资源相关的尺度可以调整,其一是缓存中的对象总数countLimit,其二是所有对象的总开销totalCostLimit。在将对象加入缓存时,可以为其制定开销值。

    当对象总数或总开销超过上限时,缓存就有可能会删减其中的对象,再有可用的系统资源趋于紧张时,也会这么做。但是,可能会删除某个对象,并不意味着一定删除这个对象。删减对象所遵照的顺序,是由具体实现来定。

    想缓存中添加对象时,只能在能很快计算出开销值的情况下,才考虑这个尺度。如果计算过程很复杂,或者必须访问磁盘、数据库才能确定文件大小,这就不太好了。如果要加入的是NSData对象,就可以指定开销值,因为数据大小只是它的一项属性。

    @implementation ZYDCacheDataManager {
        NSCache *_cache;
    }
    
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            _cache = [NSCache new];
            
            _cache.countLimit = 100;
            // 开销值,设置对象数据上线
            _cache.totalCostLimit = 500 * 1024 * 1024;
            // 总开销,设置上限值。以字节为单位
            
        }
        return self;
    }
    
    - (void)downloadDataFromUrl:(NSURL *)url {
        NSData *cacheData = [_cache objectForKey:url];
        //这里下载的url就是缓存的键,若没有缓存数据,则下载数据并存入缓存
        if (cacheData) {
            //直接处理
            
        } else {
            //下载
            
        }
    }
    

    NSPurgeableData

    NSPurgeableDataNSCache搭配起来效果很好,此类是NSMutableData的子类。而且实现了NSDiscardableContent协议,如果某个对象所占的内存能够根据需要随时丢弃,那么可以实现该协议所定义的接口。就是说,当系统资源紧张时,可以把保存NSPurgeableData对象的那块内存释放掉。

    如果要访问某个NSPurgeableData对象,先调用beginContentAccess,告诉它现在不能丢弃自己所占据的内存。

    用完之后,调用endContentAccess方法,告诉它在必要的时候可以丢弃自己所占据的内存

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

    - (void)downloadDataFromURL:(NSURL *)url {
        NSPurgeableData *cachedData = [_cache objectForKey:url];
        if (cachedData) {
            //如果要访问某个NSPurgeableData对象,
            //先调用beginContentAccess,告诉它现在不能丢弃自己所占据的内存
            [cachedData beginContentAccess];
            
            //用完之后,调用`endContentAccess`方法
            //告诉它在必要的时候可以丢弃自己所占据的内存
            [cachedData endContentAccess];
            
            //如果将NSPurgeableData对象加入NSCache,当该对象为系统所丢弃时,也会自动从缓存中移除
            //NSCache的evictsObjectsWithDiscardedContent属性可以开启或关闭此功能。
            _cache.evictsObjectsWithDiscardedContent = YES;
        } else {
            NSData *data;
            
            NSPurgeableData *purgeableData = [NSPurgeableData dataWithData:data];
            //创建NSPurgeableData对象之后,其purge引用计数加1,不需要再调用beginContentAccess,然而必须调用endContentAccess,将多出的1抵消
            [_cache setObject:purgeableData forKey:url cost:purgeableData.length];
            
            [purgeableData endContentAccess];
            
        }
    }
    

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

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

    load

    + (void)load;

    对于加入运行期系统的每个类及分类来说,必定会调用此方法,且仅调用一次。当包含类或分类的程序库载入系统时,就会执行此方法,即在应用启动的时候。

    如果分类和其所属的类都定义了load方法,则先调用类里的,再调用分类里的。

    指定load方法时,运行期系统处于脆弱状态,执行子类的load方法之前,必定会先执行所有超类的load方法,如果代码依赖其他程序库,程序库里相关类的load方法也必定会先执行。在load方法使用其他类是不安全的。

    load并不像普通方法那样,它不遵循继承规则,如果某个类本身没有实现load方法,那么不管其各级超类是否实现此方法,系统都不会调用。

    load方法必须实现得精简一些,尽量减少其所执行的操作,应为整个程序在执行load方法时都会阻塞。不要在里面等待锁,也不要调用可能会加锁的方法。总之能不做别的事情就别做。

    initialize

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

    与load方法的区别:

    • initialize是惰性调用的,只要程序用到相关类时,才会调用。load方法,是应用程序必须阻塞并等着所有累的load都执行完,才能继续。

    • 运行期系统在执行initialize方法时,是处于正常状态的,因此运行期系统完整度来说,此时可以安全使用并调用任意类中的任意方法。而且运行期系统也能确保initialize一直在线程安全的环境中执行。即,只有执行initialize的那个县城可以操作类或类实例,其他线程都要先阻塞,等着initialize执行完。

    • initialize与其他消息一样,如果这个类未实现它,而其超类实现了,就会运行超类的实现代码。

    initialize遵循通常的继承规则。

    + (void)initialize
    {
        if (self == [ZYDSubPersonModel class]) {
            //做相关处理,尽量精简。
            NSLog(@"initialize: %@",self);
        }
    }
    

    总结

    loadinitialize两个方法的实现代码都应该尽量精简,在里面设置一些状态,使本来能够正常运作就可以,不要执行耗时太久或需要加锁的任务。

    initialize方法只应用来设置内部数据,不应该在其中调用其他放阿飞,即便是本类自己的方法。若某个全局状态无法在编译期初始化,则可以放在initialize里来做。


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

    52、NSTimer会保留其目标对象

    计时器可以指定绝对的日期与时间,以便到时执行任务,也可以指定执行任务的相对延时时间,可以重复运行任务,指定任务触发频率。

    计时器要和运行循环(run loop)相关联,运行循环到时候会触发任务。创建NSTimer时可以将其预先安排在当前的运行循环中,也可以先创建好,然后由开发者调度。

    只有把计时器放在运行循环里,它才能正常触发任务。

    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
    
    
    [NSTimer scheduledTimerWithTimeInterval:1
                                     target:self
                                   selector:@selector(doSomethingMethod)
                                   userInfo:nil
                                    repeats:YES];
    

    计时器会保留其目标对象,等到自身失效的时候再释放此对象。调用invalidate方法可令计时器失效;执行完一次的计时器也会失效。若将计时器设置为重复执行模式,必须自己调用invalidate方法,才能令其停止。

    #import "ZYDUserManager.h"
    
    @implementation ZYDUserManager {
        NSTimer *_testTimer;
    }
    
    - (void)startTimer {
        _testTimer = [NSTimer scheduledTimerWithTimeInterval:1
                                                      target:self
                                                    selector:@selector(doSomethingMethod)
                                                    userInfo:nil
                                                     repeats:YES];
    }
    
    - (void)doSomethingMethod {
        
    }
    
    - (void)stopTimer {
        [_testTimer invalidate];
        _testTimer = nil;
    }
    @end
    

    这里如果创建实例对象,并调用startTimer方法,创建计时器的时候,目标对象是self,所以保留此实例,同时计时器是用实例变量存放的,所以实例保留了计时器。所以就产生了保留环。如果stopTimer没有别调用,那么计时器不会被释放,那么它所保留的self也不会被释放,所以就会造成内存泄漏。而且尤为严重,因为计时器还将继续反复地执行轮询任务。


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

    相关文章

      网友评论

          本文标题:编写高质量iOS与OSX代码的52个有效方法-第七章-系统框架

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