[iOS翻译]《iOS 7 Programming Pushing the Limits》系列:你可能不知道的Objective-C技巧
简介:
如果你阅读这本书,你可能已经牢牢掌握iOS开发的基础,但这里有一些小特点和实践是许多开发者并不熟悉的,甚至有数年经验的开发者也是。在这一章里,你会学到一些很重要的开发技巧,但这仍远远不够,你还需要积累更多的实践来让你的代码更强力。
/*
本文翻译自《iOS 7 Programming Pushing the Limits》一书的第三章“You May Not Know”,想体会原文精髓的朋友请支持原书正版。
——————(博客园、新浪微博)葛布林大帝
*/
目录:
一. 最好的命名实践
二. Property和实例变量(Ivar)的最佳实践
三. 分类(Categories)
四. 关联引用(Associative References)
五. Weak Collections
六. NSCache
七. NSURLComponents
八. CFStringTransform
九. instancetype
十. Base64 和 Percent编码
十一. -[NSArray firstObject]
十二. 总结
十三. 更多阅读
一、最好的命名实践
在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 13 - (void)setEmailAddress:(NSString *)emailAddress { 14 objc_setAssociatedObject(self, &emailAddressKey, emailAddress, OBJC_ASSOCIATION_COPY); 16 } 17 @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]; 8 }
这种方法虽然可以解决,但非常不方便,你不得不只是为了类型转换重写许多方法。
方案二:你还可以在调用时执行类型转换:
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