Effective Objective-C 2.0 读书笔记
2018-10-24 本文已影响0人
DeepChafferer
第一条 了解Objective-C语言的起源
笔记
- 使用消息结构的语言,其运行时所执行的代码由运行环境来决定,使用函数的语言则由编译器来决定
- 运行期组件本质上就是一种与开发者所编写代码相链接的“动态库”(dynamic library),只要在运行期运行组件就能提升应用程序的性能
- Objective-C语言对象分配在“堆”区不分配在“栈”区
- 重新创建一个变量让其指向同一个地址,并不会拷贝该对象,这两个变量会指向同一对象
- 在32位的计算机上指针占4字节,在64位的计算机上指针占8字节
- 分配在堆上的内存要由程序员管理,分配在栈上用于保存变量的内存则会在栈帧弹出时自动清理
- 保存基础类型不需要使用类,用结构体即可,创建对象由其余的开销,比如分配及释放内存等
要点
- Objective-C是C语言的超集,在C语言的基础上添加了面向对象的特性
- Objective-C语言使用了动态绑定的消息结构,在运行时才会检查对象的类型,接收一条消息后,究竟应该执行什么代码,由运行期环境非编译器决定
- 要掌握C语言的内存模型和指针
第二条 在类的头文件中尽量少引入其他头文件
笔记
-
@class
向前声明 - 将引入头文件的时机尽量延后,只有确有需要的时候再引入,这样就可以减少类的使用者所需要引入的头文件的数量
- 在各自文件中引入对方头文件,使用
#import
可以避免 使用#incude
所导致的死循环,但是这意味着两个类中有一个没有办法被正确编译
要点
- 一般来说,应该在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件,这样可以尽量降低类之间的耦合
- 有时候无法使用向前声明,比如要声明某给类要遵循一项协议的情况下,应该尽量把“该类遵循某协议”这条声明移至类别中,如果不行的话,就把协议单独放在一个头文件中并将其引入
第三条 多用字面量语法,少用与之等价的方法
笔记
- 字面量语法相当于是一种语法糖,在用字面量语法创建一个数组的时候,如果数组元素对象中有nil,那么会抛出
attempt insert nil object
异常,用字面量语法创建数组实际上先创建了一个数组然后再将方括号内的内容添加进去 - 用
arrayWithObjects:
方法创建数组,不会出现上述的情况,因为该方法会依次处理各个参数知道发现nil为止,如果发现nil就会结束该方法 - 字面量语法有个局限性:除了字符串以外,所创建出来的对象必须术语Foundation框架,若定义了它们的子类,则无法使用字面量语法来创建其对象
要点
- 用字面量语法创建字符串、数组、字典、数值比用创建此类对象的常规方法更加简明扼要
- 应该通过取下标操作来访问数组元素
- 用字面量语法创建数组或者字典时,若值中有nil,就会抛出异常
第四条 多用类型常量,少用#define预处理命令
笔记
- 如果常量局限于某一个“编译单元”,在Objective-C中就是
.m
文件,应在前面加上字母k,如果常量对外可见即在.h
文件中,通常要以类名作为前缀 - 用
static
来修饰变量表示其只在定义的编译单元中可见,编译器每收到一个编译单元,就会输出一份目标文件(object file),如果不在变量前面加上static
修饰,那么编译器就会为它创建一个外部符号(external symbol) - 如果一个变量即用了
static
又用了const
声明,那么编译器不会为其创建外部符号,而是会像#define
预处理指令一样,将所有遇到的变量都替换成一个常值 - 如果需要对外公开一个常量,那么需要将其放在全局符号表(global symbol tabel)中,这样的常量应该在头文件中用``extern`进行声明,在实现文件中进行定义
- 编译器看到
extern
关键字就明白应该如何在引入此头文件的代码中处理该常量,extern
告诉编译器在全局符号表中有一个叫xxx的符号,编译器不用查看其定义,就允许代码使用这个常量,因为编译器知道当链接成二进制问价后,肯定可以找到这个常量 - 用
extern
声明的常量必须要定义且只能定义一次。实现问价生成目标文件时,编译器会在数据段(data section)中为字符串分配存储空间,链接器会把此目标文件于其他目标文件链接来生成最终的二进制文件,凡是用到这种常量的地方,链接器都能将它解析 - 预处理定义常量,编译器不会对其进行检查,应该用类型常量
要点
- 不要用预处理指令定义常量,这样定义出来的常量不含类型信息,编译器只会在编译前据此执行查找和替换操作。即使有人重新定义了常量值,编译器也不会产生警告信息,这会导致应用程序中常量值的不一致
- 在实现文件中使用
static const
定义“只在编译单元内可见的常量”。由于此类常量不在全局符号表中,所以不需要为其名称加前缀 - 在头文件中用
extern
来声明全局常量,并在其相关的实现文件中定义其值,这种常量会出现在全局符号表中,所以其名称应该加上前缀来区分,通常用与之相关的类名作为前缀
扩展
// const *p vs * const p
const *p // 指向常量的指针可以理解为const (*p),p所指向的常量内容不能改变但是可以改变p所指向的常量
* const p // 常指针可以理解为 * (const p),不可以改变所指的内存,但是可以改变所指内存中常量的值
第五条 用枚举表示状态、选项、状态码
笔记
- 枚举只是一种常量命名方式。某个对象所经历的各种状态就可以定义成一个简单的枚举集
- 实现枚举所用的数据类型取决于编译器
- 一个字节含有8个二进制位,所以最多能够表示256(2^8)种枚举(编号为0-255)
- 当定义选项的时候,如果这些选项之间可以互相组合,这种情况更应该使用枚举类型,只要枚举定义得对,各个选项之间就可以通过“按位或操作符”组合起来,e.g:iOS UI框架种的UIViewAutoresizing枚举
- Foundation框架中定义了一些辅助的宏,用这些宏来定义枚举类型时,也可以指定用于保存枚举值的底层数据类型
- 凡是需要用按位或操作来组合的枚举都应该使用NS_OPTIONS定义,如果枚举不需要互相组合,则用NS_ENUM
- 用按位或运算操作两个枚举值时,C++编译模式的处理方法与非C++模式不一样,在用或运算操作两个枚举值时,C++认为运算结果的数据类型应该是枚举的底层数据类型,C++不允许将这个底层类型做“隐式转换”为枚举类型本身
- 如果用枚举来定义状态机(switch语句)最好不要写default分支
重点
- 应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,要给这些值取个易懂的名字
- 如果传递给某个方法的选项表示为枚举类型,多个选项又可以同时使用,那么就需要将各个选项值定义为2的幂,这样可以通过按位或操作将他们组合起来
- 用NS_ENUM或者NS_OPTIONS宏来定义枚举类型,并指明他们底层的数据类型。这样可以确保枚举使用开发者所选择的底层数据类型实现出来的,不会采用编译器所选的数据类型
- 在处理枚举类型的switch语句的时候不要实现default分支,这样加入新的枚举之后编译器就会发出警告,switch语句中有没有处理的枚举类型
第六条 理解“属性”这一概念
笔记
- 在对象之间传递数据并且只想任务的过程就叫做“消息传递”,Objective-C运行时环境提供了一些使得对象之间能够传递消息的重要函数,并且包含创建类实例所需要的全部逻辑
- 对象的内存布局在编译期就已经固定了,只要碰到访问某对象变量的代码,编译器就把它替换成偏移量,偏移量是硬编码,表示该变量距离存放对象的内存区域的起始地址有多远
- 为了实现动态化,Objective-C语言的做法是,把实例变量当作一种存储偏移量的特殊变量,交由类对象保管。偏移量会在运行期进行查找,如果类的定义变了,那么存储的偏移量也会随之改变
- ABI(应用程序二进制接口)定义了很多内容其中一项就是生成代码时所应遵循的规范
- 在正规的Objective-C编码中,存取方法也有着严格的命名规范,所以Objective-C语言才能根据名称自动创建出存取方法
- @property,编译器会自动写出一套存取方法,来访问给定类型中具有给定名称的变量
- 编译器会把点语法转成对存取方法的调用,使用点语法和直接调用存取方法没有任何差别
- 自动合成(autosynthesis)是编译特性,使用@dynamic关键字可以让编译器不自动合成存取方法
- 属性特质
- 原子性:由编译器所合成的方法会通过锁定机制确保其原子性,如果自定义存取方法,那么就应该遵从与属性特质相符的原子性
- 具备原子性(atomic)的获取方法通常会通过锁定机制来确保其操作的原子性,如果两个线程读写同一属性,不论何时都能看到有效的属性值
- 在iOS中使用同步锁的开销比较大,一般情况下并不要求属性必须是“原子的”,这也不能保证“线程安全”,在MAC OS X中使用atomic通常不会有性能瓶颈
重点
- 可以用@property语法来定义对象中所封装的数据
- 通过“特质”来指定存取数据所需要的正确语义
- 在设置属性所对应的存取方法时,一定要遵循该属性所声明的语义
- 开发iOS应用程序时应该使用nonatomic,atomic属性会严重影响性能