Cocoa 编码指南 框架开发者使用技巧和技术

移动开发 iOS
在面向对象软件库的设计过程中,开发人员经常忽视对类、方法、函数、常量以及其他编程接元素的命名。本节介绍的是框架开发者使用技巧和技术。

Cocoa 编码指南 框架开发者使用技巧和技术是本文要介绍的内容,相对于其他的开发者而言,框架开发者要更加注意编写代码的方式。因为许多客户应用程序可能链接到框架,而这样宽泛地暴露接口,就导致框架的任何缺点都可能通过系统放大。下面的条款讨论一些框架编程技术,框架开发者可以利用它们来确保框架的高效性和完整性。

请注意:此处所讨论的一些技术并不局限于框架开发。将之用于应用程序开发同样卓有成效。

初始化

下述意见和建议涵盖框架初始化方面的内容。

类的初始化

initialize类方法中的代码只执行一次,它是类里面***个被调用的方法。我们通常利用该方法来设置类的版本号(请参考“版本化和兼容性”一节)。

对于继承链中的每一个类,不论其是否实现initialize方法,运行时都会向它发送initialize消息。这可能导致一个类的initialize方法被多次调用(举个例子,如果子类没有实现initialize方法,则其父类的方法将被调用两次)。但通常您希望始化代码仅执行一次,为确保如此,您可以执行如下检查:

  1. if (self == [NSFoo class]) {   
  2.     // the initializing code   
  3. }  

您不应该显式地调用initialize方法。如果需要触发初始化行为,则请调用一些无害的方法,例如:

  1. [NSImage self];  

指定初始化函数

指定初始化函数是类的一个init方法,它调用超类的某个init方法(其他的初始化函数调用类自己定义的init方法)。每个公共类都应包含一个或多个指定初始化函数。举些例子,NSView的initWithFrame:方法以及NSResponder的init方法都是指定初始化函数。在某些情况下,类的init方法并不想被重载,比如NSString和其他面向类簇的抽象类,因而其子类应该实现自己的初始化方法。

您应该明确标示出指定初始化函数,因为该信息对于想根据您的类来派生子类的开发者有重要的意义。一个子类可以只重载指定初始化函数,这对其他所有初始化函数没有影响,它们仍将按其原先设计的行为工作。

在实现一个框架类时,您经常需要为其实现诸如initWithCoder:以及encodeWithCoder:这样的归档方法。请注意,对象解档时未发生的事情不要放在初始化代码路径里执行。对于实现归档功能的类,我们有一个比较好的方法可以做到这一点,那就是在类的指定初始化方法以及initWithCoder: 方法(该方法也是个指定初始化函数)中调用一个公共的例程。

初始化过程中的错误检测

为确保能够恰当地检测并传播错误,一个设计良好的初始化方法应完成如下步骤:

调用super的init方法对self重新赋值。

检测指定初始化方法的返回值是否为nil。返回值为nil表明超类的初始化过程出现错误。

如果当前类在初始化的过程中出现错误,则请释放对象并且返回nil值。

列表1 描述的方法可以完成上述步骤。

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

  1. (id)init {   
  2.     if ((self = [super init]) != nil) {   // call a designated initializer here   
  3.         // initialize object  ...   
  4.         if (someError) {   
  5.             [self release]; // [self dealloc] or [super dealloc] might be   
  6.             self = nil;     // better if object is malformed   
  7.         }   
  8.     }   
  9.     return self;   
  10. }  

版本化和兼容性

在向框架添加新类或新方法时,您通常没必要为每个新功能群指定新的版本号。因为一般情况下,开发者会执行(或者说,应该执行)诸如respondsToSelector:这种Objective-C的运行时检测来判断给定的系统是否存在某种功能。开发者比较喜欢使用这种方式来检测新功能,同时它也是最动态的方式。

不论如何,您可以使用数种技术以确保新版本的框架能被正确标志并尽可能地兼容早期版本。

框架的版本

如果已存在的新功能或者错误改正不容易通过运行时进行检测,则您应该为开发者提供检测这些变更的办法。有一种办法是把确切的框架版本号保存起来,然后让该号码对开发者可见:

把变更归档在一个版本号下面(例如,归档在发布记录中)。

设置框架的当前版本号并且提供某种方法使之全局可见。您可以把版本号保存在框架的信息属性列表(Info.plist),这样就可以从该列表获取版本号。

基于键的归档

如果框架对象需要被写入到nib文件,则它们必须能够自我归挡 。另外,如果文档使用归挡机制来保存文档数据,则您也要对它们做归挡。在归挡时,您可以使用“老风格”(利用initWithCoder:和encodeWithCoder:这样的方法)。但是,为了更好地兼容过去、现在、以及未来的框架版本,您应该基于健进行归档。

基于键进行归档,对象就可以使用键来读取或写入被归挡值。相对以往的归挡机制,该方法可以在前向和后向兼容性上提供更多的灵活性。因为老归档机制要求代码和读取或写入的值维持相同的顺序,而且它也没有什么好办法来改变已写到档案的数据。如果您需要了解更多基于键的归档机制,请参考 Cocoa归档和序列化编程指南 (Archives and Serializations Programming Guide for Cocoa)一文。

对于正在编写的新的类,请为其使用基于键的归档机制。如果之前已发布的类使用了老归档机制,您也无需再采取任何措施。如果对象实现了Mac OS X 10.2版本之前的归档机制,则它必须能从档案中读取内容并能其内容写入到档案。但如果您在Mac OS X v10.2及之后的平台上为该对象添加新属性,则您不必,实际上是不应该,将这些属性保存到老档案中(这样做可能会使老档案在更早的系统中变得不可读取),这种情况下,新属性应使用键值归档机制。

请注意下列和基于键归档相关的事实:

如果档案中的某个键丢失,则在获取这个键对应值的时候,依据所要求的类型,其返回值可能是nil、 NULL、NO、0、或者0。通过对该返回值进行测试,您可以减少写到档案中的数据。同时,还可以检测某个键是否已被写入到档案中。

如果使用旧式归档,则initWithCoder:的实现需要独自挑起兼容性的重担。但如果使用键值归档,则归档方法和解档方法都可以采取一些措施以保证兼容性。举个例子,一个新版本的类的归档方法可能使用键来写入一个新值,而依旧把早期的字段写入到档案中,这样类的旧版本仍然可以理解该对象。与此同时,我们还可以在解档方法中使用某种合理的方式以处理数值缺失的情况,从而为将来的版本保留一些灵活性。

在命名框架类的档案键时,我们提倡使用和框架其他API元素一样的前缀,前缀后面再使用实例变量名称。您只要确保它的名称不会和任意的子类或者超类名称发生冲突即可。

如果您使用一个工具函数向档案中写入一个基本数据类型(换句话说,就是一个非对象的值),则请务必为该数值使用一个唯一键。举个例子,如果您有一个对矩形归档的“archiveRect” 例程,则您应该为该函数传入一个键作为参数。您可以直接把它作为档案键;或者,如果这个例程向档案写入多个值(例如,写入四个浮点数值),则它应该把每个数值自身独有的位添加到所提供的键上面。

位字段对于编译器和比特序有依赖关系,按照位字段现有的格式进行归档可能会有危险。只有当有多个位元需要被写入档案多次时,我们才会对位字段进行归档,这主要是为了提高性能。请参看 “位字段”一节 以获取相关的建议。

对象的尺寸和保留字段

每个Objective-C的对象都有一个尺寸,它由对象自身实例变量加上对象所有超类具有的实例变量得到的总尺寸决定。如果改变了一个类的尺寸,则其拥有实例变量的子类必须重新编译。为了保持二进制兼容性,通常情况下,我们不能通过向类添加新的实例变量或者去除类里面不必要的实例变量来改变对象的尺寸。

因此,对于新的类,为其留下几个额外的”保留“字段以便于将来扩展是个不错的想法。如果一个类只会有少数的几个实例,这个想法显然不成问题。但如果这个类会被实例化数千次,则您可能需要让保留的单个变量的尺寸小一些(也就是说,任意对象都占用四个字节)。

对于较早的类的对象,如果它们的空间已经用完(并且假定实例变量没有被导出成为公共变量),则您可以移动实例变量,或者把它们捆绑在一起,使之成为一个更小的字段。通过对实例变量进行重新排布,您就有可能添加新的数据而不会导致对象的总尺寸发生改变。或者您可以把一个剩余的保留槽作为指针,使它可以指向一块额外的内存,然后您在对象初始化的时候分配这块内存(并且在对象释放的时候销毁它)。又或者您可以把额外的数据放入到一张外部的哈希表(例如放入NSDictionary);这种方法对于那些很少创建使用的实例变量具有很好的效果。

异常和错误

大多数Cocoa框架的方法不会强制要求开发者捕捉处理异常,因为程序正常执行时不会产生异常,而且我们通常也不使用异常来表示可预期的运行时错误或用户错误。下面这些例子属于可预期的运行时错误或者用户错误:

文件找不到

用户不存在

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

把字符串转化为特定编码时出现错误

但是不管怎么样, Cocoa确实会引发异常以指示下面这些编程错误或者逻辑错误:

数组索引越界

试图改变一个不可改变的对象

错误参数类型

我们认为应用程序推向市场之前,开发者会对其进行测试,发现并解决这些类型的错误。因此,应用程序不需要在运行时处理上述异常。如果出现一个异常发生但应用程序没有捕捉,则最顶层的缺省处理器通常会捕捉并且报告该异常,然后程序将继续执行。开发者可以选择替换掉这个缺省的异常处理器,新的处理器可以更详细地描述什么地方发生了错误,并且还能够让用户选择是否保存数据并且推出应用程序。

错误是Cocoa框架不同于其他软件库的又一个地方。Cocoa的方法通常不返回错误码。某些情况下,一个错误具有一个合理或者可能的原因,方法通过对一个布尔值或者对象返回值(nil/non-nil)进行简单测试以判断该情况;但是返回NO或者nil值的原因则被记录在文档中。另外,您不应该使用错误码来指示需要在运行时处理的编程错误,相反您应该引发一个异常,或者在某些情况下,您也可以只记录下该错误而不引发异常。

举个例子,NSDictionary的objectForKey:方法会返回所找到的对象,如果对象找不到,则它会返回nil值。NSArray的objectAtIndex:法不可能返回nil值(除非我们把通用的编程语言约定重载成向nil对象发送消息都会返回nil值),因为NSArray对象不能保存nil值,并且所有的越界访问都被定义成编程错误,这种错误引发异常而并不返回nil对象。如果对象不能使用用户提供的参数进行初始化,许多init方法都会返回nil值

在少数情况下,一个方法需要多个不同的错误码是合理的。这时,该方法需要将错误码指定到一个传引用参数,然后您可以利用该参数返回一个错误码,也可以返回一个本地化的错误字符串,或者一些其他的可以描述错误的信息。举个例子,您可以把错误转换成NSError对象返回(请参看Foundation框架的NSError.h头文件以获取更多的细节)。而除了这个NSError对象,方法还能直接返回相对简单的BOOL值或者nil值。另外要注意,这种方法的所有传引用参数都是可选的。因此,如果发送者不想了解错误原因,它们可以传一个NULL 值给错误码参数。

重要: NSError类在Mac OS X v10.3之后的版本对外公开。

框架数据

框架数据的处理方式可能会对性能、跨平台兼容性、以及其他某些方面产生影响。本节讨论一些和框架数据相关的技术。

常量数据

出于性能的原因,您应尽可能多地把框架数据标志为常量,因为这样可以减小Mach-O二进制文件的__DATA 段的尺寸。没有const标记的全局或静态的数据最终会存放在__DATA段的__DATA节。对于每一个使用该框架的应用程序实例,这种类型的数据都会占用内存。尽管多出500字节(举个例子)好像没那么糟糕,但是这有可能导致应用程序所需的内存页面的数量增多—即每个应用程序都需要额外的四千字节的内存。

您应该为所有不变的数据添加const标记。具有const标记的数据块,如果其中没有char *类型的指针,则该数据会被存放在__TEXT段中(这会使数据成为真正的常量);否则数据就会被存放在__DATA段中,但是这些数据不可以被写入(除非预绑定未完成,如果预邦定已经完成,则只能在加载的时候,通过移动二进制文件的方式来写入)。

您应该初始化静态变量,这样可以确保该变量被合并到__DATA段的__data节中而非__bss节。如果没有明显的值用于初始化静态变量,则请使用0、NULL、0.0、或者任何恰当的值。

位字段

如果使用有符号的值来表示位字段,而代码又假定这个位字段是布尔值,则可能会导致未定义的行为。只有一个位的位字段尤为如此。因为在这种情况下,这个位字段只能存储0和 -1(取决于编译器的实现),把它和1做比较,其结果总是不相等。因此,只有一个位的位字段应该是无符号的。举个例子,如果您在代码中遇到如下情况:

  1. BOOL isAttachment:1;   
  2. int startTracking:1;  

您应该把上述代码中的类型改为unsigned int

和位字段相关的另一个问题是归档。通常情况下,您不应该按照当前格式将其写入到磁盘或者档案中,因为当我们在另外的架构或者编译器中读取这些字段时,它们的格式可能发生变化。

内存分配

框架代码中,如果可以的话,***是完全避免分配内存。如果出于某种原因,您需要一块临时的缓冲区,则通常情况下,使用栈比分配缓冲区更好。但是,栈的大小有限(栈总的大小通常为512千字节), 因此是否使用栈取决于函数和您所需要的缓冲区的大小。通常情况下,如果您需要的缓冲区的大小不超过1000(或者是MAXPATHLE定义的值)字节,使用栈是合适的。

一个比较精细的方法是在开始的时候使用栈,但是如果所需要的内存的大小超过了栈缓冲区的大小,则切换到malloc内存分配方式。列表 2展示的代码片段就是这么做的。

列表 2  使用栈内存和通过malloc分配的缓冲区

  1. #define STACKBUFSIZE (1000 / sizeof(YourElementType))   
  2.  YourElementType stackBuffer[STACKBUFSIZE];   
  3.  YourElementType *buf = stackBuffer;   
  4.  int capacity = STACKBUFSIZE;  // In terms of YourElementType   
  5.  int numElements = 0;  // In terms of YourElementType   
  6.     
  7. while (1) {   
  8.     if (numElements > capacity) {  // Need more room   
  9.         int newCapacity = capacity * 2;  // Or whatever your growth algorithm is   
  10.         if (buf == stackBuffer) {  // Previously using stack; switch to allocated memory   
  11.             buf = malloc(newCapacity * sizeof(YourElementType));   
  12.             memmove(buf, stackBuffer, capacity * sizeof(YourElementType));   
  13.         } else {  // Was already using malloc; simply realloc   
  14.             buf = realloc(buf, newCapacity * sizeof(YourElementType));   
  15.         }   
  16.         capacity = newCapacity;   
  17.     }   
  18.     // ... use buf; increment numElements ...   
  19.   }   
  20.   // ...   
  21.   if (buf != stackBuffer) free(buf);  

语言问题

下面的条款讨论和Objective-C语言相关的问题,包括协议、对象比较、以及向对象发送autorelease消息的时机。

向nil对象发送消息

在Objective-C中,只要消息的返回值是对象、任意的指针类型、或是其尺寸小于等于sizeof(void*)的整数型标量,我们就可以将其发送给nil对象,该消息将返回nil值。这个特性一种很有价值的编程资产,但是有一个问题需要注意。如果发送给nil对象的消息的返回值非上述类型(例如,消息返回任意的struct类型,或者浮点类型,或者任意的向量类型),则消息的返回值未定义。通常情况下,如果消息的返回值不是一个对象,则不要依赖这种行为,因为那是很糟糕的做法。在Power PC系统中,向nil对象发送上述类型的消息不会有问题,但对于其他架构来说,这种行为行不通。

对象比较

通用的对象比较方法isEqual:和与对象类型关联在一起的比较方法(诸如isEqualToString:)有一个重要的不同。isEqual:方法允许任何对象作为参数,如果用于比较的对象属于不同的类,则该方法将返回NO值。而诸如isEqualToString:以及isEqualToArray:这样的方法通常都假定参数是某种特定的类型(和接收者一样的类型)。因此,这种函数不会执行类型检查,这使得它们运行速度比普通的对象比较方法快,但是安全性则不如之。对于那些从外部源获取的值,例如从应用程序的的信息属性表(Info.plist)或者应用程序的偏好设置获取的值,在对其进行比较时,我们倾向于使用isEqual:方法,因为这种方法更安全;但如果我们已知道要进行比较的值的类型,则应使用isEqualToString:方法。

关于isEqual:方法,还有一点就是它和hash方法的联系。存放在基于散列的Cocoa集合类里的对象(例如存放在NSDictionary或者NSSet中的对象)有一个基本的不变式,即如果[A isEqual:B] == YES,则[A hash] == [B hash]也成立。因此,如果您重写了类的isEqual:方法,则您也应重写hash方法以保持不变式成立。缺省情况下,isEqual:方法判断对象地址指针是否相等,而hash方法则返回一个基于对象地址产生的hash数值,因此,这个不变式就可以保持成立。

协议

协议是Objective-C中一个有趣的概念,但是它们在Cocoa API中用得有限。之所以这样做,有一个原因是协议有严格的设计要求。以某个含有十个方法的NSDataSource协议为例。如果某个开发人员遵循该协议并且实现其所有方法,而之后您又向该协议添加了一个新的方法,这就会破坏开发者和原有协议的一致性。因此,协议往往仅能包含它们***公开时所含有的方法集(除非您不期望其他开发者实现该协议)。因此,只有方法集不可能再增长时,才应使用协议。如果您必须扩展某个协议,则您应该添加一个新的协议,利用新协议来对原来的协议进行扩展,或者您可以把新方法添加在协议外部,并且在使用前检查它是否存在。

基本上,上述的原因也可以解释为何您不能使用常规协议来声明委托方法。而把委托方法声明为NSObjec上的范畴类— 即非正式的协议— 的另一个原因是我们可以选择是否实现某个方法。

自动释放的对象

返回对象值的方法或者函数,如果它们不是用于创建对象或者复制对象(new,alloc,copy 以及这些方法的变体),则请务必让其返回的对象可以autoreleased 。在此处,“自动释放”并不一定表示对象应该显式自动释放-即在返回对象之前,向对象发送autorelease 消息。一般情况下,它仅表示返回值不是由调用者释放。

出于性能方面的原因,请尽可能不要在方法实现中使用自动释放的对象,特别是那些可能会在短时间内频繁执行的代码(例如在循环结构中,循环次数未知并且可能是很多的情况)使用。举个例子,对于下面的情况,您不应该发送如下的消息:

  1. [NSString stringWithCharacters:]  

而是应该发送下面的消息:

  1. [[NSString alloc] initWithCharacters:]  

当您不再需要该字符串对象时,请显示地释放它。但是,请记住有些时候方法或者函数会返回自动释放的对象,这时您需要向对象发送autorelease消息。

存取方法

在存取方法中作什么事情才是正确呢,这是个重要的问题。举个例子,假设您在一个获取方法中直接返回一个实例变量,而后立刻调用设置方法,则在释放先前实例变量的时候就可能会有危险,因为它有可能把您之前返回的值给释放掉。

Cocoa框架的原则是让设置方法自动释放实例变量先前的值,但某些情况下,所涉及的某个设置方法会被频繁的调用(例如在一个很密集的循环中),这时候Cocoa的原则就不适用了。但在实践中,这种情况非常罕见,除非是一些底层的对象才会如此。另外,诸如NSAttributedString、NSArray、以及NSDictionary这样的通用集合不会自动释放对象,这主要是为了维护对象的存在时间。它们只是简单地留存或者释放其含有的对象。另外,它们也应该对这一事实进行归档,这样客户程序就可以了解这些对象的行为。

对于现在正在编写的框架代码, 我们建议在get方法中使用自动释放的对象,因为这是最安全的方法:

  1. (NSString *)title {   
  2.     return [[instanceVar retain] autorelease];   
  3. }   
  4.     
  5. - (void)setTitle:(NSString *)newTitle {   
  6.     if (instanceVar != newTitle) {   
  7.         [instanceVar release];   
  8.         instanceVar = [newTitle copy];   
  9.         // or retain, depending on object & usage   
  10.     }   
  11. }  

另外,我们还需要考虑设置方法是使用copy方式还是使用retain方式。如果您所感兴趣的是对象的值而非实际对象本身,则请使用copy方式。一个一般性的经验法则是对实现NSCopying协议的对象使用copy方式(您不应该在运行时检测对象是否实现NSCopying协议,而应该直接查找参考文档)。通常情况下,诸如字符串、颜色、URL这样的对象应该能被复制;而像视图、窗口这样的对象则应该可以被保持。而至于其他的对象(例如数组),是使用copy还是使用retain,则要根据具体情况决定。

小结:Cocoa 编码指南 框架开发者使用技巧和技术的内容介绍完了,希望本文对你有所帮助!关于Cocoa 编码指南的更多内容请参与编辑推荐。

责任编辑:zhaolei 来源: 互联网
相关推荐

2022-01-18 23:26:45

开发

2011-07-07 10:07:19

Cocoa 框架

2011-07-07 10:29:35

Cocoa 方法 框架

2011-06-17 16:23:49

Cocoa苹果

2011-07-07 10:39:07

Cocoa 函数

2011-04-02 13:44:08

2018-03-27 23:25:40

Paddle

2015-10-16 09:59:52

SwiftCocoa

2015-07-20 09:16:42

iOSWatchKit开发

2019-08-16 10:55:37

开发者技能AI

2023-05-08 15:59:27

UI自动化脚本鸿蒙

2024-02-01 09:37:42

Kubernetes服务网格• 命令

2019-11-14 14:44:32

开发者工具

2010-07-29 17:15:55

Flex

2011-07-07 13:51:24

Cocoa 框架

2015-04-14 09:33:17

WatchKitAPP

2022-01-02 23:26:08

开发SDK Sentry

2011-06-17 15:57:46

CocoaXcode苹果

2011-04-13 09:55:16

Mail APIBlackBerry

2011-04-13 13:38:57

选项APIBlackBerry
点赞
收藏

51CTO技术栈公众号