针对框架开发者的技术细节

框架开发者应当比其他开发者在对待代码上面,应当更加小心。很多的客户端应用都会链接到他们的框架中,正是因为这样广泛的暴露,任何框架的缺陷,都会放大到整个系统。下面的内容讨论一些你可以采纳的编程技巧,用来确保你的框架的效率和集成。

*备注: 一些技巧不仅仅局限于框架。你也可以应用到应用开发中。*

初始化

以下是包含了框架初始化的意见和建议。

类初始化

初始化类方法给你一个可以在任何其它类的方法执行之前,执行一次代码。通常被用来设置类的版本号(参见版本与兼容)。

运行时会给继承链中的每一个类发送初始化信息,及时没有实现它;所以类初始化方法可能不止被调用一次(例如,一个子类没有实现它)。通常情况下,你希望初始化代码仅执行一次。有一种方式可以确保这样,就是使用dispatch_once():

+ (void)initialize {
    static dispatch_once_t onceToken = 0;
    dispatch_once(&onceToken, ^{
        // the initializing code
    }
}

备注:因为运行时会发送初始化给每一个类,很有可能initialize在子类的上下文环境中调用。如果子类没有实现initialize,然后这个调用会传到父类。如果你需要在相关的类的上下文中执行初始化,你可以用一下检查替代 dispatch_once()的使用会更好:

if (self == [NSFoo class]) {
    // the initializing code
}

绝不要显示的调用initialize方法。如果你想要触发初始化,调用一些无害的方法,例如:

[NSImage self];

指定的初始化器

指定的初始化器是一个类的实例方法,它会调用父类的初始化方法。(其它初始化器调用类定义的初始化方法)每一个公共类都需要有一个或者多个指定的初始化器。指定初始化器的一个例子有:NSView类的initWithFrame:NSResponderinit方法。init方法并不意味着需要重写,比如NSString类和其它类集合中的抽象类,子类来自己实现。

指定的初始化器应当明确的指定,因为这对于想要依据你的类创建子类来说非常重要。子类仅重写指定的初始化器。

当你要实现一个框架的类时,你经常需要也实现它的存档方法:initWithCoder:encodeWithCoder:。注意不要当在对象解归档的时候,在初始化的代码中执行不会发生的事情。一个好的解决这个问题的方式就是,如果你的类实现了归档,可以从你指定的初始化器和initWithCoder:方法中实现一个普通的调用。

初始化过程中的错误检测

一个设计比较好的初始化方法应当通过完成以下几部来确保正确的检测和错误输出:

1.通过调用父类的指定初始化器来重新分配自己。 2.检查nil返回值,这个表明在父类初始化的过程中发生了错误。 3.如果在初始化当前类对象的时候,发生错误,释放这个对象并返回nil。

列表 1 阐明了你可能会这样做。

列表1 初始化过程中的错误检测

- (id)init {
    self = [super init];  // Call a designated initializer here.
    if (self != nil) {
        // Initialize object  ...
        if (someError) {
            [self release];
            self = nil;
        }
    }
    return self;
}

版本和兼容性

当你向你的框架中添加新的类或者方法的时候,一般来说,没有必要给每一个新增的特性组都指定一个新的版本号。开发者通常会执行(或可能执行)Objective-C运行时检查,例如respondsToSelector:来检测对于一个给定的系统是有新特性。这些运行时测试是检查新特性时候所优先采取的大多数动态方式。

然而,你可以雇佣一些技术人员来确保你每一个框架的新版本被正确的标注了,尽可能的与早期的版本兼容。

框架版本

当有一个新增特性或者bug被修复时,通过运行时测试是不容易检测出来的,你应当以某种方式告知开发者来检测这种变化。一种方式就是以归档的形式来存储框架的确切版本号,同时需要让开发者可见这些内容:

  • 在每一个版本号下做文档记录变化(例如,在发布备注中)。

  • 设置你框架当前的版本号并且提供一些方式让它能够全局访问。你可以通过plist文件来存储你框架的版本号,然后可以通过这种方式来访问。

归档关键

如果你的框架对象需要写入nib文件,他们必须能够自归档。你同样需要通过使用归档机制存储文档数据来归档任何文档。

你应当考虑以下关于归档方面的问题:

  • 如果归档中的key丢失了,请求对应的值的话,将会返回nil、NULL、NO、0或0.0等,取决于请求的数据类型。通过判断这个返回值,可以减少你的数据输出。另外,你可以确认这个key有没有被写入归档。

  • 编码和解码方法可以确保向后的兼容性。例如一个类的新版本的编码方法可能会通过使用key写入新的值,但是可能仍然返回旧的字段以便旧版本的类仍然知道这个对象。另外,解码方法想要通过一些可能的方式来处理丢失的值来保持新版本的灵活性。

  • 对于框架类的归档key的一个推荐的命名约定就是以针对于其他框架API元素的前缀并且使用实例变量的名字。这样确保命名不会和其他任何父类或子类的名字冲突。

  • 如果你有一个工具函数输出一个基本的数据类型(换言之,这个值不是对象),确保使用一个唯一的key。例如,如果你有一个archiveRect程序来归档举行,需要传入key参数,你可以使用它。或者,如果它输出多个值(例如,四个浮点数据),应当在给定的key上追加自己唯一的位。

  • 按照原样来归档位字段是很危险的,因为这个和编译器以及字节顺序依赖有关。你仅能在对于优化的原因的情况下归档位字段,例如,需要大量多次的的位输出。参见位字段。

异常和错误

大多数Cocoa框架方法不强制开发者捕获和处理异常。那是引文异常作为执行的一个不同的部分,并不会增加,而且一般不会用在运行时的通信或者用户错误。这些错误的包括以下例子:

  • 文件没有找到

  • 没有此用户

  • 在应用中试图打开一个错误的文档类型

  • 转换String到特定编码格式错误

然而,Cocoa对于以下情况会产生异常来指明程序或者逻辑错误:

  • 数组越界异常

  • 尝试变化不可变的对象

  • 错误的参数类型

期望开发者在应用发布之前能够捕获这种类型的错误;所以应用不需要在运行时处理这些异常。如果一个异常往外扩散,应用没有捕获它,高级别的默认处理器通常会处理它,并且会报告异常,然后让它们继续执行。开发者可以选择用一个能够提供更多关于错误的信息,并且提供一个可选项来保存数据并且退出应用的默认异常捕获器来处理。

错误是处于Cocoa框架中的另外一个与其他一些软件开发库不同的区域。Cocoa方法一般不会返回错误代码。万一有错误的理由,这些方法依赖于一个简单的布尔或者对象(空/非空)返回值的简单测试;返回NO或者nil的原因是记录的。你不能在运行时使用错误代码来显示程序错误,但是有些情况下,你可以听哦你不敢过打印简单的错误信息而不用抛出异常。

例如,NSDictionaryobjectForKey:方法返回发现的对象或者当没有发现返回对象的时候返回空。NSArrayobjectAtIndex:方法不会返回nil(除非重写一般语言约定,将任何信息转换成nil,导致返回nil),因为一个NSArray对象不能存储nil值,并且通过定义任何越界方法是一个会导致异常的程序错误。许多初始化方法会因为通过提供的参数不能够初始化,从而导致返回nil。

在少数情况下,一个方法有一个返回多个不同的错误代码,应当用引用参数来指定它,返回一个错误的代码,一个本地话的错误字符串,或者其他的描述错误的信息。例如,你肯能需要返回一个NSError对象来表示错误;可以查看框架中的NSError.h头文件来获取细节。这个参数一般来说是一个直接返回的BOOL或者nil。这个方法同样应当遵循引用参数是可选的并且允许将错误代码参数传入null,如果它们不期望知道这个错误。

框架数据

你如何处理框架数据会对性能,跨平台兼容性和其它方面有影响。这部分讨论涉及的框架数据的技巧。

常量数据

处于性能的考虑,尽可能的将常量数据作为框架数据,因为这样可以减少Mach-O二进制文件的__DATA段的大小。这种数据会在每一个使用这个框架应用的运行实例中占用内存。尽管额外的500字节(举个例子)看起来还好,但是它可能会导致页要求的数量的增加-每个应用有一个额外的4kB。

你应当用const标记任何常量数据。如果在block中没有char指针,会导致数据处于 TEXT段(这里会使之成为真正的常量);否则的话,它处于DATA段,但是却不能输出(除非预绑定没有完成或者通过在加载时二进制的偏移来改变它)。

你应当初始化静态变量来确保他们被合并到DATA段中的data部分,而不是__bss部分。如果没有明显的值来初始化,请使用0,NULL,0.0或者其他任何恰当的值。

位段

针对位段请使用有符号的值,特别是一位的位段,这样会导致如果代码将这个值作为boolean值,会出现未定义行为。一位的位段应当使用无符号类型。因为单个位段能够存储的值,只是0和-1(取决于编译器的实现),对比这个位段,1是false。例如:如果你在代码中遇到这些:

BOOL isAttachment:1;
int startTracking:1;

你应当将类型改为无符号整型值。

另外一个和位段相关的内容是归档。一般来说,你不能以位段本身的格式来写入到磁盘或者归档中,因为当在另外一个架构或者其它编译器上读取的时候,格式可能不同。

内存分配

在框架代码中,避免全部内存分配是最好的课程。如果在某种情况下,你需要一个临时的buffer,通常使用栈要好过于buffer的分配。然而,stack有大小限制(通常全部大小为512kb),所以使用栈的这个决定取决于函数和你需要的buffer的大小。通常,如果buffer大小是1000bytes(或者MAXPATHLEN)或者更小,可以使用stack。

一个改进就是使用stack开启偏移,但是如果大小超过了栈buffer大小,请切换到内存分配的buffer中。以下有例子:

#define STACKBUFSIZE (1000 / sizeof(YourElementType))
 YourElementType stackBuffer[STACKBUFSIZE];
 YourElementType *buf = stackBuffer;
 int capacity = STACKBUFSIZE;  // In terms of YourElementType
 int numElements = 0;  // In terms of YourElementType

while (1) {
    if (numElements > capacity) {  // Need more room
        int newCapacity = capacity * 2;  // Or whatever your growth algorithm is
        if (buf == stackBuffer) {  // Previously using stack; switch to allocated memory
            buf = malloc(newCapacity * sizeof(YourElementType));
            memmove(buf, stackBuffer, capacity * sizeof(YourElementType));
        } else {  // Was already using malloc; simply realloc
            buf = realloc(buf, newCapacity * sizeof(YourElementType));
        }
        capacity = newCapacity;
    }
    // ... use buf; increment numElements ...
  }
  // ...
  if (buf != stackBuffer) free(buf);

对象比较

你应当意识到泛型的对象比较方法isEqual: 和对象相关的比较方法,例如isEqualToString:方法之间的重要区别。isEqual: 方法允许你传入任意对象作为参数,并且如果对象不是同一个类会返回NO。诸如isEqualToString: 和isEqualToArray:方法通常假设参数是指定的类型(也就是接收者的类型)。所以它们不执行类型检查,从而它们运行会更快,但却不安全。对于从内部资源取回的值,例如应用信息属性列表(Info.plist)或者偏好设置,更倾向于使用 isEqual: ,因为它更安全。当类型未知的时候,使用isEqualToString:。

和isEqual:方法相关的一点就是它和hash方法有关。对象一个最基本的不变的地方就是被放入一个基于哈希的例如NSDictionary 或 NSSetCocoa集合中,如果[A isEqual:B] == YES,那么[A hash] == [B hash]。如果你重写你的类的isEqual:方法,你应当也要重写hash方法来维持这一个不变的条件。默认的isEqual:方法寻找和每一个对象地址相等的指针,并且hash方法返回的hash值是基于每一个对象的地址,所以还是保持了这个不变性。

Last updated