关于前端:又翻车了列表点击事件采集那些你不知道的坑

35次阅读

共计 9653 个字符,预计需要花费 25 分钟才能阅读完成。

1. 前言

在上篇七步实现列表点击事件的采集文章中咱们曾经具体介绍了如何在运行时创立子类进行 cell 点击事件采集,本篇将持续探讨在实在场景中所遇到的问题,并一一进行解决。

2. 踩过的坑

2.1. KVO

当咱们对一个对象进行 KVO 属性监听时,零碎也会为该对象的类新建一个 NSKVONotifying_ 结尾的长期类,对于 KVO 的实现可参考苹果官网文档 [1];
当咱们和零碎都为代理对象的类新建子类时,状况就会变得非常复杂。

2.1.1. 场景一

先设置代理对象,而后对代理对象进行 KVO 属性监听,如图 2-1 所示:


图 2-1 场景一的 isa 指针变动过程图
这种场景下会存在下述问题:
零碎在新建 NSKVONotifying_Delegate 类时,也会重写 – class 办法,用于暗藏这个长期类。在这个场景中 NSKVONotifying_Delegate 继承自 SensorsDelegate,因而 – class 办法的返回值为咱们新创建的子类信息,并不是原始类信息。
解决方案:
咱们能够在新建子类后,对 – addObserver:forKeyPath:options:context: 办法进行监听。如果代理对象在咱们新建子类后又进行了 KVO 属性监听,咱们就须要在零碎重写 – class 办法后,再次进行重写,并返回原始类:

[SAMethodHelper addInstanceMethodWithSelector:@selector(addObserver:forKeyPath:options:context:) fromClass:proxyClass toClass:realClass]; – (void)addObserver:(NSObject )observer forKeyPath:(NSString )keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {[super addObserver:observer forKeyPath:keyPath options:options context:context]; if (self.sensorsdata_className) {// 因为增加了 KVO 属性监听, KVO 会创立子类并重写 Class 办法, 返回原始类; 此时的原始类为神策增加的子类, 因而须要重写 class 办法 [SAMethodHelper replaceInstanceMethodWithDestinationSelector:@selector(class) sourceSelector:@selector(class) fromClass:SADelegateProxy.class toClass:[SAClassHelper realClassWithObject:self]]; }}

2.1.2. 场景二

先设置代理对象,而后进行 KVO 属性监听,最初移除 KVO 属性监听,如图 2-2 所示:


图 2-2 场景二的 isa 指针变动过程图
这种场景下没有问题。

2.1.3. 场景三

先对代理对象进行 KVO 属性监听,再进行代理对象的设置,如图 2-3 所示:


图 2-3 场景三的 isa 指针变动过程图
这种场景下会存在下述问题:
在该场景中 SensorsDelegate 继承自 NSKVONotifying_Delegate,这会对系统的 KVO 个性有所影响,在进行属性赋值时会引发解体。
解决方案:
如果代理对象的 isa 指针指向的是一个 NSKVONotifying_ 的类,那咱们便不再新建子类,而是间接重写 NSKVONotifying_ 类中的 – tableView:didSelectRowAtIndexPath: 办法:

if ([SADelegateProxy isKVOClass:realClass]) {[SAMethodHelper addInstanceMethodWithSelector:tablViewSelector fromClass:proxyClass toClass:realClass]; [SAMethodHelper addInstanceMethodWithSelector:collectionViewSelector fromClass:proxyClass toClass:realClass]; return;}

2.1.4. 场景四

先对代理对象进行 KVO 属性监听,再进行代理对象的设置,最初移除 KVO 属性监听,如图 2-4 所示:

图 2-4 场景四的 isa 指针变动过程图

这种场景下会存在下述问题:
在移除 KVO 时,零碎会将代理对象的 isa 指针间接指回原始类,这时便无奈进行点击事件采集了。
解决方案:
在 NSKVONotifying_ 的类中重写 – tableView:didSelectRowAtIndexPath: 办法的同时,对 – removeObserver:forKeyPath: 办法进行监听,在移除 KVO 属性监听时对代理对象再次执行新建子类的操作:

if ([SADelegateProxy isKVOClass:realClass]) {[SAMethodHelper addInstanceMethodWithSelector:@selector(removeObserver:forKeyPath:) fromClass:proxyClass toClass:realClass]; return;} – (void)removeObserver:(NSObject )observer forKeyPath:(NSString )keyPath {// remove 前代理对象是否归属于 KVO 创立的类 BOOL oldClassIsKVO = [SADelegateProxy isKVOClass:[SAClassHelper realClassWithObject:self]]; [super removeObserver:observer forKeyPath:keyPath]; // remove 后代理对象是否归属于 KVO 创立的类 BOOL newClassIsKVO = [SADelegateProxy isKVOClass:[SAClassHelper realClassWithObject:self]]; // 有多个属性监听时, 在最初一个监听被移除后, 对象的 isa 发生变化, 须要从新为代理对象增加子类 if (oldClassIsKVO && !newClassIsKVO) {// 清空曾经记录的原始类 self.sensorsdata_className = nil; [SADelegateProxy proxyWithDelegate:self]; }}

2.1.5. 最终流程

最终解决流程如图 2-5 所示:

图 2-5 解决流程图

2.2. RxSwift

在七步实现列表点击事件的采集文章中曾经提到对于 cell 点击音讯的解决逻辑,对 RxSwift 场景下进行了音讯转发,此时疏忽了一个重要点:
如果应用零碎形式设置了 UITableView 的 delegate,这时 RxSwift 会在外部应用 _forwardToDelegate 持有该 delegate,而后在音讯转发阶段,对该代理对象发送一次音讯,用于保障业务逻辑失常触发。
然而此时咱们曾经为 delegate 创立了子类,重写了 – tableView:didSelectRowAtIndexPath: 办法。因而在 RxSwift 对代理对象发送的音讯会被咱们接管,最终导致办法递归调用引发解体。
音讯发送如图 2-6 所示:

图 2-6 音讯发送过程
参考 _RXDelegateProxy 的源码[2],- forwardInvocation: 的实现如下所示:

  • (void)forwardInvocation:(NSInvocation )anInvocation {BOOL isVoid = RX_is_method_signature_void(anInvocation.methodSignature); NSArray arguments = nil; if (isVoid) {arguments = RX_extract_arguments(anInvocation); [self _sentMessage:anInvocation.selector withArguments:arguments]; } if (self._forwardToDelegate && [self._forwardToDelegate respondsToSelector:anInvocation.selector]) {[anInvocation invokeWithTarget:self._forwardToDelegate]; } if (isVoid) {[self _methodInvoked:anInvocation.selector withArguments:arguments]; }}

既然 RxSwift 外部会在音讯转发时调用 _forwardToDelegate 的 IMP,那么咱们在检测到 _forwardToDelegate 时间接调用 IMP,而不是再次进行音讯转发即可解决该问题,实现逻辑如下:

  • (void)tableView:(UITableView )tableView didSelectRowAtIndexPath:(NSIndexPath )indexPath {SEL methodSelector = @selector(tableView:didSelectRowAtIndexPath:); [SADelegateProxy invokeWithScrollView:tableView selector:methodSelector selectedAtIndexPath:indexPath];} + (void)invokeWithScrollView:(UIScrollView )scrollView selector:(SEL)selector selectedAtIndexPath:(NSIndexPath )indexPath {NSObject delegate = (NSObject )scrollView.delegate; Class originalClass = NSClassFromString(delegate.sensorsdata_className) ?: delegate.class; IMP originalIMP = [SAMethodHelper implementationOfMethodSelector:selector fromClass:originalClass]; if (originalIMP) {((SensorsDidSelectImplementation)originalIMP)(delegate, selector, scrollView, indexPath); } else if ([SADelegateProxy isRxDelegateProxyClass:originalClass]) {NSObject<UITableViewDelegate> *forwardToDelegate = nil; if ([delegate respondsToSelector:NSSelectorFromString(@”_forwardToDelegate”)]) {// 获取 _forwardToDelegate 属性 forwardToDelegate = [delegate valueForKey:@”_forwardToDelegate”]; } if (forwardToDelegate) {Class forwardOriginalClass = NSClassFromString(forwardToDelegate.sensorsdata_className) ?: forwardToDelegate.class; IMP forwardOriginalIMP = [SAMethodHelper implementationOfMethodSelector:selector fromClass:forwardOriginalClass]; if (forwardOriginalIMP) {((SensorsDidSelectImplementation)forwardOriginalIMP)(forwardToDelegate, selector, scrollView, indexPath); } } else {((SensorsDidSelectImplementation)_objc_msgForward)(delegate, selector, scrollView, indexPath); } } // 事件采集 // …}

然而这种解决形式又存在另外一个问题:同时应用零碎形式设置代理和应用订阅的形式订阅点击回调,那么订阅的形式将会有效,因为咱们没有再次进行音讯转发。
批改后的音讯发送如图 2-7 所示:

图 2-7 批改后的音讯发送过程
为了齐全兼容 RxSwift,咱们须要把 _RXDelegateProxy 的 – forwardInvocation: 逻辑实现一遍,间接调用其外部的办法,具体实现如下:

  • (void)tableView:(UITableView )tableView didSelectRowAtIndexPath:(NSIndexPath )indexPath {SEL methodSelector = @selector(tableView:didSelectRowAtIndexPath:); [SADelegateProxy invokeWithScrollView:tableView selector:methodSelector selectedAtIndexPath:indexPath];} + (void)invokeRXProxyMethodWithTarget:(id)target selector:(SEL)selector argument1:(SEL)arg1 argument2:(id)arg2 {Class cla = NSClassFromString([target sensorsdata_className]) ?: [target class]; IMP implementation = [SAMethodHelper implementationOfMethodSelector:selector fromClass:cla]; if (implementation) {void(imp)(id, SEL, SEL, id) = (void()(id, SEL, SEL, id))implementation; imp(target, selector, arg1, arg2); }} /// 执行 RxCocoa 中,点击事件相干的响应办法 /// 这个办法中调用的程序和 _RXDelegateProxy 中的 – forwardInvocation: 办法执行雷同 /// @param scrollView UITableView 或者 UICollectionView 的对象 /// @param selector 须要执行的办法:tableView:didSelectRowAtIndexPath: 或者 collectionView:didSelectItemAtIndexPath:/// @param indexPath 点击的 NSIndexPath 对象 + (void)rxInvokeWithScrollView:(UIScrollView )scrollView selector:(SEL)selector selectedAtIndexPath:(NSIndexPath )indexPath {// 1. 执行 _sentMessage:withArguments: 办法 [SADelegateProxy invokeRXProxyMethodWithTarget:scrollView.delegate selector:NSSelectorFromString(@”_sentMessage:withArguments:”) argument1:selector argument2:@[scrollView, indexPath]]; // 2. 执行 UIKit 的代理办法 NSObject<UITableViewDelegate> forwardToDelegate = nil; SEL forwardDelegateSelector = NSSelectorFromString(@”_forwardToDelegate”); IMP forwardDelegateIMP = [(NSObject )scrollView.delegate methodForSelector:forwardDelegateSelector]; if (forwardDelegateIMP) {forwardToDelegate = ((NSObject<UITableViewDelegate> ()(id, SEL))forwardDelegateIMP)(scrollView.delegate, forwardDelegateSelector); } if (forwardToDelegate) {Class forwardOriginalClass = NSClassFromString(forwardToDelegate.sensorsdata_className) ?: forwardToDelegate.class; IMP forwardOriginalIMP = [SAMethodHelper implementationOfMethodSelector:selector fromClass:forwardOriginalClass]; if (forwardOriginalIMP) {((SensorsDidSelectImplementation)forwardOriginalIMP)(forwardToDelegate, selector, scrollView, indexPath); } } // 3. 执行 _methodInvoked:withArguments: 办法 [SADelegateProxy invokeRXProxyMethodWithTarget:scrollView.delegate selector:NSSelectorFromString(@”_methodInvoked:withArguments:”) argument1:selector argument2:@[scrollView, indexPath]];} + (void)invokeWithScrollView:(UIScrollView )scrollView selector:(SEL)selector selectedAtIndexPath:(NSIndexPath )indexPath {NSObject delegate = (NSObject )scrollView.delegate; // 优先获取记录的原始父类, 若获取不到则是 KVO 场景, KVO 场景通过 class 接口获取原始类 Class originalClass = NSClassFromString(delegate.sensorsdata_className) ?: delegate.class; IMP originalIMP = [SAMethodHelper implementationOfMethodSelector:selector fromClass:originalClass]; if (originalIMP) {((SensorsDidSelectImplementation)originalIMP)(delegate, selector, scrollView, indexPath); } else if ([SADelegateProxy isRxDelegateProxyClass:originalClass]) {[SADelegateProxy rxInvokeWithScrollView:scrollView selector:selector selectedAtIndexPath:indexPath]; } // 事件采集 // …}

2.3. 音讯发送

上一节中尽管对 RxSwift 进行了适配,然而存在许多未知的三方库是通过音讯转发实现 cell 点击响应的,比方 Texture,咱们不能逐个适配每个三方库。
咱们的采集计划的实质是创立了子类。对于子类来说,如果重写了一个父类中的办法,咱们能够通过 super 去调用父类中的办法,而且无需关怀父类中的实现逻辑。若父类未实现,应该由零碎去做音讯转发。
然而 – tableView:didSelectRowAtIndexPath: 办法是定义在 UITableViewDelegate 协定中的,无奈应用 super 关键字,那咱们是否能够应用 runtime 相干接口实现向父类发送音讯呢?答案是必定的。
runtime 提供了 objc_msgSendSuper 的接口,定义如下:

OBJC_EXPORT id _Nullableobjc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, …) OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

super:objc_super 类型的构造体信息;
op:要调用的 selector;
…:selector 的相干参数。
最终的音讯解决逻辑如下:

  • (void)tableView:(UITableView )tableView didSelectRowAtIndexPath:(NSIndexPath )indexPath {SEL methodSelector = @selector(tableView:didSelectRowAtIndexPath:); [SADelegateProxy invokeWithTarget:self selector:methodSelector scrollView:tableView indexPath:indexPath];} + (void)invokeWithTarget:(NSObject )target selector:(SEL)selector scrollView:(UIScrollView )scrollView indexPath:(NSIndexPath )indexPath {Class originalClass = NSClassFromString(target.sensorsdata_className) ?: target.superclass; struct objc_super targetSuper = {.receiver = target, .super_class = originalClass}; // 音讯发送给原始类 void (func)(struct objc_super , SEL, id, id) = (void )&objc_msgSendSuper; func(&targetSuper, selector, scrollView, indexPath); // 当 target 和 delegate 不相等时为音讯转发, 此时无需反复采集事件 if (target != scrollView.delegate) {return;} // 事件采集 // …}
  1. 总结

本文次要对 cell 点击事件采集中所遇到的问题进行了解决,该计划的具体实现能够从神策剖析 iOS SDK 源码中找到。如果大家有更好的想法,欢送退出开源社区一起探讨。

  1. 参考文献

[1]https://developer.apple.com/l…
[2]https://github.com/ReactiveX/…

文章起源:公众号 - 神策技术社区

正文完
 0