What is a burial point?

Buried point is a way to understand user behavior, analyze user behavior, and improve user experience. There are three common solutions: code buried, visual buried, and no-buried.

  • Code burying point is mainly through handwritten code to burying point, can be very precise in the need to bury point, add code. There is a large amount of development, difficult to maintain later problems.
  • Visualization of buried points visualizes the increase and modification of buried points, improving the experience of adding and maintaining buried points.
  • No buried point is also called full buried point, buried point code does not appear in the business code, easy to manage and maintain, the disadvantage is high cost, complex parsing.

No buried points can be achieved, buried points are uniformly maintained and decoupled from business code to meet most requirements.

The realization of no buried point

The implementation of the no-buried point is basically based on the Runtime, replacing the original method at runtime to implement the buried point.

Create a class for the replacement method

Create a new class with the following code:

#import "SMHook.h"
#import <objc/runtime.h>@implementation SMHook +(void)hookClass:(Class)classObject fromSelector:(SEL)fromSelector toSelector:(SEL)toselector{ Class class = classObject; Method fromMethod = class_getInstanceMethod(class, fromSelector); ToMethod = class_getInstanceMethod(class, toselector);if (class_addMethod(class, fromSelector, method_getImplementation(toMethod), method_getTypeEncoding(fromMethod))) {
        class_replaceMethod(class, toselector, method_getImplementation(fromMethod), method_getTypeEncoding(toMethod));
    }else{
        method_exchangeImplementations(fromMethod, toMethod);
    }
}
@end
Copy the code

The main role is to exchange two IMP pointer implementation

method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 
Copy the code

IMP is an implementation of concrete methods.

Page entry times and dwell time

Hook viewWillAppear (UIViewController); And viewWillDisappear: to get the dwell time, create a Category implementation method for UIViewController

#import "ViewController+time.h"
#import "SMHook.h"
@implementation UIViewController (logger)

+(void)initialize{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL fromSelector = @selector(viewWillAppear:);
        SEL toSelector = @selector(hook_viewWillAppear:);
        [SMHook hookClass:self fromSelector:fromSelector toSelector:toSelector];
        
        SEL fromDisappear = @selector(viewWillDisappear:);
        SEL toDisappear = @selector(hook_viewWillDisAppear:);
        [SMHook hookClass:self fromSelector:fromDisappear toSelector:toDisappear];
    });
}
Copy the code

Hook method implementation

-(void)hook_viewWillAppear:(BOOL)animated{
    NSLog(@"hook viwe"); [Self comeIn]; [self comeIn]; [self hook_viewWillAppear:animated]; } -(void)hook_viewWillDisAppear:(BOOL)animated{// [self hook_viewWillDisAppear:animated]; }Copy the code

Note: in the implementationHook_viewWillAppear:Method is called again[self hook_viewWillAppear:animated]; The IMP here doesviewWillAppear:Method, pay attention to the previous

method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
Copy the code

Method, so that you don’t get hookedviewWillAppear:In the business code,viewWillAppear:Content implementation does not cause circular calls.

Click the HOOK of the event

UIButton’s click hook is basically the same as UIViewController’s viewWillAppear: hook.

// send the action. the first method is called for the event and is a point at which you can observe or override behavior. it is called repeately by the second.

- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;
Copy the code

This method, it gets called when it’s clicked. not

- (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;
Copy the code

This method.

To create a new Category for UIButton, there are three main steps

  1. Replace method
  2. Hook method implementation
  3. Upload buried point information
#import "UIButton+logger.h"
#import "SMHook.h"
#import <objc/runtime.h>
@implementation UIButton (logger)
+(void)initialize{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL fromSelector = @selector(sendAction:to:forEvent:);
        SEL toSelector = @selector(hook_sendAction:to:forEvent:);
        [SMHook hookClass:self fromSelector:fromSelector toSelector:toSelector];
    });
}

-(void)hook_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{
    [self insertAction:action to:target forEvent:event];
    [self hook_sendAction:action to:target forEvent:event];
    
}
-(void)insertAction:(SEL)action to:(id)target forEvent:(UIEvent*)event{
    NSString * actionName = NSStringFromSelector(action);
    NSString * targetName = NSStringFromClass([target class]);
    NSString *name = self.name;
    UIView *targetView = (UIView *)target;
    NSLog(@"% @",targetView);
    NSLog(@"button name == %@ actionName == %@, targetName == %@",name, actionName,targetName); // Missing method NSLog(@) to get view_path"viewPath %@",viewPath);
}

Copy the code

The key here is not just UIButton, but the View_Path of any object you want to Hook

Why View_path

Each control needs to have a unique identity so that its buried points can be analyzed. If there are multiple UIButtons in a view, it can’t be analyzed just by actionName and targetName alignment. One solution is to analyze the view hierarchy, the tree structure, as follows:

ViewController[0]/UIView[0]/UITableView[0]/UITableViewCell[0:2]/UIButton[0]
Copy the code

Among them:

  • This identifier uniquely identifies the element in the view tree of the current page.
  • Each element of the identity consists of a string representation of the current element’s class and the sequence number of the current element in its sibling, counting from 0. The second UIImageView, for example, is UIImageView1.
  • Identifies the/concatenation between different items.
  • The top layer of the logo is the ViewController where the current view resides.
  • forUITableViewCellUICollectionViewCellAnd similar custom components, the serial number part consists of two parts:sectionrowAnd with: splicing.
  • The end of the identifier is the element that is currently clicked or touched

Specific implementation:

+(NSString *)viewPath:(UIView *)currentView{
    __block NSString *viewPath = @"";
    
    for(UIView *view = currentView; view; view = view.superview) { NSLog(@"% @",view);
        if ([view isKindOfClass:[UICollectionViewCell class]]) {
            // 是一个
            UICollectionViewCell *cell = (UICollectionViewCell *)view;
            UICollectionView *cv = (UICollectionView *)cell.superview;
            NSIndexPath *indexPath = [cv indexPathForCell:cell];
            NSString *className = NSStringFromClass([cell class]);
            viewPath = [NSString stringWithFormat:@"%@[%ld:%ld]/%@",className,indexPath.section,indexPath.row,viewPath];
            continue;
        }
        
        if ([view isKindOfClass:[UITableViewCell class]]) {
            // 是一个
            UITableViewCell *cell = (UITableViewCell *)view;
            UITableView *tb = (UITableView *)cell.superview;
            NSIndexPath *indexPath = [tb indexPathForCell:cell];
            NSString *className = NSStringFromClass([cell class]);
            viewPath = [NSString stringWithFormat:@"%@[%ld:%ld]/%@",className,indexPath.section,indexPath.row,viewPath];
            continue;
        }
        
        
        if([view isKindOfClass:[UIView class]]) { [view.superview.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull  obj, NSUInteger idx, BOOL * _Nonnull stop) {if (obj == view) {
                    NSString *className = NSStringFromClass([view class]);
                    viewPath = [NSString stringWithFormat:@"%@[%ld]/%@",className,idx,viewPath]; *stop = YES; }}]; } UIResponder *responder = [view nextResponder];if ([responder isKindOfClass:[UIViewController class]]) {
            
            NSString *className = NSStringFromClass([responder class]);
            viewPath = [NSString stringWithFormat:@"% @ / % @",className,viewPath];
            returnviewPath; }}return viewPath;
}
Copy the code

The feeling is not optimal, ha ha, the above can be obtained. Path to an element in the current controller. You can use this to analyze the data. End of story. Thank you very much!