One, foreword

In the previous article “7 Steps to Collect List Click events”, we have introduced in detail how to create subclasses to collect cell click events at run time. This article will continue to discuss the problems encountered in real scenes and solve them one by one.

Two, stepped on the pit

2.1 KVO

When we listen for the KVO attribute of an object, the system will also create a temporary class starting with NSKVONotifying_ for the object. For the implementation of KVO, please refer to the apple official website document [1].

Things get very complicated when both we and the system create new subclasses for the classes of proxy objects.

2.1.1. A scene

First set the proxy object, and then monitor the KVO property of the proxy object, as shown in Figure 2-1:

Figure 2-1 ISA pointer change process in scenario 1

There are the following problems in this scenario:

The system also overrides the -class method to hide the temporary class when creating a new NSKVONotifying_Delegate class. In this scenario, NSKVONotifying_Delegate inherits from SensorsDelegate, so the -class method returns information about our newly created subclass, not the original class.

Solution:

We can in the new subclass, on – addObserver: forKeyPath: options: context: method for listening. If the proxy object listens for the KVO attribute after we create a new subclass, we need to rewrite it again after the system overrides the -class method and returns the original 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) {// If (self.sensorsdata_classname) {// If (self.sensorsdata_classname) {// If (self.sensorsdata_classname) {// If (self.sensorsdata_classname) { At this time of the original class to add a subclass of the god so you need to rewrite the class method [SAMethodHelper replaceInstanceMethodWithDestinationSelector: @ the selector (class) sourceSelector:@selector(class) fromClass:SADelegateProxy.class toClass:[SAClassHelper realClassWithObject:self]]; }}Copy the code

2.1.2. Scenario 2

First set the proxy object, then perform KVO attribute listening, and finally remove KVO attribute listening, as shown in Figure 2-2:

Figure 2-2 ISA pointer change process in scenario 2

There is no problem in this scenario.

2.1.3. Scene 3

Listen for the KVO property of the proxy object first, and then set the proxy object, as shown in Figure 2-3:

Figure 2-3 ISA pointer change process in scenario 3 The following problems may occur in this scenario:

In this scenario, the SensorsDelegate inherits from NSKVONotifying_Delegate, which affects the KVO nature of the system and causes a crash when assigning a property.

Solution:

If the agent object of isa pointer to a NSKVONotifying_ class, then we will no longer new subclasses, but direct rewriting NSKVONotifying_ category – tableView: didSelectRowAtIndexPath: methods:

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

2.1.4. Scenario 4

The KVO attribute listening is performed on the proxy object first, then the proxy object is set, and finally the KVO attribute listening is removed, as shown in Figure 2-4:

Figure 2-4 ISA pointer change process in scenario 4 The following problems may occur in this scenario:

When KVO is removed, the isa pointer of the proxy object is pointed directly back to the original class, and click event collection is not possible.

Solution:

In the class of NSKVONotifying_ rewritten – tableView: didSelectRowAtIndexPath: method at the same time, the – removeObserver: forKeyPath: Method to subclass the proxy object again when the KVO attribute listener is removed:

if ([SADelegateProxy isKVOClass:realClass]) { [SAMethodHelper addInstanceMethodWithSelector:@selector(removeObserver:forKeyPath:) fromClass:proxyClass toClass:realClass]; return; } - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {// remove whether the former proxy object belongs to the class BOOL created by KVO oldClassIsKVO = [SADelegateProxy isKVOClass:[SAClassHelper realClassWithObject:self]]; [super removeObserver:observer forKeyPath:keyPath]; // Remove Whether the proxy object belongs to the class created by KVO BOOL newClassIsKVO = [SADelegateProxy isKVOClass:[SAClassHelper realClassWithObject:self]]; If (oldClassIsKVO &&!!) {// If (oldClassIsKVO &&!!) {// If (oldClassIsKVO &&!! NewClassIsKVO) {// Empty the original class self.sensorsdata_className = nil; [SADelegateProxy proxyWithDelegate:self]; }}Copy the code

2.1.5. Final process

Figure 2-5 shows the final processing flow.

Figure 2-5 Troubleshooting flowchart

2.2 RxSwift

In the article of collecting list click events in seven steps, processing logic of cell click messages has been mentioned and message forwarding has been carried out in the RxSwift scenario. At this time, an important point is ignored:

If you set the delegate of a UITableView in system mode, RxSwift holds the delegate internally using _forwardToDelegate, and then sends a message to the delegate object once during the message forwarding phase. This ensures that the service logic can be triggered normally.

But right now we have created a subclass of the delegate, rewrite the – tableView: didSelectRowAtIndexPath: method. So messages sent to the proxy object in RxSwift are received by us, eventually causing a recursive call to the method to crash.

Figure 2-6 shows how to send messages.

Figure 2-6 Message sending process

See the source code for _RXDelegateProxy [2], -Forward Invocation:

- (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]; }}Copy the code

Since RxSwift internally calls IMP for _forwardToDelegate when a message is forwarded, we can solve the problem by calling IMP directly when _forwardToDelegate is detected, instead of forwarding the message again. The logic is as follows:

(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 attributes 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); }} // Event collection //... }Copy the code

However, there is another problem with this solution: using the system to set up the broker and subscribing to the click callback will not work because we are not forwarding the message again.

Figure 2-7 shows the message sending after the modification.

Figure 2-7 Modified message sending process To be fully compatible with RxSwift, we need to use the _RXDelegateProxy -Forward Invocation logic again and directly invoke the internal method as follows:

(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); } // Invoke the event invocation in rxDelegateProxy, click the event invocation in this method and the -forwardInvocation in _RXDelegateProxy: // @param scrollView UITableView or UICollectionView object // @param selector needs to execute a method: The tableView: didSelectRowAtIndexPath: or collectionView: didSelectItemAtIndexPath: // @param indexPath click the NSIndexPath object + (void)rxInvokeWithScrollView:(UIScrollView *)scrollView selector:(SEL)selector  selectedAtIndexPath:(NSIndexPath *)indexPath { // 1. Perform _sentMessage: withArguments: [SADelegateProxy invokeRXProxyMethodWithTarget: scrollView. Delegate selector:NSSelectorFromString(@"_sentMessage:withArguments:") argument1:selector argument2:@[scrollView, indexPath]]; 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. Perform _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; // Obtain the original parent of the record first, if not, it is the KVO scenario, The KVO scenario uses the class interface to get the originalClass 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]; } // Event collection //... }Copy the code

Message is sent

Although RxSwift was adapted in the previous section, there are many unknown tripartite libraries that realize cell click response through message forwarding, such as Texture[3]. We cannot adapt each tripartite library one by one.

The essence of our collection scheme is to create subclasses. For subclasses, if we override a method in the parent class, we can call the method in the parent class by super without worrying about the implementation logic in the parent class. If the parent class is not implemented, the system should do the message forwarding.

But – tableView: didSelectRowAtIndexPath: the method is defined in the UITableViewDelegate protocol, can’t use the super keyword, that whether we can use the runtime related interface implementation to send a message to the parent class? The answer is yes.

The Runtime provides the objc_msgSendSuper interface, defined as follows:

OBJC_EXPORT id _Nullable objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...) OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0); Super: structure information of type objc_super; Op: the selector to call; . : Related arguments to selector.Copy the code

The final message processing logic is as follows:

(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 }; // The message is sent to the original class void (*func)(struct objc_super *, SEL, id, id) = (void *)&objc_msgSendSuper; func(&targetSuper, selector, scrollView, indexPath); // If target and delegate are not equal, the message is forwarded. = scrollView.delegate) { return; } // Event collection //... }Copy the code

Third, summary

This paper introduces how to collect cell click events by creating new subclasses, which is compatible with THE KVO scenario, supports the NSProxy scenario, and realizes sending messages to the parent class. The specific implementation of this scheme can be found in the iOS SDK source code of Shenpolicy analysis [4]. If you have better ideas, welcome to join the open source community to discuss.

References:

[1] developer.apple.com/library/arc…

[2] github.com/ReactiveX/R…

[3] github.com/TextureGrou…

[4] github.com/sensorsdata…