文章目录
- 前言
- 六、理解"属性"这一概念
- 七、在对象内部尽量直接访问实例变量
- 八、理解"对象等同性"这一概念
- 九、以"类族模式"隐藏实现细节
- 十、在既有类中使用关联对象存放自定义数据
- 十一、理解objc_msgSend的作用
- 十二、理解消息转发机制
- 十四、理解"类对象"的用意
前言
这是一篇为了快手✌️xmy学长写的博客
六、理解"属性"这一概念
"属性"(property)
是OC的一项特性,用于封装对象中的数据。OC对象通常会把其所需要的数据保存为各种实例变量。其中"获取方法"用于读取变量值,而"设置方法"用于写入变量值。切此特性引入了一种新的"点语法",使开发者可以更为容易地依照类对象来访问存放于其中的数据。
我们在类接口的public区段中声明一些实例变量:
cpp
@interface EOCPerson: NSObject {
@public
NSString *_firstName;
NSString *_lastName;
@private
NSString *_someInternalData;
}
@end
然后我们添加一个实例变量:
cpp
@interface EOCPerson: NSObject {
@public
NSString *_dateOfBirth;
NSString *_firstName;
NSString *_lastName;
@private
NSString *_someInternalData;
}
@end
我们新添加的实例变量就会代替原第一个位置实例变量的偏移量。
这样的话,如果代码使用了编译器计算出来的偏移量,那么在修改类定义之后必须重新编译,否则就会出错。例如:某个代码库中的代码使用了一份旧的类定义。如果和其相链接的代码使用了新的类定义,那么运行时就会出现不兼容现象。对此类问题,OC的解决方法是,把实例变量当做一种存储偏移量所用的"特殊变量",交由"类对象"保管。偏移量会在运行期查找,如果类的定义变了,那么存储的偏移量也就变了,这样的话,无论何时访问实例变量,总能使用正确的偏移量。这就是稳固的"应用程序二进制接口"
这里我们的解决方法就是尽力使用存取方法来访问实例变量,这时@property语法就派上用场了。这种规范的命名方法OC会自动创建出存取方法。
简单来说,以下两部分代码的效果是相同的:
cpp
@interface EOCPerson: NSObject
@property NSString *firstName;
@property NSString *lastName;
@end
cpp
@interface EOCPerson: NSObject
- (NSString *)firstName;
- (void)setFirstName: (NSString *)firstName;
- (NSString *)lastName;
- (void)setLastName: (NSString *)lastName;
- @end
@dynamic关键字
如果我们不想令编译器自动合成存取方法,那我们应该怎么做呢?
那就是使用@dynamic关键字了,它会告诉编译器:不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。然后我们就需要在运行期动态创建存取方法了。
bash
@interface EOCPerson : NSManageObject
@property NSString *firstName;
@end
@implementation
@dynamic firstName;
@end
要点总结
- 可以通过**@property**语法来定义对象中所封装的数据。
- 通过"特质"来指定存储数据所需的正确语义。尤其指代属性关键字
- 在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。
- 开发iOS程序时应该使用nonatomic属性,因为atomic属性会严重影响性能,nonatomic的加锁维护了线程安全
七、在对象内部尽量直接访问实例变量
在书中作者说建议大家在读取实例变量的时候采用直接访问的形式,而在设置实例变量的时候采用属性操作
直接访问和属性访问的区别:
- 由于不经过Objective-C的〝方法派发" (methoddispatch,参见第11条)步骤,所以
直接访问实例变量的速度当然比较快。 在这种情况 下,编译器所生成的代码会直接访 问保存对象实例变量的那块内存。 - 直接访问实例变量时,不会调用其〝设置方法",这就绕过了为相关属性所定义的 "内 存 管 理 语 义 " 。 比 方 说 , 如 果 在 A
R C 下 直 接 访 问 一个 声 明 为 c o p y 的 属 性 , 那 么 并 不 会拷贝该属性,只会保留新值并释放旧值。 - 如果直接访问实例变量,那么不会触发"键值观测"(Key-ValueObserving, Kvo)。通
知。这样做是否会产生问题,还取决于具体的对象行为。 - 通过属性来访问有助于排查与之相关的错误,因为可以给 "获取方法〞和/ 或 "设置 方法〞中新增
"断点"(breakpoint),监控该属性的调用者及其访问时机。
惰性初始化:
也叫做"延迟初始化"。在惰性初始化的情况下,必须通过"获取方法"来访问属性,否则,实例变量就永远不会初始化。
一般用于:一个属性不常用,而且创建该属性的成本较高的情况。
bash
- (EOCBrain *) brain {
if (!_brain) {
_brain = [Brain new];
}
return _brain;
}
在这种情况下我们就必须使用存取方法来访问我们的属性
要点
- 在对象内部读取数据时,应该直接通过实例变量来读,而写人数据时,则应通过属性 来写。
- 在初始化方法及dealloe 方法中,总是应该直接通过实例变量来读写数据。
- 有时会使用惰性初始化技术配置某份数据,这种情况下,需要通过属性来读取数据。
八、理解"对象等同性"这一概念
"=="和"isEqual:"区别
我们先来回顾一下之前学过的"=="和"isEqual:"区别:
====是看地址(指针)==来进行判断,地址不一致即返回false
isEqual:是专门用于判断的方法,不一定是看地址,也可以是其他的标准。
在NSObject类中,==与isEqual:没有明显区别,但在NSString中,已经完成了重写,只要字符串字符序列相同,isEqual:方法就返回true。
以如下代码为例:
bash
NSString *foo = @"Badger 123";
NSString *bar = [NSString stringWithFormat:@"Badger %i", 123];
BOOL equalA = (foo == bar);//NO
BOOL equalB = [foo isEqual:bar];//YES
BoOL equalC = [foo isEqualToString:bar];//YES
可以看到== 与等同性判断方法之间的差别。NSString
类实现了一个自己独有的等 同性判断方法,名叫 "isEqualToString
: " 。传递给该方法的对象必领是NSString
,否则结果 末定义(u deined)。调用该方法比调用" isEqual:〞
方法快,因为" isEqual:〞
方法还要执行额外的步骤,因为isEqual不知道受测对象的类型。
NSObject 协议中有两个用于判断等同性的关键方法:
bash
- (BOOL) isEqual: (id) object;
- (NSUInteger) hash;
NSObject 类 对 这 两 个 方 法 的 默 认 实 现 是 : 当 且 仅 当 其 " 指 针 值 "
(pointervalue)日 完 全 相 等时,这两个对象才相等。若想在自定义的对象中正确後写这些方法,就必领先理解其约定
(contract)。如果" isEqual:〞方法判定两个对象相等,那么其hash 方法也必须返回同 一个
值。但是,如果两个对象的hash 方法返回同一个值,那么" isEqual:〞方法末必会认为两者 相 等 。
容器中可变类的等同性
当把某个对象放入collection后,就不应该再改变其哈希码了。因为collection会把各个对象按照其哈希码分装到不同的"箱子数组"中。如果某对象在放入"箱子"之后,哈希码又发生变化,那么其所处的这个箱子对他来说就是"错误"的。
要点
- 若想检测对象的等同性,请提供"isEqual:"与hash方法。
- 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同。
- 不要盲目地逐个检测每条属性,而是应该依照具体需求来指定检测方案。
- 编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的算法。
九、以"类族模式"隐藏实现细节
类方法
"类族"是一种很有用的模式,可以隐藏"抽象基类"背后的实现细节。Objective-C的系统框架中普遍使用此模式。比奶,ios 的用户 界面框架(user interface framework)UIKit中就有一个名为UIButton的类。想创建按钮,需 要 调 用 下面 这 个 " 类方法" ( class method )
bash
+ (UIButton*)buttonWithType:(UIButtonType)type;
该方法所返回的对象,其类型取决于传人的按钮类型(button type)。然而,不管返回什 么类型的对象,它们都继承自同 一个基类:UIButton。这么做的意义在于:UIButton 类的使 用者无领关心创建出来的按钮具体属于哪个 子类,也不用考虑按钮的绘制方式等实现细节。 使用者只需明白如何创建按钮,如何设置像"标题" (title)这样的属性,如何增加触摸动作 的目标对象等问题就好。
创建类族:
首先要定义抽象基类 ,也就是一个新的类,其中可以包括你的类型选取,使用枚举器和switch语句来完成,并且还的定义你的类的相关方法,再创建一个新的类,继承你之前的类,并且完成之前的定义方法,使用覆盖的原理,完成这些方法。这种"工厂模式"是创建类族的办法之一。
如果你想创建的类中没有init初始化的方法,那么这就是在暗示你该类的实例也许不应该由用户直接创建。总而言之,以后创建对象一定不要被其的表象迷惑住了,你可能觉得自己创建了某个类的实例,然而实际上创建的却是其子类的实例。
bash
//定义员工类型
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
bash
@implementation EOCEmployee
+ (EOCEmployee *)employeeWithType:(EOCEmployeeType)type {
switch (type) {
case EOCEmployeeTypeDeveloper:
return [EOCEmployeeTypeDeveloper new];
break;
case EOCEmployeeTypeDesiner:
return [EOCEmployeeTypeDesiner new];
break;
case EOCEmployeeTypeFinance:
return [EOCEmployeeTypeFinance new];
break;
}
}
- (void)doADaysWork {
// Subclasses implement this.
}
@end
bash
@interface EOCEmployeeTypeDeveloper : EOCEmployee
@end
@implementation EOCEmployeeTypeDeveloper
- (void)doADaysWork {
[self writeCode];
}
@end
Cocoa里的类族
系统框架中有许多类族,就用我们经常使用的NSArray和NSMutableArray来说,这样来看,它是两个抽象基类,但是他们两个拥有相同的方法,这个方法可能就是他们共同类族中的方法,而可变数组的特殊方法就是只适用于可变数组的方法,其他的共同方法可能就是类族中的方法。
在使用NSArray的alloc方法来获取实例时,该方法首先会分配一个属于某个类的实例,此实例充当一个"占位数组",也就是说,你把这个位置是先分配给其类族的,后来其类族才将这个位置分配给你创建的具体数据类型的。
所以像这些类的背后其实是一个类族,在对一些if条件进行判断的时候一定要注意,例如:
bash
id maybeAnArray = /* ... */;
if ([maybeAnArray class] == [NSArray class]) {
//Will never be hit
}
使用这种方法来判断两个类是否属于同一类族很明显是错的
因为NSArray是一个类族,NSArray初始化返回的对象并非NSArray类,而是隐藏在类族公共接口中的某个内部类型
就像这段代码:
不过OC仍旧提供了判断实例所属的类 是否位于类族之中 isKindOfClass
bash
id maybeAnArray = /*...*/
if(maybeAnArray isKindOfClass:[NSArray class]) {
//will be hit
}
使用这种方法即可判断是否属于同一类族
手动增加实体子类的规则
- 子类应该继承自类族中的抽象基类。
- 子类应该定义自己的数据存储方式。
- 子类应当覆写超类文档中指明需要覆写的方法。
要点总结
- 类族模式可以把实现细节隐藏在一套简单的公共接口后面。
- 系统框架中经常使用类族。(可变和不可变是最直接的例子)
- 从类族的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读。
十、在既有类中使用关联对象存放自定义数据
关联对象的出现
在iOS开发里,分类是不能添加成员变量的,只允许给分类添加属性,所以出现了关联对象
具体使用后面应该会学到,这里直接进行要点总结。
要点总结
- 可以通过"关联对象"机制来把两个对象连起来。
- 定义关联对象时可指定内存管理语义,用以模仿定义属性时所采用的"拥有关系"与"非拥有关系"。
- 只有在其他做法不可行时才应选用关联对象,因为这种做法通常会引入难于查找的bug
十一、理解objc_msgSend的作用
对于C语言来说,静态绑定意味着在编译期就能决定运行时所需要调用的函数
以以下代码为例子:
编译器在编译代码的时候就已经知道程序中有 printflhello
与printGodbye
这两个函数了,于是会直接生成调用这些函数的指令。而函数地址实际上是硬编码在指令之中的
若是对于这段代码:
这时就得使用"动态绑定 " (dynamicbinding)了,因为所要调用的函数直到运行期才能 确定。编译器在这种情况下生成的指令与刚才那个例子不同,在第一个例子中,其与else 语 句里都有函数调用指令。而在第二个例子中,只有 一个函数调用指令,不过待调用的函数地址无法硬编码在指令之中,而是要在运行期读取出来。
在OC中,如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有的方法都是普通的C语言函数,然而对象收到消息之后究竟该调用那个方法则完全取决于运行期决定,甚至可以在程序运行时改变,这些特性使得OC成为一门真正的动态语言。
消息转发过程:
来看下面一个对象发送消息:
bash
id returnValue = [someObject messageName:parameter];
在这个例子中
someObject叫做"接收者"(receiver),messageName叫做 "选择子"(selector)。 选择子与参数合起来称为 "消息" (message)。编译器看到此消息后,将其转换为一条标准的 C语言两数调用,所调用的函数乃是消息传递机制中的核心两数,叫做objc_msgSend,其 " 原型" ( prototype )如下:
bash
void objc_msgsend(id self, SEL cmd, ...)
这是个" 参数个数可变的两数" ( variadic function)。能接受两个或两个以上的参数。第一 个参数代表接收者,第二个参数代表选择子(SEL 是选择子的类型),后续参数就是消息中的 那些参数,其顺序不变。选择子指的就是方法的名字。"选择子〞与"方法" 这两个词经常 交替使用。编译器会把刚才那个例 子中的消息转换为如下函数:
bash
id returnValue = objc msgSend(someobject, @selector (messageName:), parameter);
边界情况:
要点
- 消息由接收者、选择子及参数构成。给某对象"发送消息"也就相当于在该对象上"调用方法"。
- 发给某对象的全部消息都要由"动态消息派发系统"来处理,该系统会查出对应的方法,并执行其代码。
十二、理解消息转发机制
上一条强调了消息是如何传递下去的,这一条深入理解一下在某些出现问题的时刻系统是如何解决问题的。
消息转发
- 某个对象收到了无法解读的消息之后会发生什么情况?这就是OC的消息转发机制。
- 对于在编译期向类发出的无法解读的消息之后不会报错,因为可以在运行期继续向类里面添加方法,所以在编译时期出现了对象无法解读的消息就会启动消息转转发机制。
- 消息转发分为两大阶段,第一阶段 先征询接收者,所属的类,看其是否能动态添加方法,处理当前这个"未知的选择子",这叫做"动态方法解析 "。第二阶段涉及"完整的消息转发机制"。
动态方法解析
对象在收到无法解读的消息后,首先将调用其所属类的下列类方法:
bash
+ (BOOL)resolveInstanceMethod:(SEL)selector
该方法的参数就是那个未知的选择子,其返回值为Boolean类型,表示这个类是否能新增一个实例方法用以处理此选择子。在继续往下执行转发机制之前,本类有机会新增一个处理此选择子的方法,假如尚未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另一个方法,该方法与"resolveInstanceMethod :"类似,叫做 "resolveClassMethod"。
动态方法解析的前提
对于上述的消息转发第一步,前提是我们相关的实现代码已经写好了,只需要等着运行时的时候插入类里面即可
以以下代码举例子:
bash
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface MyClass : NSObject
@end
@implementation MyClass
// 动态方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(dynamicMethod)) {
class_addMethod([self class], sel, (IMP)dynamicMethodImplementation, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void dynamicMethodImplementation(id self, SEL _cmd) {
NSLog(@"Dynamic method has been resolved and called.");
}
@end
int main() {
@autoreleasepool {
MyClass *obj = [MyClass new];
// 调用未实现的方法,触发动态方法解析
[obj dynamicMethod];
}
return 0;
}
备援接受者
在第一步还是没有找到写好的方法之后,当前接受者还有第二次机会处理未知的选择子,在这一步里运行期的系统会询问接受者能不能找到其他接受者处理该消息,这里也有一个方法:
bash
- (id)forwardingTargetForSelector:(SEL)selector;
方法参数代表未知的选择子,若当前接收者能找到各授对象,则将其返回,若找不到, 就 返 回 nil。 通过此方案,我们可以用"组合"(composition) 来模拟出"多重继承"( multipleinheritance )的某些特性。在一个对象内部,可能还有一系列其他对象,该对象可经由此方法 将能够处理某选择 子的相关内部对象返回,这样的话,在外界看来,好像是该对象亲自处理了这些消息似的。
请注意,我们无法操作经由这 一步所转发的消息。若是想在发送给备援接收者之前先修改消息内容,那就得通过完整的消息转发机制来做了。
例子:
bash
#import <Foundation/Foundation.h>
// 定义备用接收者对象
@interface AnotherObject : NSObject
- (void)anotherMethod;
@end
@implementation AnotherObject
- (void)anotherMethod {
NSLog(@"Method implemented in AnotherObject.");
}
@end
// 主要对象
@interface MyClass : NSObject
@end
@implementation MyClass
// 备用接收者
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(anotherMethod)) {
return [AnotherObject new];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
int main() {
@autoreleasepool {
MyClass *obj = [MyClass new];
// 调用未实现的方法,触发备用接收者
[obj performSelector:@selector(anotherMethod)];
}
return 0;
}
在这个方法中,当选择器为 @selector(anotherMethod) 时,返回了 AnotherObject
的实例。因此,当你在 MyClass
对象上调用 anotherMethod
时,实际上是调用了 AnotherObject
的实例上的方法。
完整的消息转发
如果转发算法已经来到这一步的话,那么唯一能做的就是启用完整的消息转发机制 了。首先创建NSInvocation 对象,把与尚未处理的那条消息有关的全部细节都封于其中 。 此对象包含选择子、目标(target )及参数。在触发NSInvocation 对象时," 消息派发系统"( message-dispatchsystem )将亲自出马,把消息指派给目标对象。
此步骤会调用 下列方法来转发消息:
bash
- (void)forwardInvocation:(NSInvocation*)invocation
- 这个方法的实现方式有2种,一种是只需要改变调用目标,和备援接受者方法实现的等效,第二种则是在触发消息前,先以某种方式改变消息内容,比如追加另一个参数,或者改变选择子等
- 实现此方法时,若发现某调用操作不应由本类来处理,则需调用超类的同名方法。这样的话,继承体系中的每个类都有机会处理此调用请求,直至NSObject。如果最后调用NSObject类的方法,那么该方法还会继而调用"doesNotRecognizeSelector:"以抛出异常,此异常表明选择子最终未能得到处理。
消息转发全流程
要点
- 若对象无法响应某个选择子,则进人消息转发流程。
- 通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加人类中。
- 对象可以把其无法解读的某些选择子转交给其他对象来处理。
- 经过上述两步之后,如果还是没办法处理选择 子,那就启动完整的消息转发机制。
十四、理解"类对象"的用意
1.id类型:
一般情况下,应该指明消息接收者的具体类型,这样的话,如果向其发送了无法解读的消息,那么编译器就会产生警告信息。而类型为id的对象则不然,编译器假定它能响应所有信息。
每个OC对象实例都是指向某块内存数据的指针,所以在声明变量时,类型后面要跟一个*字符。
描述Objective-C对象所用的数据结构定义在运行期程序库的头文件里,id类型本身也在定义在这里:
bash
typedef struct objc_object {
Class isa;
} *id;
2.Class对象:
bash
typedef struct objc_class *Class;
struct objc_class {
Class isa;
Class super_class;
const char *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;
};
此结构体存放类的"元数据"。其中的super_class,它定义了本类的父类。类对象所属的类型(也就是isa指针所指向的类型)是另一个类,叫做"元类"(metaclass)。并且每个类仅有一个"类对象",而每个"类对象"仅有一个与之相关的"元类"。
这里注意isa
是元类,而super_class
是父类
而对于isMemberOfClass
与isKindOfClass
在此博客【iOS】类、元类、父类的关系已经讲述,不再赘述
要点:
-
每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成了类的继承体系。
-
如果对象类型无法在编译期确定,那么就应该使用类型信息查询方法来探知。
-
尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。