美文网首页iOS备忘录环信
环信源码分析---聊天页面相关的事件分发

环信源码分析---聊天页面相关的事件分发

作者: Code_Ninja | 来源:发表于2016-03-07 18:41 被阅读894次

    今日研究了下项目中使用的环信SDK源码中的聊天模块,重点研究了下其中的事件分发的实现。结合最近在看的《Effective Objective-C 2.0》中讲到的事件分发,对iOS事件分发机制有了更深入的了解。

    一、环信类的封装

    用于展示聊天消息的单元格Cell之间的继承关系如下:

    而EMChatViewBaseCell拥有一个EMChatBaseBubbleView类型的属性。EMChatBaseBubbleView类如下:

    因为消息分很多种类型,所以具体消息的展现用EMChatBaseBubbleView子类来实现的,EMChatBaseBubbleView有如下子类,分别实现对应消息的展现以及事件的处理:

    EMChatViewBaseCell有一个属性MessageModel,EMChatViewBaseCell会根据MessageModel类型来创建与之对应的上面五个EMChatBaseBubbleView的子类中的一个。

    二、环信事件分发流程

    1、环信给UIResponder类创建了一个分类,并添加了一个新的分发事件的分发:

    #import <UIKit/UIKit.h>
    
    @interface UIResponder (Router)
    
    /**
     *  发送一个路由器消息, 对eventName感兴趣的 UIResponsder 可以对消息进行处理
     *
     *  @param eventName 发生的事件名称
     *  @param userInfo  传递消息时, 携带的数据, 数据传递过程中, 会有新的数据添加
     *
     */
    - (void)routerEventWithName:(NSString *)eventName userInfo:(NSDictionary *)userInfo;
    
    @end
    
    
    #import "UIResponder+Router.h"
    
    @implementation UIResponder (Router)
    
    - (void)routerEventWithName:(NSString *)eventName userInfo:(NSDictionary *)userInfo
    {
        [[self nextResponder] routerEventWithName:eventName userInfo:userInfo];
    }
    
    @end
    
    

    2、点击事件传递

    在EMChatViewCell中有一个点击重发聊天消息的按钮事件:

    // 重发按钮事件
    -(void)retryButtonPressed:(UIButton *)sender
    {
        [self routerEventWithName:kResendButtonTapEventName
                           userInfo:@{kShouldResendCell:self}];
    }
    

    其实是走的其父类EMChatViewBaseCell中的实例方法:

    
    //点击头像事件
    -(void)headImagePressed:(id)sender
    {
        [super routerEventWithName:kRouterEventChatHeadImageTapEventName 
        userInfo:@{KMESSAGEKEY:self.messageModel}];
    }
    
    -(void)routerEventWithName:(NSString *)eventName userInfo:(NSDictionary *)userInfo
    {
        [super routerEventWithName:eventName userInfo:userInfo];
    }
    
    //注意,这里都是用的super 而不是self,
    因为当前类中也实现了一个同名routerEventWithName:userInfo:方法。
    调用super,走NSResponder+Ronter中添加的方法。
    

    然后在EMChatBaseBubbleView的五个子类中分别具体分发相关的点击事件,比如播放音频的Cell中:

    -(void)bubbleViewPressed:(id)sender
    {
        [self routerEventWithName:kRouterEventAudioBubbleTapEventName 
        userInfo:@{KMESSAGEKEY:self.model}];
    }
    
    

    3、事件最终捕获与处理

    上面说的UITableViewCell都是展示在聊天页面UITableView中的,在聊天页面ChatViewController中实现了捕获各种类型的点击事件并具体实现。

    - (void)routerEventWithName:(NSString *)eventName userInfo:(NSDictionary *)userInfo
    {
        MessageModel *model = [userInfo objectForKey:KMESSAGEKEY];
        // 点击url链接
        if ([eventName isEqualToString:kRouterEventTextURLTapEventName]) {
            [self chatTextCellUrlPressed:[userInfo objectForKey:@"url"]];
        }
        // 点击音频
        else if ([eventName isEqualToString:kRouterEventAudioBubbleTapEventName]) {
            [self chatAudioCellBubblePressed:model];
        }
        // 点击图片
        else if ([eventName isEqualToString:kRouterEventImageBubbleTapEventName]){
            [self chatImageCellBubblePressed:model];
        }
        // 点击地理位置
        else if ([eventName isEqualToString:kRouterEventLocationBubbleTapEventName]){
            [self chatLocationCellBubblePressed:model];
        }
        // 点击重发
        else if([eventName isEqualToString:kResendButtonTapEventName]){
            EMChatViewCell *resendCell = [userInfo objectForKey:kShouldResendCell];
            MessageModel *messageModel = resendCell.messageModel;
            if ((messageModel.status != eMessageDeliveryState_Failure) && (messageModel.status != eMessageDeliveryState_Pending))
            {
                return;
            }
            id <IChatManager> chatManager = [[EaseMob sharedInstance] chatManager];
            [chatManager asyncResendMessage:messageModel.message progress:nil];
            NSIndexPath *indexPath = [self.tableView indexPathForCell:resendCell];
            [self.tableView beginUpdates];
            [self.tableView reloadRowsAtIndexPaths:@[indexPath]
                                  withRowAnimation:UITableViewRowAnimationNone];
            [self.tableView endUpdates];
        }
        // 点击视频
        else if([eventName isEqualToString:kRouterEventChatCellVideoTapEventName]){
            [self chatVideoCellPressed:model];
        }
    }
    
    

    三、iOS事件分发机制

    1、hit-Testing

    iOS中的事件大概分为三种,分别是 Milti-Touch Events, Motion Events 和Remote Control Events(events for controlling multimedia)。

    每当我们点击了一下iOS设备的屏幕,UIKit就会生成一个事件对象UIEvent,然后会把这个Event分发给当前active的app。

    告知当前活动的app有事件之后,UIApplication 单例就会从事件队列中去取最新的事件,然后分发给能够处理该事件的对象。UIApplication 获取到Event之后,Application就纠结于到底要把这个事件传递给谁,这时候就要依靠HitTest来决定了。

    iOS中,hit-Testing的作用就是找出这个触摸点下面的View是什么,HitTest会检测这个点击的点是不是发生在这个View上,如果是的话,就会去遍历这个View的subviews,直到找到最小的能够处理事件的view,如果整了一圈没找到能够处理的view,则返回自身。如图:

    hit-Testing.jpg

    假设我们现在点击到了图中的E,hit-testing将进行如下步骤的检测(不包含重写hit-test并且返回非默认View的情况)

    1、触摸点在ViewA内,所以检查ViewA的Subview B、C

    2、触摸点不在ViewB内,触摸点在ViewC内部,所以检查ViewC的Subview D、E

    3、触摸点不在ViewD内,触摸点发生在ViewE内部,并且ViewE没有subview,所以ViewE属于ViewA中包含这个点的最小单位,所以ViewE变成了该次触摸事件的hit-TestView

    PS.

    1、默认的hit-testing顺序是按照UIView中Subviews的逆顺序

    2、如果View的同级别Subview中有重叠的部分,则优先检查顶部的Subview,如果顶部的Subview返回nil, 再检查底部的Subview

    3、Hit-Test也是比较聪明的,检测过程中有这么一点,就是说如果点击没有发生在某View中,那么该事件就不可能发生在View的Subview中,所以检测过程中发现该事件不在ViewB内,也直接就不会检测在不在ViewF内。也就是说,如果你的Subview设置了clipsToBounds=NO,实际显示区域可能超出了superView的frame,你点击超出的部分,是不会处理你的事件的,就是这么任性!

    <strong>hit-Test 是事件分发的第一步,就算你的app忽略了事件,也会发生hit-Test。确定了hit-TestView之后,才会开始进行下一步的事件分发。</strong>

    2、The Responder Chain

    响应链简单来说,就是一系列的相互关联的对象,从firstResponder开始,到application对象结束,如果firstResponder 无法响应事件,则交给nextResponder来处理,直到结束为止。iOS中很多类型的事件分发,都依赖于响应链;在响应链中,所有对象的基类都是UIResponder,也就是说所有能响应事件的类都是UIResponder的子类,UIApplication/UIView/UIViewController都是UIResponder的子类,这说明所有的Views,绝大部分Controllers(不用来管理View的Controller除外)都可以响应事件。

    PS.CALayer 不是UIResponder的子类,这说明CALayer无法响应事件,这也是UIView和CALayer的重要区别之一。

    nextResponder的事件传递过程:


    nextResponder的事件传递过程.jpg

    PS.View处理事件的方式有手势或者重写touchesEvent方法或者利用系统封装好的组件(UIControls)。

    图中所表示的正是nextResponder的查找过程,两种方式分别对应两种app的架构,左边的那种app架构比较简单,只有一个VC,右边的稍微复杂一些,但是寻找路线的原则是一样的,先解释一下,UIResponder本身是不会去存储或者设置nextResponder的,所谓的nextResponder都是子类去实现的(这里说的是UIView,UIViewController,UIApplication),关于nextResponder的值总结如下:

    1、UIView的nextResponder是直接管理它的UIViewController(也就是VC.view.nextResponder=VC),如果当前View不是ViewController直接管理的View,则nextResponder是它的superView(view.nextResponder = view.superView)

    2、UIViewController的nextResponder是它直接管理的View的superView(VC.nextResponder = VC.view.superView)

    3、UIWindow的nextResponder是UIApplication

    4、UIApplication的nextResponder是UIApplicationDelegate(官方文档说是nil)

    <strong>
    需要注意的是:

    如果你自己想自定义一个非TouchEvent的事件,当需要继续传递事件的话,切记不要在实现内直接显示的调用nextResponder的对应方法, 而是直接调用父类中对应的事件处理方法并让UIKit来处理响应链的遍历。
    </strong>

    参考:
    1.Event Delivery: The Responder Chain

    2.iOS事件分发机制(一) hit-Testing

    3.iOS事件分发机制(二)The Responder Chain

    相关文章

      网友评论

        本文标题:环信源码分析---聊天页面相关的事件分发

        本文链接:https://www.haomeiwen.com/subject/nhtzkttx.html