[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

[iOS翻译]《iOS 7 Programming Pushing the Limits》系列:你可能不知道的Objective-C技巧,布布扣,bubuko.com

[iOS翻译]《iOS 7 Programming Pushing the Limits》系列:你可能不知道的Objective-C技巧

上一篇:iOS开发-微博客户端-基本界面搭建(01)


下一篇:WPF自定义控件与样式(10)-进度控件ProcessBar自定义样