effective objc 阅读笔记:ch1 &ch2

本文是Effective ObjC阅读笔记,涵盖语言起源、对象、消息传递、运行期特性和最佳实践。讲解了Objective-C中如何使用字面量语法、枚举、属性、消息转发等关键概念,旨在提升Objective-C编程技能。

一、熟悉 Objective-C

第1条、了解语言的起源

Objective-C 是一门消息型语言,使用消息结构,而非函数调用。对于消息结构,调用函数由执行代码后的运行环境决定;对于函数调用,则由编译器决定(除非调用函数是多态,才会在运行时根据虚函数表决定具体执行的函数);

Objective-C 是 C 的超集,对于栈上的局部变量,随栈帧的弹出自动清理,对于堆上的对象,按“引用计数”的方式,由内存直接管理;


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

通过“向前声明”的方式,避免在头文件中引入其他头文件,而是在 .m 文件中引入,将引入头文件的时机尽量延后,只在确定要使用时引入,减少类的使用者所需引入的头文件的数量,减少编译时间;该方式的另一个好处是避免头文件之间循环引用(虽然 #import 也可以保证每个头文件只被引用一次,通过 #ifdef )

#import <Foundation/Foundation.h>

@class EOCEmployer;

@interface EOCPerson: NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, strong) EOCEmployer *employer;
@end

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

使用字面量语法,与通过 alloc、init 方法声明变量等价,但可以减少代码长度,使其更为易读;

NSString *someString = @"Effective Objective-C";
NSString *someString = [[NSString alloc] initWithString: xxx];

NSNumber *number = [NSNumber numberWithInt:1];
NSNumber *number = @1;

在声明和使用数组时(以 NSArray 为例),直接声明的字面量语法等价于 arrayWithObjects,取下标访问等价于 objectAtIndex,可以视作系统提供的“语法糖”(计算机语言中与另一套语法完全等价但是开发者使用更加方便的语法);

使用字面量语法更加安全,对于 arrayA,arrayB,若 object2 为 nil,那么 arrayWithObjects 会提前结束(该方法会依次处理各个参数,直到发现 nil 为止),而字面量语法会抛出异常,有助于更快发现错误;

NSArray *arrayA = [NSArray arrayWithObjects: object1, object2, object3, nil];
NSArray *arrayB = @[object1, object2, object3]

字面量语法生效的对象必须属于 Foundation 框架,且创建出的对象都是不可变的,若要求可变,则需进行一次转换;

NSMutableArray *array = [@[@1, @2, @3] mutableCopy];

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

预处理指令的缺陷:定义出的常量没有类型信息,所有引入了声明该常量的头文件都会进行替换,如果定义被替换编译器也不会警告;

命名规则:若常量局限于实现文件,则在前面加字母k;若常量在类之外可见,则在前面加类名为前缀(因为 Objective-C 没有命名文件,声明常量相当于声明了一个全局变量,加上类名能够有效避免冲突);

static const NSTimeInterval kAnimationDuration = 0.3;

static 限制了变量的范围,只在此变量的编译文件中可见,因此无需加类名作为命名前缀,加字母k即可,否则编译器会为它创建一个外部符号(external symbol),并在外部文件中通过 extern 关键字也可使用,该关键字说明当链接成二进制之后,肯定能在全局符号表中找到这个符号;


第5条、用枚举表示状态、选项、状态码
typedef NS_ENUM(NSUInteger, EOCConnectionState) {
  EOCConnectionStateDisconnected,
  EOCConnectionStateConnecting,
  EOCConnectionStateConnected,
};

typedef NS_OPTIONS(NSUInteger, EOCPermittedDirection) {
  EOCPermittedDirectionUp    = 1 << 0,
  EOCPermittedDirectionDown  = 1 << 1,
  EOCPermittedDirectionLeft  = 1 << 2,
  EOCPermittedDirectionRight = 1 << 3,
}

Foundation 封装的两种构造枚举值的方法如下,NS_ENUM 和 NS_OPTIONS 的区别在于,NS_ENUM 在按 C++ 编译时,C++ 不允许声明枚举的底层类型经过隐式转换为枚举类型本身,因此在需要各枚举型变量进行组合时,尽量使用 NS_OPTIONS,避免类型转换操作,例如下面场景:

EOCPermittedDirection permittedDirection = EOCPermittedDirectionLeft | EOCPermittedDirectionUp;

二、对象、消息、运行期

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

使用 @property 声明的类的成员,一般地,编译器会自动生成属性对应的实例变量与存取方法(getter & setter),实例变量由类保管,通过存储偏移量进行查找;使用点语法对类的成员变量进行存取时,编译器默认使用了存取方法;

EOCPerson *aPerson = [[Person alloc] init];
aPerson.firstName = @"Bob" 
// 等价于 [aPerson setFirstName:@"Bob"]

声明 @property 时,通常还附带了特性:原子性(atomic,nonatomic),通过锁机制保证存取方法的原子性,若自己定义存取方法,也要保证与属性特质相符的原子性;读写权限(readwrite,readonly),默认是 readwrite;内存管理语义(assign,strong,weak,copy,unsafe_unretained),assign 修饰基础数据类型;strong 可以理解为浅拷贝;copy 可以理解为深拷贝;weak 弱引用;

unsafe_unretained 可以理解为适用于对象类型的 assign,不拥有对象,当对象释放时会导致悬垂指针;(悬垂指针:指针指向的空间已经释放,但是指针没有修改为 NULL;野指针:指向垃圾内存的指针,一般是声明指针时为初始化造成的)。在开发 iOS 程序时,属性一般声明为 nonatomic,避免锁机制的开销,并且 atomic 也不能一定保证线程安全;

在自定义 init 方法时,也一定要遵守属性定义中宣称的语义,即使是 readonly 变量,编译器不会创建对应的 setter 方法,但仍有必要写明 copy & strong,因为该修饰字说明了该属性在初始化时的操作,同时,在对象内部尽量直接访问实例变量;

- (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName {
  if (self = [super init]) {
    _firstName = [firstName copy];
    _lastName = [lastName copy];
  }
  return self;
}

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

读取类的成员有两种方法:直接访问,通过 _firstName,通过属性访问,self.firstName 或 [self firstName];

直接访问是通过存储偏移量直接访问类的成员的方式,不经过类的消息发送,因而执行更快;绕过了存取方法,即绕过了内存管理语义,增加了程序的不确定性,同时也不会触发 KVO 机制,或是设置的断点。合理方案是设置变量时,通过 setter 方法,控制了对属性的写入操作,而在读取变量时,通过偏移量直接访问,读取速度更快;

在懒加载的场景下,必须使用存取方法,否则该属性永远不会被初始化;

-(EOCBrain*)brain {
  if (!_brain) {
    _brain = [Brain new];
  }
  return _brian;
}

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

通过 hash 和 isEqual 判断两个对象是否相等,在向 collection 加入对象时,也可能根据 hash 值放入不同数组;

// 实现 hash
- (NSUInteger)hash{
  NSUInteger firstNameHash = [_firstName hash];
  NSUInteger lastNameHash  = [_lastName hash];
  NSUInteger ageHash = _age;
  return firstNameHash ^ lastNameHash ^ ageHash;
}

在制定 isEqual 方法时,可根据实际情况制定检测方案,例如,主键;

在使用容器类时,对已经放入容器的对象,不应该进行改变,否则有可能发生错误问题;

NSMutableSet *set = [NSMutableSet new];
NSMutableArray *arrayB = [@[@1] mutableCopy];
[set addObject:[@[@1, @2] mutableCopy]];
[set addObject:arrayB];
// 此时对容器内已添加元素进行修改
[arrayB addObject:@2];
// output set = {(1, 2), (1, 2)}

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

类族模式:灵活创建多个类,将它们的实现隐藏在抽象类后,系统框架经常使用类族,因此类创建时,返回的数据类型很有可能是其子类,应使用 isKindClass 判断类对象是否等同;

+ (EOCEmployer*)employeeWithType:(EOCEmployeeType)type {
  switch (type) {
    case EOCEmployeeTypeDeveloper:
      return [EOCEmployeeTypeDeveloper new];
      break;
    case EOCEmployeeTypeDesigner:
      return [EOCEmployeeTypeDesigner new];
      break;
    case EOCEmployeeTypeFinance:
      return [EOCEmployeeTypeFinance new];
      break;
  }
}

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

所有关联对象由一个全局的 AssociationManager 管理, block 也可以存放在关联对象中;

可以通过关联对象机制,将两个对象连接起来,例如在处理多个 UIAlertView 时,使用关联对象实现可以简化代码;


第11条、理解 objc_msgSend 的作用

在 Objective-C 中,函数调用通过 objc_msgSend 实现,该过程由发送者、选择子、参数构成,通过动态绑定的方式实现,并将匹配结果缓存到”快速映射表“,加快方法调用速度;


第12条、理解消息转发机制

消息转发机制分三步:动态方法解析、重定向机制、完整的消息转发机制;


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

使用 method-swizzling 方法,可以为某些不知道实现的方法增加日志记录功能,该方法只在调试程序时有用,其他情况下很少使用这种技术永久改动类的功能;


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

id 等同于 objc_object*,Class 等同于 objc_class*;

struct objc_object {
  Class isa;
}

struct objc_class {
  Class isa; // 指向元类,存储类方法
  Class super_class; // 指向父类
  const char* name;
  ...
  long instance_size;
  ...
}

使用 id 与 具体类声明的区别在于:具体类声明的对象在使用某些未声明方法时,编译器会产生警告,而 id 声明可以避免警告;

id aName = @"123";
NSString *aName = @"123";

isMemberOfClass,判断实例是否属于某个类;isKindOfClass,判断实例是否属于类及其派生类;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值