- Effective Objective-C 2.0 读书笔记
- Effective Objective-C 2.0笔记(二)
- Effective Objective-C 2.0笔记(一)
- Effective Objective-C 2.0笔记(三)
- Effective Objective-C 2.0笔记(五)
- 《Effective Objective-C 2.0 》 阅读笔
- Effective Objective-C 2.0 无废话精简篇
- Effective Objective-C 2.0 脑图- [O
- iOS开发读书笔记:Effective Objective-C
- iOS开发读书笔记:Effective Objective-C
第1章:熟悉Objective-C
通论该语言的核心概念
第1条:了解 Objective-C 语言的起源
- Objective-C从Smalltalk语言是从Smalltalk语言演化而来,
Smalltalk是消息语言的鼻祖 - Objective-C为C语言添加了面向对象特性,是其超集。Objective-C使用动态绑定的消息结构,也就是说,在运行时才会检查对象类型。接收- -条消息之后,究竟应执行何种代码,由运行期环境而非编译器来决定。
- 消息结构与函数调用的关键区别在于:函数调用的语言,在编译阶段由编译器生成一些虚方法表,在运行时从这个表找到所要执行的方法去执行。而使用了动态绑定的消息结构在运行时接到一条消息,接下来要执行什么代码是运行期决定的,而不是编译器。
NSString *someString = @"Hello World";
NSString *anotherString = @"Hello World";
NSLog(@"someString:%p --- anotherString:%p", someString, anotherString);
印结果:
someString:0x1000102e8 --- anotherString:0x1000102e8
两个变量为指向同一块内存的相同指针。此时将 anotherString
赋值为 “Hello World!!!”
NSString *anotherString = @"Hello World!!!";
NSLog(@"someString:%p --- anotherString:%p", someString, anotherString);
打印结果:
someString:0x1000102e8 --- anotherString:0x100010308
此时,两者变为不同的内存地址。所以,对象的本质是指向某一块内存区域的指针,指针的存储位置取决于对象声明的区域和有无成员变量指向。若在方法内部声明的对象,内存会分配到栈中,随着栈帧弹出而被自动清理;若对象为成员变量,内存则分配在堆区,声明周期需要程序员管理。
第2条:在类的头文件中尽量少引入其他头文件
- 除非确有必要,否则不要引入头文件,一般来说,应该在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件。这样做可以尽量降低类之间的耦合。
- 有时无法使用向前声明,比如要声明某个类遵循一项协议,尽量把“该类遵循某协议” 的这条声明移至“class-continuation 分类中”。如果不行的话,就把协议单独放在某一个头文件中,然后将其引入。
//Student.h
@class Book; //向前引用,避免在 .h 里导入其他文件
@interface Student : NSObject
@property (nonatomic, strong) BOOK *book;
@end
//student.m
#import "Book.h"
@implementation Student
- (void)readBook {
NSLog(@"read the book name is %@",self.book);
}
@end
这样做有什么优点呢:
- 不在A的头文件中引入B的头文件,就不会一并引入B的全部内容,这样就减少了编译时间。
- 可以避免循环引用:因为如果两个类在自己的头文件中都引入了对方的头文件,那么就会导致其中一个类无法被正确编译。
但是个别的时候,必须在头文件中引入其他类的头文件:
- 该类继承于某个类,则应该引入父类的头文件。
- 该类遵从某个协议,则应该引入该协议的头文件。而且最好将协议单独放在一个头文件中
第3条:多用字面量语法,少用与之等价的方法
- 应该使用字面量语法来创建字符串、数值、数组、字典。与创建此类对象的常规方法相比,这么做更加简明扼要。
- 应该通过取下标操作来访问数组下标或字典中的键所对应的元素。
- 用字面量语法创建数组或字典,若值中有 nil ,则会抛出异常。因此,务必确保值里不含 nil。
- 声明时的字面量语法:
在声明NSNumber
,NSArray
,NSDictionary
时,应该尽量使用简洁字面量语法。
NSNumber *intNumber = @1;
NSNumber *floatNumber = @2.5f;
NSArray *animals =[NSArray arrayWithObjects:@"cat", @"dog",@"mouse", @"badger", nil];
Dictionary *dict = @{@"animal":@"tiger",@"phone":@"iPhone 6"};
- 集合类取下标的字面量语法:
NSArray,NSDictionary,NSMutableArray,NSMutableDictionary 的取下标操作也应该尽量使用字面量语法。
NSString *cat = animals[0];
NSString *iphone = dict[@"phone"];
字面语法的局限性:
-
字面量语法所创建的对象必须属于 Foundation 框架,自定义类无法使用字面量语法创建。
-
使用字面量语法创建的对象只能是不可变的。若希望其变为可变类型,可将其深复制一份
NSMutableArray *arrayM = [@[@1,@"123",@"567"] mutableCopy];
第4条:多用类型常量,少用 #define 预处理指令
- 不要用预处理指令定义常量。这样定义的常量不含类型信息,编译器只是会在编译前据此执行查找与替换操作。即使有人重新定义了常量值,编译器也不会产生警告信息⚠️,这将导致应用程序中的常量值不一致。
- 在实现文件中使用
static const
来定义“只在编译单元内可见的常量”。由于此类常量不在全局符号表中,所以无需为其名称加前缀。 - 在头文件中使用
extern
来声明全局常量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以名称应该加以区隔,通常用与之相关的类名做前缀。
在OC中,定义常量通常使用预处理命令,但是并不建议使用它,而是使用类型常量的方法。
两种方法的区别:
- 预处理命令:简单的文本替换,不包括类型信息,并且可被任意修改。
- 类型常量:包括类型信息,并且可以设置其使用范围,而且不可被修改。
我们可以看出来,使用预处理虽然能达到替换文本的目的,但是本身还是有局限性的:不具备类型 + 可以被任意修改,总之给人一种不安全的感觉。
知道了它们的长短处,我们再来简单看一下它们的具体使用方法:
预处理命令:
#define W_LABEL (W_SCREEN - 2*GAP)
这里,(W_SCREEN - 2*GAP)替换了W_LABEL,它不具备W_LABEL的类型信息。而且要注意一下:如果替换式中存在运算符号,最好用括号括起来。
类型常量:
static NSString* const kEnableGestureRecognizer = @"EnableGestureRecognizer";
这里:
const 将其设置为常量,不可更改。
static意味着该变量仅仅在定义此变量的编译单元中可见。如果不声明static,编译器会为它创建一个外部符号(external symbol)。我们来看一下对外公开的常量的声明方法:
注意:const修饰符在常量类型中的位置。常量定义应从右至左解读,所以在本例中,kEnableGestureRecognizer 就是“ 一个常量,而这个常量是指针,指向NSString对象”。这与需求相符:我们不希望有人改变此指针常量,使其指向另-个NSString对象。
对外公开某个常量:
如果我们需要发送通知,那么就需要在不同的地方拿到通知的“频道”字符串,那么显然这个字符串是不能被轻易更改,而且可以在不同的地方获取。这个时候就需要定义一个外界可见的字符串常量。
//header file
extern NSString *const NotificationString;
//implementation file
NSString *const NotificationString = @"Finish Download";
我们通常在头文件声明常量,在其实现文件里定义该常量。由实现文件生成目标文件时,编译器会在“数据段”为字符串分配存储空间。
最后注意一下公开和非公开的常量的命名规范:
- 公开的常量:常量的名字最好用与之相关的类名做前缀。
- 非公开的常量:局限于某个编译单元(tanslation unit,实现文件 implementation file)内,在签名加上字母k。
第5条:用枚举表示状态、选项、状态码
- 应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起个易懂的名字。
- 如果把传递给某个方法的选项表示为枚举类型,而多个选项又可以同时使用,那么就将各选项定义为2的幂,以便通过按位或操作将其组合起来。
- 用 NS_ENUUM 与 NS_OPTIONS 宏来定义枚举类型,并指明其底层数据类型。这样做可以确保枚举是用开发者所选的底层数据类型实现出来的,而不会采用编译器所选类型。
- 在处理枚举类型的switch语句中不要实现 default分支。这样的话,加入新枚举之后,编译器就会提示开发者:switch 语句并未处理所以枚举。
/// 位移枚举
typedef NS_OPTIONS(NSUInteger, Direction) {
DirectionTop = 0,
DirectionBottom = 1 << 0,
DirectionLeft = 1 << 1,
DirectionRight = 1 << 2,
};
/// 常量枚举
typedef NS_ENUM(NSInteger,ShowType){
ShowTypeForce,
ShowTypeNormal
};
第2章:对象、消息、运行时
对象之间能够关联与交互,这是面向对象语言的重要特征。本章讲述这些特征,并深人研究代码在运行期的行为。
第6条:理解“属性”这一概念
- 可以用 @property 语法来定义对象中所封装的数据。
- 通过“特质”来指定存储数据所需的正确语义。
- 在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。
- 开发iOS程序时应该使用nonatomic属性,因为atomic属性会严重影响性能。
- 存取方法
在设置完属性后,编译器会自动写出一套存取方法,用于访问相应名称的变量:
@interface EOCPerson : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end
@interface EOCPerson : NSObject
- (NSString*)firstName;
- (void)setFirstName:(NSString*)firstName;
- (NSString*)lastName;
- (void)setLastName:(NSString*)lastName;
@end
访问属性,可以使用点语法。编译器会把点语法转换为对存取方法的调用:
aPerson.firstName = @"Bob"; // Same as:
[aPerson setFirstName:@"Bob"];
NSString *lastName = aPerson.lastName; // Same as:
NSString *lastName = [aPerson lastName];
如果我们不希望编译器自动生成存取方法的话,需要设置@dynamic 字段:
@interface EOCPerson : NSManagedObject
@property NSString *firstName;
@property NSString *lastName;
@end
@implementation EOCPerson
@dynamic firstName, lastName;
@end
- 属相特质
定义属性的时候,通常会赋予它一些特性,来满足一些对类保存数据所要遵循的需求。
原子性:
- nonatomic:不使用同步锁
- atomic:加同步锁,确保其原子性
读写
- readwrite:同时存在存取方法
- readonly:只有获取方法
内存管理
- assign:纯量类型(scalar type)的简单赋值操作
- strong:拥有关系保留新值,释放旧值,再设置新值
- weak:非拥有关系(nonowning relationship),属性所指的对象遭到摧毁时,属性也会清空
- unsafe_unretained :类似assign,适用于对象类型,非拥有关系,属性所指的对象遭到摧毁时,属性不会清空。
- copy:不保留新值,而是将其拷贝
注意:遵循属性定义
如果属性定义为copy,那么在非设置方法里设定属性的时候,也要遵循copy的语义
- (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName
{
if ((self = [super init])) {
_firstName = [firstName copy];
_lastName = [lastName copy];
}
return self;
}
第7条:在对象内部尽量直接访问实例变量
关于实例变量的访问,可以直接访问,也可以通过属性的方式(点语法)来访问。书中作者建议在读取实例变量时采用直接访问的形式,而在设置实例变量的时候通过属性来做。
直接访问属性的特点:
绕过set,get语义,速度快;
通过属性访问属性的特点:
不会绕过属性定义的内存管理语义
有助于打断点排查错误
可以触发KVO
因此,有个关于折中的方案:
设置属性:通过属性
读取属性:直接访问
不过有两个特例:
- 初始化方法和dealloc方法中,需要直接访问实例变量来进行设置属性操作。因为如果在这里没有绕过set方法,就有可能触发其他不必要的操作。
- 惰性初始化(lazy initialization)的属性,必须通过属性来读取数据。因为惰性初始化是通过重写get方法来初始化实例变量的,如果不通过属性来读取该实例变量,那么这个实例变量就永远不会被初始化。
网友评论