本文来自:https://github.com/oa414/objc-zen-book-cn#%E9%94%99%E8%AF%AF%E5%A4%84%E7%90%86
错误处理
当方法返回一个错误参数的引用的时候,检查返回值,而不是错误的变量。
推荐:
<span style="font-size:14px;">NSError *error = nil;
if (![self trySomethingWithError:&error]) {
// Handle Error
}</span>
此外,一些苹果的 API 在成功的情况下会对 error 参数(如果它非 NULL)写入垃圾值(garbage values),所以如果检查 error 的值可能导致错误 (甚至崩溃)。
常量
常量应该用
static
声明,不要使用
#define
,除非它就是明确作为一个宏来用的。
常量应该使用驼峰命名法,并且为了清楚,应该用相关的类名作为前缀。
推荐
<span style="font-size:14px;">static const NSTimeInterval ZOCSignInViewControllerFadeOutAnimationDuration = 0.4;
static NSString * const ZOCCacheControllerDidClearCacheNotification = @"ZOCCacheControllerDidClearCacheNotification";
static const CGFloat ZOCImageThumbnailHeight = 50.0f;</span></span>
常量应该在 interface 文件中这样被声明:
<span style="font-size:14px;">extern NSString *const ZOCCacheControllerDidClearCacheNotification;</span>
并且应该在实现文件中实现它的定义。
字面值
NSString
,
NSDictionary
,
NSArray
, 和
NSNumber
字面值应该用在任何创建不可变的实例对象。特别小心不要把
nil
放进
NSArray
和
NSDictionary
里,这会导致崩溃
例子:
<span style="font-size:14px;">NSArray *names = @[@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul"];
NSDictionary *productManagers = @{@"iPhone" : @"Kate", @"iPad" : @"Kamal", @"Mobile Web" : @"Bill"};
NSNumber *shouldUseLiterals = @YES;
NSNumber *buildingZIPCode = @10018;</span>
不要这样:
<span style="font-size:14px;">NSArray *names = [NSArray arrayWithObjects:@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul", nil];
NSDictionary *productManagers = [NSDictionary dictionaryWithObjectsAndKeys: @"Kate", @"iPhone", @"Kamal", @"iPad", @"Bill", @"Mobile Web", nil];
NSNumber *shouldUseLiterals = [NSNumber numberWithBool:YES];
NSNumber *buildingZIPCode = [NSNumber numberWithInteger:10018];</span>
对于那些可变的副本,我们推荐使用明确的如
NSMutableArray
,
NSMutableString
这些类。
Initializer和dealloc
init
方法应该是这样的结构:
<span style="font-size:14px;">- (instancetype)init
{
self = [super init]; // call the designated initializer
if (self) {
// Custom initialization
}
return self;
}</span>
为什么设置
self
为
[super init]
的返回值,以及中间发生了什么呢?这是一个十分有趣的话题。
让我们后退一步:我们一直写类似
[[NSObject alloc] init]
的表达式,而淡化了
alloc
和
init
的区别 。一个 Objective-C 的特性叫 两步创建 。 这意味着申请分配内存和初始化是两个分离的操作。
-
表示对象分配内存,这个过程涉及分配足够的可用内存来保存对象,写入alloc
指针,初始化 retain 的计数,并且初始化所有实例变量。isa
-
是表示初始化对象,这意味着把对象转换到了个可用的状态。这通常是指把可用的值赋给了对象的实例变量。init
alloc
方法会返回一个合法的没有初始化的实例对象。每一个发送到实例的消息会被翻译为
objc_msgSend()
函数的调用,它的参数是指向
alloc
返回的对象的、名为
self
的指针的。这样之后
self
已经可以执行所有方法了。 为了完成两步创建,第一个发送给新创建的实例的方法应该是约定俗成的
init
方法。注意在
NSObject
的
init
实现中,仅仅是返回了
self
。
关于
init
有一个另外的重要的约定:这个方法可以(并且应该)在不能成功完成初始化的时候返回
nil
;初始化可能因为各种原因失败,比如一个输入的格式错误,或者未能成功初始化一个需要的对象。 这样我们就理解了为什么需要总是调用
self = [super init]
。如果你的超类没有成功初始化它自己,你必须假设你在一个矛盾的状态,并且在你的实现中不要处理你自己的初始化逻辑,同时返回
nil
。如果你不是这样做,你看你会得到一个不能用的对象,并且它的行为是不可预测的,最终可能会导致你的 App 发生 crash。
重新给
self
赋值同样可以被
init
利用为在被调用的时候返回不同的实例。一个例子是 类簇 或者其他的返回相同的(不可变的)实例对象的 Cocoa 类。
Designated和Secondary Initializers
初始化模式
类簇(class cluster)
an architecture that groups a number of private, concrete subclasses under a public, abstract superclass. (一个在共有的抽象超类下设置一组私有子类的架构)
class cluster 的想法很简单,你经常有一个抽象类在初始化期间处理信息,经常作为一个构造器里面的参数或者环境中读取,来完成特定的逻辑并且实例化子类。这个"public facing" 应该知晓它的子类而且返回适合的私有子类。
Class clusters 在 Apple 的Framework 中广泛使用:一些明显的例子比如
NSNumber
可以返回不同类型给你的子类,取决于 数字类型如何提供 (Integer, Float, etc...) 或者
NSArray
返回不同的最优存储策略的子类。
一个经典的例子是如果你有为 iPad 和 iPhone 写的一样的 UIViewController 子类,但是在不同的设备上有不同的行为。
<span style="font-size:14px;">@implementation ZOCKintsugiPhotoViewController
- (id)initWithPhotos:(NSArray *)photos
{
if ([self isMemberOfClass:ZOCKintsugiPhotoViewController.class]) {
self = nil;
if ([UIDevice isPad]) {
self = [[ZOCKintsugiPhotoViewController_iPad alloc] initWithPhotos:photos];
}
else {
self = [[ZOCKintsugiPhotoViewController_iPhone alloc] initWithPhotos:photos];
}
return self;
}
return [super initWithNibName:nil bundle:nil];
}
@end</span></span>
上面的代码的例子展示了如何创建一个类簇。
单例
如果可能,请尽量避免使用单例而是依赖注入。 然而,如果一定要用,请使用一个线程安全的模式来创建共享的实例。对于 GCD,用
dispatch_once()
函数就可以咯。
<span style="font-size:14px;">+ (instancetype)sharedInstance
{
static id sharedInstance = nil;
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}</span>
使用 dispatch_once(),来控制代码同步,取代了原来的约定俗成的用法。
dispatch_once()
的优点是,它更快,而且语法上更干净,因为dispatch_once()的意思就是 “把一些东西执行一次”,就像我们做的一样。 这样同时可以避免 possible and sometimes prolific crashes.
经典的使用单例对象的例子是一个设备的 GPS 以及动作传感器。即使单例对象可以被子类化,这个情况也可以十分有用。这个接口应该证明给出的类是趋向于使用单例的。然而,通常使用一个单独的公开的
sharedInstance
类方法就够了,并且不可写的属性也应该被暴露。
把单例作为一个对象的容器来在代码或者应用层面上共享是糟糕和丑陋的,这是一个不好的设计。
属性
你总应该用 getter 和 setter ,因为:
- 使用 setter 会遵守定义的内存管理语义(
,strong
,weak
etc...) ,这个在 ARC 之前就是相关的内容。举个例子,copy
属性定义了每个时候你用 setter 并且传送数据的时候,它会复制数据而不用额外的操作。copy
- KVO 通知(
,willChangeValueForKey
) 会被自动执行。didChangeValueForKey
- 更容易debug:你可以设置一个断点在属性声明上并且断点会在每次 getter / setter 方法调用的时候执行,或者你可以在自己的自定义 setter/getter 设置断点。
- 允许在一个单独的地方为设置值添加额外的逻辑。
你应该倾向于用 getter:
- 它是对未来的变化有扩展能力的(比如,属性是自动生成的)。
- 它允许子类化。
- 更简单的debug(比如,允许拿出一个断点在 getter 方法里面,并且看谁访问了特别的 getter
- 它让意图更加清晰和明确:通过访问 ivar
你可以明确的访问_anIvar
.这可能导致问题。在 block 里面访问 ivar (你捕捉并且 retain 了 self,即使你没有明确的看到 self 关键词)。self->_anIvar
- 它自动产生KVO 通知。
- 在消息发送的时候增加的开销是微不足道的。更多关于新年问题的介绍你可以看 Should I Use a Property or an Instance Variable?。
属性定义
属性可以存储一个代码块。为了让它存活到定义的块的结束,必须使用
copy
(block 最早在栈里面创建,使用
copy
让 block 拷贝到堆里面去)
为了完成一个共有的 getter 和一个私有的 setter,你应该声明公开的属性为
readonly
并且在类扩展总重新定义通用的属性为
readwrite
的。
相等性
一个完整的 isEqual 方法应该是这样的:
<span style="font-size:14px;">- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
}
if (![object isKindOfClass:[ZOCPerson class]]) {
return NO;
}
return [self isEqualToPerson:(ZOCPerson *)object];
}
- (BOOL)isEqualToPerson:(Person *)person {
if (!person) {
return NO;
}
BOOL namesMatch = (!self.name && !person.name) ||
[self.name isEqualToString:person.name];
BOOL birthdaysMatch = (!self.birthday && !person.birthday) ||
[self.birthday isEqualToDate:person.birthday];
return haveEqualNames && haveEqualBirthdays;
}</span>
一个对象实例的
hash
计算结果应该是确定的。当它被加入到一个容器对象(比如
NSArray
,
NSSet
, 或者
NSDictionary
)的时候这是很重要的,否则行为会无法预测(所有的容器对象使用对象的 hash 来查找或者实施特别的行为,如确定唯一性)这也就是说,应该用不可变的属性来计算 hash 值,或者,最好保证对象是不可变的。
Categories
Protocols
Pragma
pragma-mark
#pragma-mark - NSObject
关于pragma
如果你知道你的代码不会导致内存泄露,你可以通过加入这些代码忽略这些警告。
如:performSelector may cause a leak because its selector is unknown(执行 selector 可能导致泄漏,因为这个 selector 是未知的)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[myObj performSelector:mySelector withObject:name];
#pragma clang diagnostic pop
注意我们是如何在相关代码上下文中用 pragma 停用 -Warc-performSelector-leaks 检查的。这确保我们没有全局禁用。如果全局禁用,可能会导致错误。
忽略没用使用变量的编译警告
NSString *foo;
#pragma unused (foo)
明确编译器警告和错误
对象间的通讯
Block
深入block
- block 是在栈上创建的
- block 可以复制到堆上
- block 有自己的私有的栈变量(以及指针)的常量复制
- 可变的栈上的变量和指针必须用 __block 关键字声明
self的循环引用
当使用代码块和异步分发的时候,要注意避免引用循环。 总是使用
weak
引用会导致引用循环。 此外,把持有 block 的属性设置为 nil (比如
self.completionBlock = nil
) 是一个好的实践。它会打破 block 捕获的作用域带来的引用循环。
例子:
__weak __typeof(self) weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
[weakSelf doSomethingWithData:data];
}];
多个语句的例子:
<pre name="code" class="objc">__weak __typeof(self)weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf doSomethingWithData:data];
[strongSelf doSomethingWithData:data];
}
}];
。。。。。。。
在一个 ARC 的环境中,如果尝试用
->
符号来表示,编译器会警告一个错误:
Dereferencing a __weak pointer is not allowed due to possible null value caused by race condition, assign it to a strong variable first. (对一个 __weak 指针的解引用不允许的,因为可能在竞态条件里面变成 null, 所以先把他定义成 strong 的属性)
可以用下面的代码展示
__weak typeof(self) weakSelf = self;
myObj.myBlock = ^{
id localVal = weakSelf->someIVar;
};
委托和数据源
委托模式是单向的,消息的发送方(委托方)需要知道接收方(委托),反过来就不是了。对象之间没有多少耦合,因为发送方只要知道它的委托实现了对应的 protocol。
本质上,委托模式只需要委托提供一些回调方法,就是说委托实现了一系列空返回值的方法。不幸的是 Apple 的 API 并没有尊重这个原则,如UITableViewDelegate协议。
- 委托模式:事件发生的时候,委托者需要通知委托
- 数据源模式: 委托方需要从数据源对象拉取数据
多重委托
面向切面编程