一个UIWindow引起的奇怪问题

简介: # 问题现象在2月20日接到了一个bug,系统分享页面无法响应事件。比如在POI详情页,点击底部的分享-更多-备忘录,在这个界面(下图)会使任何交互失效,除了杀app别无他法。![](https://ata2-img.oss-cn-zhangjiakou.aliyuncs.com/neweditor/2fd68bb3-8447-4c41-9933-b05ae964339b.png)# 问题定

问题现象

在2月20日接到了一个bug,系统分享页面无法响应事件。比如在POI详情页,点击底部的分享-更多-备忘录,在这个界面(下图)会使任何交互失效,除了杀app别无他法。

问题定位

根据先期排查分析,发现出现该问题的前置条件是唤醒过小德,或者从行中退出。从UI层级上可以看到,前置操作之后,会创建用于承载小德面板的window——AMapVUIWindow,一个尺寸和当前屏幕一样的window,同时从代码中删掉创建AMapVUIWindow的逻辑之后,问题也的确不再复现,所以基本可以断定,是由于这个window导致的bug。

问题分析

首先,从代码上看,AMapVUIWindow因为重写了- hitTest:withEvent:方法

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    UIView *hitView = [super hitTest:point withEvent:event];
    return (hitView == self)?nil:hitView;
}

理论上来说,他本身已经不参与事件响应了,除非点击区域在他的某个子视图范围内(然而我们的这个bug显然是点击在了空白区域,断点查看也是如此)。
为了找到问题的根本原因,排除其他干扰,我新建了一个空工程,只在启动阶段额外创建一个window,然后调起系统分享,来重现问题现场。结果同样在备忘录的界面出现无法交互的情况。最初我认为是对UIWindow的使用不正确导致的,所以仔细阅读了相关的文档,并且尝试了几乎所有可能修改的设置,但均无进展。
于是我想到了事件处理的响应者链,看看到底是谁在响应事件。第一个发现是个意外。由于demo是在viewController的- touchesBegan:withEvent:方法中调起分享的。当进入备忘录分享页面的时候,虽然界面失去响应,但是点击时,控制台出现了下面的内容:

也就是说,当前的viewController响应了点击时间!而众所周知,viewController的事件是window发给他的,所以接下来我使用重写了- sendEvent:方法的自定义window来看看到底是谁在发事件。结果是,从头到尾,所有的点击事件都是由默认的主window发出的,模拟小德的window就从来没发过事件(其实这也是符合预期的)。
那这就奇怪了,为什么加了一个不处理事件的window,分享页面就不能点击了呢?吃完午饭之后,突然来了灵感,window的事件是谁发的?是UIApplication,那我看看UIApplication把事件发给谁了。二话不说方法交换走起:

@implementation UIApplication (asdfaew)

+ (void)load {
    Method viewMethod = class_getInstanceMethod([UIApplication class], @selector(sendEvent:));
    Method toViewMethod = class_getInstanceMethod([UIApplication class], @selector(aaas_sendEvent:));
    method_exchangeImplementations(viewMethod, toViewMethod);
}

- (void)aaas_sendEvent:(UIEvent *)event {
    [self aaas_sendEvent:event];
}

@end

不出所料,事件都是从UIApplication发出,值得注意的是,在备忘录分享的界面,事件发给了一个叫_UISizeTrackingView的私有类,查看视图层级,发现他还有个子view叫_UIRemoteView,然后就没了。此时我内心中稍微升起了一点疑惑,为啥备忘录的那个界面这里看不到了,即使代码查看_UIRemoteView的subviews也是空的。

接下来我又尝试单window,不添加小德window的情况。当然分享页面是可以正常响应的。但是精彩的部分来了,分享页面的点击事件不是UIApplication发出的!也就是说,备忘录的那个分享页面不属于当前的进程。
冷静下来仔细分析,我分析问题产生的原因是这样的。
首先,系统分享页面是单独的进程,不属于当前的应用,所以事件会由其所在的进程分发。我们创建的小德window始终位于顶部,而弹出分享页面的window在小德window的下面,于是当系统分享页面出现时,它在层级上也仍然处于小德window的下方(从视觉上也是如此,例如demo中小德window是半透明红色,与是在分享页面也会被半透明的红色所覆盖)。当点击屏幕时,事件分发给哪个进程,取决于最上层的View属于哪个进程(推测)。而由于小德window处于分享页面之上,所以虽然它自己不处理事件,但由于它的影响,事件被发到了它所在的进程,于是分享页面就没有机会处理点击事件了。

解法

不要在没有必要的情况下,在App的主window上层再添加window。对于小德语音,会修改为在对话面板关闭后移除window。

目录
相关文章
|
iOS开发
IOS AlertView 自动消失
IOS AlertView 自动消失
31 0
|
iOS开发
iOS CollectionView reloadData 闪烁解决办法
iOS CollectionView reloadData 闪烁解决办法
872 0
UIWindow 原理与巧妙使用 makeKeyAndVisible、makeKeyWindow、becomeKeyWindow、resignKeyWindow.你真的懂了吗?
UIWindow 简介 UIWindow 是 UIView 的子类,其在 UIView 添加了一些视图层级,管理视图,转发 UIEvent 对象的属性和 Method。
3160 0
|
iOS开发
iOS开发之UIView与UIViewController的生命周期总结
iOS开发中,创建View常见的两种方式一个是纯代码,一个是借助于XIB;创建ViewController常见的也有两种方式一个是纯代码,一个是借助于StoryBoard。
1237 0
|
iOS开发 索引 编解码
有关UIView、subview的几个基础知识点-IOS开发 (实例)
首先要弄懂几个基本的概念。   一)三个结构体:CGPoint、CGSize、CGRect   1.  CGPoint C代码   /* Points. */          struct CGPoint {       CGFloat x;       CGFloat y;     };     typedef struct CGPoint CGPoint;   看到这个想必你已经懂了,不再解释。
1734 0