Effective Objective-C 2.0笔记(接口设计/协议/框架)

dd

Effective Objective-C 2.0笔记(接口设计/协议/框架)
Effective Objective-C 2.0笔记(runtime/内存/多线程)

第一章 熟悉Objective-C

之前看了《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》这本五星好书,受益颇多,在一定程度上提高了编写的代码的质量。现在就对这52个有效方法做简单笔记。

第1条:了解OC语言的起源

OC为C语言添加了面向对象特性,是其超集。OC与其他面向对象的语言在很多方面都有差别。OC语言由Smalltalk(消息型语言鼻祖)演化而来。OC语言使用动态绑定的“消息结构”而非“函数调用”,也就是说在运行时才会检查对象类型,接收一条消息后,究竟执行何种代码,由运行时环境而非编译器来决定。

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

在类的头文件中引入其他头文件可能会有如下问题:

  1. A类头文件引入了B类的头文件,B类头文件又引入了A类头文件,这两个类就相互引用了,编译不过。
  2. A类头文件引入了C类的头文件,当B类引入A类头文件时,不需要的C类也被引入了B类。而另一个类又引入B类时,没有用到的A类C类也被引入了。如此持续下去,则要引入许多根本用不到的内容,会增加编译时间。

取而代之,我们应尽量使用向前声明@class声明,而在.m文件中引入头文件。@class让编译器知道有这样一个类,而不需要知道类的全部细节。这样将引入头文件的时机尽量的延后,只在需要的时候才引入。
但是有时候必须要在头文件中引入其他头文件,有如下情况:

  1. 继承至父类的子类,必须在头文件中引入父类的头文件。
  2. 类遵循某个协议时,该协议必须有完整的定义,编译器要知道该协议的方法,而不能使用@class。(其实也可以在.m文件中的扩展中遵循协议,那么就不用在头文件中去引入协议头文件了)

总的说来,每次在头文件中引入其他头文件之前,都要先问问自己这样做是否有必要。除非确有必要,否则不要引入,而应该使用向前声明@class。

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

Foundation框架中NSString,NSNumber,NSArray,NSDictionary都有对应的字面量语法:@"",@1,@[],@{}。字面量语法特点:

  1. 语法精简,缩减代码长度更为易读。
  2. 字面量语法有利于数组,字典的操作:可以通过下标的形式进行操作,简明扼要。
  3. 使用字面量创建数组/字典时,若集合中元素对象有nil,则会抛出异常,而非字面量创建时会到nil对象为止。这种差别表明,字面量语法更加安全,抛出异常总比创建的集合元素莫名的少了要好。
  4. 字面量创建的对象是不可变的,若想要可变的则需复制一份,类似@[].mutableCopy。

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

  1. 预处理指令定义的常量不含类型信息,编译器只是会在编译前据此执行查找与替换操作。在我们用错类型时,编译时并不能及时发现错误。如果不小心重新定义了常量值,编译器也不会产生警告信息,这将导致应用程序的常量值不一致。
  2. 类型常量包含类型消息,清楚地描述了常量的含义,可以让代码更易被理解。使用时更好了利用编译器的某些特性。
  3. 常量命名规则:若常量局限于实现文件之内,则在前面加字母k;若常量在类之外可见,则通常以类名为前缀。
  4. 常量一般使用static const声明,const声明的变量可以防止被修改,如果试图修改会得到编译错误。而static修饰的变量意味着该变量仅在定义此变量的编译单元(及实现文件)中可见。
  5. 有时需要公开某个常量,以便可以在定义该变量的编译单元之外使用。此类常量需要放在“全局符号表”中,这就需要在头文件用extern关键字声明,而在实现文件中定义(不能使用static)。使用extern关键字,编译器无须查看其定义,即允许代码使用此常量。因为它知道,当链接成二进制文件后,肯定能找到这个常量。
  6. 预处理指令定义的字符串,编译器可以对相同的字符串进行优化,只保存一份到 .rodata 段。甚至有相同后缀的字符串也可以优化,"Hello world" 与 "world" 两个字符串,只存储前面一个。取的时候只需要给前面和中间的地址,如果是整形、浮点型会有多份拷贝,但这些数写在指令中。占的只是代码段而已,大量用预处理指令会导致二进制文件变大。

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

对于状态 选项等,使用枚举写出来的代码更易读懂。编译器为枚举分配一个独有的编号,从0开始,每个枚举递增1。也可以自己设值,不使用编译器分配的。设置的枚举值可以用二进制表示,这样通过“按位或操作符”可组合多个选项。
也可以使用typedef关键字重新定义枚举类型,使定义枚举变量的方式更简洁。Foundation框架中定义了一些辅助的宏,可以很方便的定义枚举类型,也可以指定用于保存枚举值的底层数据类型。相关的宏有两种:

#define NS_ENUM(...) CF_ENUM(__VA_ARGS__)
#define NS_OPTIONS(_type, _name) CF_OPTIONS(_type, _name)

两种宏的区别:作为选项的枚举值经常使用按位或运算组合,在运算两个枚举值时,C++认为运算的结果应该是枚举的底层数据类型也就是NSUInteger。C++不允许将底层类型“隐式转换”为枚举类型本身。若编译器按C++模式编译,而按位运算的类型与枚举底层数据类型不一致,这时使用NS_ENUM会报错。而NS_OPTIONS很好的兼容了这个问题,若非C++模式编译其和NS_ENUM一样,若是C++模式编译,NS_OPTIONS会将按位操作的结果显式转换,为我们省去类型转换的操作。鉴于此,凡是需要按位运算组合的枚举都应使用NS_OPTIONS,若不需要组合,则应使用NS_ENUM。

typedef NS_OPTIONS(NSUInteger, UIControlState) {
    UIControlStateNormal       = 0,
    UIControlStateHighlighted  = 1 << 0,                  // used when UIControl isHighlighted is set
    UIControlStateDisabled     = 1 << 1,
    UIControlStateSelected     = 1 << 2,                  // flag usable by app (see below)
    UIControlStateFocused NS_ENUM_AVAILABLE_IOS(9_0) = 1 << 3, // Applicable only when the screen supports focus
    UIControlStateApplication  = 0x00FF0000,              // additional flags available for application use
    UIControlStateReserved     = 0xFF000000               // flags reserved for internal framework use
};

还有就是,在处理枚举类型的switch语句中不要实现default分支。这样的话,加入新枚举后,编译器会提示我们有未处理的枚举,以免遗漏。

第三章 接口与API设计

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

  • OC没有命名空间,为了避免潜在的命名冲突,可以使用加前缀的方式
  • 选择公司,应用程序或有关联的名称作为类名的前缀,并在所有代码中使用这一前缀

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

  • 全能初始化方法:为对象提供必要信息以便其能完成工作的初始化方法,类似UITableViewCell初始化方法:
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(nullable NSString *)reuseIdentifier

这里必要信息是:style,reuseIdentifier样式及重用标识

  • 有多个初始化方法时,可以在其中选定一个作为全能初始化方法,令其他初始化方法都调用他。这样,这有全能初始化方法存储内部数据,需要改变内部结构时,只需要改全能初始化方法而不用改动其他初始化方法
  • 如果超类的初始化方法不适用于子类,那么应该覆写这个超类的方法,并在其中抛出异常

第17条:实现description方法

通过NSLog打印并查看对象信息时,对象会收到description消息,返回对象相关的信息。但默认的description方法返回的信息,有时并不是我们想要的,通过覆写description可以实现输出我们在定义的信息。类似的,还有debugDescription方法,它和description区别:debugDescription方法是开发者在调试器以控制台命令打印对象时调用的(LLDB "po"命令)。当我们通过LLDB "po"命令打印对象信息时,就可以覆写debugDescription返回我们需要的信息。

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

  • 使用属性时,默认情况下属性是“可读可写”的,这样设计出来的类都是“可变的”。
  • 为了防止对象被更改,应该尽量把对外公布的属性设为只读,而只在必要的时候才对外公布。
  • 若某属性仅可于对象内部修改,可以在分类中将readonly属性扩展为readwrite属性。
  • 不要把可变的集合(collection)作为属性公开,而应提供方法,通过方法修改对象的可变collection。

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

  • 方法命名:方法名使用“驼峰大小写命名法”;使用“in”,“for”,“with”等介词连接,使得代码读起来和句子一样;方法名要明确每个参数等含义,把表示参数类型的名词放在参数前面;方法有返回值时,方法名的首个词最好是返回值的类型;布尔属性应该根据其功能,选用类似has, is当前缀;不要使用类似str简称,应该使用全称;方法名也不能长得太过分,应尽量在用意表达清楚的基础上做到言简意赅;
  • 类和协议的命名:类和协议的名称要加上前缀,避免命名冲突;命名应该把词汇组织好,从左至右读起来通顺;定义委托协议时,把委托接口的类名放在前面,后面加Delegate一词。

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

为私有方法名加前缀,很容易区分公共方法和私有方法,有助于调试。使用何种前缀,由个人喜好决定,一般用p_作为前缀,尽量不要单独使用_作为前缀只是预留给苹果爸爸的。

第21条:理解错误模型

  1. 用异常机制处理错误
  • 通过@throw抛出异常(NSException)
  • 通过@try @catch捕获并处理异常

但这种机制,如果抛出异常,那么本应在作用域末尾释放的对象不会自动释放,也就是说默认情况下不是“异常安全的”;所以,异常只应该应用于极其严重的错误,异常抛出后无须考虑恢复问题,而且应用程序此时应该退出。

  1. NSError
  • 处理不是很严重的错误,表明有错误发生,程序不用退出。
  • 用法灵活,对象里封装了3条消息:Error domain错误范围;Error code错误码;Userinfo用户信息;
  • NSError一个常见用法是通过委托协议传递此错误。
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
  • 另一个用法是经由方法的输出参数返回调用者,传递的参数是个指针,这个指针又指向另一个指针,另一个指针指向NSError对象。
- (BOOL)createDirectoryAtPath:(NSString *)path withIntermediateDirectories:(BOOL)createIntermediates attributes:(nullable NSDictionary<NSFileAttributeKey, id> *)attributes error:(NSError **)error

第22条:理解NSCopying协议

  • 若想令自己所写的对象具有拷贝功能(copy),需要实现NSCoping协议。通过实现- (id)copyWithZone:(NSZone *)zone方法返回新的对象
  • 如果自定义的类分为可变和不可变版本,要同时实现NSCopying和NSMutableCopying协议

第四章 协议与分类

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

  • 委托模式(代理模式)是一种实现对象间通信的设计模式,可将数据与业务逻辑解耦;主旨是定义一套接口,某一对象想接受另一对象的委托,需遵循此协议,成为其代理;另一对象可以给委托对象回传消息;
  • 委托协议名通常是相关类名后加Delegate,方便理解;
  • 委托协议的方法默认是@require的,使用@optional可以指明可选方法;委托对象调用可选方法,必须提前使用类型信息查询方法判断是否响应选择器:respondsToSelector:
  • 定义委托协议方法时,应该提供一个委托发起对象的实例作为参数,方便根据该实例分别执行不同的代码;

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

  • 一个类的方法过多时会变得难于管理,可以使用分类把类的实现代码划分成便于管理的小块;
  • 将不想公开的方法归入单独一个分类中,隐藏实现细节;

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

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

  • 除了Extension外,其他分类都无法向类中新增实例变量,因此无法把属性所需的实例变量合成出来;
  • 可以通过关联对象技术解决分类中不能合成实例变量的问题,实现为分类添加属性,这样做在内存管理问题上容易出错,应该尽量避免
  • 有时只读属性还是可以在分类中使用,实现属性getter方法,就不会为该属性合成实例变量了;(类似于swift中计算属性的使用)

第27条:使用class-continuation分类隐藏实现细节

class-continuation分类即Extension

  • 可以通过Extension为类添加属性,Extension定义在原类的实现文件中
  • 可以将“私有”的属性声明在Extension里面,这样外界既不能访问该属性也完全不知道这个属性,从而隐藏相关细节
  • 类所遵循协议也可以在Extension中声明,从而使协议也不为外界所知
  • 如果属性在.h文件声明为readonly,而类的内部又要改动该属性,可以在Extension中将属性声明为readwrite

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

匿名对象:协议可以提供匿名类型:遵从该协议的纯id类型,如委托对象delegate

  • 需要隐藏类型名称,可以使用匿名对象;
  • 如果具体类型不重要,只要对象能响应协议的方法,也可以使用匿名对象;

第七章 系统框架

第47条:熟悉系统框架

  • 常用系统库:CFNetwork,CoreAudio,AVFoundation,CoreData,CoreText,CoreFoundation,Foundation,UIKit
  • OC编程经常需要使用底层的C语言级API;C语言API,可以绕过OC的运行期系统,可以提升执行速度;想要成为优秀的OC开发者,应该掌握C语言的核心概念;

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

  • 遍历集合有4种方式:最基本的for循环,其次是NSEnumerator遍历及for in快速遍历法,最新,最先进的是块枚举法
[@[@"1",@"2"] enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        
 }];
  • 块枚举遍历,遍历时可以直接从块里获取更多信息;
  • 块枚举法本身能通过GCD来并发执行遍历操作,无须另行编写代码;
  • 块枚举法能修改块的方法签名,以免进行类型转换操作;如上面代码可以改写为如下:
// 明确数组是字符串数组,可以直接修改块参数类型;
[@[@"1",@"2"] enumerateObjectsUsingBlock:^(NSString  *str, NSUInteger idx, BOOL * _Nonnull stop) {
        
 }];

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

  • 通过无法桥接技术,实现Foundation框架OC对象与CoreFoundation框架的C语言数据结构之间来回转换;
  • Foundation框架OC对象与CoreFoundation框架的C语言内存管理不一样,需要使用桥接技术处理:
  1. 使用__bridge将Foundation对象转换为CoreFoundation对象,ARC仍然具备这个OC对象的所有权,不用额外处理;
  2. 使用__bridge_retained将Foundation对象转换为CoreFoundation对象,ARC交出对象的所有权,使用完对象后需使用CFRelease()函数手动释放对象;
  3. 使用__bridge_transfer将CoreFoundation对象转换为Foundation对象,ARC获得该对象所有权,不用手动释放;

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

  • NSCache类似NSDictionary,但更好:
  1. 当系统资源将要耗尽时,它可以自动删减缓存;
  2. NSCache时线程安全的:不需要加锁代码,多个线程可以同时访问NSCache;
  3. 可以给NSCache对象设置上限,限制缓存的对象个数及大小;

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

有这样的场景:类必须执行某些初始化操作,然后才能正常使用;initialize与load方法可用来实现这种操作;initialize与load方法由系统调用,绝不应该通过代码手动调用;

  • + (void)load:
  1. 加入运行期系统的每个类及分类(就算没有使用这个类),必定会调用这个方法,而且仅会调用一次;
  2. 在子类的load方法调用前,必定会调用所有超类的load方法;
  3. 在load方法里使用其他类是不安全的,因为其他类可能还没执行load方法即还没加载好;
  4. 如果某个类本身没有实现load方法,不管各级超类是否实现了load方法,系统都不会调用;
  • + (void) initialize:
  1. 该方法在程序首次用该类之前调用,且也是只能调用一次;
  2. initialize方法是首次使用类时才调用,可以安全使用并调用其他类;
  3. initialize是在线程安全的环境中执行的,其他线程都会先阻塞等着initialize执行完;
  4. 和大部分方法一样:如果某个类本身没有实现该方法,如果各级超类是实现了该方法,那么就会调用超类的方法;
  • 使用场景:
  1. 一般在分类中的load方法执行method swizzle;
  2. 若某个全局变量无法在编译期初始化,则可以放在initialize方法里做;
  • initialize与load都应该实现的精简一些,尽量不用使用这两个方法;

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

NSTimer会保留其目标对象容易形成循环引用;
之前也总结过:NSTimer/CADisplayLink那些事

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

推荐阅读更多精彩内容