【iOS】小蓝书学习(二)

小蓝书学习(二)

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

"属性"(property)

这是OC中的一项特性,用于封装对象中的数据。OC对象通常会将其所需要的数据保存为各种实例变量 。实例变量一般通过"存取方法"accessmethod来访问。其中,"获取方法"getter用于读取变量值,而"设置方法"setter用于写入变量值。

objective-c 复制代码
@interface ECOPerson:NSObject {
@public 
  NSString* _firstName;
  NSString* _lastName;
@private
  NSString* _someInternalData;
}

我们在_fristName前再加一个实例变量,这个时候,偏移量硬编码就会读取到错误的值。

objective-c 复制代码
@interface ECOPerson:NSObject {
@public
  NSString* _dadaOfBirth;
  NSString* _firstName;
  NSString* _lastName;
@private
  NSString* _someInternalData;
}

这时,就会出现下图所示的问题:

这里我们就需要注意到,如果代码使用了编译期计算出来的偏移量,那么在修改类定义之后必须重新编译,否则就会出错

所以在我们使用的时候,尽量不要直接访问实例变量,而是通过存取方法来做,虽然属性最终还是得通过实例变量来实现,但是它却提供了一种简洁的抽象机制。

objectivec 复制代码
@interface ECOPerson:NSObject
@property NSString* firstName;
@property NSString* lastName;
@end

对于该类的使用者来说,上述代码写出来的类和下面这种写法是等效的:

objectivec 复制代码
@interface ECOPerson:NSObject
- (NSString*) firstName;
- (void)setFirstName:(NSString*)firstName;
- (NSString*) lastName;
- (void)setLastname:(NSString*)lastName;
@end

@dynamic关键词:

在上述语法中,编译器会自动合成存取方法,如果我们想阻止编译器自动合成存取方法,就需要使用@dynamic关键词。这个关键词会告诉编译器不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。

objectivec 复制代码
@interface RCOPerson:NSManagerObejct
@property NSString* firstName;
@property NSString* lastName;
@end

@implementation ECOperson
@dynamic firstName,lastName;
@end

要点:

  • 可以用@property语法来定义对象中所封装的数据。
  • 通过"特质"来制定存储数据所需的正确语义。
  • 在设置属性所对应的实例变量是,一定要遵从该属性所声明的语义。
  • 开发iOS程序时应该使用nonatomic属性,因为atomic属性会严重影响性能。

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

在对象外部访问实例变量时,我们总是应该通过属性来做,而在对象内部访问实例变量时,有人认为应该"通过属性访问",有人又说应该"直接访问",也有人认为两种方法应该搭配使用。但是本书建议大家在读取实例变量时采用直接访问的形式,而在设置实例变量的时候通过属性来做。

我们以下面这个类为例,举例说明:

objectivec 复制代码
@interface ECOPerson : NSObject
@property (nonatomic, copy)NSString* firstName;
@property (nonatomic, copy)NSString* lastName;

-(NSString*) fullName;
-(void) setFullName:(NSString*) fullName;
@end

fullNamesetFullName这两个"便捷方法"可以这样来实现:

objectivec 复制代码
- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@",self.firstName, self.lastName];
}

- (void)setFullName:(NSString *)fullName {
    NSArray* components = [fullName componentsSeparatedByString:@" "];
    self.firstName = [components objectAtIndex:0];
    self.lastName = [components objectAtIndex:1];
}

下面我们重写这两个方法,不经由存取方法,而是直接访问实例变量:

objectivec 复制代码
- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@",_firstName, _lastName];
}

- (void)setFullName:(NSString *)fullName {
    NSArray* components = [fullName componentsSeparatedByString:@" "];
    _firstName = [components objectAtIndex:0];
    _lastName = [components objectAtIndex:1];
}

直接访问和属性访问的区别:

  • 由于不经过Objective-C的〝方法派发" (methoddispatch,参见第11条)步骤,所以直接访问实例变量的速度当然比较快。 在这种情况 下,编译器所生成的代码会直接访 问保存对象实例变量的那块内存。
  • 直接访问实例变量时,不会调用其〝设置方法",这就绕过了为相关属性所定义的 "内 存 管 理 语 义 " 。 比 方 说 ,如 果 在 ARC 下 直 接 访 问 一个 声 明 为copy 的 属 性 , 那 么 并 不 会拷贝该属性,只会保留新值并释放旧值。
  • 如果直接访问实例变量,那么不会触发"键值观测"(Key-Value Observing, KVO)。通
    知。这样做是否会产生问题,还取决于具体的对象行为。
  • 通过属性来访问有助于排查与之相关的错误,因为可以给 "获取方法〞和/ 或 "设置 方法〞中新增"断点"(breakpoint),监控该属性的调用者及其访问时机。

惰性初始化

在这种情况下,我们必须通过"获取方法"来访问属性,否则实例变量永远也不会初始化。

举例说明,ECOPerson类也许会用一个属性来表示人脑中的信息,这个属性所指代的对象相当复杂。由于该属性不常使用,并且创建的成本比较高,我们就可能会在"获取方法"中执行惰性初始化:

objectivec 复制代码
-(EOCBrain*)brain {
   if(!_brain) {
      _brain = [Brain new]; 
   }
   return _brain;
}

若没有调用"获取方法"就直接访问实例变量,则会看到尚未设置好的brain,所以说,如果使用了"惰性初始化"技术,那么必须通过存取方法来访问brain属性。

要点:

  • 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应通过属性来写。
  • 在初始化方法及dealloc方法中,总是应该直接通过实例变量来读写数据。
  • 有时会使用惰性初始化技术配置某份数据,这种情况下,需要通过属性来读取数据。

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

根据"等同性"(equality)来比较对象是一个非常有用的功能。但是按照==操作符比较出来的结果未必是我们想要的结果,这是由于该操作比较的是两个指针本身,而不是其所指的对象。我们应该使用NSObject协议中声明的isEqual来判断两个对象的等同性。

objectivec 复制代码
    NSString* foo = @"Badger 123";
    NSString* bar = [NSString stringWithFormat:@"Badger %i", 123];
    BOOL equalA = (foo == bar);//<equalA = NO
    BOOL equalB = [foo isEqual:bar];//<equalB = YES
    BOOL equalC = [foo isEqualToString:bar];//<equalC = YES

在NSObject协议中有两个用于判断等同性的关键方法:

objectivec 复制代码
- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;

NSObject类对这两个方法默认实现是:当且仅当其"指针值"完全相同时,这两个对象才相等。如果使用isEqual方法判定两个对象相等,那么其hash方法也必须返回同一个值,但是如果两个对象的hash方法返回同一个值,那么isEqual方法未必会认为两者相等。

容器中可变类的等同性

在容器中放入可变类对象时,把某个对象放入collection后,就不应再改变其哈希码了。collection会把各个对象按照其哈希码装到不同的箱子中去,如果在装入箱子后改变其哈希码,那么对于所处的这个箱子来说就是一个错误。

要点:

  • 若想检测对象的等同性,请提供isEqual:hash方法。
  • 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同。
  • 不要盲目地逐个检测每条属性,而是应该依照具体需求来指定检测方案。
  • 编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的算法。

第9条:以"类族模式"隐藏实现细节

"类族"是一种很有用的模式,可以隐藏"抽象基类"背后的实现细节。OC中的系统框架普遍使用这种模式,这里我们以UIButton为例说明。

当我们想要创建一个按钮的时候,我们需要使用下面这个类方法:

objectivec 复制代码
+ (UIButton*)buttonWithType:(UIButtonType)type;

该方法返回的对象,其类型取决于传入的按钮类型。然而,不管返回什么类型的对象,他们都继承自同一个基类:UIbutton,这么做的意义是让使用者不用关心创建的按钮继承于UIButton的那一个子类,只需明白如果创建按钮等问题即可。

创建类族

下面我将举例展示如何创建类族:

objectivec 复制代码
//定义员工类型
typedef NS_ENUM(NSUInteger, EOCEmployeeType) {
    EOCEmployeeTypeDeveloper,
    EOCEmployeeTypeDesiner,
    EOCEmployeeTypeFinance
};

@interface EOCEmployee : NSObject
//定义属性
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger salary;
//定义方法
+ (EOCEmployee *)employeeWithType:(EOCEmployeeType)type;

- (void)doADaysWork;

@end
objectivec 复制代码
@implementation EOCEmployee

+ (EOCEmployee *)employeeWithType:(EOCEmployeeType)type {
    switch (type) {
        case EOCEmployeeTypeDeveloper:
            return [EOCEmployeeTypeDeveloper new];
        case EOCEmployeeTypeDesiner:
            return [EOCEmployeeTypeDesiner new];
        case EOCEmployeeTypeFinance:
            return [EOCEmployeeTypeFinance new];
    }
}

- (void)doADaysWork {
    // Subclasses implement this.
}

@end
objectivec 复制代码
@interface EOCEmployeeTypeDeveloper : EOCEmployee
@end

@implementation  EOCEmployeeTypeDeveloper

- (void)doADaysWork {
	[self writeCode];
}

@end

这个例子中,基类实现了一个类方法,该方法根据创建的雇员类别分配好对应的雇员类实例,这种"工厂模式"是创建类族的办法之一。如果你想创建的类中没有init初始化的方法,那么这就是在暗示你该类的实例也许不应该由用户直接创建。总而言之,以后创建对象一定不要被其的表象迷惑住了,你可能觉得自己创建了某个类的实例,然而实际上创建的却是其子类的实例。

Cocoa里的类族

系统框架中有许多类族,就用我们经常使用的NSArrayNSMutableArray来说,这样来看,它是两个抽象基类,但是他们两个拥有相同的方法 ,这个方法可能就是他们共同类族中的方法,而可变数组的特殊方法就是只适用于可变数组的方法其他的共同方法可能就是类族中的方法

在使用NSArrayalloc方法来获取实例时,该方法首先会分配一个属于某个类的实例,此实例充当一个"占位数组",也就是说,你把这个位置是先分配给其类族的,后来其类族才将这个位置分配给你创建的具体数据类型的。所以像这些类的背后其实是一个类族,在对一些if条件进行判断的时候一定要注意,例如:

objectivec 复制代码
id maybeAnArray = /* ... */;
if ([maybeAnArray class] == [NSArray class]) {
	//Will never be hit
}

使用这种方法来判断两个类是否属于同一类族很明显是错的,因为NSArray是一个类族,NSArray初始化返回的对象并非NSArray类,而是隐藏在类族公共接口中的某个内部类型。

OC中也提供了isKindOfClass方法来判断实例所属的类是否位于类族之中。

手动增加实体子类的规则

  • 子类应该继承自类族中的抽象基类。
  • 子类应该定义自己的数据存储方式。
  • 子类应当覆写超类文档中指明需要覆写的方法。

要点

  • 类族模式可以把实现细节隐藏在一套简单的公共接口后面。
  • 系统框架中经常使用类族。
  • 从类族的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读。

第11条:理解objc_msgSend的作用

在对象上调用方法是OC中经常使用的功能。用OC中的术语来说,叫做"传递消息"。消息有"名称"或"选择子",可以接受参数,而且可能有返回值。

OC是C的超集,这里我们先理解C语言的函数调用方式,C语言采用"静态绑定",在编译期就能决定运行时所应调用的函数,如下图所示:

这样写的话,如果不考虑"内联",编译器在编译代码是就已经知道程序中有printHelloprintGoodbye两个函数了,就会直接生成调用这些函数的指令,而函数地址实际上是硬编码在指令中。

内联是一种编译器优化技术。当一个函数被声明为内联函数时,编译器在编译过程中会尝试将函数的代码直接嵌入到调用该函数的地方,而不是像普通函数调用那样通过函数指针进行跳转和返回。(这里笔者仅上网查询参考,了解不够深入)

如果按照上图中的写法,就得使用"动态绑定",因为所要调用的函数直到运行期才能确定。在这张图片中只有一个函数调用指令,待调用的函数地址无法编码在指令之中,而需要在运行期读取出来。

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

给对象发送消息

objectivec 复制代码
id returnValue = [someObject messageName:parameter];

在上述例子中,someObject叫做"接受者",messageName叫做"选择子",选择子和参数喝起来称为"消息"。编译器看到这个消息以后,将其转化为一条标准的C语言函数调用,所调用的函数乃事消息传递机制中的核心函数,叫做objc_magSend,它的原型如下:

objectivec 复制代码
void objc_magSend(id self, SEL cmd, ...)

这是个" 参数个数可变的两数" 。能接受两个或两个以上的参数。第一 个参数代表接收者,第二个参数代表选择子(SEL 是选择子的类型),后续参数就是消息中的 那些参数,其顺序不变。选择子指的就是方法的名字。"选择子〞与"方法" 这两个词经常交替使用。编译器会吧刚刚那个例子中的消息转换成如下函数:

objectivec 复制代码
id returnValue = objc_magSend(someObject, @selector(messageName:), parameter);

objc _msgSend函数会依据接收者与选择子的类型来调用适当的方法。为了完成此操作该方法需要在接收者所属的类中搜寻其"方法列表"(list ofmethods),如果能找到与选择-名称相符的方法,就跳至其实现代码。若是找不到,那就沿着继承体系继续向上查找,等到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行"消息转发"(messagforwarding)操作。消息转发将在第 12 条中详解。

边界情况:

要点:

  • 消息由接收者、选择子及参数构成。给某对象"发送消息"也就相当于在该对象上"调用方法"。
  • 发给某对象的全部消息都要由"动态消息派发系统"来处理,该系统会查出对应的方法,并执行其代码。

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

这一条我们主要理解当对象在收到无法解读的消息之后会发生什么情况

动态方法解析

对象在收到无法解读的消息后,首先将调用其所属类的下列方法:

objectivec 复制代码
+ (BOOL)resolveInstanceMethod:(SEL)selector

这个方法的参数就是那个未知的选择子,其返回值为Boolean类型,表示这个类是否能新增一个实例方法用以处理此选择子。

如果尚未实现的方法是一个类方法,则会调用另一个方法:

objectivec 复制代码
- (id)forwardingTargetForSelector:(SEL)aSelector;

备援接受者

当前接受者还有第二次机会可以处理未知的选择子,在这一步中,运行期系统会问它:能不能把这条消息转给其他接受者来处理。与该步骤对应的处理方法如下:

objectivec 复制代码
- (id)forwardingTargetForSelector:(SEL)selector

若当前接受者能找到备援对象,则将其返回,若找不到,就返回nil。

完整的消息转发

如果已经来到这一步,就只能使用完整的消息转发机制。首先创建NSInvocation对象,把尚未处理的消息有关的全部细节都封于其中。包含选择子,目标以及参数。在出发NSInvocation对象时,"消息派发系统"将亲自出马,把消息指派给目标对象。此步骤会调用下列方法来转发消息:

objectivec 复制代码
- (void)forwardInvocation:(NSInvocation*)invocation

全流程

要点

  • 若对象无法响应某个选择子,则进入消息转发流程
  • 通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中
  • 对象可以把其无法解读的某些选择子转交给其他对象来处理
  • 经过上面两步后,如果还是没办法处理选择子,那就启用完整的消息转发流程。
相关推荐
xinyu39125 分钟前
iOS Framework代码中加载图片资源
macos·ios·cocoa
键盘敲没电32 分钟前
【iOS】小蓝书学习(四)
学习·ios·cocoa
我要昵称干什么1 小时前
STM32学习——RTC实时时钟(BKP与RTC外设)
c语言·stm32·单片机·嵌入式硬件·学习·实时音视频
可可鸭~1 小时前
前端面试基础知识整理(一)
javascript·vue.js·学习·面试·elementui
爱上妖精的尾巴1 小时前
3-2 WPS JS宏 工作簿的打开与保存(模板批量另存为工作)学习笔记
javascript·笔记·学习·js·wps
姜来可期2 小时前
Go Test 单元测试简明教程
开发语言·后端·学习·golang·单元测试
IT、木易3 小时前
大白话React第七章深入学习 React 高级特性与优化阶段
javascript·学习·react.js
昨今4 小时前
学习Flask:[特殊字符] Day 3:数据库集成
数据库·学习·flask
天若有情6734 小时前
【学习方法】学习软件专业课程的思考方式
学习·学习方法