《编写高质量iOS与OS X代码的52个有效方法》总结

2018-02-12  本文已影响15人  黑化肥发灰

最近看完了《编写高质量iOS与OS X代码的52个有效方法》,也就是《Effective Objective-C 2.0》这本书,现在总结一下,以便今后回顾。

熟悉 Objective-C


1. 了解Objective-C语言的起源

2. 在类的头文件中尽量少引入其他头文件

3. 多用字面量语法,少用与之等价的方法

NSString *someString = @"Effective Objective-C 2.0";
NSNumber *intNumber = @1;
NSNumber *floatNumber = @2.5f;
NSNumber *boolNumber = @YES;
NSNumber *charNumber = @'a'
NSNumber *animals = @[@"cat", @"dog", @"mouse", @"badger"];
NSNumber *dog = animals[1];
NSDictionary *personData = @{@"firstName": @"Matt", @"lastName": @"Galloway", @"age": @28};

4. 多用类型常量,少用 #define 预处理指令

// In the header file
extern NSString * const EOCStringConstant;

// In the  implementation file
NSString * const EOCStringConstant = @"VALUE"

5. 用枚举表示状态、选项、状态码

typedef NS_ENUM (NSUInteger, EOCConnectionState) {
        EOCConnectionStateDisconnected,
        EOCConnectionStateConnecting,
        EOCConnectionStateConnected
};

typedef NS_OPTIONS (NSUInteger, EOCPermittedDirection) {
        EOCPermittedDirectionUp = 1 << 0,
        EOCPermittedDirectionDown = 1 << 1,
        EOCPermittedDirectionLeft = 1 << 2,
        EOCPermittedDirectionRight = 1 << 3
}

对象、消息、运行期


6. 理解 “属性” 这一概念

如果开发过iOS程序,你就会发现,其中所有属性都声明为 nonatomic。这样做的历史原因是:在iOS中使用同步锁的开销较大,这会带来性能问题。一般情况下并不要求属性必须是 “原子的”,因为这并不能保证 “线程安全”(thread safety),若要实现 “线程安全” 的操作,还需采用更为深层的锁定机制才行。例如,一个线程在连续多次读取某属性值的过程中有别的线程在同时改写该值,那么即便将属性声明为 atomic,也还是会读到不同的属性值。因此,开发iOS程序时一般都会使用 nonatomic 属性。但是在开发Mac OS X 程序时,使用atomic 属性通常都不会有性能瓶颈。

7. 在对象内部尽量直接访问实例变量

8. 理解 “对象等同性” 这一概念

9. 以 “类族模式” 隐藏实现细节

“类族”(class cluster)是一种很有用的模式(pattern),可以隐藏 “抽象基类”(abstract base class)背后的实现细节。Objective-C 的系统框架中普遍使用此模式。比如,iOS的用户界面框架(user interface framework)UIKit 中就有一个名为 UIButton 的类。想创建按钮,需要调用下面这个 “类方法”(class method):
+ (UIButton *)buttonWithType:(UIButtonType) type;

10. 在既有类中使用关联对象存放自定义数据

// 创建完警告视图之后,设定一个与之关联的 “块”(block),等到执行 delegate 方法时再将其读出来,此方案的实现代码如下:
#import <objc/runtime.h>

static void *EOCMyAlertViewKey = "EOCMyAlertViewKey";
- (void)askUserAQuestion {
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle: @"Question" message: @"What do you want to do?" delegarte: self cancelButtonTitle: @"cancel" otherButtonTitles: @"Continue", nil ];
    void (^block)(NSInteger) = ^(NSInteger buttonIndex) {
        if (buttonIndex == 0) {
            [self doCancel];
        } else {
            [self doContinue];
        }
    };

    objc_setAssociatedObject(alert, EOCMyAlertViewKey, block, OBJC_ASSOCIATION_COPY);
    [alert show];
}

// UIAlertViewDelegate protocol method
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex: (NSInteger)buttonIndex
{
    void (^block)(NSInteger) = objc_getAssociatedObject(alertView, EOCMyAlertViewKey);
    block(buttonIndex);
}

// 以这种方式改写之后,创建警告视图与处理操作结果的代码都放在一起了,这样比原来更易读懂,因为我们无须在两部分代码之间来回游走,即可明白警告视图的用处。

11. 理解 objc_msgSend 的作用

12. 理解消息转发机制

动态方法解析阶段
+ (BOOL)resolveInstanceMethod: (SEL)selector

备援接受者阶段
- (id)forwardingTargetForSelector: (SEL)selector

启用完整的消息转发
- (void)forwardInvocation: (NSInvocation *)invocation

13. 用 “方法调配技术” 调试 “黑盒方法”

交换 lowercaseString 与 uppercaseString 方法实现:
Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);

14. 理解 “类对象” 的用意

// 描述Objective-C 对象所用的数据结构定义在运行期程序库的头文件里,id类型本身也定义在这里:
typedef struct objc_object {
    Class isa;
} * id;

// 由此可见每个对象结构体的首个成员是Class类的变量。该变量定义了对象所属的类,通常称为 “is a” 指针。例如,刚才的例子中所用的对象 “是一个” (is a)NSString,所以其 “is a” 指针就指向 NSString。Class 对象也定义在运行期程序库的头文件中:

typedef struct objc_class *Class;
struct objc_class {
    Class isa;
    Class super_class;
    const char *name;
    long version;
    long info;
    long instance_size;
    struct objc_ivar_list *ivars;
    struct objc_method_list **methodLists;
    struct objc_cache *cache;
    struct objc_protocol_list *protocols;
};

// 此结构体存放类的 “元数据” (metadata),例如类的实例实现了几个方法,具备多少个实例变量等信息。此结构体的首个变量也是isa指针,这说明Class 本身也是Objective-C对象。结构体里还有个变量叫做 super_class,它定义了本类的超类。类对象所属的类型(也就是isa指针所指向的类型)是另外一个类,叫做 “元类” (metadata),用来表述类对象本身所具备的元数据。“类方法” 就定义于此,因为这些方法可以理解成类对象的实例方法。每个类仅有一个 “类对象”,而每个 “类对象” 仅有一个与之相关的 “元类”。

接口与API设计


15. 用前缀避免命名空间冲突

16. 提供 “全能初始化方法”

17. 实现 description 方法

18. 尽量使用不可变对象

将属性在对象内部重新声明为readwrite 这一操作可于 “class-continuation 分类” 中完成,在公共接口中声明的属性可于此处重新声明,属性的其他特质必须保持不变,而 readonly 可扩展为 readwrite。

19. 使用清晰而协调的命名方式

20. 为私有方法名加前缀

与公有方法不同,私有方法不出现在接口定义中。有时可能要在 “class-continuation 分类”里声明私有方法,然而最近修订的编译器已经不要求在使用方法前必须先行声明了。所以说私有方法一般只在实现的时候声明。

21. 理解 Objective-C 错误模型

Objective-C 语言现在所采用的办法是:只在极其罕见的情况下抛出异常,异常抛出之后,无须考虑恢复问题,而且应用程序此时也应该退出。这就是说,不用再编写复杂的 “异常安全” 代码了。

22. 理解 NSCopying 协议

mutableCopy 这个 “辅助方法”(helper)与 copy 相似,也是用默认的zone参数来调 “mutableCopyWithZone:”。如果你的类分为可变版本(mutable variant)与不可变版本(immutable variant),那么就应该实现 NSMutableCopying。若采用此模式,则在可变类中覆写 “copyWithZone;” 方法时,不要返回可变的拷贝,而应该返回一份不可变的版本。无论当前实例是否可变,若需获取其可变版本的拷贝,均应调用 mutableCopy 方法。同理,若需要不可变的拷贝,则总应通过copy方法来获取。
对于不可变的 NSArray 与 可变的 NSMutableArray 来说,下列关系总是成立的:
- [NSMutableArray copy] => NSArray
- [NSArray mutableCopy] => NSMutableArray

协议与分类


23. 通过委托与数据源协议进行对象间通信

24. 将类的实现代码分散到便于管理的数个分类之中

25. 总是为第三方类的分类名称加前缀

26. 勿在分类中声明属性

属性是封装数据的方式。尽管从技术上说,分类里也可以声明属性,但这种做法还是要尽量避免。原因在于,出了 “class-continuation分类” 之外,其他分类都无法向类中新增实例变量,因此,它们无法把实现属性所需的实例变量合成出来。

27. 使用 “class-continuation 分类” 隐藏实现细节

“class-continuation 分类” 和普通的分类不同,它必须定义在其所接续的那个类的实现文件里。其重要之处在于,这是唯一能声明实例变量的分类,而且此分类没有特定的实现文件,其中的方法都应该定义在类的主实现文件里。与其他分类不同,“class-continuation 分类” 没有名字。

为什么需要这种分类呢?因为其中可以定义方法和实例变量。为什么能在其中定义方法和实例变量呢?只因为 “ 稳定的ABI” 这一机制,使得我们无须知道对象大小即可使用它。由于类的使用者不一定需要知道实例变量的内存布局,所以,它们也就未必得定义在公共接口中了。基于上述原因,我们可以像在类的实现文件里那样,于 “class-continuation 分类” 中给类新增实例变量。

“class-continuation 分类” 还有一种合理用法,就是将public接口中声明为 “只读” 的属性扩展为 “可读写” ,以便在类的内部设置其值。我们通常不直接访问实例变量,而是通过设置访问方法来做,因为这样能够触发 “键值观测” (Key-Value Observing, KVO)通知,其他对象有可能正监听此事件,出现在 “class-continuation 分类” 或其他分类中的属性必须同类接口里的属性具备相同的特质 (attribute),不过,其 “只读” 状态可以扩充为 “可读写” 。

28. 通过协议提供匿名对象

@property (nonatomic, weak) id<EOCDelegate> delegate

// 由于该属性的类型是 id<EOCDelegate> ,所以实际上任何类型的对象都能充当这一属性,即便该类不继承自 NSObject 也可以,只要遵循 EOCDelegate 协议就行。对于具备此属性的类来说,delegate 就是 “匿名的” (anonymous)。如有需要,可在运行期查出此对象所属的类型。然而这样做不太好,因为指定属性类型时所写的那个 EOCDelegate 契约已经表明此对象的具体类型无关紧要了。

内存管理


29. 理解引用计数

在Objective-C 的引用计数架构中,自动释放池是一项重要特性。调用release会立刻递减对象的保留计数(而且还有可能令系统回收此对象),然而有时候可以不调用它,改为调用autorelease,此方法会在稍后递减计数,通常是在下一次 “事件循环”(event loop)时递减,不过也可能执行得更早些。

- (NSString *)stringValue {
    NSString *str = [[NSString alloc] initWithFormat: @“I am this: %@”, self];
    return [str autorelease];
}

30. 以ARC简化引用计数

31. 在dealloc 方法中只释放引用并解除监听

- (void)dealloc {
    CFRelease(coreFoundationObject);
    [[NSNotificationCenter defaultCenter] removeObserver: self];
}

32. 编写 “异常安全代码” 时留意内存管理问题

@try {
   EOCSomeClass *object = [[EOCSomeClass alloc] init];
   [object doSomethingThatMayThrow];
}
@catch (...) {
   NSLog(@"Whoops, there was an error.  Oh well...");
}
// 现在问题更大了,由于不能调用release,所以无法像手动管理引用计数时那样把释放操作移到 @finally 块中。你可能认为这种状况 ARC 自然会处理的。但实际上ARC不会自动处理,因为这样做需要加入大量样板代码,以便跟踪待清理的对象,从而在抛出异常时将其释放。可是,这段代码会严重影响运行期的性能,即便在不抛异常时也如此。而且,添加进来的额外代码还会明显增加应用程序的大小。这些副作用都不甚理想。

// 虽说默认状况下未开启,但ARC依然能生成这种安全处理异常所用的附加代码。-fobjc-arc-exception 这个编译器标志用来开启此功能。其默认不开启的原因是:在 Objective-C 代码中,只有当应用程序必须因异常状况而终止时才应抛出异常。因此如果应用程序即将终止,那么是否还会发生内存泄露就已经无关紧要了,在应用程序必须立即终止的情况下,还去添加安全处理异常所用的附加代码是没有意义的。

33 以弱引用避免保留环

用 unsafe_unretained 修饰的属性特质,其语义通assign特质等价。然而,assign通常只用于 “整体类型”(int、float、结构体等),unsafe_unretained则多用于对象类型。这个词本身就表明其所修饰的属性可能无法安全使用(unsafe)。Objective-C中还有一项与ARC相伴的运行期特性,可以令开发者安全使用弱引用:这就是weak属性特质,它与unsafe_unretained的作用完全相同。然而,只要系统把属性回收,属性值就会自动设为nil。

34. 以 “自动释放池块” 降低内存峰值

35. 用 “僵尸对象” 调试内存管理问题

Cocoa 提供了 “僵尸对象” (Zombie Object)这个非常方便的功能。启用这项调试功能之后,运行期系统会把所有已经回收的实例转化为特殊的 “僵尸对象”,而不会真正回收它们。这种对象所在的核心内存无法重用,因此不可能遭到覆写。僵尸对象收到消息后,会抛出异常,其中准确说明了发送过来的消息,并描述了回收之前的那个对象。僵尸对象是调试内存管理问题的最佳方式。

僵尸对象的工作原理是什么呢?它的实现代码深植于 Objective-C 的运行期程序库、Foundation框架及 CoreFoundation框架中。系统在即将回收对象时,如果发现通过环境变量启用了僵尸对象功能,那么还将执行一个附加步骤。这一步就是把对象转化为僵尸对象,而不彻底回收。

36. 不要使用 retainCount

块与大中枢派发


37. 理解 “块” 这一概念

38. 为常用的块创建 typedef

39. 用handle 块降低代码分散程度

40. 用块引用其所属性对象是不要出现保留环

41. 多用派发队列,少用同步锁

42. 多用GCD,少用 performSelector 系列方法

43. 掌握GCD及操作队列的使用时机

44. 通过Dispatch Group机制,根据系统资源状况来执行任务

45. 使用 dispatch_once 来执行只需运行一次的线程安全代码

46. 不要使用 dispatch_get_current_queue

系统框架


47. 熟悉系统框架

48. 多用块枚举,少用for循环

49. 对自定义其内存管理语义的collection使用无缝桥接

50. 构建缓存时选用NSCache 而非 NSDictionary

51. 精简 initialize 与 load 的实现代码

52. 别忘了 NSTimer 会保留其目标对象

上一篇 下一篇

猜你喜欢

热点阅读