![1081255447.jpg 640?wx_fmt=jpeg](https://i-blog.csdnimg.cn/blog_migrate/021a8d0db4d9d79066f9da0c4e0e3818.jpeg)
作者丨彭序猿(粉丝投稿)
https://www.jianshu.com/p/c076f9649a21
在 iOS 中,判断两个对象是否相等,一般调用 isEqual 方法或者是 "变型" 方法(isEqualToString 等)。用 == 来判断两个对象是否相等,其实是判断两个对象的地址是否相等,这个是我们需要注意的。
根据业务需求,自定义对象可能需要根据自身某个属性来判断是否相等(例如:根据对象 id 来判断两个对象是否相等),但是 isEqual 系统默认实现是比较两个对象的指针,这个时候我们就需要重写对象的 isEqual 方法来实现自身逻辑。
hash 方法的存在,是因为将对象加到 NSSet 等集合中时,需要利用对象的 Hash 值来标示对象在集合中的位置,将集合查找元素的时间复杂度优化成 O(1)。对于 Hash 值,系统默认是返回该对象的内存地址。
重写对象的 isEqual 方法
下面是重写对象 isEqual 方法代码:
- (BOOL)isEqual:(id)object {
//1. == 判断地址
if (self == object) return YES;
//2.isKindOfClass 判断对象类型
if (![object isKindOfClass:[self class]]) return NO;
//3. 进行业务逻辑判断
return [self isEqualToFather:(Father *)object];
}
- (BOOL)isEqualToFather:(Father *)object {
//业务逻辑
if ([self.name isEqualToString:object.name]) {
return YES;
}else {
return NO;
}
}
重写 isEqual 方法不是很难,只要根据自身的业务逻辑去实现就可以了。在这里,我们先判断对象地址是否相等,再判断对象类型,最后进行业务逻辑的判断,这样子可以更加高效、安全的去实现 isEqual 方法。
重写对象 hash 方法
我们知道 NSSet 不会添加重复元素,所以添加元素时候会判断对象是否与集合中的元素相等,流程如下:
1.判断集合内的 hash 值是否和目标对象 hash 值一致,如果不一致则添加该对象,一致则进入第二步
2.调用 isEqual 方法来判断对象是否一致,如果不一致则添加该对象,一致则不添加
这里我们可以知道:Hash 值是判断对象是否相等的充分非必要条件。
对于计算对象的 Hash 值,我们应该做到快速、重复率低、均匀等特性。Mattt 大神说:实际上,对于关键属性的散列值进行一个简单的 XOR操作,就能够满足在 99% 的情况下的需求了。具体可以看文末参考链接。
对象 isEqual 和 hash 方法需要同时重写
很多时候为了图方便,只会重写 isEqual 方法,忽略 hash 方法,这里我们看看下面这种情况:
重写 isEqual 方法,hash 方法没重写
这个时候会出现 isEqual 判断两个对象相同,但是 hash 值不同,但是这两个对象在 Set 集合中可以同时存在,这个在业务逻辑上是不合理的。
只要弄清楚 isEqual 和 hash 两个方法存在的意义,且什么时候调用,就可以合理的重写这两个方法了。
参考文献
iOS开发 之 不要告诉我你真的懂isEqual与hash!
https://www.jianshu.com/p/915356e280fc
Equality
https://nshipster.cn/equality/
Implementing Equality and Hashing
https://www.mikeash.com/pyblog/friday-qa-2010-06-18-implementing-equality-and-hashing.html
前言
在 iOS 中,常见的事件有:触摸事件、加速计事件、远程控制事件等。在这里我们主要讨论触摸事件,对于触摸事件的传递流程,我们需要先了解响应者对象和响应者链是什么,这样子才可以更加清晰的认识事件的传递流程和响应流程,然后再利用这些知识点来解决业务需求。
响应者对象
只有响应者对象才可以接收处理事件,在 iOS 中,只有 UIResponder 及其子类称为响应者对象,平时我们的 UIApplication、UIViewController、UIView 都是继承自 UIResponder,所以它们都是响应者对象,可以接收处理事件。对于 CALayer 不是继承自 UIResponder 的,这就是为什么 CALayer 没有响应事件的能力。
对于触摸事件,UIResponder 提供了下面方法来处理触摸事件:
-- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);
触摸事件的产生和传递
用户触摸屏幕产生事件,系统将事件交给 UIApplication 管理分发,UIApplication 将事件分发给 KeyWindow,然后再寻找出一个最合适的响应者来响应这个事件。
如何寻找出最合适的响应者,主要依靠下面两个函数:
//返回最合适的 View 来响应事件
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
// 判断当前的触摸点是否在 View 中
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
这里引用 初探 iOS 事件分发机制 解释:
Hit-Test View:当用户与触摸屏产生交互时,硬件就会探测到物理接触并且通知操作系统。操作系统就会创建相应的事件,并将其传递给当前正在运行的应用程序的事件队列。然后这个事件会被事件循环传递给优先响应对象,既 Hit-Test View
Hit-Testing:Hit-Test View 就是事件被触发时和用户交互的对象,寻找 Hit-Test View 的过程就叫做 Hit-Testing
现在我们知道事件的传递是靠上面两个方法来寻找最合适的响应者,找到响应者后会调用响应者的 touch 函数进行事件处理,大概流程是:
产生触摸事件 -> UIApplication 事件队列 -> [UIWindow hitTest:withEvent:] -> 返回更合适的view -> [子控件 hitTest:withEvent:] -> 返回最合适的view -> [Application sendEvent] -> 调用最合适 view 的 touch 函数处理事件
响应者链及事件响应流程
页面的控件具有层级关系,响应者也会有层级关系,由响应者组成层级关系称为响应者链。UIResponder 中有个 nextResponder 属性返回下一个响应者对象。当一个响应者接收到事件但是不能处理时候,会交给下一个响应者去处理,最终要是谁都处理不了该事件,则会抛弃这个事件。
对于响应者链,可以参考下图:
事件传递和事件响应区别
事件传递是从父控件到子控件传递,从上到下;事件响应是顺着响应者链向上传递(从子控件到父控件),从下到上。
实战-子视图和父视图同时处理事件
子视图重写 touch 函数来处理事件,然后再调用 super touch 将事件传递给父视图:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%s",__func__);
//子视图处理该事件
//调用 super 让父视图也处理该事件
[super touchesBegan:touches withEvent:event];
}
实战-扩大一个视图的点击范围
可以通过 pointInside 函数,将该视图周围的触摸事件也当成自己的事件处理:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
CGRect relativeFrame = self.bounds;
UIEdgeInsets hitTestEdgeInsets = UIEdgeInsetsMake(-15, -15, -15, -15);
CGRect hitFrame = UIEdgeInsetsInsetRect(relativeFrame, hitTestEdgeInsets);
return CGRectContainsPoint(hitFrame, point);
}
实战-深层级 View 通讯
假设控制器上面添加 AView,AView 添加了 BView,BView 又添加了 CView 等,在 CView 产生了一个事件需要让控制器来处理,这个时候如果用 Block、Delegate、Notification 都会比较麻烦,这个时候可以通过响应者链,将消息传递上去。
1.首先我们为 UIResponder 写个分类方法,类似 Router 方法
2.只需要在 CView 中调用该方法,让控制器去监听该方法就 OK 了
具体代码实现:
//UIResponder 分类实现
/**
发送一个路由器消息, 对eventName感兴趣的 UIResponsder 可以对消息进行处理
@param eventName 发生的事件名称
@param userInfo 传递消息时, 携带的数据, 数据传递过程中, 会有新的数据添加
*/
- (void)routerEventWithName:(NSString *)eventName userInfo:(NSObject *)userInfo {
[[self nextResponder] routerEventWithName:eventName userInfo:userInfo];
}
//CView 调用
[self routerEventWithName:@"CViewEvent" userInfo:nil];
//控制器监听
- (void)routerEventWithName:(NSString *)eventName userInfo:(NSObject *)userInfo {
NSLog(@"%s eventName:%@",__func__,eventName);
}
实战-HitTest 大概实现
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (self.alpha <= 0.01 || self.userInteractionEnabled == NO || self.hidden) {
return nil;
}
BOOL inside = [self pointInside:point withEvent:event];
if (inside) {
NSArray *subViews = self.subviews;
// 对子视图从上向下找
for (NSInteger i = subViews.count - 1; i >= 0; i--) {
UIView *subView = subViews[i];
CGPoint insidePoint = [self convertPoint:point toView:subView];
UIView *hitView = [subView hitTest:insidePoint withEvent:event];
if (hitView) {
return hitView;
}
}
return self;
}
return nil;
}
这篇我们主要了解了响应者对象是什么,事件的传递流程以及事件响应流程。了解了这些知识后,还是对我们平时开发有所帮助的。
参考文献
对于更加详细的介绍,可以看看后面的博客链接。
史上最详细的iOS之事件的传递和响应机制-原理篇
https://www.jianshu.com/p/2e074db792ba
深入浅出iOS事件机制
https://zhoon.github.io/ios/2015/04/12/ios-event.html
iOS事件处理,看我就够了~
https://segmentfault.com/a/1190000013265845
推荐↓↓↓
![640?wx_fmt=png](https://i-blog.csdnimg.cn/blog_migrate/43ad5a9db68807cba1574dedf3368bcf.jpeg)
长
按
关
注
?【16个技术公众号】都在这里!
涵盖:程序员大咖、源码共读、程序员共读、数据结构与算法、黑客技术和网络安全、大数据科技、编程前端、Java、Python、Web编程开发、Android、iOS开发、Linux、数据库研发、幽默程序员等。