[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技巧

时间: 2025-01-13 17:54:41

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

Android Programming: Pushing the Limits -- Chapter 7:Android IPC -- Messenger

Messenger类实际是对Aidl方式的一层封装.本文只是对如何在Service中使用Messenger类实现与客户端的通信进行讲解,对Messenger的底层不做说明.阅读Android Programming: Pushing the Limits -- Chapter 7:Android IPC -- AIDL了解如何使用Aidl的方式实现服务端与客户端的通信. 在Service中使用Messenger,大部分代码还是跟Android的消息机制打交道,具体一点就是跟Handler,Mes

Android Programming: Pushing the Limits -- Chapter 7:Android IPC -- ApiWrapper

前面两片文章讲解了通过AIDL和Messenger两种方式实现Android IPC.而本文所讲的并不是第三种IPC方式,而是对前面两种方式进行封装,这样我们就不用直接把Aidl文件,java文件拷贝到客户端了,而是为客户端提供一个aar(Anroid Archive)包.通过这个aar包对AIDL或者Messenger进行封装,最终客户端就可以像调用一般的java类一样,而不用处理通信方面的内容.(实际上书中说是要打包成jar包,但是在新建的Java Library Module中,我没能成功

iOS 7 Programming Pushing the Limits

Book Description In some ways, iOS 7 is the most radical change to iOS since the software development kit (SDK) was released in iPhone OS 2. The press and blogosphere have discussed every aspect of the new "flat" user interface and what it means

Android Programming: Pushing the Limits -- Chapter 3: Components, Manifests, and Resources

Android Components Manifest文件 Resource and Assets v\:* {behavior:url(#default#VML);} o\:* {behavior:url(#default#VML);} w\:* {behavior:url(#default#VML);} .shape {behavior:url(#default#VML);} Normal 0 false 7.8 磅 0 2 false false false EN-US ZH-CN X-N

Android Programming: Pushing the Limits -- Chapter 2: Efficient Java Code for Android

Android's Dalvik Java 与 Java SE 进行比较 Java代码优化 内存管理与分配 Android的多线程操作 Android’s Dalvik Java 与 Java SE 进行比较: @.Dalvik虚拟机是register-based machine:Java SE虚拟机是stack machine. @.从Android 2.2 版本开始引进JIT(Just In Time)编译器,之前是纯解释器. @.Android SDK 使用dx这个工具把Java SE s

Android Programming: Pushing the Limits -- Chapter 1: Fine-Tuning Your Development Environment

ADB命令 Application Exerciser Monkey Gradle ProGuard 代码重用 版本控制 静态代码分析 代码重构 开发者模式   ADB命令: @.adb help:查看adb命令. @.adb devices:列出所有连接的安卓设备和模拟器. @.adb push <local> <remote> 把计算机里的文件拷贝到设备中. adb push e:\test.xml /sdcard/files.ldb/ @.adb pull <remot

Android Programming: Pushing the Limits -- Chapter 7:Android IPC -- AIDL

服务端: 最终项目结构: 这个项目中,我们将用到自定义类CustomData作为服务端与客户端传递的数据. Step 1:创建CustomData类 package com.ldb.android.example.aidl; import android.os.Parcel; import android.os.Parcelable; import android.util.Log; import java.util.ArrayList; import java.util.Date; impor

Android Programming: Pushing the Limits -- Chapter 6: Services and Background Tasks

什么时候使用Service 服务类型 开启服务 后台运行 服务通信 附加资源 什么时候使用Service: @.任何与用户界面无关的操作,可移到后台线程,然后由一个Service来控制这个线程. 服务类型: @.First is the one that performs work for the application independent of the user’s input. 如:后台执行的音乐播放器. @.The other type of Service is one that’s

Android Programming: Pushing the Limits -- Chapter 4: Android User Experience and Interface Design

User Stories Android UI Design 附加资源 User Stories: @.通过写故事来设计应用. @.每个故事只关注一件事. @.不同的故事可能使用相同的组件,因此尽早地对故事进行分类. @.把目标用户构想到故事里,描述他们的基本特征,会在什么时候.什么地点使用该应用等信息,因此来确定故事的优先级. Android UI Design: @.构思应用需要展示的界面及内容,不需要详细的界面设计. @.确定各界面的跳转关系. @.用户界面原型设计,可通过工具进行,比如A