《Effective Objective-C 2.0》:概念篇

《Effective Objective-C 2.0》:概念篇_第1张图片
蜂鸟.jpg

想必大家都知道这本书,iOS开发者推荐书籍之一。之前读过这本书,但是并没有认真的整理,本文纯属笔者的学习笔记,如有侵犯还请见谅。

思维导图文件

《Effective Objective-C 2.0》:概念篇_第2张图片
结构图.png

本篇涉及到的面试题

  • @prorperty的本质是什么?ivar, getter, setter是如何添加到类中的?
  • @protocolCategory中如何使用@property
  • ARC下,如果不指定任何属性关键字,默认的关键字有哪些?
  • @synthesize@dynamic分别有什么作用?
  • @synthesize合成实例变量的规则是什么?假如property名为foo,存在一个名为_foo的实例变量,那么还会自动合成新变量吗?
  • 在有了自动合成实例变量之后,@synthesize还有哪些使用场景?
  • 如何比较两个对象是否相等?如何为自定义对象实现等同性比较?
  • objc中向一个对象发送消息[obj foo]objc_msgSend() 函数之间有什么关系?
  • 什么时候会出现 unrecognized selector的异常?
  • 一个objc对象如何进行内存布局?
  • 一个objc对象的isa的指针指向什么?有什么作用?
    *objc向一个nil对象发送消息会发生什么?

第1条:了解Objective-C语言的起源

Objective-C是由 Smalltalk 演变而来的,后者被称为消息型语言的鼻祖。

Objective-CJava 等面向对象语言的区别:

  • OC : 消息结构
  • Java、C++: 函数调用

消息结构和函数调用的区别?

消息结构的语言,其运行时所应执行的代码由运行环境来决定;而使用函数调用的语言,则由编译器决定。

什么是动态绑定?

如果调用的函数是多态的,那么在运行的时候通过“虚函数表”,查找需要执行函数的哪个实现。而采用消息结构语言,不论是不是多态,总是在运行时才去查找所要执行的方法。编译器不用关心接收对象的类型,接收消息的对象问题也要在运行时处理,这就是动态绑定。

什么是运行时组件?

运行时组件本质上是一种与开发者所编写的代码相链接的“动态库(dynamic library)” ,将开发者编写的代码整合到一起。运行时组件包含全部的内存管理方法,OC全部的数据结构和函数都在运行时组件里面。它的优点是:只要更新运行时组件,就可以提升应用性能,而其他语言则需要重新编译应用程序代码。

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

什么是属性?

”属性“是 Objective-C 的一个特性,用于封装对象中的数据。Objective-C对象通常会把其所需要的数据保存为各种实例变量。实例变量通过存取方法来访问。通过“setter”方法写入变量,“getter”方法读取变量。

Java、C++Objective-C属性对比


@interface EOCPerson : NSObject {
    @public
    NSString *_firstName;
    NSString *_lastName;
    @private
    NSString *_someInternalData;
}
@end

上述写法是JavaC++写法,但是这种写法存在一些问题:对象布局在编译期就固定,每个变量对应一个“偏移量”,表示变量距离存放对象的内存区域的起始地址有多远。
如果,添加一个变量_dateOfBrith_firstName之前,那么之前定义的变量的偏移量都会发生变化。各种编程语言都有解决方法,那么,Objective-C是如何解决这个问题的呢?

第一种方法
把实例变量当成一种存储偏移量所用的“特殊变量”,交由“类对象”保管。偏移量会在运行期查找,存储的偏移量随着类的定义改变而改变,无论何时访问偏移量,都能正确获取。甚至可以在运行期添加新的实例变量,这就是“应用程序二进制接口(Application Binary Interface,ABI)”

ABI作用
ABI定义了生成代码时所应遵循的规范。有了“稳固的”ABI,我们可以在“clas-continuation分类”或实现文件中定义实例变量。我们可以将实例变量从Public区域移走,以保护与类实现有关的内容信息。
PS:Swift3.0 ABI还没有“稳固”。

第二种方法
尽量不直接访问实例变量,而是通过存取方法来访问。

@property语法

@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

上述代码对类的使用者来说是等效的。通过@property语法,编译器自动生成一套存取方法。

点语法

EOCPerson *person = [EOCPerson new];
person.firstName = @"Bob"; // <=> [person setFirstName:@"Bob"];
NSString *firstName = person.firstName; // <=> [person firstName];

注: 使用“点语法”和调用存取方法没有毫差别。

通过上面学习知道属性可以自动生成存取方法,其实属性功能还很多。自动生成实例变量的名字,在属性名前加下划线前缀。也可以手动指定实例变量的名字。通过@synthesize语法。

@implementation EOCPerson
@synthesize firstName = _firstName;
@synthesize lastName = _lastName;
@end

注:推荐默认,提高可读性。

不想用自动生成存取方法,如何自己实现呢?这就需要@dynamic语法,可以阻止编译器自动合成存取方法。

@interface EOCPerson : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end

@implementation EOCPerson
@dynamic firstName, lastName;
@end

在编译访问属性的代码时,即使编译器发现没有定义存取方法,也不会报错,它相信这些方法能在运行期找到。(运行期找不到就会Crash,废话)

属性特质

属性特质分为四种: 原子性读写权限内存管理语义方法名

@property (nonatomic, readwrite, copy, getter=myFirstName) NSString *firstName;

原子性

  • atomic 原子性,使用同步锁。
  • nonatomic 非原子性

两者区别?
具备atomic 特质的获取方法会通过锁定机制来确保其操作的原子性。就是为了防止多个线程同时读取同一属性。然而,在iOS程序中,所有属性都声明为“nonatomic”。这是因为iOS使用同步锁开销较大,另外,不能真正实现“线程安全”。

读/写权限

  • readwrite 读/写
  • readonly 只读

内存管理语义

  • assign : 只针对“纯量类型”。
  • strong :”拥有关系“
  • weak : “非拥有关系”
  • unsafe_unretained:与“assign”相似,但是适用于“对象类型”,“非拥有关系“类似于”weak“。
  • copy :“拷贝”

方法名

  • getter= :用于指定”获取方法“的方法名。
  • setter= :用于指定”设置方法“的方法名,不常用。

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

比较对象时,使用 == 操作符比较的是两个指针本身,而不是指针指向的对象。

那么如何比较对象呢?对象相等的依据是什么?

在Objective-C中,NSObject协议有两个用于判断等同性的方法

- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;

NSObject协议对两个方法的默认实现是:当且仅当内存地址完全相同时,这两个对象才相等

知道了如何比较两个对象等同性,那么自定义的对象如何实现等同性呢?想必你已经猜到了,那就是覆写上面两方法。覆写的约定:对象相等,则其哈希码相等,但是哈希码相等的对象未必相等。

例子:

@interface EOCPerson : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, assign) NSUInteger age;
@end

实现isEqual:方法:

//覆写父类方法
- (BOOL)isEqual:(id)object {
    
    //1. 如果指针相同,如果相同表示指向同一地址
    if (self == object) {
        return YES;
    }
    //2. 比较对象所属的类
    if ([self class] != [object class]) {
        return NO;
    }
    
    //3. 检测对象的所有属性,只要有不相等的属性,则判断对象不相等
    EOCPerson *otherPerson = (EOCPerson *)object;
    
    if (![_firstName isEqualToString: otherPerson.firstName]) {
        return NO;
    }
    if (![_lastName isEqualToString: otherPerson.lastName]) {
        return NO;
    }
    if (_age != otherPerson.age) {
        return NO;
    }
    return YES;
}

实现hash方法:

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

上述方法既能保持较高效率,又能使生成的哈希码至少位于一定范围之内,而不会过于频繁重复。在编写hash方法时,要在减少碰撞频度和降低运算复杂度之间取舍。

特定类所具有的等同性判定方法

  • isEqualToString:
  • isEqualToArray:
  • isEqualToDictionary:

第11条:理解objc_msgSend的作用

之前提到Objective-C是 “消息结构” 语言。那什么是消息?消息有“名称”或“选择子(selector)",可以接收参数,而且可能还有返回值。

为什么说Objective-C是真正的动态语言?

如果向某个对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有的方法都是C函数,然而当对象接收到消息之后,究竟该调用哪个方法则完全由运行期决定,甚至可以在运行期修改,这些特性使得OC成为一门真正的动态语言。

给对象发消息:

id returnValue = [someObject messageName: parameter];
  • someObject : 接收者
  • messageName: 选择子(selector)

selector和接收者合起来成为 “消息”。

实际上,编译器调用下面函数,是消息机制中的核心函数。此函数是 “参数个数可变的函数”。

void objc_msgSend(id self, SEL cmd, ...)

将代码转换成:

id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);

objc_msgSend函数的工作流程:

objc_msgSend函数会依据接收者与选择子的类型来调用适当的方法。首先,搜寻接收者的 “方法列表(list of methods)” , 如果找到就跳转执行,否则沿着继承体系向上查找,找到即执行,最终未找到,就执行 “消息转发(message forwarding)" 操作。

注: 为了提高查找效率,提供了一个”快速映射表(fast map)“。用来缓存经常用到的方法,这是方法的缓存机制。

边界情况

  • objc_msgSend_stret : 返回结构体
  • objc_msgSend_fpret : 返回浮点数
  • objc_msgSendSuper : 给超类发消息。例如 [super message: parameter]

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

对象在收到无法解读的消息之后会发生什么?当对象接收到无法解析的消息后,就会启动 “消息转发(message forwarding)” 机制。

消息转发流程:

第一阶段:先征求接收者所属的类是否能动态添加方法,以处理当前这个“未知的选择子”,这叫做“动态方法解析”。

第二阶段:”完整的消息转发机制“。分为两种情况:如果,接收者查看其它对象(备援的接收者)能否处理消息,如果能转发处理。否则,启动完整消息转发机制,运行期系统就会把与消息有关的全部细节都封装到NSInvocation对象中,在给接收者最后一次机会,处理当前消息。

动态方法解析

+ (BOOL)resolveInstanceMethod:(SEL)selector;

selector :是未知的选择子。该方法表示类能否新增一个实例方法处理选择子。如果是类方法,则使用:resolveClassMethod:

备援接收者

- (id)forwardingTargetForSelector:(SEL)selector;

是否能找到备援对象,找到返回,否则返回nil。

完整消息转发

- (void)forwardInvocation:(NSInvocation *)invocation;

实现方式:在触发前,先以某种方式改变消息内容。比如,追加参数或者修改选择子等。如果,最终无法处理消息,则会调用NSObject类中doesNotRecoginzeSelector:抛出异常。

在《Effective Objective-C》中给出了完整的例子演示动态方法解析。需要注意的是,在CoreAnimation框架中,CALayer就是用这种方法实现的,可以随意添加属性,然后以键值对形式访问。

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

Class对象定义在运行期程序库的头文件中:

typedef struct objc_class *Classs;
struct objc_class {
  Class isa;
  Class super_class;
  const chat *name;
  long version;
  long info;
  long instance_size;
  struct objc_ivar_list *ivars;
  struct objc_method_list **methodLists;
  struct objc_cache *cache;
  struct objc_protocol_list *protocols;
}
  1. isa:指向metaClass(元类)的指针。
  2. super_class:指向该类的父类,如果该类是根类(NSObject或NSProxy),则为NULL
  3. version:记录类的版本信息。主要用于对象的序列化,可以通过它识别出不同定义版本中实例变量布局的改变。
  4. cache:用于缓存常用的方法。当接收对象收到消息时,根据isa指针去查找相应的对象。实际上,这个对象中有很多方法,只有一少部分经常使用,很多方法不常使用或者根本用不上。这种情况下,如果每次接收到消息都去遍历methodLists,性能较差,所以使用cache保存经常使用的方法,在接收到消息后,首先查找cache,如果没有则查找methodLists,提高查找效率。
    Objective-C Runtime:类与对象之前学习笔记。

super_class指针确立了继承关系,而isa指针描述了实例所属的类。通过他们可以实现“类型信息查询”。

在类继承体系中查询类型信息

  • isMemberOfClass: 能够判断出对象是否为某个特定类的实例。
  • isKindOfClass: 判断对象是否为某类或其派生类的实例。
NSMutableDictionary * dict = [NSMutableDictionary new];
[dict isMemberOfClass:[NSDictionary class]]; // NO
[dict isMemberOfClass:[NSMutableDictionary class]]; //YES
[dict isKindOfClass:[NSDictionary class]]; //YES
[dict isKindOfClass:[NSArray Class]]; // NO

像这种类型查询通过isa指针获取对象所属的类,通过super_class指针在继承体系中游走。

第21条:理解Objective-C错误模型

Objective-C

  • 只有发生了使整个应用程序崩溃的严重错误事,才会使用异常。

  • 错误不那么严重的情况下,可以指派”委托方法“来处理错误,也可以把错误信息放在NSError对象里,经由”输出参数“返回给调用者。

NSError对象里封装了三条信息:

  • Error domain (错误范围,其类型为字符串)
  • Error code (错误码,其类型为整数)
  • User info (用户信息,其类型为字典)

NSError常见用法:

  1. 通过协议来传递错误信息,最常见是NSURLConnection中。
  2. 经由方法的”输出参数“返回给调用者。例如:
  - (BOOL)doSomething:(NSError **)error;

使用范例:

 NSError *error = nil;
  BOOL ret = [self doSomething: &error];
  if (error) {
      //错误信息
  }

自定义Error

//EOCError.h
extern NSString *const EOCErrorDomain;
typedef NS_ENUM(NSUInteger, EOCError) {
    EOCErrorUnknown         = -1,
    EOCErrorInternalInconsistency = 100,
    EOCErrorGeneralFault        = 105,
    EOCErrorBadInput        = 500,
};
//EOCError.m
NSString *const EOCErrorDomain = @"EOCErrorDomain";

错误范围应该定义成NSString型的全局常量,而错误码则定义成枚举类型。

第22条:理解NSCopying协议

在Objective-C中,如果要让自定义的类支持拷贝操作,需要实现NSCoping协议,该协议唯一的方法:

- (id)copyWithZone:(NSZone *)zone;

NSZone 是历史遗留下来,可以忽略

自定义类实现Copy功能示例:

#import 

@interface EOCPerson : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, assign) NSUInteger age;

- (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age;
@end

实现协议中规定的方法:

@implementation EOCPerson {
    NSMutableSet *_friends;
}

- (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age {
    self = [super init];
    if (self) {
        _firstName = firstName;
        _lastName = lastName;
        _age = age;
        _friends = [NSMutableSet new];
    }
    return self;
}
- (instancetype)copyWithZone:(NSZone *)zone {
    
    EOCPerson *copy = [[[self class] allocWithZone: zone] initWithFirstName:_firstName lastName:_lastName age:_age];
    copy->_friends =  [_friends mutableCopy];
    return copy;
}
@end 

由于friends只是EOCPerson的一个实例变量不是属性,无法用点语法访问,使用->

有时候我们实现类分为可变版本和不可变版本那么实现拷贝方法需要怎么实现呢?

实现可变版本复制需要使用:

- (id)mutableCopyWithZone:(NSZone *)zone;

通常情况下,为了方便可变版本与不可变版本之间的转换,当时用”copyWithZone:“时返回不可变版本,使用”mutableCopyZone:“返回可变版本。例如:

- [NSMutableArray copy] => NSArray 
- [NSArray mutableCopy] => NSMutableArray

深复制和浅复制
深复制: 在拷贝对象自身时,将其底层数据也一并复制过去。
浅复制:只复制对象指针,底层数据还是原来一份。Foundation中所有的集合类型默认情况下都执行浅复制。

第29条:理解引用计数

什么是引用计数?
引用计数是Objective-C管理内存的方式。在Java中,使用垃圾收集器管理内存,在Objective-C中则使用引用计数管理内存。

NSObject协议声明了三个方法用于操作计数器:

  • retain :递增引用计数
  • release : 递减引用计数
  • autorelease :清理”自动释放池“时,在递减引用计数
    查看引用计数的方法:retainCount

自动释放池

在Objective-C的引用计数架构中,自动释放池是一个重要特性。调用release会立即减少对象引用计数,然而有时我们不调用它,改为调用autorelease,此方法会在稍后递减对象引用计数。

循环引用

循环引用是对象间存在相互引用的情况,对象的引用计数永远不会降到0,造成内存泄露。解决方法:通过”弱引用“解决这个问题,或者外界命令某个对象不再强引用另一个对象。这两种方法都可以打破循环引用。

第30条:以ARC简化引用计数

什么是自动引用计数?

顾名思义,就是自动管理引用计数。ARC的是基于核心的内存管理语义构建的。

在ARC中,我们不能直接操作内存管理方法,如下方法:

  • retain
  • release
  • autorelease
  • dealloc

如果无意中调用,结果当然是编译错误。

需要注意的是:ARC只负责管理Objective-C对象的内存。CoreFoundation对象不归ARC管理,开发者必须适时调用CFRetain / CFRelease。

第37条:理解”块“这一概念

基本概念

块和函数类似,只不过是直接定义在另一个函数里面的,和定义它的那个函数共享同一范围的东西。块用“^”符号来实现,后面跟着花括号,括号里面是块的实现代码。
示例:

^{
      //block implementation here
}

块其实就是值,而且自由其相关类型。有Objective-C其他值和对象一样,可以把块赋给其他变量,然后像使用其他变量一样使用它。

块的语法结构:

return_type (^block_name)(parameter)

块的强大之处:在声明它的范围值,所有变量都可以为其所捕获。

在默认情况下,为块所捕获的变量,是不可以在块里面修改的。 需要添加__block修饰符。

注意:self也是对象,因而块在捕获它时也会将其保留,如果self的那个对象同时也保留了块,那么这样情况通常会导致“循环引用”。

块的底层实现,在我之前整理的笔记中重识Objective-C:Block底层实现,这里就不重复了。

第47条:熟悉系统架构

什么是框架?
将一系列代码封装为动态库(dynamic library),并在其中放入描述其接口的头文件,这样做出来的东西称为框架。

常用框架:

  • Foundation :像NSObject,NSArray,NSDictionary等类都在其中,Foundation框架中的类,使用NS做前缀。

  • CoreFoundation: 确切的说它不是Objective-C框架,但是在Foundation框架中的许多功能,在它中可以找到对应的C语言的API。

  • CFNetwork :提供C语言级别的网络通讯能力。

  • CoreAudio :可以用来操作设备上的音频硬件。

  • AVFoundation : 可以用来回放和录制音频和视频。

  • CoreData :提供数据保存功能。

  • CoreText : 此框架提供的C语言接口可以高效执行文字排版及渲染操作。

请注意: 用纯C写成的框架与用Objective-C写成的框架一样重要。

小结

以上是《Effective Objective-C 2.0》概念部分的全部内容,后续内容稍后陆续整理出来。温故而知新,重读此书收获颇丰。

哎吆,我去,原来是这个意思啊,之前咋没想到呢!

你可能感兴趣的:(《Effective Objective-C 2.0》:概念篇)