effective-Objective-C 第二章阅读条记
对象,消息,运行期媒介
在对象之间传输数据并执行任务的过程叫做”消息传递“,这部分内容主要有关于运行期环境中各个部分的协同工作的内容
理解“属性”这一概念
属性是OC的一项特性,用于封装对象中的数据。一样平常我们采用getter来访问属性中的数据,通过setter来写入变量值,这里OC同样引入了一个点语法来进行一个访问存放在属性中的数据。
这里我们先看这段代码:
https://i-blog.csdnimg.cn/img_convert/dcbd922530cdd65fb4c834dc6c06a9b5.png
这种在C++中很常见,但是在OC中我们一样平常采用属性,至于原因是由于这种方式采用硬编码,我们访问实例变量的时间,编译器会把他替换成距离存放地点的起始地点的一个偏移量来直接访问对应的数据。
https://i-blog.csdnimg.cn/img_convert/b0f66e4adc47b84491a751749c45fb62.png
假设这时间多了一个实例变量,这种情况会在访问不到我们原来的数据。
https://i-blog.csdnimg.cn/img_convert/a5bd51db4128adddcb6a2e9d64575d5e.png
所以我们呢在修改类界说之后肯定要重新编译,否则就会出错,OC为了办理这个问题采用的是通过类变量来对他进行一个管理。这样子做我们就可以在分类大概实现文件中界说实例变量了。
同时在OC中我们只管不要直接访问实例变量,而是通过存取方法来获取对应的数据。这里我们之前也学习过他的语法:
https://i-blog.csdnimg.cn/img_convert/630496193accda93d685b857bf7a0f54.png
他和下面的方法是等效的:
https://i-blog.csdnimg.cn/img_convert/7dcd5e0c2ce5d164d7cb5b845a8ab94c.png
当我们利用了属性之后,编译器会自动编写访问这些属性所需的方法,这个过程在编译期执行,编译器会给类中添加对应类型的一个实例变量,他自动生成的实例变量是属性名前面加一个下划线,我们也可以利用@dynamic来让我们自动合成存取方法以及生成实例变量。
属性修饰符
@property (nonatomic, readwrite, copy) NSString *firstName;
这里我们有三种差别的修饰符
原子性
nonatimic
这个不会利用同步锁,也就是他由编译器所合成的方法会通过锁定机制确保其原子性。
atomic
这个是原子性的,会通过锁定机制包管他的一个原子性,他只能包管读写操纵的一个安全性,不能包管他的其他操纵(访问大概是添加)的一个安全。
好比说atomic修饰的是一个数组的话,那么我们对数组进行赋值和取值是可以包管线程安全的。但是假如我们对数组进行操纵,好比说给数组添加对象大概移除对象,是不在atomic的负责范围之内的,所以给被atomic修饰的数组添加对象大概移除对象是没办法包管线程安全的。
读/写权限
readwrite包管这个属性有getter和setter,只要采用@synthesize编译器就会自动生成这两个方法。
readonly的属性仅仅拥有获取方法,仅仅生成getter方法。
内存管理语义
修饰符解释strong强引用,当一个对象被声明为strong属性,ARC会增长该对象的引用计数。设置方法会先保存新值开释旧值weak只能修饰对象类型;2. ARC 下才能利用;3. 修饰弱引用,不增长对象引用计数,主要可以用于避免循环引用;4. weak 修饰的对象在被开释之后,会自动将指针置为 nil,不会产生悬垂指针。设置方法先不留新值,不开释旧值assign一样平常用于修饰基本类型; setter 方法的实现是直接赋值,一样平常用于基本数据类型 ; 修饰基本数据类型,如 NSInteger、BOOL、int、float 等;copy指定属性为拷贝引用,即属性会拷贝对象的值,而不是持有原始对象的引用unsafe_unretained基本和weak相似,但是他不会在该指针所引用的对象被采取后将指针赋为nil 方法名
[*]getter=< name >指定对应的一个getter的方法名字。
@property (nonatomic, getter=isOn) BOOL on;
[*]setter方法也是这样的
自界说初始化方法
@interface Person : NSObject
@property (nonatomic, copy) NSString* name;
@property (nonatomic, copy) NSString* firstName;
-(id) initWithName:(NSString*)firstName lastName:(NSString*)name;
@end
- (id)initWithName:(NSString *)firstName lastName:(NSString *)name {
if (self = ) {
_firstName = ;
_name = ;
}
return self;
}
这里注意我们初始化的函数中注意一个点就是我们对应的属性的修饰符对应起来。copy就用copy方式赋值。
小结
[*]用@property来进行一个修饰
[*]开辟的时间只管利用nonatomic、
在对象内部只管直接访问实例变量
读取实例变量的时间采用直接访问的情势,在设置实例变量的时间通过属性来做。
我们访问实例变量的时间采用点语法和直接访问有几个区别:
[*]直接访问不通过OC的方法派发,所以直接访问实例变量的速率比较快。
[*]直接访问的时间,会绕过相干属性设置的时间的内存管理语义的部分
[*]直接访问不会触发KVO
[*]通过属性访问更有利于排查与之相干的一些错误,由于可以在我们的一个存取方法中添加断点。
所以有一种折中方案,可以在写入实例变量的时间,通过设置方法来做,在读取的时间,则直接访问。
下面是几个要注意的地方:
[*]首先是我们的在初始化方法中应该直接访问实例变量,由于子类大概会复写设置方法。
- (void)setName:(NSString *)name {
if (!) {
;
}
self.name = name;
}
这里其实有一个循环调用set方法的问题,所以这里应该是直接访问实例变量。出了一种情况,假如将待初始化的实例变量放在超类中,我们无法在子类中直接访问此实例变量的话,这里才需要调用设置方法。
另有一个懒加载的内容,我们利用懒加载的话,我们就必须要通过getter方法来获取对应的属性。
小结
[*]在对象内部,通过实例变量读取,写入通过set
[*]在初始化中应该直接通过实例变量来读写数据
[*]假如利用懒加载,则肯定要通过属性来读取数据。
对象等同性
这部分内容主要是在讲isEqual的内容,首先我们要明白==是用来判定两个的地点是否相同的。
一样平常情况我们是要利用NSObject中的isEqual:来进行一个判定,这里简朴介绍一下NSString的isEqual:和isEqualToString:两个方法的一个差别,后者更快速。前者还要执行更多的一个步调。
NSObject中有两个用于判定等同性的方法:
-(NSUInteger)hash;
- (BOOL)isEqual:(id)object
默认实现是地点完全相同的时间两个对象才相等。这里我们假如自主实现的话,我们是hash方法必须返回同一个值,同时在通过自界说的isEqual方法正确后后才能来判定其是否正确。
我们本身设计一个类的isEqual的时间要公道。
https://i-blog.csdnimg.cn/img_convert/4dd757d3116f5af270a36356f7af8fd9.png
同时我们还要包管hash值也要相同,在编写hash方法的时间要注意镌汰hash碰撞,假如设置所有hash值同等,那么我们把他添加到集合中就会出现问题,由于我们其实照旧通过hash值来寻找对应的一个数组的,然后在判定isEqual是否有相同的数据来进行一个判定,假如hash值同等,会导致每一次添加都会扫瞄所有的对象。
特定类的isEqual
NSArray和NSDictionary都有本身的isqueal。
分别是isEqualToArray isEqualToDictionary,这里我们要注意下不可以穿入错误类型的一个数据,否则会抛出非常。
有些时间我们需要本身写一个等同性方法,想之前提到的一个peson类,我们可以设置isEqualToPerson这个方法来进行一个判定比较
在编写判定方法时,也应该一并复写isEqual方法,假如两者相同就掉用本身的判定方法,否则就交给父类来判定。
执行深度
创建等同性判定方法的时间,需要根据整个对象,在更具对象中的几个字段,就像NSArray的检测行为来判定两个数组中所含对象的个数是否相同,在比对内部的一个对象。
容器类可变类的等同性
在容器中加入可变类型的对象的时间,注意不要在修改他的一个值。
否则就会出现一个set中出现两个重复的数据的内容。
NSMutableSet *set = ;
NSMutableArray* ary = [@[@1] mutableCopy];
NSMutableArray* ary1 = ;
;
;
NSLog(@"%@", set);
;
NSLog(@"%@", set);
NSLog(@"%@", );
者段代码的结果是:
https://i-blog.csdnimg.cn/img_convert/9c63cdb72cb78a0b5e21305d69cb3c86.png
这里经过了一个copy又出现了一个bug,发现数据又少了一个部分,所以我们这里的问题是只管不要在set后对内部的可变数据进行一个操纵。
[*]假如检验对象的等同性,提供isEqual:和hash方法
[*]相同的对象要有相同的哈希码,相同hash吗的对象却不愿定相同
[*]选择哈希碰撞低的算法。
类族
类族的概念其实是一个工厂模式的实现,类族的概念是一种很有用的模式,可以隐蔽抽象基类的实现
比方说我们的一个UIButton的内容,我们创建UIButton的函数和别的创建不太一样,这里其实就是一种类族的方式来进行一个判定的。
这里我们可以吧各种按钮的绘制逻辑都放在一个类内里,根据差别的按钮类型来进行一个切换
https://i-blog.csdnimg.cn/img_convert/c5cf5b2a20aab176a0b3ce2f725685fb.png
CoCo类族
这里我觉得比较紧张的是认识一下我们的一个CoCo框架中的类族,比方说我们的之前提到过的一个NSString的内容,NSString就是一个类族,我们还需要知道大部分集合类都是类族,基类提供一个接口,然后我们的子类有各自的一个底层实现。
这里我们来看一段代码:
NSArray* ary = @[@23];
NSLog(@"%@, %@, %ld", , , == );
输出结果如下:
NSConstantArray, NSArray, 0
Type: Notice | Timestamp: 2025-01-16 15:33:27.173111+08:00 | Process: fragment | Library: fragment | TID: 0x1fe410b
所以我们判定类别的方式要采用isKindOfClass的方式,而不是[<name> class] == [<name> class]
我们也是可以给Cocoa中的NSArray这样的类族来新增一些子类的,但是照旧要遵循一些规则
[*]子类要继承与抽象基类
[*]子类要界说本身的数据存储方式
[*]子类要重写超类文档中应该重写的方法
小结
[*]可以吧实现细节隐蔽在一套简朴的公共接口后面
[*]体系框架中常常利用类族
利用关联对象来存放自界说数据
关联对象用来办理在某些情况下,有的类的实例是由某种机制创建的,我们无法本身创建一个子类实例,这时间才会用到关联对象的一个内容。比方说:给一个分类添加属性,大概是不能修改类界说的时间,就会变得非常常用。
这里先看一下有关于关联对象的一个类型:
https://i-blog.csdnimg.cn/img_convert/b7410630d498bc053d275aaa5a4c14d1.png
这里是假如关联对象成为了一个对应的属性就会具有关联对象同属性一样所对应的一个语义。
下面有三个方法来管理关联对象:
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
id _Nullable value, objc_AssociationPolicy policy);//以给定的值来给某对象设置关联对象值
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key);//获取相应的一个关联对象的值
objc_removeAssociatedObjects(id _Nonnull object);//移除所有关联对象
这里简朴介绍一下有关于给分类添加属性的内容:
//.h
@interface UIView (Associated)
@property (nonatomic, strong) NSString* string;
@end
//.m
#import <objc/runtime.h>
@implementation UIView (Associated)
- (void)setString:(NSString *)string {
objc_setAssociatedObject(self, @selector(string), string, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)string {
return objc_getAssociatedObject(self, @selector(string));
}
@end
UIView* view = [ init];
view.string = @"2134";
NSLog(@"%@", view.string);
经过上述操纵就可以完成一个添加的结果:
2134
Type: Notice | Timestamp: 2025-01-16 16:15:07.480281+08:00 | Process: fragment | Library: fragment | TID: 0x1fee74c
这种做法结果很不错但是大概会带来循环引用的一些问题。
小结
[*]通过关联对象来毗连两个对象
[*]可以给分类添加属性
[*]这种做法大概会引入一些bug
理解objc_msgSend
传递消息是OC中非常紧张的一个内容,消息闻名称或选择子,可以接受参数还大概有返回值,在C语言中采用的是一个静态绑定,在编译期就可以决定运行的时间应该调用的一个函数,在OC中假如向某个对象传递消息,那么就会利用动态绑定机制来决定需要调用的一个方法。
给对象发送消息可以写成这样:
id objc_msgSend(id self, SEL op, ...);
//self:消息的接收者(目标对象)。
//op:消息对应的选择器(方法名,类型为 SEL)。
//...:方法所需的参数(可选)。
这里笔者直接引用一段书中的话:
objc_msgSend 函数会依据接收者与选择子的类型来调用得当的方法。为了完成此操纵,该方法需要在接收者所属的类中搜寻其“方法列表”(list of methods),假如能找到与选择子名称相符的方法,就跳至其实现代码。假如找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。
同时OC另有一个缓存机制,这里的objc_msgSend会将匹配结果缓存在(hash map)内里,每个类都有一个缓存,假如之后查找相同的方法,就会直接从缓存中调用。
这里另有部分边界情况的一个派发:
https://i-blog.csdnimg.cn/img_convert/71aac0339ce5abf993301e2892afd047.png
这里的OC对象的每一个方法都可以理解为C语言函数:
<return_type> Class_selector(id self, SEL _cmd, ...);
每一个类都是一个表格,其中的指针会指向这些函数,选择子的名称则是查找的建。
这里吧所有的函数的都和我们的一个objc_msgSend函数大抵类似,这里可以实现一个尾递归优化。
尾递归优化:是一种编译器或运行时对递归函数的优化技能。当递归调用是函数中的最后一个操纵时,编译器可以优化递归调用,避免创建新的栈帧,从而节流内存并防止栈溢出。
小结
[*]消息由接受者,选择子及其参数构成,给某个对象发送消息,也就相当于在该对象上调用方法
[*]发给某对象的所有消息都要由动态派发体系来进行一个处理,该体系会查出对应的方法,而且执行对应的代码
理解消息转发机制
当对象接收到无法解读的消息后,就会启动消息转发机制,步伐猿可以由此过程告诉如何处理未知消息。
动态方法解析
对象在收到无法解读的消息后,会先调用这个类方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel;
该方法的参数就是谁人未知的选择子,期返回值为Boolean类型,表现这里新增一个类来处理这个选择子。
假如是失败的是一个类方法,那么就会调用这个类方法:
+ (BOOL)resolveClassMethod:(SEL)sel
备援接受者
前面谁人方法失败后,当前接受者另有第二次机会来处理选择子,这一步是寻找别人帮助处理这个选择子:
- (id)forwardingTargetForSelector:(SEL)aSelector;
假如找的到人资助他,那么就将其返回,假如找不到就返回nil。
这样的话我们可以通过继承的方式来办理这种问题,由于在一个对象内部大概另有很多别的对象,可以内部的其他对象来进行一个处理
完整的消息转发
也就是上一步也失败之后,我们就会进入下一个步调:
首先创建 NSInvocation 对象,把与尚未处理的那条消息有关的全部细节都封于其中。此对象包含选择子、目标(target)及参数。在触发 NSInvocation 对象时,“消息派发体系”(message-dispatch system) 将切身出马,把消息指派给目标对象。
这时间回调用:
- (void)forwardInvocation:(NSInvocation *)anInvocation
一样平常情况下,我们在触发这个消息的时间,会先以某种方式改变消息内容,比方说追加另一个参数,大概是改换一个选择子。假如发现不应该由这个类来处理,他会调用他的超类,继承连中的每一个类都有机会来处理这个请求,知道NSobject
最后还没有找到的话,会调用
- (void)doesNotRecognizeSelector:(SEL)aSelector
全流程
https://i-blog.csdnimg.cn/img_convert/d9c3ea8d369e0315c521c2181d797856.png
这里只管让他在前面处理对应的一个消息,
简朴的例子
//
//EOCDictionary.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface EOCDictionary : NSObject
@property (nonatomic, strong) NSMutableDictionary* dictary;
@property (nonatomic, copy) NSString* string;
@property (nonatomic, strong) NSDate* date;
@end
NS_ASSUME_NONNULL_END
//
//EOCDictionary.m
#import "EOCDictionary.h"
#import <objc/runtime.h>
@implementation EOCDictionary
@dynamic string, date;//这里用@dynamic来实现运行期的操作
- (instancetype)init
{
self = ;
if (self) {
self.dictary = ;
}
return self;
}
id autoDictionaryGet(id self, SEL _cmd) {
EOCDictionary* typdefSelf = (EOCDictionary*)self;
NSMutableDictionary* backStore = typdefSelf.dictary;
NSString* key = NSStringFromSelector(_cmd);
return ;
}
void autoDictionarySet(id self, SEL _cmd, id value) {
EOCDictionary* typdefSelf = (EOCDictionary*)self;
NSMutableDictionary* backStore = typdefSelf.dictary;
NSMutableString* key = ;
;// 删除:
;//删除set
NSString* lowercaseFirstChar = [ lowercaseString];
;
if (value) {
;
} else {
;
}
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString* selcetString = NSStringFromSelector(sel);
if () {
class_addMethod(self, sel, (IMP)autoDictionarySet, "v@:@"); //这个方法向类中动态添加方法,处理遮盖力给定的一个选择子
} else {
class_addMethod(self, sel, (IMP)autoDictionaryGet, "@@:");
}
return YES;
}
@end
上面的代码实现了一个自动合成对应的一个字典,可以持有多个属性来分别访问,假如想添加新的属性只用用@property,再声明成@dynamic。其实笔者也不是很理解这部分内容的一个具体实现
在iOS的CoreAnimation 框架中,CALayer 类就用了与本例相似的实现方式,这使得 CALayer 成为“兼容于键值编码的”(key-value-coding-compliant)。容器类,也就等于说,可以或许向内里随意添加属性,然后以键值对的情势来访问。于是,开辟者就可以向其中新增自界说的属性了,这些属性值的存储工作由基类直接负责,我们只需在CALayer的子类中界说新属性即可。
小结
[*]运行期无法响应某个消息子,则进入消息转发
[*]通过运行期的一个动态解析,可以在用到的时间把他添加进去
[*]还无法处理可以转交给其他对象
[*]最后启用完整的消息转发机制
用方法调配技能调试黑盒方法
方法调配,可以让我们不用通过继承子类来重写方法就能改变这个类的巨大的功能,新功能能在本类的所有实例中生效,而不是仅限于重写了相干方法的子类实例。
类的方法列表会把选择子的名称映射到相干的方法实现之上,使得“动态消息派发体系”可以或许据此找到应该调用的方法。这些方法均以函数指针的情势来表现,这种指针叫做 IMP,
id (*IMP)(id, SEL, ...)
下面是一个映射表:
https://i-blog.csdnimg.cn/img_convert/8a47da836b7cabaa03a109c8e3241062.png
在运行期我们可以操纵这个表,可以让他酿成差别的样式:
https://i-blog.csdnimg.cn/img_convert/ffe2951ffd0550c6337dd0069417a7ce.png
可以看到表上的内容不仅仅多了一个newSelector,还吧low和upper进行了一个互换。
Method originalMahon = class_getInstanceMethod(, @selector(lowercaseString));
Method swpperMahton = class_getInstanceMethod(, @selector(uppercaseString));
method_exchangeImplementations(originalMahon, swpperMahton);
NSString* str = @"ThIS iS tHE";
NSString* low = ;
NSString* up = ;
NSLog(@"%@, %@", low, up);
这段代码实现了一个互换upper和lower两个方法名
THIS IS THE, this is the
Type: Notice | Timestamp: 2025-01-16 19:36:58.911202+08:00 | Process: fragment | Library: fragment | TID: 0x2010234
看上去没什么意义,但是这里我们可以通过这种本领来给原先的方法添加新功能
//创建了一个分类,给NSString添加新方法
- (NSString *)eocString {
NSString* lowcase = ;
NSLog(@"%@ => %@", self, lowcase);
return lowcase;
}
NSString* string = @"This is low";
Method originalMahon = class_getInstanceMethod(, @selector(lowercaseString));
Method swpperMahton = class_getInstanceMethod(, @selector(eocString));
method_exchangeImplementations(originalMahon, swpperMahton);
NSLog(@"%@", );
输出结果:
This is low => this is low
Type: Notice | Timestamp: 2025-01-16 19:52:55.610695+08:00 | Process: fragment | Library: fragment | TID: 0x2014300
this is low
Type: Notice | Timestamp: 2025-01-16 19:52:55.610749+08:00 | Process: fragment | Library: fragment | TID: 0x2014300
小结
[*]云溪区可以给类中添加新的选择子所对应的方法实现
[*]可以用一个新的实现来替换原来的一个方法实现
[*]这种方法只有在调试的时间才需要在运行期修改方法实现,这种做法不要滥用
理解类对象
“在运行期检视对象类型”这一操纵也叫做“类型信息查询”(introspection,“内省”),这个强大而有用的特性内置于 Foundation 框架的NSObject 协议里,凡是由公共根类(commonroot class,即 NSObject 与NSProxy)继承而来的对象都要遵从此协议。这里我们首先要相识类对象,以及OC每个对象的一个底层实现。
该变量界说了对象所属的类,通常称为’is a’指针
https://i-blog.csdnimg.cn/img_convert/14cdeea7958fa682df24760a9b955a27.png
此结构体存放类的“元数据”(metadata),例如类的实例实现了几个方法,具备多少个实例变量等信息。此结构体的首个变量也是isa 指针,这阐明 Class 本身亦 Objective-C 对象。结构体里另有个变量叫做super_class,它界说了本类的超类。类对象所属的类型(也就是isa指针所指向的类型)是另外一个类,叫做“元类”(metaclass),用来表述类对象本身所具备的元数据。“类方法”就界说于此处,由于这些方法可以理解成类对象的实例方法。每个类仅有一个“类对象”,而每个“类对象”仅有一个与之相干的“元类”。
https://i-blog.csdnimg.cn/img_convert/37f286253d01db2834148e1308ca9e71.png
继承体系中查询类型关系
isMemberOfClass用于判定实例,isKindOfClass用于判定是否属于某个类,这里的内容笔者在OC的isa指针的简朴理解讲过了,这里就不多赘述,我们想比较类对象是否等同的时间可以通过==来准确的判定出对象是否为某类实例
小结
[*]每一个实例都有一个指向Class对象的指针,来表明类型
[*]对象类型无法在编译器确定,那么就应该用类型查询方法来探知
[*]只管利用类型信息查询方法来确定对象类型,而不是直接比较类对象,由于有些对象实现了消息转发
总结
这一章内容比较多,还需要之后本身多看一下这部分内容。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]