zoukankan      html  css  js  c++  java
  • [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

  • 相关阅读:
    算法训练 P1103
    算法训练 表达式计算
    算法训练 表达式计算
    基础练习 时间转换
    基础练习 字符串对比
    Codeforces 527D Clique Problem
    Codeforces 527C Glass Carving
    Codeforces 527B Error Correct System
    Codeforces 527A Glass Carving
    Topcoder SRM 655 DIV1 250 CountryGroupHard
  • 原文地址:https://www.cnblogs.com/yangfaxian/p/3825336.html
Copyright © 2011-2022 走看看