美文网首页iOS Kit
《Effective Objective-C 2.0》读书笔记

《Effective Objective-C 2.0》读书笔记

作者: 锦鲤跃龙 | 来源:发表于2019-03-09 16:08 被阅读1次

第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的全部内容,这样就减少了编译时间。
  • 可以避免循环引用:因为如果两个类在自己的头文件中都引入了对方的头文件,那么就会导致其中一个类无法被正确编译。

但是个别的时候,必须在头文件中引入其他类的头文件:

  1. 该类继承于某个类,则应该引入父类的头文件。
  2. 该类遵从某个协议,则应该引入该协议的头文件。而且最好将协议单独放在一个头文件中

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

  • 应该使用字面量语法来创建字符串、数值、数组、字典。与创建此类对象的常规方法相比,这么做更加简明扼要。
  • 应该通过取下标操作来访问数组下标或字典中的键所对应的元素。
  • 用字面量语法创建数组或字典,若值中有 nil ,则会抛出异常。因此,务必确保值里不含 nil。
  1. 声明时的字面量语法:
    在声明NSNumberNSArrayNSDictionary时,应该尽量使用简洁字面量语法。
NSNumber *intNumber = @1;
NSNumber *floatNumber = @2.5f;

NSArray *animals =[NSArray arrayWithObjects:@"cat", @"dog",@"mouse", @"badger", nil];
Dictionary *dict = @{@"animal":@"tiger",@"phone":@"iPhone 6"};
  1. 集合类取下标的字面量语法:

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属性会严重影响性能。
  1. 存取方法
    在设置完属性后,编译器会自动写出一套存取方法,用于访问相应名称的变量:
@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
  1. 属相特质

定义属性的时候,通常会赋予它一些特性,来满足一些对类保存数据所要遵循的需求。

原子性:

  • 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方法来初始化实例变量的,如果不通过属性来读取该实例变量,那么这个实例变量就永远不会被初始化。

相关文章

网友评论

    本文标题:《Effective Objective-C 2.0》读书笔记

    本文链接:https://www.haomeiwen.com/subject/hgwspqtx.html