本文来自:https://github.com/oa414/objc-zen-book-cn#%E9%94%99%E8%AF%AF%E5%A4%84%E7%90%86
错误处理
当方法返回一个错误参数的引用的时候,检查返回值,而不是错误的变量。
推荐:
NSError *error = nil;
if (![self trySomethingWithError:&error]) {
// Handle Error
}
此外,一些苹果的 API 在成功的情况下会对 error 参数(如果它非 NULL)写入垃圾值(garbage values),所以如果检查 error 的值可能导致错误 (甚至崩溃)。
常量
常量应该用 static
声明,不要使用 #define
,除非它就是明确作为一个宏来用的。
常量应该使用驼峰命名法,并且为了清楚,应该用相关的类名作为前缀。
推荐
static const NSTimeInterval ZOCSignInViewControllerFadeOutAnimationDuration = 0.4;
static NSString * const ZOCCacheControllerDidClearCacheNotification = @"ZOCCacheControllerDidClearCacheNotification";
static const CGFloat ZOCImageThumbnailHeight = 50.0f;
常量应该在 interface 文件中这样被声明:
extern NSString *const ZOCCacheControllerDidClearCacheNotification;
并且应该在实现文件中实现它的定义。
字面值
NSString
, NSDictionary
, NSArray
, 和 NSNumber
字面值应该用在任何创建不可变的实例对象。特别小心不要把 nil
放进 NSArray
和 NSDictionary
里,这会导致崩溃
例子:
NSArray *names = @[@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul"];
NSDictionary *productManagers = @{@"iPhone" : @"Kate", @"iPad" : @"Kamal", @"Mobile Web" : @"Bill"};
NSNumber *shouldUseLiterals = @YES;
NSNumber *buildingZIPCode = @10018;
不要这样:
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];
对于那些可变的副本,我们推荐使用明确的如 NSMutableArray
, NSMutableString
这些类。init
方法应该是这样的结构:
- (instancetype)init
{
self = [super init]; // call the designated initializer
if (self) {
// Custom initialization
}
return self;
}
为什么设置 self
为 [super init]
的返回值,以及中间发生了什么呢?这是一个十分有趣的话题。
让我们后退一步:我们一直写类似 [[NSObject alloc] init]
的表达式,而淡化了 alloc
和 init
的区别 。一个 Objective-C 的特性叫 两步创建 。 这意味着申请分配内存和初始化是两个分离的操作。
alloc
表示对象分配内存,这个过程涉及分配足够的可用内存来保存对象,写入isa
指针,初始化 retain 的计数,并且初始化所有实例变量。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 子类,但是在不同的设备上有不同的行为。
@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
上面的代码的例子展示了如何创建一个类簇。
单例
如果可能,请尽量避免使用单例而是依赖注入。 然而,如果一定要用,请使用一个线程安全的模式来创建共享的实例。对于 GCD,用 dispatch_once()
函数就可以咯。
+ (instancetype)sharedInstance
{
static id sharedInstance = nil;
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
使用 dispatch_once(),来控制代码同步,取代了原来的约定俗成的用法。
dispatch_once()
的优点是,它更快,而且语法上更干净,因为dispatch_once()的意思就是 “把一些东西执行一次”,就像我们做的一样。 这样同时可以避免 possible and sometimes prolific crashes.
经典的使用单例对象的例子是一个设备的 GPS 以及动作传感器。即使单例对象可以被子类化,这个情况也可以十分有用。这个接口应该证明给出的类是趋向于使用单例的。然而,通常使用一个单独的公开的 sharedInstance
类方法就够了,并且不可写的属性也应该被暴露。
把单例作为一个对象的容器来在代码或者应用层面上共享是糟糕和丑陋的,这是一个不好的设计。
属性你总应该用 getter 和 setter ,因为:
strong
, weak
, copy
etc...) ,这个在 ARC 之前就是相关的内容。举个例子,copy
属性定义了每个时候你用 setter 并且传送数据的时候,它会复制数据而不用额外的操作。willChangeValueForKey
, didChangeValueForKey
) 会被自动执行。你应该倾向于用 getter:
_anIvar
你可以明确的访问 self->_anIvar
.这可能导致问题。在 block 里面访问 ivar (你捕捉并且 retain 了 self,即使你没有明确的看到 self 关键词)。 属性可以存储一个代码块。为了让它存活到定义的块的结束,必须使用 copy
(block 最早在栈里面创建,使用 copy
让 block 拷贝到堆里面去)
为了完成一个共有的 getter 和一个私有的 setter,你应该声明公开的属性为 readonly
并且在类扩展总重新定义通用的属性为 readwrite
的。
相等性
一个完整的 isEqual 方法应该是这样的:
- (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;
}
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
忽略没用使用变量的编译警告
NSString *foo;
#pragma unused (foo)
明确编译器警告和错误
对象间的通讯
Block
深入block
self的循环引用
当使用代码块和异步分发的时候,要注意避免引用循环。 总是使用 weak
引用会导致引用循环。 此外,把持有 block 的属性设置为 nil (比如 self.completionBlock = nil
) 是一个好的实践。它会打破 block 捕获的作用域带来的引用循环。
例子:
__weak __typeof(self) weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
[weakSelf doSomethingWithData:data];
}];
__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协议。
面向切面编程