Effective Objective-C 2.0编写高质量iOS与OS X代码的52个有效方法

2017/12/31


第一章 熟悉Objective-C

第1条 了解Objective-C语言的起源

  • Objective-C为C语言添加了面向对象的特性,是其超急。Objective-C使用动态绑定的消息结构,也就是说,在运行时才会检查对象类型。接收一条消息后,究竟应执行何种代码,由运行环境而非编译器来决定。
  • 理解C语言的核心概念有助于写好Objective-C程序。尤其要掌握内存模型。

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

  • 除非确有必要,否则不要进入头文件。一般来说,应在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件。这样做可以降低类之间的耦合。
  • 有时无法使用向前声明,比如要声明某个类要遵守某个协议。这种情况下,尽量把“该类遵守某协议”的这条声明移至“分类”中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入。

第3条 多用字面量语法

  • 应该使用字面量语法, 以使代码更简明扼要。
    • 字符串 @“str” ✔️
    • 数值 @10 ✔️
    • 数组 @[] ✔️
    • 字典 @{} ✔️
  • 应使用下标来访问数组,键访问字典。
    • arr[10] ✔️
    • dict[@"name"] ✔️
  • 字面量创建数组或字典时,若值中有nil,会抛出异常。请确保值不含nil
    • @[nil] ❌
    • @{nil} ❌

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

  • 不要用预处理指令定义常量。这样定义出来的常量不含类型信息,编译器只会在编译时做替换操作。即使有人重新定义了常量值,编译器也不会发出警告。还有个弊端是会产生N多份临时变量。
  • 在实现文件中使用static const来定义只在当前文件可见的常量,例如static NSString *const kAnimationName = @“greatWave”
  • 在头文件中使用extern来声明全局常量,并在相关实现文件中定义值。这种常量会出现在全局符号中,所以其名称应以类名做前缀,如UIAplicationDidBecomeActiveNotification

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

  • 应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起个易懂的名字。
  • 如果值为可选项类型(可多选),那就定义值为2^N,以便可以使用按位或操作。
  • NS_ENUMNS_OPTIONS宏来定义枚举类型,并指明底层数据类型例如NS_ENUM(NSUInteger, VLCPlayerState)
  • 在用switch处理枚举时不要有default分支,这样在加入新枚举后编译器会提示有未处理的枚举。

第二章 对象、消息、运行期

第6条 理解“属性”这一概念

  • @property来定义对象的属性。
  • 通过“特质”来指定存储数据所需的正确语义。
  • iOS开发应该用nonatomic,因为atomic会严重影响性能。

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

  • 在对象内部读取数据时,应该直接通过追李变量来读,而写入数据时,则应该通过属性来写。
  • 在初始化方法及dealloc方法总,总是应该直接通过实例变量来读写数据。
  • 懒加载时,通过属性来读取数据。

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

  • 检测对象等同性应提供isEqualhash方法。
  • ==只会对比两个指针所指对象的地址。
  • 相同对象必须具有相同hash,但hash相同未必对象相同。
  • 编写hash方法时,应使用计算速度快而且hash碰撞率低的算法。

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

  • 类族模式可以把实现细节隐藏在一套简单的公共接口后面
  • 系统框架里的大部分collection类都是类族,例如NSArray、NSMutableArray。

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

  • 只有在其他做法不可行时采用,否则可能引入难以查找的bug。
  • 使用方法:
void setAssociatedObject(id object, void\*key, id value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id object, void\*key);
void objc_removeAssociatedObjects(id object)

第11条 理解objc_msgSend的作用

  • void objc_msgSend(id self, SEL cmd, ...)
  • 举例
    id return = [git commit:parameter];
    上面的Objective-C方法在运行时会转换成如下函数:
    id return = objc_msgSend(git, @selector(commit), parameter);

第12条 理解消息转发机制

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

第14条 理解“类对象”的用意

第三章 接口与API

第15条 用前缀避免命名冲突

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

  • 所有初始化方法最终都会调用到这个方法,便于统一管理

第17条 实现description方法

  • debugDescription方法是在开发者在调试器中以控制台命令打印对象时调用的,如po

第18条 尽量使用不可变对象

  • 尽量把对外开放的属性设为readonly,而且可能的话尽量不要对外开放(隐藏细节)。
  • 不想被外部改变的属性,对外声明时应该使用readonly,而在内部分类中重新声明为readwrite。
  • 不要把可变的collection作为属性公开,而应该提供相关方法,以此修改对象中的可变collection。
  • 例如:
// Person.h
@interface Person:NSObject
@property (nonatomic, strong, readonly) NSSet *friends;
@end

// Person.m
@implementation Person {
    NSMutableSet *_internalFriends;
}

- (NSSet *)friends {
    return [_internalFriends copy];
}
- (void)addFriend:(Person *)person {
    [_internalFriends addObject:person];
}
@end

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

  • 尽量不要使用缩略词。
  • 如果方法返回值是新创建的,那么方法名的首个词应是返回值的类型(如-intValue;+string),除非另有修饰词(如:- lowercaseString;)。
  • Boolean属性应选用has、is当前缀。
  • 将get这个前缀留给借由“输出参数”来保存返回值的方法,如
    -(void)getBytes:(uchar \*)buffer range:(Range)aRange;

第20条 为私有方法名加前缀

  • 给私有方法名加上前缀,建议使用p_,便于与公开方法区分。
  • 不要使用单个下划线_作为私有方法名的前缀,因为这是预留给苹果公司用的,这可能会覆盖苹果的私有方法。

第21条 理解Objective-C错误模型

  • NSError封装了三个属性 {domain code info}
  • 只有发生了严重错误时才使用异常NSException
  • 小错误可以返回NSError给调用者处理

第22条 理解NSCopying协议

  • 要实现copy就要重写- (id)copyWithZone:(NSZone \*)zone;
  • 要实现mutableCopy就要重写- (id)mutableCopyWithZone:(NSZone \*)zone;
    (以前会把内存分成不同的zone,现在只有一个zone,所以不必担心这个参数)
  • 复制对象时需决定深拷贝还是浅拷贝,尽量用浅拷贝。

第四章 协议与分类

第23条 通过代理与数据源协议进行对象间通信

  • delegate属性一定要是weak,否则会引入“保留环”
// 如果频繁调用如下代码:
if ([_delegate respondsToSelecter:@selecter(someClassDidSomething:)]) {
    [_delegate someClassDidSomething];
}
// 可考虑将检查加过缓存到本地标记:
_delegateFlags.didUpdateProgressTo = [_delegate respondsToSelecter:@selecter(someClassDidSomething:)]
...
if (_delegateFlags.didUpdateProgressTo) {
    [_delegate didUpdateProgressTo:progress]
}

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

  • 使用分类机制把类的实现代码划分成易于管理的小块。
  • 将应该视为“私有”的方法归入名为Private的分类中,以隐藏实现细节。

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

  • 添加分类的时候要注意是否会覆写已有方法,因加载时机不确定很可能发生覆盖或被覆盖,这种bug很难查。
  • 向第三方类中添加分类时,应该给其名称及方法加上你的专用前缀,以减少覆写情况。

第26条 勿在分类中声明属性

  • 为分类添加属性技术上可行(见第6条),但尽量避免,因为这种方法无法把实现属性所需的实例变量合成出来。
  • 把封装数据所用的全部属性都定义在主接口里。
  • 在分类中可以定义存取方法,但尽量不要定义属性。

第27条 使用匿名扩展隐藏实现细节

  • 通过扩展向类中增加实例变量。
  • 如果某属性在主接口中声明为readonly,那可以在扩展中重新声明为readwrite。
    若想使类遵循的下一不为人知,可在扩展中声明。

第28条 通过协议提供匿名对象

第5章 内存管理

第29条 理解引用计数

第30条 以ARC简化引用计数

  • ARC只负责管理OC对象的内存,CF开头的对象要开发者管理。

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

  • dealloc里应该做的事是释放指向其他对象的引用,并取消订阅KVO或Notification等通知,不要做其他事。
  • 如果持有文件秒速符等系统资源,就应该编写一个方法来释放这种资源。
  • 不要在dealloc里做异步任务,因为此时对象已经在回收状态了。

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

  • OC中抛出异常的时候可能会引起内存泄漏
  • 在@try捕获异常中清理干净

第33条 以弱引用避免保留环

  • 以弱引用避免“保留环(Retain Cycle)”

第34条 以“自动释放池”降低内存峰值

  • 自动释放池排布在栈中。
  • 自动释放池一般放在可能引起内存峰值比较高的地方,如在for循环创建大量临时变量。
  • 写法:
@autoreleasepool {
    // ...
}

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

  • 在Scheme->Run->Diagnostics->“Enable Zombie Objects”,勾选即可开启此功能。
  • 开启此功能后,系统回收对象时不是释放对象,而是转为僵尸对象,僵尸对象能响应所有选择子,响应方式为打印相关信息然后终止程序。

第36条 不要使用retainCount

  • 对象的保留计数并不能绝对准确反映对象的生命期。
  • 在苹果引入ARC之后retainCount已经正式废弃,任何时候都不要调用这个retainCount方法来查看引用计数了,因为这个值实际上已经没有准确性了。但是在MRC下还是可以正常使用

块与大中枢派发

第37条 理解“块”这一概念

  • 块是闭包。
  • 块可以分配在栈块、堆块或全局块上,要注意块的作用域
// 不安全的块,因为A块、B块都是分配在栈上的,离开了if语句可能会被释放
void (^block)();
if (condition_1) {
    block = ^{ print("A") };
} else {
    block = ^{ print("B") };
}
block();

// 若将块拷贝copy就会搬到堆上,就会变成对象,就会安全
block = [^{ print("A") } copy];
block = [^{ print("B") } copy];

第38条 为常见的块类型创建typedef

  • 主要是为了考虑代码可读性。

第39条 用handler块降低代码分散程度

  • 异步操作中使用block的方式设计,这样业务相关的代码会比较紧凑,不会显得那么凌乱。

第40条 用块引用其所属对象时不要出现保留环

  • 不是一定得在block中使用weakself,block 不是被self所持有的,在block中就可以直接使用self

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

  • 这种写法效率很低,也不能保证线程中绝对安全:
// 1. synchronized
@synchronized(self) {
    return _someString;
}
// 2. nslock
NSLock *_locker = [[NSLock alloc] init];
[_locker lock];
// safe code
[_locker unlock];
  • 应该用GCD来替换:
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

//读取字符串
- (NSString *)someString {
    __block NSString *localSomeString;
     dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
     return localSomeString;
}
- (void)setSomeString:(NSString*)someString {
     dispatch_barrier_async(_syncQueue, ^{
        _someString = someString;
    });
}

第42条 多用GCD,少用performSelector系列方法

  • performSelector有很多缺点:
    • 内存管理问题:在ARC下我们经常会看到编译器发出如下警告:warning: performSelector may cause a leak because its selector is unknown [-Warc-performSelector-leaks];
    • 返回值只能是void或对象类型;
    • 无法处理带有多个参数的选择子,最多只能处理两个参数
  • 用dispatch系列函数代替

第43条 掌握GCD以及操作队列的使用时机

  • 需要的时候可以用NSOperation和NSOperationQueue代替GCD
  • 使用NSOperation和NSOperationQueue的优点:
    • 支持取消某个操作
    • 支持指定操作间的依赖关系
    • 支持指定操作的优先级
    • 重用NSOperation对象

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

  • DispatchGroup可将任务分组,然后等待这组任务执行完毕时会有通知,开发者可以拿到结果然后继续下一步操作:
// A B C将会同步执行
dispatch_group_async({...A})
dispatch_group_async({...B})
dispatch_group_async({...C})
// 以上都执行完再执行notify
dispatch_group_notify({...})
  • 通过dispatch group在并发队列上同时执行多项任务的时候,GCD会根据系统资源状态来帮忙调度这些并发执行的任务。

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

  • dispatch_once比较高效,没有重量级的同步机制, 常用于单例模式:
+ (instancetype)sharedInstance {
    static id _instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [[super allocWithZone: NULL] init];
    });
    return _instance;
}

+ (id)allocWithZone:(struct _NSZone *)zone {
    return [self sharedInstance];
}

- (id)copyWithZone:(struct _NSZone *)zone {
    return [[self class] sharedInstance];
}

- (instancetype)init {
    if (self = [super init]) {
        <#code#>
    }
    return self;
}

第46条 不要使用dispatch_get_current_queue

  • dispatch_get_current_queue 函数的行为常常与开发者所预期的不同,iOS 6.0起此函数已经废弃,只应做调试之用。

第7章 系统框架

第47条 熟悉系统框架

  • Foundation OC基础框架,其中的类以NS为前缀
  • CoreFoundation C语言框架,CF前缀,可与Foundation无缝桥街
  • AVFoundation OC对象可用来回访并录制音频及视频。
  • CFNetwork C语言级别的网络通信能力。
  • CoreAudio C语言API可以用来操作设备上的音频硬件。
  • CoreData OC接口可以将对象放入数据库,将数据持久化。
  • CoreText C语言接口可以高效执行文字排版以及渲染操作。
  • SpriteKit 游戏框架
  • CoreLocation、MapKit 定位地图相关框架
  • Social 社交网络框架
  • AddressBook 需要使用通讯录时才使用该框架
  • MusicLibraries 音乐库相关框架
  • HealthKit 健康相关框架
  • HomeKit 为智能化硬件提供的框架
  • CloudKit iCloud相关的框架
  • NSLinguisticTagger 解析自然语言,如动词,名词等
  • UIKit 构建在基础框架之上,包含UI元素和粘合机制
  • CoreAnimation 是QuartzCore框架的一部分,OC级别的核心动画库,用以图形渲染
  • CoreGraphics C语言级别2D渲染所必备的数据结构与函数,如CGRect、CGPoint

第48条 多用块枚举,少用for循环

  • 遍历有4方法:for、NSEnumerator、for—in、块枚举
  • 块枚举法是通过GCD来并发执行遍历操作,当属最高效方法。
  • 若提前知道待遍历的collection含有何种对象,则应修改块签名,指出对象的具体类型。
  • 例如字典遍历:
[dict enumeratKeysAndObjectsUsingBlock: ^(NSString \*key, NSString \*obj){
    // ...
}];

第49条 对自定义其内存管理语义的collecion使用无缝桥接

  • 通过无缝桥接技术,可以在定义于Foundation框架中的类和CoreFoundation框架中的C语言数据结构之间来回转换。
    下面代码展示了简单的无缝桥接:
NSArray *anNSArray = @[@1, @2, @3, @4, @5];
CFArrayRef aCFArray = (__bridge CFArrayRef)anNSArray;
NSLog(@"Size of array = %li", CFArrayGetCount(aCFArray));
//Output: Size of array = 5

转换操作中的__bridge告诉ARC如何处理转换所涉及的OC对象,也就是ARC仍然具备这个OC对象的所有权。__bridge_retained则意味着ARC将交出所有权。

  • 使用CFRelease(aCFArray)释放对象。

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

  • NSCache会在系统资源紧急时自动释放缓存。
  • NSCache是线程安全的。
  • NSCache会先行释放最久未使用的对象。
  • 只有重新计算、获取开销很大的数据才值得缓存,如网络获取,磁盘读取。

第51条 精简initialize与load的实现代码

  • oad与initialize 方法都应该实现的精简一点,这样有助于保持应用程序的响应能力,也可以减少引入依赖环的几率

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

  • 在iOS开发中经常会用到定时器:NSTimer,由于NSTimer会生成指向其使用者的引用,而其使用者如果也引用了NSTimer,那就形成了该死的循环引用。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容