这是一篇为了快手✌️xmy学长写的博客
“属性”(property)
是OC的一项特性,用于封装对象中的数据。OC对象通常会把其所需要的数据保存为各种实例变量。其中“获取方法”用于读取变量值,而“设置方法”用于写入变量值。切此特性引入了一种新的“点语法”,使开发者可以更为容易地依照类对象来访问存放于其中的数据。
我们在类接口的public区段中声明一些实例变量:
@interface EOCPerson: NSObject {
@public
NSString *_firstName;
NSString *_lastName;
@private
NSString *_someInternalData;
}
@end
然后我们添加一个实例变量:
@interface EOCPerson: NSObject {
@public
NSString *_dateOfBirth;
NSString *_firstName;
NSString *_lastName;
@private
NSString *_someInternalData;
}
@end
我们新添加的实例变量就会代替原第一个位置实例变量的偏移量。
这样的话,如果代码使用了编译器计算出来的偏移量,那么在修改类定义之后必须重新编译,否则就会出错。例如:某个代码库中的代码使用了一份旧的类定义。如果和其相链接的代码使用了新的类定义,那么运行时就会出现不兼容现象。对此类问题,OC的解决方法是,把实例变量当做一种存储偏移量所用的“特殊变量”,交由“类对象”保管。偏移量会在运行期查找,如果类的定义变了,那么存储的偏移量也就变了,这样的话,无论何时访问实例变量,总能使用正确的偏移量。这就是稳固的“应用程序二进制接口”
这里我们的解决方法就是尽力使用存取方法来访问实例变量,这时@property语法就派上用场了。这种规范的命名方法OC会自动创建出存取方法。
简单来说,以下两部分代码的效果是相同的:
@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
@dynamic关键字
如果我们不想令编译器自动合成存取方法,那我们应该怎么做呢?
那就是使用@dynamic关键字了,它会告诉编译器:不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。然后我们就需要在运行期动态创建存取方法了。
@interface EOCPerson : NSManageObject
@property NSString *firstName;
@end
@implementation
@dynamic firstName;
@end
要点总结
在书中作者说建议大家在读取实例变量的时候采用直接访问的形式,而在设置实例变量的时候采用属性操作
直接访问和属性访问的区别:
惰性初始化:
也叫做“延迟初始化”。在惰性初始化的情况下,必须通过“获取方法”来访问属性,否则,实例变量就永远不会初始化。
一般用于:一个属性不常用,而且创建该属性的成本较高的情况。
- (EOCBrain *) brain {
if (!_brain) {
_brain = [Brain new];
}
return _brain;
}
在这种情况下我们就必须使用存取方法来访问我们的属性
要点
“==”和“isEqual:”区别
我们先来回顾一下之前学过的“==”和“isEqual:”区别:
====是看地址(指针)==来进行判断,地址不一致即返回false
isEqual:是专门用于判断的方法,不一定是看地址,也可以是其他的标准。
在NSObject类中,==与isEqual:没有明显区别,但在NSString中,已经完成了重写,只要字符串字符序列相同,isEqual:方法就返回true。
以如下代码为例:
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 协议中有两个用于判断等同性的关键方法:
- (BOOL) isEqual: (id) object;
- (NSUInteger) hash;
NSObject 类 对 这 两 个 方 法 的 默 认 实 现 是 : 当 且 仅 当 其 “ 指 针 值 ”
(pointervalue)日 完 全 相 等时,这两个对象才相等。若想在自定义的对象中正确後写这些方法,就必领先理解其约定
(contract)。如果“ isEqual:〞方法判定两个对象相等,那么其hash 方法也必须返回同 一个
值。但是,如果两个对象的hash 方法返回同一个值,那么“ isEqual:〞方法末必会认为两者 相 等 。
容器中可变类的等同性
当把某个对象放入collection后,就不应该再改变其哈希码了。因为collection会把各个对象按照其哈希码分装到不同的“箱子数组”中。如果某对象在放入“箱子”之后,哈希码又发生变化,那么其所处的这个箱子对他来说就是“错误”的。
要点
类方法
“类族”是一种很有用的模式,可以隐藏“抽象基类”背后的实现细节。Objective-C的系统框架中普遍使用此模式。比奶,ios 的用户 界面框架(user interface framework)UIKit中就有一个名为UIButton的类。想创建按钮,需 要 调 用 下面 这 个 “ 类方法” ( class method )
+ (UIButton*)buttonWithType:(UIButtonType)type;
该方法所返回的对象,其类型取决于传人的按钮类型(button type)。然而,不管返回什 么类型的对象,它们都继承自同 一个基类:UIButton。这么做的意义在于:UIButton 类的使 用者无领关心创建出来的按钮具体属于哪个 子类,也不用考虑按钮的绘制方式等实现细节。 使用者只需明白如何创建按钮,如何设置像“标题” (title)这样的属性,如何增加触摸动作 的目标对象等问题就好。
创建类族:
首先要定义抽象基类,也就是一个新的类,其中可以包括你的类型选取,使用枚举器和switch语句来完成,并且还的定义你的类的相关方法,再创建一个新的类,继承你之前的类,并且完成之前的定义方法,使用覆盖的原理,完成这些方法。这种“工厂模式”是创建类族的办法之一。
如果你想创建的类中没有init初始化的方法,那么这就是在暗示你该类的实例也许不应该由用户直接创建。总而言之,以后创建对象一定不要被其的表象迷惑住了,你可能觉得自己创建了某个类的实例,然而实际上创建的却是其子类的实例。
//定义员工类型
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
@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
@interface EOCEmployeeTypeDeveloper : EOCEmployee
@end
@implementation EOCEmployeeTypeDeveloper
- (void)doADaysWork {
[self writeCode];
}
@end
Cocoa里的类族
系统框架中有许多类族,就用我们经常使用的NSArray和NSMutableArray来说,这样来看,它是两个抽象基类,但是他们两个拥有相同的方法,这个方法可能就是他们共同类族中的方法,而可变数组的特殊方法就是只适用于可变数组的方法,其他的共同方法可能就是类族中的方法。
在使用NSArray的alloc方法来获取实例时,该方法首先会分配一个属于某个类的实例,此实例充当一个“占位数组”,也就是说,你把这个位置是先分配给其类族的,后来其类族才将这个位置分配给你创建的具体数据类型的。
所以像这些类的背后其实是一个类族,在对一些if条件进行判断的时候一定要注意,例如:
id maybeAnArray = /* ... */;
if ([maybeAnArray class] == [NSArray class]) {
//Will never be hit
}
使用这种方法来判断两个类是否属于同一类族很明显是错的
因为NSArray是一个类族,NSArray初始化返回的对象并非NSArray类,而是隐藏在类族公共接口中的某个内部类型
不过OC仍旧提供了判断实例所属的类 是否位于类族之中 isKindOfClass
id maybeAnArray = /*...*/
if(maybeAnArray isKindOfClass:[NSArray class]) {
//will be hit
}
使用这种方法即可判断是否属于同一类族
手动增加实体子类的规则
要点总结
关联对象的出现
在iOS开发里,分类是不能添加成员变量的,只允许给分类添加属性,所以出现了关联对象
具体使用后面应该会学到,这里直接进行要点总结。
要点总结
对于C语言来说,静态绑定意味着在编译期就能决定运行时所需要调用的函数
编译器在编译代码的时候就已经知道程序中有 printflhello
与printGodbye
这两个函数了,于是会直接生成调用这些函数的指令。而函数地址实际上是硬编码在指令之中的
若是对于这段代码:
这时就得使用“动态绑定” (dynamicbinding)了,因为所要调用的函数直到运行期才能 确定。编译器在这种情况下生成的指令与刚才那个例子不同,在第一个例子中,其与else 语 句里都有函数调用指令。而在第二个例子中,只有 一个函数调用指令,不过待调用的函数地址无法硬编码在指令之中,而是要在运行期读取出来。
在OC中,如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有的方法都是普通的C语言函数,然而对象收到消息之后究竟该调用那个方法则完全取决于运行期决定,甚至可以在程序运行时改变,这些特性使得OC成为一门真正的动态语言。
消息转发过程:
来看下面一个对象发送消息:
id returnValue = [someObject messageName:parameter];
在这个例子中
someObject叫做“接收者”(receiver),messageName叫做 “选择子”(selector)。 选择子与参数合起来称为 “消息” (message)。编译器看到此消息后,将其转换为一条标准的 C语言两数调用,所调用的函数乃是消息传递机制中的核心两数,叫做objc_msgSend,其 “ 原型” ( prototype )如下:
void objc_msgsend(id self, SEL cmd, ...)
这是个“ 参数个数可变的两数” ( variadic function)。能接受两个或两个以上的参数。第一 个参数代表接收者,第二个参数代表选择子(SEL 是选择子的类型),后续参数就是消息中的 那些参数,其顺序不变。选择子指的就是方法的名字。“选择子〞与“方法” 这两个词经常 交替使用。编译器会把刚才那个例 子中的消息转换为如下函数:
id returnValue = objc msgSend(someobject, @selector (messageName:), parameter);
要点
上一条强调了消息是如何传递下去的,这一条深入理解一下在某些出现问题的时刻系统是如何解决问题的。
消息转发
对象在收到无法解读的消息后,首先将调用其所属类的下列类方法:
+ (BOOL)resolveInstanceMethod:(SEL)selector
该方法的参数就是那个未知的选择子,其返回值为Boolean类型,表示这个类是否能新增一个实例方法用以处理此选择子。在继续往下执行转发机制之前,本类有机会新增一个处理此选择子的方法,假如尚未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另一个方法,该方法与“resolveInstanceMethod:”类似,叫做 “resolveClassMethod”。
动态方法解析的前提
对于上述的消息转发第一步,前提是我们相关的实现代码已经写好了,只需要等着运行时的时候插入类里面即可
以以下代码举例子:
#import
#import
@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;
}
在第一步还是没有找到写好的方法之后,当前接受者还有第二次机会处理未知的选择子,在这一步里运行期的系统会询问接受者能不能找到其他接受者处理该消息,这里也有一个方法:
- (id)forwardingTargetForSelector:(SEL)selector;
方法参数代表未知的选择子,若当前接收者能找到各授对象,则将其返回,若找不到, 就 返 回 nil。 通过此方案,我们可以用“组合”(composition) 来模拟出“多重继承”( multipleinheritance )的某些特性。在一个对象内部,可能还有一系列其他对象,该对象可经由此方法 将能够处理某选择 子的相关内部对象返回,这样的话,在外界看来,好像是该对象亲自处理了这些消息似的。
请注意,我们无法操作经由这 一步所转发的消息。若是想在发送给备援接收者之前先修改消息内容,那就得通过完整的消息转发机制来做了。
例子:
#import
// 定义备用接收者对象
@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 )将亲自出马,把消息指派给目标对象。
此步骤会调用 下列方法来转发消息:
- (void)forwardInvocation:(NSInvocation*)invocation
要点
1.id类型:
一般情况下,应该指明消息接收者的具体类型,这样的话,如果向其发送了无法解读的消息,那么编译器就会产生警告信息。而类型为id的对象则不然,编译器假定它能响应所有信息。
每个OC对象实例都是指向某块内存数据的指针,所以在声明变量时,类型后面要跟一个*字符。
描述Objective-C对象所用的数据结构定义在运行期程序库的头文件里,id类型本身也在定义在这里:
typedef struct objc_object {
Class isa;
} *id;
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)。并且每个类仅有一个“类对象”,而每个“类对象”仅有一个与之相关的“元类”。
而对于isMemberOfClass
与isKindOfClass
在此博客【iOS】类、元类、父类的关系已经讲述,不再赘述
要点:
每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成了类的继承体系。
如果对象类型无法在编译期确定,那么就应该使用类型信息查询方法来探知。
尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。