OC 基础 2
文章目录
一、Category 分类
1、什么是分类
分类:
- 在不修改原有类代码的情况下,为类添对象方法或者类方法
- 为类关联新的属性、协议、成员变量
- 分解庞大的类文件
2、原理
Categroy 底层结构也是一个结构体 struct category_t
,里面存储着分类的 对象方法、类方法、属性、协议信息。
在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)
3、Category 和 Extension 的区别
- Class Extension在编译的时候,它的数据就已经包含在类信息中
- Category是在运行时,才会将数据合并到类信息中
4、Category 中的load方法
Category 中有 load方法吗?load方法是什么时候调用的?load 方法能继承吗?
- 有load方法
- load方法在 runtime 加载类、分类的时候调用
- load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用
5、Category 与成员变量
Category能否添加成员变量?如果可以,如何给Category添加成员变量?
- 不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果
- Category 是发生在 运行时,编译完毕,类的内存布局已经确定,无法添加成员变量(Category的底层数据结构也没有成员变量的结构)
- 可以通过 runtime 动态的关联属性
//set方法的实现
- (void)setName:(NSString *)name{
objc_setAssociatedObject(self, tg_name, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
//get方法的实现
- (NSString *)name{
return objc_getAssociatedObject(self,tg_name);
}
二、Extension 扩展
1、什么是扩展
类扩展可以理解为Category的其中的一种,可以用来给当前类添加属性和新方法。
// 类扩展是写在.m中的,相信你一看就知道
@interface ViewController ()
@property (nonatomic, strong) NSString *boss;
- (void)bossAngry; // 扩展方法
@end
2、Category、Extension异同点分析
category
- category只能添加“方法”,不能添加成员变量。
- 分类中添加的成员变量,要通过getter、setter方法进行添加。
- 分类中可以访问原来类中的成员变量,但是只能访问@protect和@public属性。
- 添加方法加上前缀,添加方法会覆盖父类的同名方法,可以防止意外覆盖,也防止被别人覆盖。
extension
- 类扩展的属性和方法都是私有的,也可以定义在.h中,这样就是共有的;
- 类扩展中的方法是一定要实现的方法。Category没有这个限制。
三、Protoco 协议
1、什么是协议
协议:协议是一套标准,这个标准中声明了很多方法,但是不关心具体这些方法是怎么实现的,具体实现是由遵循这个协议的类去完成的。
在OC中,一个类可以实现多个协议,通过协议可以弥补单继承的缺陷。
但是协议跟继承不一样,协议只是一个方法列表,方法的实现得靠遵循这个协议的类去实现。
2、正式协议 & 非正式协议
- 正式协议: @protocol
- 非正式协议:凡是在 NSObject或其子类 Foundation 框架中的类增加 类别(分类),都是非正式协议
四、delegate 代理
1、什么是代理
delegate(委托) 是 protocol(协议) 的一种,顾名思义,就是委托他人帮自己去做什么事。
- 非常严格的语法。所有将听到的事件必须是在delegate协议中有清晰的定义,语法清晰,易读;
- 如果delegate中的一个方法没有实现那么就会出现编译警告/错误
- 在释放代理对象时,需要小心的将delegate改为nil。一旦设定失败,那么调用释放对象的方法将会出现内存crash
五、Notification 通知
1、概念
在IOS应用开发中有一个”Notification Center“
的概念。它是一个单例对象,允许当事件发生时通知一些对象。
NSNotification 发通知的操作是同步的,并且通知处理是在发通知的那个线程。
优点、特性
- 对于一个发出的通知,多个对象能够做出反应,即 1对多的方式实现简单
- controller能够传递 context 对象(dictionary),context对象携带了关于发送通知的自定义的信息
- 在调试的时候应用的工作以及控制过程难跟踪;
2、如何自己去设计一个通知中心?
个人理解: 参考现有的通知中心
- 创建通知中心单例类,并在里面有个一个保存通知的全局NSDictionary;
- 对于注册通知的类,将其注册通知名作为key,,执行的方法和类,以及一些参数做为一个数组为值;
- 发送通知可以调用通知中心,通过字典key(通知名),找到对应的 类和方法进行执行调用传值。
3、iOS 9 以后通知不再需要手动移除
- 通知 NSNotification 在注册者被回收时需要手动移除,是一直以来的使用准则。 原因是在 MRC 时代,通知中心持有的是注册者的 unsafe_unretained 指针,在注册者被回收时若不对通知进行手动移除,则指针指向被回收的内存区域,变为野指针。此时发送通知会造成 crash 。
- 而在 iOS 9 以后,通知中心持有的是注册者的 weak 指针,这时即使不对通知进行手动移除,指针也会在注册者被回收后自动置空。因为向空指针发送消息是不会有问题的。
4、Notification 和KVO区别
- KVO提供一种机制,当指定的被观察的对像的属性被修改后,KVO会自动通知响应的观察者,KVC(键值编码)是KVO的基础
- 通知:是一种 广播机制,在实践发生的时候,通过通知中心对象,一个对象能够为所有关心这个时间发生的对象发送消息,两者都是观察者模式,不同在于KVO是被观察者直接发送消息给观察者,是对象间的直接交互,通知则是两者都和通知中心对象交互,对象之间不知道彼此。
- 本质区别,底层原理不一样。kvo 基于 runtime, 通知则是有个通知中心来进行通知。
5、如何选择delegate、notification、KVO?
三种模式都是一个对象传递事件给另外一个对象,并且不要他们有耦合。
- delegate. 一对一
- notification 一对多,多对多
- KVO 一对一
三者各有自己的特点:
- delegate 语法简洁,方便阅读,易于调试
- notification 灵活多变,可以跨越多个类之间进行使用
- KVO 实现属性监听,实现model和view同步
- 可以根据实际开发遇到的场景来使用不同的方式
六、Block
1、block的原理
- block 本质其实是OC对象
- block 内部封装了函数调用以及调用环境
Block是为了解决什么问题而使用的
- block为了多线程之间调度产生的;
- block 也是一个OC对象,可以当参数传递,使用方便简单,灵活,很少的代码就可以实现代码回调.比协议省很多代码
2、block 使用
声明一个函数,传入值是一个输入输出参数都是 int的 block 函数
- (void)test_Function:(int(^)(int num)) block{}
3、block的属性修饰词为什么是copy?使用block有哪些使用注意?
- block 一旦没有进行copy操作,就不会在堆上
- 使用注意:循环引用问题 (外部使用__weak 解决)
4、 __block
1)如果需要在 block 内部修改 外部的 局部变量 的值,就需要使用 __block
修饰(全局变量和静态变量不需要加 __block
可以修改);
-
通过查看Block 源码,可以发现,block 内部如果单纯使用 外部变量, 会在 block 内部创建同样的一个变量,并且将 外部变量的值引用过来…( 只是将外部变量值拷贝到 block 内部),内部这个变量和外部实际已经没关系了。
-
从另一方面分析,block 本质也是一个 函数指针,外部的变量也是一个局部变量,很有可能 block 在使用这个变量时候,外部变量已经释放了,会造成错误
-
加了
__block
修饰以后, 会将外部变量的内存拷贝到堆中,内存由 block 去管理。
局部变量的数据结构就会发生改变,底层会变成一个 结构体 的对象,结构内部会声明 一个__block
修饰变量的成员,并且将__block
修饰变量的地址保存到堆内存中。
后面如果修改 这个变量的值,可以通过 isa 指针找到这个结构体,进来修改 这个变量的值;
2)block在修改NSMutableArray,需不需要添加__block?
- 如果是操作 NSMutableArray 对象不需要,因为 block 内部拷贝了 NSMutableArray 对象的内存地址,实际是通过内存地址操作的。
- 如果 NSMutableArray 对象要重新赋值,就需要加
__block
5、Block 和 Protocol的区别
Block 是为了解决什么问题而使用的 ?
- 代理 和 block的共同特性是 回调机制。不同的是,代理的方法比较多,比较分散,公共接口、方法较多也选择用delegate进行解耦。
- 使用 block 的代码比较集中统一,异步和简单的回调用block更好。
6、Block 和 函数指针 区别
相同点:
- 二者都可以看成是一个代码片段。
- 函数指针类型和 Block 类型都可以作为变量和函数参数的类型
不同点:
- 函数指针只能指向预先定义好的函数代码块,函数地址是在编译链接时就已经确定好的。从内存的角度看,函数指针只不过是指向代码区的一段可执行代码
- block 本质是 OC对象,是 NSObject的子类,是程序运行过程中在栈内存动态创建的对象,可以向其发送copy消息将block对象拷贝到堆内存,以延长其生命周期。
七、KVC
1、什么是 KVC
KVC 的全称是: Key-Value Coding,俗称 键值编码
,定义在 NSKeyValueCoding.h 文件中,是一个非正式协议。
一个对象拥有某些属性。比如说,一个 Person 对象有一个 name 和一个 address 属性。以 KVC 说法,Person 对象分别有一个 value 对应他的 name 和 address 的 key。 key 只是一个字符串,它对应的值可以是任意类型的对象。
KVC提供了一种 间接访问 其属性方法或成员变量的机制,可以通过 字符串 来访问对应的属性方法或成员变量。包含:gette r方法 valueForKey:
,和 setter方法 setValue:forKey:
,以及其衍生的 keyPath 方法:
2、使用方法
@interface ViewController ()
@property(nonatomic,copy)NSString *name;
@property(nonatomic,strong)Person *person;
@end
@interface Person : NSObject
@property(nonatomic,assign)NSInteger age;
@end
在使用KVC时,直接将属性名当做key,并设置value,即可对属性进行赋值。
[someBody setValue:@"lise" forKey:@"name"];
KeyPath
[someBody setValue:@(18) forKeyPath:@"person.age"];
如果是动态设置属性,则优先考虑调用 setA 方法;
如果没有该方法则优先考虑搜索成员变量 _a
;
如果仍然不存在则搜索成员变量a,如果最后仍然没搜索到则会调用这个类的 setValue:forUndefinedKey:
方法(注意搜索过程中不管这些方法、成员变量是 私有的还是公共的都能正确设置。
如果是动态读取属性,则优先考虑调用 a方法(属性a的getter方法);
如果没有搜索到则会优先搜索成员变量_a,如果仍然不存在则搜索成员变量a;
如果最后仍然没搜索到则会调用这个类的 valueforUndefinedKey:
方法(注意搜索过程中不管这些方法、成员变量是私有的还是公共的都能正确读取)
3、场景举例
修改placeholderLabel颜色
[txfield setValue:[UIFont systemFontOfSize:18] forKeyPath:@"_placeholderLabel.font"];
[txfield setValue:[UIColor magentaColor] forKeyPath:@"_placeholderLabel.textColor"];
4、KVC 和 Notification 的区别?
notification 比 KVO 多了 发送通知 的一步。
两者都是一对多,但是对象之间直接的交互,notification 明显得多,需要notificationCenter 来做为中间交互。而 KVO 如我们介绍的,设置观察者->处理属性变化,至于中间通知这一环,则隐秘多了,只留一句“交由系统通知”,具体的可参照以上实现过程的剖析。
notification 的优点是监听不局限于属性的变化,还可以对多种多样的状态变化进行监听,监听范围广,例如键盘、前后台等系统通知的使用也更显灵活方便。
5、与 delegate 的不同
和 delegate 一样,KVO 和 NSNotification 的作用都是类与类之间的通信。但是与 delegate 不同的是:
这两个都是负责发送接收通知,剩下的事情由系统处理,所以不用返回值;而 delegate 则需要通信的对象通过变量(代理)联系;
delegate 一般是一对一,而这两个可以一对多。
八、KVO
1、什么是 KVO
KVO:Key-Value Observing 。
是一个对象能够观察另外一个对象的属性的值,并且能够发现值的变化。
用途:
-
能够提供一种简单的方法实现两个对象间的同步。例如:model和view之间同步;
-
能够对非我们创建的对象,即内部对象的状态改变作出响应,而且不需要改变内部对象(SKD对象)的实现;
实现的三个方法:
- watchPersonForChangeOfAddress: 实现观察
- observeValueForKeyPath:ofObject:change:context: 在被观察的 key path 的值变化时调用。
- dealloc 停止观察
比如键盘弹出的监听
KVO其实是一种观察者模式,利用它可以很容易实现视图组件和数据模型的分离,当数据模型的属性值改变之后作为监听器 的视图组件就会被激发,激发时就会回调监听器自身。
在 ObjC 中要实现KVO则必须实现 NSKeyValueObServing 协议,不过幸运的是 NSObject 已经实现了该协议,因此几乎所有的 ObjC 对象都可以使用 KVO
2、原理本质
基于 isa-swizzling
当你观察一个对象a 时,一个新的类 B 会被动态创建。
这个类B 继承自该对象a 的原本的类A,并 重写了被观察属性的 setter 方法 。
重写的 setter 方法会负责 在调用原 setter 方法之前和之后,通知所有观察对象:值的更改。
最后通过isa 混写(isa-swizzling),把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。
3、使用步骤
- 通过
addObserver: forKeyPath: options: context:
为被监听对象(它通常是数据模型)注册监听器 - 重写监听器的
observeValueForKeyPath: ofObject: change: context:
方法。
系统会在运行期动态地创建该类的一个派生类 NSKVONotifying_Person,在这个派生类中重写该类中被观察的属性的 setter 方法。我们知道,重写方法,要调用super。
所以KVO的本质就是监听对象的属性进行赋值的时候有没有调用setter方法. 如果有调用setter方法, 就会接收到属性变更的通知, 反之则没有.
-
利用 RuntimeAPI 动态生成一个子类,并且让 instance 对象的 isa 指向这个全新的子类
-
当修改 instance 对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
willChangeValueForKey:
父类原来的setterdidChangeValueForKey:
-
内部会触发监听器(Oberser)的监听方法(
observeValueForKeyPath:ofObject:change:context:
。
4、如何手动触发KVO?
手动调用willChangeValueForKey:
和 didChangeValueForKey:
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc]init];;
[p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[p willChangeValueForKey:@"name"];
[p didChangeValueForKey:@"name"];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"被观测对象:%@, 被观测的属性:%@, 值的改变: %@\n, 携带信息:%@", object, keyPath, change, context);
}
5、直接修改成员变量会触发KVO么?
- 不会触发KVO
6、通过KVC修改属性会触发KVO么?
- 会触发 KVO
- KVC 在赋值时候,内部会触发监听器(Oberser)的监听方法
observeValueForKeyPath:ofObject:change:context:
发送通知
7、如何对 NSMutableArray 进行 KVO
-
一般情况下只有通过调用 set 方法对值进行改变才会触发 KVO。
但是在调用NSMutableArray的 addObject或removeObject 系列方法时,并不会触发它的 set 方法。所以为了实现NSMutableArray的 KVO,官方为我们提供了如下方法:
@property (nonatomic, strong) NSMutableArray *arr;
//添加元素操作
[[self mutableArrayValueForKey:@"arr"] addObject:item];
//移除元素操作
[[self mutableArrayValueForKey:@"arr"] removeObjectAtIndex:0];
8、KVC 与 KVO 的不同?
KVC(键值编码),即 Key-Value Coding,一个非正式的 Protocol,使用字符串(键)访问一个对象实例变量的机制。而不是通过调用 Setter、Getter 方法等显式的存取方式去访问。
KVO(键值监听),即 Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,对象就会接受到通知,前提是执行了 setter 方法、或者使用了 KVC 赋值。
九、NSCoding
1、为了将应用数据存储到硬盘中,iOS提供基本的文件API、Property List序列化、SQLite、CoreData以及NSCoding。对于轻量级的数据要求,NSCoding因其简单而成为一种比较合适的方式。 NSCoding是一个你需要在数据类上要实现的协议以支持数据类和数据流间的编码和解码。数据流可以持久化到硬盘。
2、是类对象本身数据的写入到本地文件。
需要实现两个方法: encodeWithCoder 和 initWithEncoder。
- (void)encodeWithCoder:(NSCoder *)aCoder{
[aCoder encodeObject:foo forKey:kFooKey];
[aCoder encodeObject:bar forKey:kBarKey];
[aCoder encodeInt:someInt forKey:kSomeIntKey];
[aCoder encodeFloat:someFloat forKey:kSomeFloat];
}
若要我们在项目中支持归档,必须使用正确的编码方法将所有实例变量编码成encoder。
如果要子类化某个也遵循NSCoding的类,还需要确保对超类调用 encodeWithCoder: 方法,你的方法将如下所示:
- (void)encodeWithCoder:(NSCoder *)aCoder{
[super encodeWithCoder:aCoder];
[aCoder encodeObject:foo forKey:kFooKey];
[aCoder encodeObject:bar forKey:kBarKey];
[aCoder encodeInt:someInt forKey:kSomeIntKey];
[aCoder encodeFloat:someFloat forKey:kSomeFloat];
}
十、NSCopying/NSMutableCopying 协议
如果类想要支持copy操作,则必须实现NSCopying协议,也就是说实现 copyWithZone
方法;
如果类想要支持mutableCopy操作,则必须实现NSMutableCopying协议,也就是说实现 mutableCopyWithZone
方法;
iOS系统中的一些类已经实现了NSCopying或者NSMutableCopying协议的方法,如果向未实现相应方法的系统类或者自定义类发送copy或者mutableCopy消息,则会crash。
- (id)copyWithZone:(NSZone *)zone {
Person *model = [[[self class] allocWithZone:zone] init];
model.firstName = self.firstName;
model.lastName = self.lastName;
//未公开的成员
model->_nickName = _nickName;
return model;
}
- (id)copyWithZone:(NSZone *)zone {
AFHTTPRequestSerializer *serializer = [[[self class] allocWithZone:zone] init];
serializer.mutableHTTPRequestHeaders = [self.mutableHTTPRequestHeaders mutableCopyWithZone:zone];
serializer.queryStringSerializationStyle = self.queryStringSerializationStyle;
serializer.queryStringSerialization = self.queryStringSerialization;
return serializer;
}