iOS 面试题实现 isEqual 和 hash 方法时要注意什么?触摸事件传递流程描述

640?wx_fmt=gif

640?wx_fmt=jpeg

Linux编程 点击右侧关注,免费入门到精通! 640?wx_fmt=jpeg



作者丨彭序猿(粉丝投稿)

https://www.jianshu.com/p/c076f9649a21


640?wx_fmt=gif

前言


在 iOS 中,判断两个对象是否相等,一般调用 isEqual 方法或者是 "变型" 方法(isEqualToString 等)。用 == 来判断两个对象是否相等,其实是判断两个对象的地址是否相等,这个是我们需要注意的。


根据业务需求,自定义对象可能需要根据自身某个属性来判断是否相等(例如:根据对象 id 来判断两个对象是否相等),但是 isEqual 系统默认实现是比较两个对象的指针,这个时候我们就需要重写对象的 isEqual 方法来实现自身逻辑。


hash 方法的存在,是因为将对象加到 NSSet 等集合中时,需要利用对象的 Hash 值来标示对象在集合中的位置,将集合查找元素的时间复杂度优化成 O(1)。对于 Hash 值,系统默认是返回该对象的内存地址。


640?wx_fmt=gif重写对象的 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 方法。


640?wx_fmt=gif重写对象 hash 方法


我们知道 NSSet 不会添加重复元素,所以添加元素时候会判断对象是否与集合中的元素相等,流程如下:


1.判断集合内的 hash 值是否和目标对象 hash 值一致,如果不一致则添加该对象,一致则进入第二步

2.调用 isEqual 方法来判断对象是否一致,如果不一致则添加该对象,一致则不添加

这里我们可以知道:Hash 值是判断对象是否相等的充分非必要条件。


对于计算对象的 Hash 值,我们应该做到快速、重复率低、均匀等特性。Mattt 大神说:实际上,对于关键属性的散列值进行一个简单的 XOR操作,就能够满足在 99% 的情况下的需求了。具体可以看文末参考链接。


640?wx_fmt=gif对象 isEqual 和 hash 方法需要同时重写


很多时候为了图方便,只会重写 isEqual 方法,忽略 hash 方法,这里我们看看下面这种情况:


640?wx_fmt=gif重写 isEqual 方法,hash 方法没重写


这个时候会出现 isEqual 判断两个对象相同,但是 hash 值不同,但是这两个对象在 Set 集合中可以同时存在,这个在业务逻辑上是不合理的。


640?wx_fmt=gif

总结


只要弄清楚 isEqual 和 hash 两个方法存在的意义,且什么时候调用,就可以合理的重写这两个方法了。


640?wx_fmt=gif参考文献


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


640?wx_fmt=gif

描述一下触摸事件传递流程


640?wx_fmt=gif前言


在 iOS 中,常见的事件有:触摸事件、加速计事件、远程控制事件等。在这里我们主要讨论触摸事件,对于触摸事件的传递流程,我们需要先了解响应者对象和响应者链是什么,这样子才可以更加清晰的认识事件的传递流程和响应流程,然后再利用这些知识点来解决业务需求。


640?wx_fmt=gif响应者对象


只有响应者对象才可以接收处理事件,在 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);


640?wx_fmt=gif触摸事件的产生和传递


用户触摸屏幕产生事件,系统将事件交给 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 函数处理事件


640?wx_fmt=gif响应者链及事件响应流程


页面的控件具有层级关系,响应者也会有层级关系,由响应者组成层级关系称为响应者链。UIResponder 中有个 nextResponder 属性返回下一个响应者对象。当一个响应者接收到事件但是不能处理时候,会交给下一个响应者去处理,最终要是谁都处理不了该事件,则会抛弃这个事件。


对于响应者链,可以参考下图:


640?wx_fmt=other


640?wx_fmt=gif事件传递和事件响应区别


事件传递是从父控件到子控件传递,从上到下;事件响应是顺着响应者链向上传递(从子控件到父控件),从下到上。


640?wx_fmt=gif实战-子视图和父视图同时处理事件


子视图重写 touch 函数来处理事件,然后再调用 super touch 将事件传递给父视图:


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"%s",__func__);
    //子视图处理该事件

    //调用 super 让父视图也处理该事件
    [super touchesBegan:touches withEvent:event];
}


640?wx_fmt=gif实战-扩大一个视图的点击范围


可以通过 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);
}


640?wx_fmt=gif实战-深层级 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);
}


640?wx_fmt=gif实战-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;
}


640?wx_fmt=gif

总结


这篇我们主要了解了响应者对象是什么,事件的传递流程以及事件响应流程。了解了这些知识后,还是对我们平时开发有所帮助的。


640?wx_fmt=gif参考文献


对于更加详细的介绍,可以看看后面的博客链接。


史上最详细的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

?16个技术公众号】都在这里!

涵盖:程序员大咖、源码共读、程序员共读、数据结构与算法、黑客技术和网络安全、大数据科技、编程前端、Java、Python、Web编程开发、Android、iOS开发、Linux、数据库研发、幽默程序员等。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值