OC 底层(KVC、KVO、Delegate、Category、Extension、通知)

目录

1.KVC
2.KVO
3.通知
4.代理、委托、协议
5.Block、KVO、通知、代理之间的区别
6.分类 Category 和类扩展 Extension
7.类方法、实例方法、构造方法

1.KVC

概念:KVC(Key-Value Coding)键值编码,是一种可以直接通过字符串类型的属性名 key 来访问或赋值某个类属性的机制,而不是通过调用 Setter、Getter 方法访问。这样就可以在运行时动态地访问和修改对象的属性,而不是在编译时确定,这也是 iOS 开发中的黑魔法之一。它是利用 NSKeyValueCoding 非正式协议实现的一种机制,对象采用这种机制来提供对其属性的间接访问。

说明:

1.写下 KVC 代码并点击跟进 setValue 会发现 NSKeyValueCoding 是在 Foundation 框架下,KVC 通过对 NSObject 的扩展来实现的,所有继承了 NSObject 的类可以使用 KVC。

2.NSArray、NSDictionary、NSMutableDictionary、NSOrderedSet、NSSet 等也遵守 KVC 协议,除少数类型(结构体)以外都可以使用 KVC。

3.KVC 既支持带有对象值的属性,也支持基本数据类型和结构,基本数据类型会被自动封装和解装,比如 KVC 的 keyPath 可以是属性、实例变量、成员变量等。

相关链接:https://juejin.cn/post/6844904086744104968

1.KVC 常用 API 方法

1.setValue:(id)value forKey:(NSString *)key 和 setValue:(id)value forKeyPath:(NSString *)keyPath 的区别:key 是直接根据属性的名字设置,如果按路径找会报错。keyPath 相当于根据路径去寻找属性,一层一层往下找。
2.(id)valueForKey:(NSString *)key 和 (id)valueForKeyPath:(NSString *)keyPath 的区别同理。

//通过 Key 读取和存储
- (nullable id)valueForKey:(NSString *)key;//直接通过Key来取值
- (void)setValue:(nullable id)value forKey:(NSString *)key;//通过Key来设值
//通过 keyPath 读取和存储
- (nullable id)valueForKeyPath:(NSString *)keyPath;//通过KeyPath来取值           
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;//通过KeyPath来设值  
//默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
+ (BOOL)accessInstanceVariablesDirectly;

// KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确,为不正确的值做一个替换值或者拒绝设置新值并返回错误原因
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

// 这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

// 如果Key不存在,且KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常
- (nullable id)valueForUndefinedKey:(NSString *)key;

// 和上一个方法一样,但这个方法是设值
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;

//如果你在SetValue方法时给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;

//输入一组Key,返回该组Key对应的Value,再转成字典返回,用于将Model转到字典
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

2.KVC 赋(设)值和取值流程 - 底层原理

赋值流程:

1.首先会去找类的 set 方法,如果找不到会去找 带下划线的 set 方法。
2.如果都找不到,则会看 +(BOOL)accessInstanceVariablesDirectly 方法中的返回(默认为 YES) 。
3.返回 YES 时:会按照 _key、_isKey、key、isKey 的顺序找属性赋值,如果类中没有上面的这些属性则会调用 -(void)setValue:(id)value forUndefinedKey:(NSString *)key 方法(自己简单实现一下比如打个 NSLog(),否则报错);返回 NO 时:会直接调用 -(void)setValue:(id)value forUndefinedKey 方法 。

取值流程:

1.首先取值会按 getKey、key、isKey、_key 的顺序取。
2.找不到也会根据 +(BOOL)accessInstanceVariablesDirectly 返回值。
3.返回 YES 时:会按照 _key、_isKey、key、isKey 的顺序找属性取值,如果类中没有这些属性则会调用 -(id)valueForUndefinedKey:(NSString *)key 方法(自己实现一下,否则报错);返回 NO 时:直接调用 -(id)valueForUndefinedKey。

KVC 赋(设)值流程:首先会去找类的 set 方法,如果找不到会去找带下划线的 set 方法,如果都找不到则会看 +(BOOL)accessInstanceVariablesDirectly 方法中的返回,默认为YES。返回 YES 时会按照 _key、_isKey、key、isKey 的顺序找属性赋值,如果类中没有上面的这些属性则会调用 -(void)setValue:(id)value forUndefinedKey:(NSString *)key 方法(自己实现一下,否则报错),返回 NO 时会直接调用 -(void)setValue:(id)value forUndefinedKey 方法
KVC 取值流程:首先取值会按 getKey、key、isKey、_key 的顺序取,找不到也会根据 +(BOOL)accessInstanceVariablesDirectly 返回值。返回 YES 时,会按照 _key、_isKey、key、isKey 的顺序找属性取值,如果类中没有这些属性则会调用 -(id)valueForUndefinedKey:(NSString *)key 方法(自己实现一下,否则报错),返回 NO 时直接调用 -(id)valueForUndefinedKey
setValue:forKey: 的原理
valueForKey: 的原理
KVC 的底层实现

3.KVC 的作用

KVC 应用场景:

动态地取值和设值
Model 和字典转换
修改一些控件的内部属性
用 KVC 来访问和修改私有变量
操作集合
用 KVC 实现高阶消息传递
......

相关链接:https://juejin.cn/post/6844903602545229831

KVC 应用场景
KVC 的作用

4.KVC 处理异常(nil、UndefinedKey)

KVC 中最常见的异常就是不小心使用了错误的 key 或者在设值中不小心传递了 nil 的值,KVC 中有专门的方法来处理这些异常。

  • KVC 处理 nil 异常

通常情况下 KVC 不允许你要在调用 setValue:属性值 forKey:(或者 keyPath)时对非对象传递一个 nil 的值。因为值类型是不能为 nil,如果你不小心传了,KVC 会调用 setNilValueForKey: 方法。这个方法默认是抛出异常,所以一般而言最好还是重写这个方法。

//重写
- (void)setNilValueForKey:(NSString *)key {
    NSLog(@"不能将%@设成nil", key);
}
  • KVC 处理 UndefinedKey 异常

通常情况下 KVC 不允许你要在调用 setValue:属性值 forKey:(或者 keyPath)时对不存在的 key 进行操作,不然会报错 forUndefinedKey 发生崩溃,重写 forUndefinedKey 方法避免崩溃。

//重写

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"出现异常,该key不存在%@",key);
    return nil;
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"出现异常,该key不存在%@", key);
}

2.KVO

KVO(Key Value Observing)键值监听(键值观察),可以用于监听某个对象属性值的改变,也可以监听集合对象的变化。KVO 和 NSNotification 都是 iOS 中观察者模式的一种实现。

注意:

1.KVO 只能监听通过 set 方法修改的值。
2.如果使用 KVO 监听某个对象的属性,当对象释放之前一定要移除监听。
3.KVO 的定义都是对 NSObject 的扩展来实现的,Objective-C 中有个显式的 NSKeyValueObserving 类别名,所以对于所有继承了 NSObject 的类型都能使用KVO(一些纯Swift类和结构体是不支持 KVC 的,因为没有继承 NSObject)。
4.自动键值观察是使用称为 isa-swizzling 的技术实现,该 isa 指针指向对象的类,它保持一个调度表,该调度表主要包含指向类实现的方法的指针以及其他数据。当观察者注册观察对象的某属性时,被观察对象的 isa 指针被修改,指向中间类而不是真正的类;因此 isa 指针的值不一定反映实例的实际类。永远不要依赖 isa 指针来确定类成员身份,应该使用该 class 方法来确定对象实例的类。
5.KVO 监听集合对象变化时,需要通过 KVC 的 mutableArrayValueForKey: 等可变代理方法获得集合代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发 KVO 的监听方法。集合对象包含 NSArray 和 NSSet。

相关链接:https://juejin.cn/post/6844904090569277447

KVO 举例:要监听 Person 中的 age 属性,我们就创建一个 observer 用来监听 age 的变化,当我们 age 一旦发生改变,就会通知 observer。KVO 的作用:可以监听某个对象属性的改变

1.KVO 的底层

KVO 的 keyPath 可以是属性、实例变量、成员变量,KVO 的底层基于 runtime 机制实现, 它的原理是修改 setter 方法,因此使用 KVO 必须调用 setter,若直接访问属性对象则没有效果。即当一个类型为 ObjectA 的对象被添加了观察后,系统会生成一个 NSKVONotifying_ObjectA 类,并将对象的 isa 指针指向新的类,也就是说这个对象的类型发生了变化,这个类相比较于 ObjectA,会重写以下几个方法:重写 setter、重写 class、重写 dealloc、重写 _isKVOA。

KVO 的内部具体实现原理:

1.KVO 是基于 runtime 机制实现的,当某个类的属性对象第一次被观察时,系统就会在运行期间动态地创建该类的一个派生类,在这个派生类中重写基类的任何被观察属性的 setter 方法,派生类在被重写的 setter 方法内实现真正的通知机制。举例:如果原类为 Person,那么生成的派生类名为 NSKVONotifying_Person。
2.每一个类中都有一个 isa 指针指向当前类,所有系统就是在当一个类的对象第一次被观察的时候,系统就会偷偷将 isa 指针指向动态生成的派生类,从而在被监听属性赋值时被执行的是派生类的 setter 方法。
3.键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey: ;在一个被观察属性发生改变之前 willChangeValueForKey: 一定会被调用,这就会记录旧的值。而当改变发生后 didChangeValueForKey: 会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。

总结 KVO 的调用顺序:调用 willChangeValueForKey: -> 调用原来的setter实现 -> 调用didChangeValueForKey:(内部会调用observer的observeValueForKeyPath:ofObject:change:context:方法)

注意:KVO 的这套实现机制中苹果还偷偷重写了 class 方法,让我们误认为还是使用的当前类,从而达到隐藏生成的派生类。

KVO 实现原理

底层原理-例子1:

底层原理-例子1
  • NSKVONotifying_ 类名中的方法
- (void)viewDidLoad {
    [super viewDidLoad];

    self.person = [[LZPerson alloc] init];
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];

    unsigned int intCount;
    Method *methodList = class_copyMethodList(objc_getClass("NSKVONotifying_LZPerson"), &intCount);

    for (unsigned int intIndex=0; intIndex<intCount; intIndex++) {

        Method method = methodList[intIndex];
        NSLog(@"SEL:%@,IMP:%p",NSStringFromSelector(method_getName(method)), method_getImplementation(method));
    }
}

// 打印结果
SEL:setNickName:,IMP:0x18a5d8520
SEL:class,IMP:0x18a5d6fd4
SEL:dealloc,IMP:0x18a5d6d58
SEL:_isKVOA,IMP:0x18a5d6d50
底层原理-例子1

底层原理-例子2:

底层原理-例子2
底层原理-例子2
底层原理-例子2
底层原理-例子2
底层原理-例子2

2.KVO 的用法(监听过程)

KVO 使用三部曲:添加/注册 KVO 监听、实现监听方法以接收属性改变通知、 移除 KVO 监听。

1.调用方法 addObserver:forKeyPath:options:context: 给被观察对象添加观察者。

2.在观察者类中实现 observeValueForKeyPath:ofObject:change:context: 方法以接收属性改变的通知消息。

3.当观察者不需要再监听时,调用 removeObserver:forKeyPath: 方法将观察者移除。需要注意的是,至少需要在观察者销毁之前,调用此方法,否则可能会导致 Crash。

几个参数解释:

observer:观察者,也就是 KVO 通知的订阅者,订阅着必须实现。

keyPath:描述将要观察的属性,相对于被观察者。

options:KVO 的一些属性配置;有四个选项(NSKeyValueObservingOptionNew:change字典包括改变后的值、NSKeyValueObservingOptionOld:change字典包括改变前的值、NSKeyValueObservingOptionInitial:注册后立刻触发KVO通知、NSKeyValueObservingOptionPrior:值改变前是否也要通知(这个key决定了是否在改变前改变后通知两次)

context: 上下文,这个会传递到订阅着的函数中,用来区分消息,所以应当是不同的。

KVO 实际场景举例:https://juejin.cn/post/6844903972528979976

具体步骤如下

1.注册观察者

消息中的上下文指针 context 包含任意数据,这些数据将在相应的更改通知中传回给观察者;可以指定 NULL 并完全依赖 keyPath 字符串来确定更改通知的来源,但这样可能会导致父类由于不同原因也在观察相同键路径的对象时出现问题。

[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];

2.属性变化通知

//在这里 change 这个字典保存了变更信息,具体是哪些信息取决于注册时的 NSKeyValueObservingOptions
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"%@",change);
    }
}

3.移除观察者(要记得)

如果被观察者是单例,那么如果被观察者所在界面销毁时不移除观察者会崩溃(被观察者未释放,值改变方法还要调用,但界面被释放,这个方法找不到了所以崩溃)

[self.person removeObserver:self forKeyPath:@"name" context:NULL];

4.设置 context上下文,区分通知来源

static void *PersonNickContext = &PersonNickContext;
static void *PersonNameContext = &PersonNameContext;

[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickContext];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if (context == PersonNickContext) {
        NSLog(@"nick:%@",change);
        return;
    }

    if (context == PersonNameContext){
        NSLog(@"name:%@",change);
        return;
    }
}

3.手动关闭 KVO、手动触发 KVO

  • +(BOOL)automaticallyNotifiesObserversForKey 手动关闭 KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    return NO;
}
  • willChangeValueForKey、didChangeValueForKey 手动触发 KVO
[LZPerson willChangeValueForKey:@"name"];
_name = name;
[LZPerson didChangeValueForKey:@"name"];
手动 KVO(禁用 KVO)举例
手动 KVO(禁用 KVO)举例

4.监听可变数组

self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
[self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    // 这种写法不能收到KVO通知,因为KVO基于KVC,访问 集合对象 有三种不同的代理方法
    // if(self.person.dateArray.count == 0){
    //     [self.person.dateArray addObject:@"1"];
    // }
    // else{
    //     [self.person.dateArray removeObjectAtIndex:0];
    // }
    
    if(self.person.dateArray.count == 0){
        [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
    }
    else{
        [[self.person mutableArrayValueForKey:@"dateArray"] removeObjectAtIndex:0];
    }
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@",change);
}

- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"dateArray"];
}
接着上面代码说明
/* Possible values in the NSKeyValueChangeKindKey entry in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information.
*/
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,      //赋值
    NSKeyValueChangeInsertion = 2,    //插入
    NSKeyValueChangeRemoval = 3,      //移除
    NSKeyValueChangeReplacement = 4,  //替换
};

5.KVO 和线程

KVO和线程

6.其他说明

  • 通过 KVC 修改属性会触发 KVO 么?

会触发 KVO。

  • 直接修改成员变量会触发 KVO 么?

不会触发 KVO,因为直接修改成员变量并没有走 set 方法。

3.通知

iOS 中存在三种常见的事件通知方式:NSNofiticationCenter、KVO Notification 和 User Notifications,其中 User Notifications 是用户通知也就是常说的推送,在这里只讲解 NSNofiticationCenter。

相关链接:https://juejin.cn/post/6844903457984348167

1.NSNotification

NSNotification 是方便 NSNotificationCenter 广播到其他对象时的封装对象,简单讲即通知中心对通知调度表中的对象广播时发送 NSNotification 对象。其中 NSNotification 对象包含:名称、object、字典三个属性。

名称:是用来标识通知的标记;
object:是要通知的对象可以为 nil;
字典:用来存储发送通知时附带的信息也可以为 nil。

@interface NSNotification : NSObject <NSCopying, NSCoding>

@property (readonly, copy) NSNotificationName name;
@property (nullable, readonly, retain) id object;
@property (nullable, readonly, copy) NSDictionary *userInfo;

2.NSNotificationCenter

NSNotificationCenter 是类似一个广播中心站,使用 defaultCenter 来获取应用中的通知中心,它可以向应用任何地方发送和接收通知。在通知中心注册观察者,发送者使用通知中心广播时,以 NSNotification 的 name 和 object 来确定需要发送给哪个观察者。为保证观察者能接收到通知,所以应先向通知中心注册观察者,接着再发送通知这样才能在通知中心调度表中查找到相应观察者进行通知。

3.NSNotificationQueue

NSNotificationQueue 通知队列,用来管理多个通知的调用。通知队列通常以先进先出(FIFO)顺序维护通。NSNotificationQueue 就像一个缓冲池把一个个通知放进池子中,使用特定方式通过 NSNotificationCenter 发送到相应的观察者,特定的方式即合并通知和异步通知。

4.NSNotificatinonCenter 实现原理

1.NSNotificatinonCenter 是使用观察者模式来实现的用于跨层传递消息,用来降低耦合度。
2.NSNotificatinonCenter 用来管理通知,将观察者注册到 NSNotificatinonCenter 的通知调度表中,然后发送通知时利用标识符 name 和 object 识别出调度表中的观察者,然后调用相应的观察者的方法,即传递消息(在 Objective-C 中对象调用方法,就是传递消息,消息有 name 或者 selector,可以接受参数,而且可能有返回值),如果是基于 block 创建的通知就调用 NSNotification 的 block。

相关链接:https://juejin.cn/post/6844904080742023176

5.通知常见的使用方法

5.1简单常用举例(不带参数):

1.注册发送通知:[[NSNotificationCenter defaultCenter] postNotificationName:@"changeColor" object:self];
2.接收处添加观察者: [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(changeColor) name:@"changeColor" object:nil];
3.addObserver 处 remove 掉观察者:- (void)dealloc{
[[NSNotificationCenter defaultCenter]removeObserver:self];
}

相关链接:http://t.zoukankan.com/-ios-p-10608973.html

  • 发送者

发送通知可使用以下方法发送通知:

- (void)postNotification:(NSNotification *)notification;
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject;
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
发送者
  • 观察者

你可以使用以下两种方式注册观察者:

- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;

- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block NS_AVAILABLE(10_6, 4_0);
    // The return value is retained by the system, and should be held onto by the caller in
    // order to remove the observer with removeObserver: later, to stop observation.
观察者
  • 移除观察者

在对象被释放前需要移除掉观察者,避免已经被释放的对象还接收到通知导致崩溃。移除观察者有两种方式:

- (void)removeObserver:(id)observer;
- (void)removeObserver:(id)observer name:(nullable NSNotificationName)aName object:(nullable id)anObject;
__block __weak id<NSObject> observer = [[NSNotificationCenter defaultCenter] addObserverForName:kChangeNotifition object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) { NSLog(@"-[NSNotificationCenter addObserverForName:object:queue:usingBlock:]"); [[NSNotificationCenter defaultCenter] removeObserver:observer]; }];

5.2其他举例(带参数):

例子1:

[[NSNotificationCenter defaultCenter]postNotificationName:@"BBXOrderBillBackNotification" object:nil userInfo:@{@"index":@"3"}];//返回送客地图刷新(订单已经支付情况)

[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(BBXOrderBillBackNotificationAction:) name:@"BBXOrderBillBackNotification" object:nil];//从账单页面确认完成直接回来(订单已支付情况),不走账单详情(代支付)

//从账单页面确认完成直接返回来刷新(不跳账单详情)
- (void)BBXOrderBillBackNotificationAction:(NSNotification *)sender {
    NSDictionary *dic  = sender.userInfo;
    NSString *index = dic[@"index"];
    if ([index isEqualToString:@"3"]) {
        [self refreshEvent:NO];//网络请求,刷新界面
    }
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

例子2:

[[NSNotificationCenter defaultCenter] postNotificationName:@"GaodeNaviUpdateMapNaviPopupView" object:nil userInfo:@{@"orderViewModel":orderViewModel}];//通知订单面板刷新

[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(gaodeNaviUpdateMapNaviPopupView:) name:@"GaodeNaviUpdateMapNaviPopupView" object:nil];//导航中更换终点导航刷新订单面板

//通知-高德导航中改变终点刷新订单面板
- (void)gaodeNaviUpdateMapNaviPopupView:(NSNotification *)sender {
    NSDictionary *dic = sender.userInfo;
    OrderViewModel *orderViewModel = dic[@"orderViewModel"];
    [orderViewModel refreshPrice];
    self.mapNavigationPopupView.orderViewModel = orderViewModel;//更新数据
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}
发送通知
添加观察者
通知回调方法
移除通知

5.3通知其他写法:

//普通写法
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(p_showGuideView) name:@"showGuideView" object:nil];
//其他写法
[[NSNotificationCenter defaultCenter] addObserverForName:@"updateSectionData" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
   [self reloadUI];
}];
通知其他写法

4.代理、委托、协议

1.概念

通常所说的代理又称委托代理,是 iOS 中常用的一种设计模式,可以理解成一种代理机制,这个代理机制由代理对象、委托者、协议三部分组成。

代理 Delegate:用代理对象代理被代理对象的行为(比如:找枪手代考),代理对象可以通过被代理对象的指针执行被代理对象的行为,与此同时代理对象可以对这些行为进行增强实现。很多iOS开发者认为代理和委托是一个意思,其实二者是不同的两种设计理念。它是 iOS 开发中的一种重要的消息传递方式和设计模式,例如 UITableView、UITextField、UISearchView 等都是用代理机制实现消息传递。

委托:根据指定的协议,指定代理去完成什么功能。委托是一个对象保存另外一个对象的引用,被引用的对象实现了事先确定的协议,该协议用于将引用对象中的变化通知给被引用对象。委托和委托方双方的 property 声明属性都是 assign 而不是 retain,为了避免循环引用造成的内存泄露。

协议 Protocol:一般被用来定义一套公用的方法接口,别的类可以遵守这个协议使用这些方法,方法分为 required 和 optional 两种,required 是必须实现的方法,optional 是可选的方法,即用来指定代理双方可以做什么,必须做什么。比如 tableViewdelegate 这个协议,他定义了很多接口,然后 VC 去遵循这个协议并去实现里面的接口,tableView 不能去实现里面的接口所以他委托 VC 代理去处理里面的接口。

总结:

1.采用委托代理的好处在于:避免子类化带来的过多的子类以及子类与父类的耦合、通过委托传递消息机制实现分层解耦。

2.委托代理机制是一种设计模式,以 @protocol 形式体现,一般是一对一传递。设置代理属性都使用 weak 以规避循环引用,通常我们定义的指针默认都是 __strong 类型的,而属性本质上也是一个成员变量和 set、get 方法构成的,strong 类型的指针会造成强引用,必定会影响一个对象的生命周期,这也就会形成循环引用,所以代理用 weak。

3.在我们写项目时,特别是主界面会随着处理的逻辑越来越多它会越来越肥,对于新项目来说 MVVM 设计模式是一种最好的选择,但是对于一个已经很复杂的项目来说,代理是很好的方式,可以用代理给 ViewController 瘦身。

4.协议类似于 Java 和 C# 中的接口,一个类可以遵循多个协议,一个协议也可以被多个类遵循(就像一个演员可以扮演多个角色,一个角色也可以由多个演员扮演)。

5.协议的特点:协议是一组方法的集合,协议中只有方法的声明,没有方法的实现,因为这些方法是留给遵循该协议的类做出多态实现的方法。毫无关系的类可以遵循相同的协议从而具有相同的行为,这是自然的,就如同超人、鸟、飞机都会飞,但是他们其实没有什么必然的关联,我们可以把飞这个行为定义到一个协议中。

相关链接:https://blog.csdn.net/qq_42376419/article/details/98884229
为什么代理属性要用 weak 修饰:http://www.jianshu.com/p/661a01405802

协议补充说明
协议补充说明
委托回调补充说明

2.三者的关系

通俗来说就是某个对象 A 把要做的事情委托给另一个对象 B 按照双方约定的的协议去做,其中 A 称作委托者,B 称作是被委托者也就是代理。

相关链接:
https://juejin.cn/post/6844903752554512391
http://www.cnblogs.com/36bian/p/5240517.html
http://blog.csdn.net/mad1989/article/details/8463460

  • 通俗的举例说明:

三者角色比作:委托方 = 老板,代理方 = 员工,协议 = 合同。那么委托方传递信息或者事件到代理方,代理方执行相关操作。这个就可以理解成:老板把工作材料和工作内容交给员工,员工去干活。

代理对象、委托者、协议之间的关系
代理对象、委托者、协议之间的关系
委托方通过某种方式把任务分派出去给代理方处理,而两者之间的联系便是协议
代理对象、委托者、协议三者一起用的
代理对象和委托者对应关系

3.工作分配

  • 委托需要做的工作有:

1.定义协议与方法
2.声明委托变量
3.设置代理
4.通过委托变量调用委托方法

  • 代理需要做的工作有:

1.遵循协议
2.实现委托方法

实现代理需要注意什么
协议和代理的理解
  • 举例说明

比如学生想要买一本专业书,书店没有这本书,自己又不直接去出版社,于是学生就委托书店帮忙买书,书店就是学生的代理,学生就是委托者。

委托者 - 学生 Student.h

#import <Foundation/Foundation.h>

//定义协议与方法
@protocol StudentBuyBookDelegate<NSObject>
-(void)buyBook:(NSString *)name price:(int)p;
@end

@interface Student : NSObject
//声明委托变量
@property(nonatomic,retain)id<StudentBuyBookDelegate> stu_delegate;

-(void)wantBuy;

@end

委托者 - 学生 Student.m

#import "Student.h"

@implementation Student

-(void)wantBuy {
    
    NSLog(@"学生:我想买IOS开发的书");
    //通过委托变量调用委托方法
    [self.stu_delegate buyBook:@"IOS开发" price:50];
}

@end

代理 - 书店 BookShop.h

#import <Foundation/Foundation.h>
#import "Student.h"
//书店遵守StudentBuyBookDelegate的委托协议
@interface BookShop : NSObject<StudentBuyBookDelegate>

@end

代理 - 书店 BookShop.m

#import "BookShop.h"

@implementation BookShop

//书店实现协议的方法
-(void)buyBook:(NSString *)name price:(int)p {
    
    NSLog(@"我可以以%i元的价格把%@卖个你",p,name);
}

@end

在 ViewController.m 里面

Student *student =[[Student alloc]init];
    BookShop *bookshop = [[BookShop alloc]init];
    
    //学生设置代理,委托书店买书
    student.stu_delegate = bookshop;
    
    //判断书店是否实现了协议,避免未实现带来的崩溃
    if ([bookshop respondsToSelector:@selector(buyBook:price:)])
    {
        [student wantBuy];
    }

4.委托代理机制 - 实现原理

实现流程关系

5.协议&代理&委托简单举例

协议 protocol - demo:

代理 delegate - demo:

委托回调 - demo:

5.Block、KVO、通知、代理之间的区别

https://blog.csdn.net/dqjyong/article/details/7685933
http://www.jianshu.com/p/f819abf40509
https://www.zybuluo.com/SanMao/note/125908

  • block 和代理的区别

相同点:block 和代理都是回调的方式,使用场景相同。

不同点:

1.block 集中代码块,而代理分散代码块。所以 block 更适用于轻便、简单的回调,如网络传输。 代理适用于公共接口较多的情况,这样做也更易于解耦代码架构。

2.block 运行成本高,block 需要将使用的数据从栈内存拷贝到堆内存,如果是对象就是加计数,使用完或 block 置为 nil 后才消除。 代理只是保存了一个对象指针,直接回调,并没有额外消耗,相对 C 的函数指针,只是多做了一个查表动作。

block 和代理实际场景选择:比如 tableview 中 cell 自定义一个按钮,这个按钮的回调可以用 block 来实现。比如bbx司机端地图页面中选中订单气泡弹起的订单面板页面可以通过代理来实现面板上面各个按钮控件的回调如电话、滑条、刷新等,因为控件和需要回调操作比较多。

bbx司机端地图页面中选中订单气泡弹起的订单面板页面
  • 通知和代理的区别

1.效率:代理比通知高。
2.关联:代理是强关联,委托和代理双方互相知道。通知是弱关联,不需要知道是谁发,也不需要知道是谁接收。
3.代理是一对一的关系,通知是一对多的关系。代理一般是行为需要别人来完成,通知是全局通知。
4.代理要实现对多个类发出消息可以通过将代理者添加入集合类后遍历,或通过消息转发来实现。

其他说明:协议有控制链(has-a)的关系,通知没有。分析下通知和代理的行为模式,简单来说通知的话它可以一对多,一条消息可以发送给多个消息接受者;代理按我们的理解,到不是直接说不能一对多,比如我们知道的明星经济代理人,很多时候一个经济人负责好几个明星的事务,只是对于不同明星间代理的事物对象都是不一样的,一一对应,不可能说明天要处理A明星要一个发布会,代理人发出处理发布会的消息后,别称B的发布会了。但是通知就不一样,他只关心发出通知,而不关心多少接收到感兴趣要处理。因此控制链(has-a)从英语单词大致可以看出,单一拥有和可控制的对应关系。

通知和代理的区别
  • KVO和通知的区别

1.相同:都是一对多的关系。
2.不同:通知是需要被观察者先主动发出通知,观察者注册监听再响应,比 KVO 多了发送通知这一步。
3.监听范围:KVO 只能用于监听对象属性的变化,即监听一个值的变化。通知不局限于监听属性的变化,还可以对多种多样的状态变化进行监听,监听范围广,使用更灵活,即可以监听任何你感兴趣的东西。
4.使用场景:KVO 的一般使用场景是监听数据变化,通知是全局通知。
5.2.KVO 发出消息由系统控制,通知由开发者控制。
6.KVO 自动记录新旧值变化,通知只能记录开发者传递的参数。

注意:KVO 和 通知 NSNotification 都是 iOS 中观察者模式的一种实现。

6.分类 Category 和类扩展 Extension

https://juejin.cn/post/6844904067987144711

6.1 分类 Category

Category

1.什么是 Category(分类)?

Category(分类)是 OC 2.0 添加的语言特性,又叫类别等,能够在不改变原来类内容的基础上为类增加一些方法,即主要作用是为已经存在的类添加方法。Category 可以做到在既不子类化,也不侵入一个类的源码的情况下,为原有的类添加新的方法,从而实现扩展一个类或者分离一个类的目的。在日常开发中我们常常使用 Category 为已有的类扩展功能。

说明:

1.继承能为已有类增加新的方法,还能直接增加属性,但继承关系增加了不必要的代码复杂度,在运行时,也无法与父类的原始方法进行区分。所以我们可以优先考虑使用自定义 Category。

2.Category 的特性是:可以在运行时阶段动态地为已有类添加新行为。 Category 是在运行时期间决定的,而成员变量的内存布局已经在编译阶段确定好了,如果在运行时阶段添加成员变量的话,就会破坏原有类的内存布局,从而造成可怕的后果,所以 Category 无法添加成员变量。

3.Category 在运行期决议的,除了为已经存在的类添加方法之外 apple 还推荐了 category 的另外两个使用场景:第一种:可以把类的实现分开在几个不同的文件里面。这样做有几个显而易见的好处,1)可以减少单个文件的体积;2)可以把不同的功能组织到不同的category里;3)可以由多个开发者共同完成一个类;4)可以按需加载想要的category 等等。第二种:声明私有方法。不过除了apple推荐的使用场景,广大开发者脑洞大开,还衍生出了category的其他几个使用场景:1).模拟多继承,2).把framework的私有方法公开。

相关链接:
https://juejin.cn/post/6844903461260263431
http://tech.meituan.com/DiveIntoCategory.html

2. Category 的作用以及常见的应用场景

  • 把类的不同实现方法分开到不同的文件里。
  • 声明私有方法。
  • 模拟多继承。
  • 将 framework 私有方法公开化。
Category 的作用以及常见的应用场景
Category 的使用场合
Category 中都可以添加哪些内容

3. Category 的简单使用

  • 实例代码(以文本框的占位符颜色来简单讲解)
然后在需要的控制器中去导入头文件,然后直接调用方法就可实现(打断点处即是)
Category 的使用注意
Category 的优缺点、特点、注意点

4. Category 的底层原理

https://juejin.cn/post/6844903896708562952

Category 的实现原理

5.关于 Category 的几个提问

通过探索 Category 底层原理回答以下问题:

1.Category 是否可以添加方法、属性、成员变量?Category 是否可以遵守 Protocol?

2.Category 的本质是什么,在底层是怎么存储的?

3.Category 的实现原理是什么,Catagory 中的方法是如何调用到的?

4.Category 中是否有 Load 方法,load 方法是什么时候调用的?

5.Category 中 load、initialize 的区别?

相关链接:https://www.jianshu.com/p/ecc9873a3d8e

  • 1.Category 能不能添加成员变量呢?如何给 Category 添加成员变量?

因为分类底层结构的限制,不能直接给 Category 添加成员变量,但是可以通过关联对象间接实现 Category 有成员变量的效果。传送门:OC - Association 关联对象

Category 不能添加成员变量
Category 不能添加成员变量
Category 不能添加成员变量
  • 2.为什么分类中属性不会自动生成 setter、getter 方法的实现,不会生成成员变量,也不能添加成员变量?

因为类的内存布局在编译的时候会确定,但是分类是在运行时才加载,在运行时Runtime会将分类的数据,合并到宿主类中。

  • 3.Category 能不能添加属性(property)呢?
Category 允许添加属性但无法使用
  • 4.Category 中有 +load 方法吗?+load 方法是什么时候调用的?+load 方法能继承吗?

分类中有 +load 方法;
+load 方法在 Runtime 加载类、分类的时候调用;
+load 方法可以继承,但是一般情况下不会手动去调用 +load 方法,都是让系统自动调用。

传送门:OC - load 和 initialize

  • 5.Category 中 load、initialize 的区别?

https://juejin.cn/post/7038890265028853791

Category 中 load 和 initialize 区别
Category 中 load 和 initialize 相关面试题
Category 中 load 和 initialize 小结
Category 中 load 和 initialize 小结
  • 6.为什么将以前的方法列表挪动到新的位置用 memmove 呢?

为了保证挪动数据的完整性。而将分类的方法列表合并进来,不用考虑被覆盖的问题,所以用 memcpy 就好。

  • 7.为什么优先调用最后编译的分类的方法?

attachCategories() 方法中,从所有未完成整合的分类取出分类的过程是倒序遍历,最先访问最后编译的分类。然后获取该分类中的方法等列表,添加到二维数组中,所以最后编译的分类中的数据最先加到分类二维数组中,最后插入到宿主类的方法列表前面。而消息传递过程中优先查找宿主类中靠前的元素,找到同名方法就进行调用,所以优先调用最后编译的分类的方法。

  • 8objc_class 结构体中的 baseMethodList 和 methods 方法列表的区别?

baseMethodList 基础的方法列表是 ro 只读的,不可修改,可以看成是合并分类方法列表前的methods的拷贝;而 methods 是 rw 可读写的,将来运行时要合并分类方法列表。

6.2 扩展 Extension

Extension

1. 什么是 Extension(扩展)?

① Extension 有一种说法叫“匿名分类”,因为它很像分类,但没有分类名,严格来说要叫类扩展。
② Extension 的作用是将原来放在 .h 中的数据放到 .m 中去,私有化,变成私有的声明。
③ Extension 是在编译的时候就将所有数据都合并到类中去了(编译时决议),而 Category 是在程序运行的时候通过 Runtime 机制将所有数据合并到类中去(运行时决议)。

说明:

1.Extension 有时候也被称为匿名分类,看起来和 Category 有点相似但两者实质上是不同的东西。

2.Extension 是在编译阶段与该类同时编译的,是类的一部分。而且 Extension 中声明的方法只能在该类的 @implementation 中实现,这也就意味着,你无法对系统的类(例如 NSString 类)使用 Extension。

3.Extension 不但可以声明方法,还可以声明成员变量,这是 Category 所做不到的。

2. Extension 作用

① 声明私有属性
② 声明私有方法
③ 声明私有成员变量

3. Extension 特点

① 编译时决议(在编译的时候就将扩展的所有数据都合并到类中去了)
② 只以声明的形式存在,多数情况下寄生于宿主类的 .m 中
③ 不能为系统类添加扩展

6.3 Category 与 Extension 区别

https://www.cnblogs.com/stevenwuzheng/p/8205321.html

Category 与 Extension 区别
Category 与 Extension 区别

7.类方法、实例方法、构造方法

https://blog.csdn.net/lianai911/article/details/103400835
https://juejin.cn/post/6844903558744113160

类方法(静态方法)

类方法:也称静态方法或者工厂方法,以 + 开头,在 C++ 中指的是用 static 关键字修饰的方法。类方法属于整个类,是属于类本身的方法,不属于类的某一个实例对象,不需要实例化类,用类名即可使用,通过类方法将消息发送给类。在项目中工具类的封装多用工厂方法调用,调用格式 [类名 类方法]。类方法也可以同样传参数进去,比如进行网络请求时候,可以把网络请求封装,传入不同的请求体即可。

类方法优点:
1.节约空间:因为调用类方法不需要创建对象,所以节约了空间。
2.提高效率:调用类方法不需要拐弯抹角,直接找到类。

注意:
1.类方法不可以使用实例变量,类方法可以使用 self,因为 self 不是实例变量。
2.在类方法中不能直接调用类的属性,不可以调用实例方法,但是类方法可以通过创建对象来访问实例方法。
3.类方法创造的对象要不要用 release 释放:不需要,因为这个对象被放到自动释放池中,在 ARC 中已经不需要考虑这个问题了。

类方法说明
类方法说明
类方法举例

实例方法(对象方法)

实例方法:也称对象方法,以 - 开头,在 C++ 中指的是不用 static 关键字修饰的方法,它属于类的某一个或某几个实例对象,即类对象必须实例化后才可以使用的方法,调用方式 [对象名 对象方法]。

对象方法声明
对象方法举例

构造方法

构造方法:初始化对象的方法。一般情况下在 OC 当中创建1个对象分为两部分(new 做的事):1.alloc:分配内存空间,2.init:初始化对象。构造方法分为系统自带和自定义构造方法,如果是系统自带的构造方法需要重写父类中自带的构造方法比如 init。如果是自定义构造方法,属于对象方法那么以 - 号开头,返回值一般为 id 或者 instancetype 类型,方法名一般以 init 开头。

构造方法的作用是:

1.用作初始化对象的成员变量
2.把 C 语言指针初始化为 NULL
3.把 OC 对象初始化为 nil
4.把基本数据类型初始化为0

相关链接:
http://t.zoukankan.com/sleepingSun-p-5123931.html
https://www.jianshu.com/p/482290b19b7a

  • 系统构造方法举例:
- (instancetype)init {
  self = [super init];
  if (self) {     

  }
  return self;
}
  • 自定义构造方法举例:
@property int age;//年龄
@property NSString *name;//姓名

//自定义构造方法,在初始化的时候为属性"年龄"和"姓名"赋值
- (instancetype)initWithAge:(int)age andName:(NSString *)name;

//实现自定义构造函数,在初始化的时候为属性赋值
- (id)initWithAge:(int)age andName:(NSString *)name {
  if (self = [super init]) {
    _age = age;
    _name = name;
  }
  return self;
}

iOS 类方法与实例方法区别

类方法与实例方法区别

https://blog.csdn.net/C_philadd/article/details/114526484

类方法与实例方法区别
  • 类方法

1.类方法以+号开头。
2.类方法是属于类的,只能由类来调用,直接[类名 类方法名]调用,对象不能调用。
3.类方法不能访问实例变量(成员变量)。
4.类方法中不能直接调用对象方法,要想调用对象方法必须创建或传入对象。

  • 实例方法

1.对象方法以-号开头。
2.对象方法是属于对象的,只能用对象来调用,创建对象后[对象名 对象方法]调用,没有对象该方法无法执行。
3.对象方法能访问实例变量(成员变量)。
4.对象方法中可以调用当前对象的对象方法,也可以调用其他对象的对象方法。
5.对象方法中不可以调用类方法。

  • 其他说明

1.当不需要访问成员变量的时候,尽量用类方法,并且类方法执行效率更高。
2.类方法和实例方法可同名:类方法存储在元类里,元类的结构里有存储类方法列表的数据结构,所以类方法和对象方法可以同名,并且也遵循 OC 的消息转发机制。
3.举例说明:Person *p1 = [Person new]; [Person 方法名] 就是类方法,[p1 方法名] 就是对象方法。

构造方法和实例方法的区别

1.iOS 中构造方法是指和类同名,用于构造对象(即生成对象)的方法。而实例方法指的是在实例生成之后,实例调用的方法。
2.构造方法 -> 构造实例 -> 实例产生 -> 调用实例方法。详细来说就是:类调用构造方法,来生成了一个实例,而这个实例产生了以后,才会调用实例方法来完成一些行为。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容