The problem background

In this era of data king, the market has users of the APP, will log dot, we are no exception.

If one page to dot, it is time-consuming and laborious, we can not help but want to Hook the method we want through AOP, we can do a dot, the purpose of unified management.

For example, for the entry and exit of a page, only viewWillAppear and viewWillDisappear are recorded.

In view of this, for UIKit on iOS, our project has a set of event management scheme implemented based on Method Swizzle.

However, we found a problem:

After joining a third-party library, there was a crash. After investigation, it was determined to be related to the initialization sequence of the parent and child classes.

Method Swizzle scheme

First, let’s briefly talk about our Method Swizzle scheme for dot tracking.

As your App starts up, you start doing method swaps

We started the method exchange in TrackerCenter at APP startup:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    [[HZTrackerCenter sharedInstance] beginTracker];
    return YES;
}

Copy the code

The methods of the various UI classes in beginTracker are swapped:

- (void)beginTracker
{
    ...
    [UIControl HZ_swizzle];
    [UICollectionViewHZ_swizzle]; . }Copy the code

Method Swizzle’s unified Method

The key code for method exchange is HZ_swizzleMethod:newSel:, which is also a common code in the market, as follows:

+ (BOOL)HZ_swizzleMethod:(SEL)originalSel newSel:(SEL)newSel {
    Method originMethod = class_getInstanceMethod(self, originalSel);
    Method newMethod = class_getInstanceMethod(self, newSel);
    
    if (originMethod && newMethod) {
        if (class_addMethod(self, originalSel, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) {
            
            IMP orginIMP = method_getImplementation(originMethod);
            class_replaceMethod(self, newSel, orginIMP, method_getTypeEncoding(originMethod));
        } else {
            method_exchangeImplementations(originMethod, newMethod);
        }
        return YES;
    }
    return NO;
}
Copy the code

Idea is:

1. Obtain two methods according to SEL, and judge whether the two methods exist and can be exchanged

2. Use class_addMethod(). If there is no originalSel Method in the class, addMethod first. If there’s an implementation with the same name in this class, then the function will return NO, so we’ll just use method_exchangeImplementations for exchanging the two methods.

3. If class_addMethod() succeeds, the IMP of originalSel has been implemented as newMethod. The next step is to replace newSel with IMP with class_replaceMethod.

Note: But why not use method_exchangeImplementations. Instead, we will add and swap methods first, so that we can ensure that methods are exchanged only in the subclass, without affecting the parent class. If there is no originalSel implementation, class_getInstanceMethod() returns a Method object of the parent class, swapping the IMP of the parent class with the Swizzle IMP of the same class. Affects the entire parent class and its subclasses.

If you don’t understand SEL,Method, and IMP, take a look at objective-C Runtime

Different classes, method exchange strategy

Common UI class, direct operation

For common UI classes, such as UIControl, we swap directly, as follows:



@implementation UIControl (Tracker)

- (void)HZ_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
 
    if(target && action && ! [NSStringFromSelector(action) hasPrefix:@ "_"]) {
        // Log events
    }
    
    [self HZ_sendAction:action to:target forEvent:event];
}

+ (void)HZ_swizzle {
    [UIControl HZ_swizzleMethod:@selector(sendAction:to:forEvent:)
                         newSel:@selector(ET_sendAction:to:forEvent:)];
}

@end


Copy the code

A specific UI class that operates on a proxy object

Unlike other UI classes, uITableViews and UicollectionViews cannot be swapped directly to count their click events. Because its corresponding event implementation is in the delegate object.

The delegate object is then operated on during setDelegate:.

For example UICollectionView


@implementation UICollectionView (Tracker)

- (void)HZ_setDelegate:(id<UICollectionViewDelegate>)delegate {
    if ([delegate isKindOfClass:[NSObject class]]) {
        SEL sel = @selector(collectionView:didSelectItemAtIndexPath:);
        
        / / newSel name: HZ_collectionView: didSelectItemAtIndexPath:
        SEL newSel = [NSObject HZ_newSelFormOriginalSel:sel];
        
        Method originMethod = class_getInstanceMethod(delegate.class, sel);
          
        if (originMethod && ![delegate.class HZ_methodHasSwizzed:sel]) {

        
            IMP newIMP =  (IMP)HZ_collectionViewDidSelectRowAtIndexPath;
            class_addMethod(delegate.class, newSel,newIMP, method_getTypeEncoding(originMethod));
            
            [delegate.class HZ_swizzleMethod:sel newSel:newSel];
            [delegate.class HZ_setMethodHasSwizzed:sel];
        }
    }
    [self HZ_setDelegate:delegate];
}

+ (void)HZ_swizzle {
    [UICollectionView HZ_swizzleMethod:@selector(setDelegate:)
                                newSel:@selector(HZ_setDelegate:)];
}

@end
Copy the code

Here’s the idea:

  1. Generate a newSel based on Sel.
  2. Method to judge Sel exists, and Sel has not been swizzle.
  3. For the delegate object class, add the newSel implementation.
  4. Swap sel and newSel
  5. The pairs have swizzle to Sel tags

Problem scenario

The above does swizzle for specific UI classes, UITableView and UICollectionView proxy objects. At first glance, there is no problem, and stable operation for a long time.

Until one day, we introduced a third-party library.

This third party is a UIView, but this UIView is a delegate object to a UICollectionView.

There is nothing wrong with that in itself.

For business purposes, we inherited it and implemented a subclass. Subclasses also don’t overwrite UICollectioView’s proxy method.

At this point, there is the problem of circular calls.

The process in question

The investigation found that as long as the parent class performs the swap operation before the child class, then click the child class, a circular call will occur, resulting in a crash.

Let’s reconstruct the entire problem flow:

1. Swizzle is carried out for the parent class according to the scheme above, and the result is:

SEL OriginSel NewSel
IMP NewImp OriginImp
  • The OriginSel implementation corresponds to NewImp
  • The NewSel implementation corresponds to OriginImp

2. Swizzle the subclass, first insert NewSel, implementation corresponding to NewImp:

SEL NewSel
IMP NewImp

OriginSel NewImp (class_addMethod, NewImp)

SEL OriginSel NewSel
IMP NewImp NewImp

4. In the subclass exchange method, perform class_replaceMethod on NewSel:


IMP orginIMP = method_getImplementation(originMethod);
class_replaceMethod(self, newSel, orginIMP,method_getTypeEncoding(originMethod));

Copy the code

In step 4 above, the problem becomes apparent because originalSel is not implemented in the subclasses. The originMethod is obtained by class_getInstanceMethod(self, originalSel), which is actually the implementation of the parent class originalSel. See the table above, the parent class has been swapped, and the obtained implementation is NewImp.

So replace doesn’t really achieve its purpose.

The result of the final subclass is still:

SEL OriginSel NewSel
IMP NewImp NewImp

Performance problems

At this point, the process of restoring the loop begins.

Where NewImp is implemented as a static method for logging:


void HZ_collectionViewDidSelectRowAtIndexPath(id self, SEL _cmd, UICollectionView *collectionView, NSIndexPath *indexPath) 
{
    
    //do your track thing
    
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    
    SEL sel = [NSObject HZ_newSelFormOriginalSel:@selector(collectionView:didSelectItemAtIndexPath:)];
    [self performSelector:sel
               withObject:collectionView 
               withObject:indexPath];
#pragma clang diagnostic pop
}
Copy the code

The above method will finally call NewSel.

A normal call chain would be:

OriginSel->NewImp->NewSel->OriginImp.

Once you’re done in NewImp, call the system’s real OriginImp, and you’re done.

In our current subclass of Swizzle, the call chain looks like this:

OriginSel->NewImp->NewSel->NewImp->NewSel->NewImp..

NewImp->NewSel->NewImp->NewSel.. Russian nesting dolls, which caused the crash.

Problem solving

The problem is that when a subclass enters Swizzle, the subclass itself does not implement the OriginalMethod, which uses the method_getImplementation method to retrieve the implementation of the parent class. The parent classes have been swapped, and the result is NewImp.

If we want to solve the problem, we can’t control the order in which the user calls the parent class, we need to make a judgment before Swizzle to avoid this situation.

Preliminary solution

At first, I thought of judging by the implementation of OriginalSel.

Because the implementation of OriginalSel is always in the parent class, and it’s dangerous to get it the second time.

Swizzle is determined by whether the flag bit is true or false:

  • When the parent class OriginalSel is modified and the subclass comes in, the Swizzle operation is no longer performed.

  • When the subclass OriginalSel is modified and the parent class comes in, it doesn’t Swizzle anymore.

But tests show that if you add a grandchild class, that’s when problems arise. It’s still very similar, so if you’re interested, you can try it out.

Final solution

The final solution is to go straight to the heart of the point, judge OriginalIMP and NewIMP.

The problem is that OriginalIMP actually becomes NewIMP.

OriginalIMP = NewIMP; OriginalIMP = NewIMP;

  • If they are the same, a parent implementation has been swapped. No more Swizzle.
  • If not, secure Sizzle operations can be performed.

The core code is as follows:

IMP originIMP = method_getImplementation(originMethod); IMP newIMP = (IMP)HZ_collectionViewDidSelectRowAtIndexPath; if (originMethod && ! (originIMP==newIMP)) { class_addMethod(delegate.class, newSel,newIMP, method_getTypeEncoding(originMethod)); [delegate.class HZ_swizzleMethod:sel newSel:newSel]; }Copy the code

After this processing, our log log will work normally and we will no longer have to worry about the parent and child repeating the Swizzle in a loop.

Sample code, including problems and solutions, has been uploaded to GitHub.