《iOS 7 Programming Pushing the Limits》系列:你可能不知道的ObjC技

来源:互联网 发布:js base64转换为文件 编辑:程序博客网 时间:2024/06/06 04:07
一、最好的命名实践
 
在iOS开发里,命名规范极其重要。在下面的部分,我们将学习如何正确命名各种条目,以及为什么这样命名。
 
1. 自动变量
Cocoa是动态类型的语言,你很容易对所使用的类型感到困惑。集合(数组、字典等等)没有关联它们的类型,所以这样的意外很容易发生:
  1. NSArray *dates = @[@”1/1/2000”]; 
  2. NSDate *firstDate = [dates firstObject]; 
编译器没有警告,但当你使用firstDate时,它很可能会报错(an unknown selector exception)。错误是调用一个string dates数组。这个数组应该调用dateStrings,或者应该包含NSDate对象。这样小心的命名将会避免很多令人头痛的错误。
 
2. 方法
1)方法名应该清楚表明接收和返回的类型
例如,这个方法名是令人困惑的:
  1. - (void)add; // 令人困惑 
看起来add应该带一些参数,但它没有。难道它是增加一些默认对象?
 
这样命名就清楚多了:
  1. - (void)addEmptyRecord; 
  2. - (void)addRecord:(Record *)record; 
现在addRecord:接收一个Record参数,看起来清楚多了。
 
2)对象的类型应符合名称,如果类型和名称不匹配,则容易弄混
这个例子展示了一个常见错误:
  1. - (void)setURL:(NSString *)URL; // 错误的 
这里错误是因为调用setURL时,应该接收一个NSURL,而不是一个NSString。如果你需要string,你需要增加一些指示让它更明朗:
  1. - (void)setURLString:(NSString *)string; 
  2. - (void)setURL:(NSURL *)URL; 
这个规则不应过度使用。如果类型很明显,别添加类型信息到变量上。一个叫做name的属性就比叫做nameString的属性更好。
 
3)方法名也有与内存管理和KVC相关的特定原则
虽然ARC使得其中的一些规则不再重要,但在ARC与非ARC进行交互时(包括Apple框架的非ARC代码),不正确的命名规则仍会导致非常具有挑战性的错误。
 
方法名应该永远是小写字母开头,驼峰结构。
 
如果一个方法名以alloc、new、copy或者nutableCopy开头,调用者拥有返回的对象。如果你的property的名字像newRecord这样,这个规则可能会导致问题,请换一个名字。
 
get方法的开头应该返回一个参照值,例如:
  1. - (void)getPerson:(Person **)person; 
不要使用get前缀作为property accessor的一部分,property name的getter应该为-name。
 
二、Property和实例变量(Ivar)的最佳实践
 
Property应该代表一个对象的状态,Getter应该没有外部影响(它们可以具有内部影响,例如caching,但那些应该是调用者不可见的)。
 
避免直接访问实例变量,使用accessor来代替。
 
在早期的ARC里,引起bug最常见的原因就是直接访问实例变量。开发者没有正确的retain和release实例变量,它们的应用就会崩溃或者内存泄露。由于ARC自动管理retain和release,一些开发者认为这个规则已经不再重要,但仍还有其他使用accessors的原因:
 
* KVO
也许使用accessor的最关键原因是,property可以被观察到。如果你不使用accessor,你需要在每次修改property里的实例变量时调用willChangeValueForKey: 和 didChangeValueForKey: ,而accessor会在需要时自动调用这些方法。
 
* Side effects
在setter里,你或者你的子类可能包含side effects。通知可能被传送、事件可能被注册到NSUndoManager里,你不应该绕过这些side effects,除非它是必要的。
 
* Lazy instantiation
如果一个property被lazily instantiated,必须使用accessor来确保它的正确初始化。
 
* Locking
如果引进locking到一个property里来管理多线程代码,直接访问实例变量将违背你的lock,并可能导致程序崩溃。
 
* Consistency
在看到前面的内容后,有人可能会说应该只使用accessor,但这使得代码很难维护。怀疑和解释每一个直接访问的实例变量,而不是记住哪些需要accessor哪些不需要,这样使得代码更容易审核、审阅和维护。Accessor,特别是synthesized accessors,已经在OC里被高度优化,它们值得使用。
 
这就是说,你不应该在这几个地方使用accessor:
 
* Accessor内部
显然,你不能在accessor内部使用自身。通常,你也不想在getter和setter内部使用它们自己(这可能创建无限循环),一个accessor应该访问其自身的实例变量。
 
* Dealloc
ARC极大地减少了dealloc,但它有时仍会出现。最好调用dealloc里的外部对象,该对象可能处于不一致的状态,并很可能造成混淆。
 
* Initialization
类似dealloc,对象可能在初始化过程中处于不一致状态,你不应该在此时销毁通知或者其他的side effects。
 
三、 分类(Categories)
 
分类允许你在运行中的类里添加方法。任何类(甚至是由Apple提供的Cocoa类)都可以通过分类来拓展,这些新方法对类的所有实例都是可用的,分类声明如下:
  1. @interface NSMutableString (PTLCapitalize) 
  2. - (void)ptl_capitalize; 
  3. @end 
PTLCapitalize是分类的名称,注意这里没有声明任何实例变量。
 
分类不能声明实例变量,也不能synthesize properties。
 
分类可以声明properties,因为它只是声明方法的另一种方式。
 
分类不能synthesize properties,因为这会创建一个实例变量。
 
1. +load
分类在运行时附加到类,这可能定义分类为动态加载,所以分类可以很晚添加(虽然你不能在iOS里编写自己的动态库,但系统框架是动态加载的,并且包括分类)。OC提供了一个名为 +load 的东西,在分类首次附加时运行。随着 +initialize,你可以使用它来实现指定分类的设定,例如初始化静态变量。你不能安全的在分类里使用+initialize,因为类可能已经实现它。如果有多个分类实现+initialize,那么运行一个没有意义。
 
我希望你已经准备好要问一个显而易见的问题:“如果分类不能使用+initialize,因为他们可能与其他分类冲突,那么多个分类实现+load呢?”这正是OC runtime神奇的地方之一, +load方法是runtime的特例,是每一个分类能实现它,并且所有的实现都运行。当然,你不应该尝试手动调用+load。
 
四、关联引用(Associative References)
 
关联引用允许你附加key-value数据到任何对象。这个能力有多种用途,但最常用的是允许你的分类添加数据的property。
 
考虑一个Person类的情况,你想使用分类来添加一个叫做emailAddress的新property。也许你在其他程序里使用Person类,并且有时使用email address而有时不用,因此使用分类是可以避免开销的很好解决方案。或者,你没有自己的Person类,并且维护者不会为你添加property,你该如何解决这个问题?首先来看一下基础的Person类:
  1. @interface Person : NSObject 
  2. @property (nonatomic, readwrite, copy) NSString *name; 
  3. @end 
  4.  
  5. @implementation Person 
  6. @end 
现在你可以添加新的property了,在分类里使用关联引用:
  1. #import <objc/runtime.h> 
  2. @interface Person (EmailAddress) 
  3. @property (nonatomic, readwrite, copy) NSString *emailAddress; 
  4. @end 
  5.  
  6. @implementation Person (EmailAddress) 
  7. static char emailAddressKey; 
  8.  
  9. - (NSString *)emailAddress { 
  10.  return objc_getAssociatedObject(self, &emailAddressKey); 
  11.  
  12. - (void)setEmailAddress:(NSString *)emailAddress { 
  13.  objc_setAssociatedObject(self, &emailAddressKey, emailAddress, OBJC_ASSOCIATION_COPY); 
  14. }  
  15. @end 
注意关联引用是基于key的内存地址,而不是它的值。emailAddressKey里存储什么并不重要,它只需要有一个唯一、不变的地址,这就是为什么它通常使用未分配的static char作为key。
 
关联引用有很好的内存管理,用以参照objc_setAssociatedObject的参数传递正确处理copy、assign或者retain。当相关的对象被deallocated,它们会released。这实际上意味着在另一个对象被销毁时,你可以使用相关的对象进行追踪,例如:
  1.  const char kWatcherKey; 
  2.   
  3.  @interface Watcher : NSObject 
  4.  @end 
  5.   
  6.  #import <objc/runtime.h> 
  7.   
  8.  @implementation Watcher 
  9.  - (void)dealloc { 
  10.      NSLog(@"HEY! The thing I was watching is going away!"); 
  11.  } 
  12.  @end 
  13.  ... 
  14.  NSObject *something = [NSObject new]; 
  15.  objc_setAssociatedObject(something, &kWatcherKey, [Watcher new], OBJC_ASSOCIATION_RETAIN); 
这种技术对于调试非常有用,同时也可用于非调试任务,例如执行清理。
 
使用关联引用是附加相关对象到alert panel或者control的好方法,例如你可以附加一个“represented object”到alert panel,代码如下:
  1. ViewController.m (AssocRef) 
  2. id interestingObject = ...; 
  3. UIAlertView *alert = [[UIAlertView alloc] 
  4.                                   initWithTitle:@"Alert" message:nil 
  5.                                  delegate:self 
  6.                                  cancelButtonTitle:@"OK" 
  7.                                  otherButtonTitles:nil]; 
  8. objc_setAssociatedObject(alert, &kRepresentedObject, 
  9.                           interestingObject, 
  10. [alert show]; 
许多程序在调用里使用实例变量处理这个任务,但关联引用更简洁。对于那些熟悉Mac的开发者,这些代码类似于representedObject,但却更灵活。
 
联想引用的一个限制是,它们没有与encodeWithCoder:整合,因此它们很难通过一个分类来序列化。
 
五、Weak Collections
 
大多数Cocoa的集合例如NSArray、NSSet和NSDictionary都具有强大功能,但它们不适合某些情况。NSArray与NSSet会保留你存储进去的对象,NSDictionary会保存value和key,这些行为通常是你想要的,但对于某些工作它们并不适合。幸运的是,自从iOS6开始,一些其他的集合开始出现:NSPointerArray、NSHashTable与NSMapTable。它们统称为Apple文档的指针集合类(pointer collection classes),并且有时使用NSPointerFunctions类来进行配置。
 
NSPointerArray类似NSArray, NSHashTable类似NSSet,而NSMapTable类似NSDictionary。每个新的集合类都可以配置为保持弱引用,指向空对象或者其他异常情况。NSPointerArray的一个额外好处是它还可以存储NULL值。
 
指针集合类可以使用NSPointerFunctions来广泛的配置,但大多数情况下,它只是简单的传送一个NSPointerFunctionsOptions flag到–initWithOptions:。最常见的情况,例如+weakObjectsPointerArray,有自己的构造函数。
 
六、 NSCache
 
NSCache有几个被低估的功能,比如事实上它是线程安全的,你可能在任何无锁的线程里改变一个NSCache。NSCache也被设计来融合对象遵从<NSDiscardableContent>,其中最常见的类型是NSPurgeableData,通过调用beginContentAccess 与 endContentAccess,你可以控制何时安全放弃这个对象。这不仅在你的应用运行时提供自动缓存管理,它甚至有助于你的应用被暂停。通常情况下,当内存紧张时,内存警告没有释放出足够的内存,iOS会开始杀死暂停在后台的应用。在这种情形下,你的应用没有得到delegate信息,就这样被杀死。不过如果你使用NSPurgeableData,iOS会释放这块内存给你,即使你的应用被暂停。
 
想得到更多关于NSCache的信息,请参考官方文档NSDiscardableContent与NSPurgeableData。
 
七、NSURLComponents
 
有时,Apple会悄悄添加一些有趣的类。在iOS7里,Apple增加了NSURLComponents,但却没有相关的参考文档,你需要到NSURL.h里来查看它(NSURL.h里有许多有趣的方法,你可以进去仔细研究)。
 
NSURLComponents让取出URL的各个部分变得容易,例如:
  1. NSString *URLString = 
  2.       @"http://en.wikipedia.org/wiki/Special:Search?search=ios"
  3. NSURLComponents *components = [NSURLComponents 
  4.      componentsWithString:URLString]; 
  5. NSString *host = components.host; 
你也可以使用NSURLComponents来组成或修改URL:
  1. components.host = @"es.wikipedia.org"
  2. NSURL *esURL = [components URL]; 
 
八、 CFStringTransform
 
CFStringTransform可以以神奇的方式来音译字符串,例如,你可以使用选项kCFStringTransformStripCombiningMarks: 来删除重音符号:
  1. CFMutableStringRef string = CFStringCreateMutableCopy(NULL, 0, CFSTR("Schläger")); 
  2. CFStringTransform(string, NULL, kCFStringTransformStripCombiningMarks, false); 
  3. ... => string is now “Schlager” CFRelease(string); 
当你在处理非拉丁文字系统时(例如中文和阿拉伯语),CFStringTransform更是如虎添翼,它可以转换许多书写系统为拉丁文字。例如,你可以将中文转换为拼音:
  1. CFMutableStringRef string = CFStringCreateMutableCopy(NULL, 0, CFSTR("你好")); 
  2. CFStringTransform(string, NULL, kCFStringTransformToLatin, false); 
  3. ... => string is now “nˇ? hˇao” 
  4. CFStringTransform(string, NULL, kCFStringTransformStripCombiningMarks, 
  5. false); 
  6. ... => string is now “ni hao” CFRelease(string); 
 
九、 instancetype
 
Objective-C中早就有了一些微妙的子类的问题。考虑下面的情况:
  1. @interface Foo : NSObject 
  2. + (Foo *)fooWithInt:(int)x; @end 
  3. @interface SpecialFoo : Foo 
  4. @end 
  5. ... 
  6. SpecialFoo *sf = [SpecialFoo fooWithInt:1]; 
这段代码会产生一个警告:“Incompatible pointer types initializing ’SpecialFoo *’ with an expression of type ’Foo *’。”问题在于fooWithInt返回了一个Foo对象,而编译器无法知道返回的类型确实是一个更具体的类(SpecialFoo),这种情况相当常见。
 
有几种解决这个问题的方案。
 
方案一:首先,你可能重载fooWithInt:,代码如下:
  1. @interface SpecialFoo : Foo 
  2. + (SpecialFoo *)fooWithInt:(int)x;  
  3. @end 
  4.   
  5. @implementation SpecialFoo 
  6. + (SpecialFoo *)fooWithInt:(int)x { 
  7.     return (SpecialFoo *)[super fooWithInt:x]; 
这种方法虽然可以解决,但非常不方便,你不得不只是为了类型转换重写许多方法。
 
方案二:你还可以在调用时执行类型转换:
  1. SpecialFoo *sf = (SpecialFoo *)[SpecialFoo fooWithInt:1];  
这种方法虽然也可以解决,但对调用者很不方便,加入大量的类型转换也会消除类型检查,因此它更容易出错。
 
方案三:最常见的解决办法是返回ID类型:
  1. @interface Foo : NSObject + (id)fooWithInt:(int)x;  
  2. @end 
  3.  
  4. @interface SpecialFoo : Foo 
  5. @end 
  6. ... 
  7. SpecialFoo *sf = [SpecialFoo fooWithInt:1]; 
这种办法相当方便,而且消除了类型检查。这是上面三个方案中最好用的,这就是为什么id无处不在的原因。
 
方案四:使用instancetype作为返回类型
 
instancetype表示“当前类”(id与instancetype的区别请自行Google),比使用id更适合解决这个问题。代码如下:
  1. @interface Foo : NSObject 
  2. + (instancetype)fooWithInt:(int)x;  
  3. @end 
  4.  
  5. @interface SpecialFoo : Foo 
  6. @end 
  7. ... 
  8. SpecialFoo *sf = [SpecialFoo fooWithInt:1]; 
为了保持一致性,最好使用instancetype作为双方的init方法和便利的构造函数的返回类型。
 
十、Base64 和 Percent编码
 
Cocoa早就需要方便的访问Base64编码和解码。Base64是许多Web协议的标准,并且在许多你需要存储任意数据到一个字符串里的情况下非常有用。
 
在iOS7,新的NSData方法例如initWithBase64EncodedString:options: 和 base64EncodedStringWithOptions: 可以用来在Base64和NSData间转换。
 
Percent编码对于Web协议同样重要,特别是URLs,你现在可以使用[NSString stringByRemovingPercentEncoding]来对percent编码进行解码。尽管已经有stringByAddingPercentEscapesUsingEncoding:方法来进行percent编码,iOS7还是添加了一个stringByAddingPercentEncodingWithAllowedCharacters:方法,允许你控制percent编码的字符。
 
十一、 -[NSArray firstObject]
 
这是一个极小的改变,但是我仍要提到它,因为我们等待它已久:多年来,许多开发者用实现分类来获取数组的首个对象,现在Apple终于添加了方法firstObject。就像lastObject一样,如果数组是空的,firstObject返回nil,而不是objectAtIndex:0。
 
十二、摘要
 
Cocoa有很长的历史,充满了传统和惯例,同时Cocoa也是一个发展的、活跃的框架。在这个章节里面,你已经学习到一些数十年里OC开发的最佳实践。你学会了为类、方法和变量选择最好的命名方式;学到了一些并不众所周知的功能例如associative references和NSURLComponents。即使作为老练的OC开发者,你仍希望学到一些之前并不知道的Cocoa技巧。
 
十三、更多阅读
 
1. 官方文档
 
CFMutableString
Reference?CFStringTokenizer
Reference?Collections Programming Topics?
Collections Programming Topics, “Pointer Function Options”
Programming with Objective-C
 
2. 其他资源
 
nshipster.com
马特·汤普森的博客,每周更新
https://github.com/00StevenG/NSString-Japanese
如果你需要处理日文文本,这是一个非常有用的分类,用来处理各种复杂的书写系统
 
本书源代码:http://pan.baidu.com/s/1bnnJZIJ
0 0