欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

[编写高质量iOS代码的52个有效方法](十一)系统框架

程序员文章站 2023-10-17 14:05:31
先睹为快 47.熟悉框架 48.多用块枚举,少用for循环 49.对自定义其内存管理语义的容器使用无缝桥接 50.构建缓存时选用nscache而非nsdictionary 51.精简initiali...

先睹为快

47.熟悉框架

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

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

50.构建缓存时选用nscache而非nsdictionary

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

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

 

第47条:熟悉系统框架

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

开发者会碰到的主要框架就是foundation,像是nsobject、nsarray、nsdictionary等类都在其中。foundation框架中的类都使用ns前缀(表示nextstep操作系统,mac os x的基础)

还有个与foundation相伴的框架,叫corefoundation。其中有很多对应foundation框架中功能的c语言api。corefoundation中的c语言数据结构可以与foundation框架中的objective-c对象无缝桥接。

除此之外还有以下常用框架:

cfnetwork 提供c语言级别的网络通信能力

coreaudio 操作设备音频硬件的c语言api

avfoundation 提供objective-c对象来回访并录制音频及视频

coredata 提供objective-c接口将对象放入,便于持久保存

coretext 可以高效执行文字排版及渲染操作的c语言接口

appkit/uikit mac os x/ios应用程序的ui框架

用纯c语言写成的框架与用objective-c写成的一样重要,若想成为优秀的objective-c开发者,应该掌握c语言的核心概念。

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

在中经常需要列举容器中的元素,当前objective-c语言有多种办法实现此功能,首先是老式的for循环。

nsarray *array = /* ... */;
for (int i = 0; i < array.count; i++) {
    id object = array[i];
    // do something with 'object'
}

nsdictionary *dictionary = /* ... */;
nsarray *keys = [dictionary allkeys];
for (int i = 0; i < keys.count; i++) {
    id key = keys[i];
    id value = dictionary[key];
    // do something with 'key' and 'value'
}

这是最基本的方法,因而功能非常有限。由于字典和set都是无序的,所以遍历它们需要额外创建一个数组(本例中为keys)。

第二种方法是使用nsenumerator抽象基类来遍历

nsarray *array = /* ... */;
nsenumerator *enumerator = [array objectenumerator];
id object;
while ((object = [enumerator nextobject]) != nil) {
        // do something with 'object'
}

nsdictionary *dictionary = /* ... */;
nsenumerator *enumerator = [dictionary keyenumerator];
id key;
while ((key = [enumerator nextobject]) != nil) {
    id value = dictionary[key];
    // do something with 'key' and 'value'
}

这种方法与标准for循环相比,优势在于无论遍历哪种容器,语法都十分类似,如果需要反向遍历,也可以获取反向枚举器。

nsarray *array = /* ... */;
nsenumerator *enumerator = [array reverseobjectenumerator];

objective-c 2.0引入了快速遍历。与使用nsenumerator类似,而语法更简洁,它为for循环开始了in关键字。

nsarray *array = /* ... */;
for (id object in array){
    // do something with 'object'
}

nsdictionary *dictionary = /* ... */;
for (id key in dictionary){
    id value = dictionary[key];
    // do something with 'key' and 'value'
}

如果某个类的对象支持快速对象,只需要遵守nsfastenumeration协议,该协议只定义了一个方法:

- (nsuinteger)countbyenumeratingwithstate:(nsfastenumerarionstate*)state object:(id*)stackbuffer count:(nsuinteger)length

由于nsenumerator也实现了nsfastenumeration协议,所以反向遍历可以这样实现:

nsarray *array = /* ... */;
for (id object in [array reverseobjectenumerator]){
    // do something with 'object'
}

这种方法允许类实例同时返回多个对象,使循环更高效。但缺点有两个,一是遍历字典时不能同时获取键和值,需要多一步操作,二是此方法无法轻松获取当前遍历操作所针对的下标(有可能会用到)。

最后一种方法是基于块的遍历,也是最新的方法

nsarray *array;
[array enumerateobjectsusingblock:^(id obj, nsuinteger idx, bool *stop) {
    // do something with 'object'
    if (shouldstop) {
        *stop = yes;
    }
}];

nsdictionary *dictionary;
[dictionary enumeratekeysandobjectsusingblock:^(id key, id obj, bool *stop) {
    // do something with 'key' and 'value'
    if (shouldstop) {
        *stop = yes;
    }
}];

此方式的优势在于,遍历时可以直接从块里获取更多信息,并且能够通过修改块的方法名,避免进行类型转换操作。若已知字典中的对象必为字符串:

nsdictionary *dictionary;
[dictionary enumeratekeysandobjectsusingblock:^(nsstring *key, nsstring *obj, bool *stop) {
    // do something with 'key' and 'value'
}];

当然,此方法也可以传入选项掩码来执行反向遍历

[array enumerateobjectswithoptions:nsenumerationreverse usingblock:^(id obj, nsuinteger idx, bool *stop) {
        // do something with 'object'
    }];

在options处传入nsenumerationconcurrent,可开启并行执行功能,通过底层gcd来实现并处理。

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

无缝桥接可以实现foundation框架中的类和corefoundation框架中的数据结构之间的互相转换。下面是一个简单的无缝桥接:

nsarray *ansarray = @[@1,@2,@3];
cfarrayref acfarray = (__bridge cfarrayref)ansarray;
cfrelease(acfarray);

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

__bridge // 不改变对象的原所有权
__bridge_retained // arc交出对象的所有权,手动管理内存
__bridge_transfer // arc获得对象的所有权,自动管理内存

手动管理内存的对象需要用cfretain与cfrelease来保留或释放。

第50条:构建缓存时选用nscache而非nsdictionary

开发ios程序时,有些程序员会将因特网上下载的图片保存到字典中,这样的话稍后使用就无须再次下载了,其实用nscache类更好,它是foundation框架专门为处理这种任务而设计的。

nscache胜于nsdictionary之处在于,当系统资源将要耗尽时,它可以自动删除最久未使用的缓存。nscache并不会拷贝键,而是保留它,在键不支持拷贝操作的情况下,使用更方便。另外nscache是线程安全的,不需要编写加锁代码的情况下,多个线程也可以同时访问nscache。

下面是缓存的用法

#import 

// 网络数据获取器类
typedef void(^eocnetworkfetchercompletionhandler)(nsdata *data);

@interface eocnetworkfetcher : nsobject

- (id)initwithurl:(nsurl*)url;
- (void)startwithcompletionhandler:(eocnetworkfetchercompletionhandler)handler;
@end

// 使用获取器及缓存结果的类
@interface eocclass : nsobject
@end

@implementation eocclass{
    nscache *_cache;
}

- (id)init{
    if ((self = [super init])) {
        _cache = [nscache new];
        // 设置缓存的对象数目上限为100,总开销上限为5mb
        _cache.countlimit = 100;
        _cache.totalcostlimit = 5 * 1024 * 1024;
    }
    return self;
}

- (void)downloaddataforurl:(nsurl*)url{
    // nspurgeabledata为nsmutabledata的子类,采用与内存管理类似的引用计数,当引用计数为0时,该对象占用的内存可以根据需要随时丢弃
    nspurgeabledata *cachedata = [_cache objectforkey:url];
    if (cachedata) {
        // 缓存命中
        // 引用计数+1
        [cachedata begincontentaccess];
        // 使用缓存数据
        [self usedata:cachedata];
        // 引用计数-1
        [cachedata endcontentaccess];
    }else{
        // 缓存未命中
        eocnetworkfetcher *fetcher = [[eocnetworkfetcher alloc] initwithurl:url];
        [fetcher startwithcompletionhandler:^(nsdata *data) {
            // 创建nspurgeabledata对象,引用计数+1
            nspurgeabledata *purgeabledata = [nspurgeabledata datawithdata:data];
            [_cache setobject:purgeabledata forkey:url cost:purgeabledata.length];
            // 使用缓存数据
            [self usedata:cachedata];
             // 引用计数-1
            [purgeabledata endcontentaccess];
        }];
    }
}
@end

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

有时候类必须先执行某些初始化操作,然后才能正常使用。在objective-c中,绝大多数类都继承自nsobject这个根类,而该类有两个方法可以用来实现这种初始化操作。首先是load方法:

+ (void)load

加入运行期系统中的每个类及分类,都会调用此方法,而且仅调用一次。在ios中,这类方法会在应用程序启动时执行(mac os x中可以使用动态加载,程序启动之后再加载)。在执行load方法时,是先执行超类的load方法,再执行子类的,先执行类的,再执行其所属分类的。如果代码还依赖了其他程序库,则会有限执行该程序库中的load方法。但在给定的某个程序库中,无法判断出各个类的载入顺序。

#import 
#import "eocclassa.h" // 来自同一个库

@interface eocclassb : nsobject
@end

@implementation eocclassb

+ (void)load{
    nslog(@"loading eocclassb");
    eocclassa *object = [eocclassa new];
    // ues object
}
@end

这段代码不安全,因为无法确定eocclassa已在执行eocclassb load方法时已经加载好了。

load方法不遵从普通方法的继承规则,如果某个类本身没实现load方法,那么不管其超类是否实现此方法,系统都不会调用。

load方法应该尽量精简,因为整个程序执行load方法时都会阻塞。不要在里面等待锁,也不要调用可能会加锁的方法。总之,能不做的事情就别做。

想要执行与类相关的初始化操作,还有个方法,就是重写下列方法

+ (void)initialize

对于每个类来说,该方法会在程序首次调用该类之前调用,而且只调用一次。initialize与load方法主要有3个区别:
1. initialize方法只有当程序用到了相关类才会调用,而load不同,程序必须阻塞并等所有类的load都执行完毕,才能继续。
2. 运行期系统执行initialize方法时,处于正常状态,而不是阻塞状态。为保证线程安全,只会阻塞其他操作该类或类实例的线程。
3. 如果某个类未实现initialize方法,而超类实现了它,那么就会运行超类的方法。

initialize方法也应当尽量精简,只需要在里面设置一些状态,使本类能够正常运作就可以了,不要执行那种耗时太久或需要加锁的任务,也尽量不要在其中调用其他方法,即使是本类的方法。

若某个全局状态无法在编译期初始化,则可以放在initialize里来做。

// eocclass.h
#import 

@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 new];
    }
}
@end

整数可以在编译期定义,然而可变数组不行,下面这样创建对象会报错。

static nsmutablearray *ksomeobjects = [nsmutablearray new];

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

nstimer(计时器)是一种很方便很有用的对象,计时器要和运行循环相关联,运行循环到时候会触发任务。只有把计时器放到运行循环里,它才能正常触发任务。例如,下面这个方法可以创建计时器,并将其预先安排在当前运行循环中:

+ (nstimer *)scheduledtimerwithtimeinterval:(nstimeinterval)ti target:(id)atarget selector:(sel)aselector userinfo:(id)userinfo repeats:(bool)yesorno;

此方法创建出来的计时器会在指定的间隔时间之后执行任务。也可以令其反复执行任务,直到开发者稍后将其手动关闭为止。target和selector表示在哪个对象上调用哪个方法。执行完任务后,一次性计时器会失效,若repeats为yes,那么必须调用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
}

如果创建了本类实例,并调用了startpolling方法。创建计时器的时候,由于目标对象是self,所以要保留此实例。然而,因为计时器是用实例变量存放的,所以实例也保留了计数器,于是就产生了保留环。

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

再修改stoppolling方法:

- (void)startpolling{
    __weak eocclass *weakself = self;
    _politimer = [nstimer eoc_scheduledtimerwithtimeinterval:5.0 block:^{
        eocclass *strongself = weakself;
        [strongself p_dopoll];
    } repeats:yes];
}

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