In the era of mobile Internet, data plays an extremely important role. Buried point, as the simplest and most direct statistical method of user behavior, can collect users’ usage habits and iterative feedback of each function point comprehensively and accurately. Only with these data can we better drive the decision design of products and the planning of new business scenarios. The purpose of this paper is to propose a lightweight non-invasive embedding scheme, which has the following three advantages

  • Supports dynamic delivery of buried point configuration
  • Physically separate buried code from business code
  • Plug-in buried point function implementation

The solution maintains a JSON file to specify the class and method where the buried point is located, and then uses AOP to dynamically embed the buried point code when the corresponding class and method are executed. For the scenario that requires logical judgment to determine the buried point value, the input parameter of hook method is provided, as well as the reading of the attribute value of the class, and different buried points are set according to the corresponding state value

Buried point configuration

Buried point configuration JSON table contains the name of the class to be hooked and the specific event information. The event information includes the hook method and the corresponding buried point value. As shown below.

{
    "version": "0.1.0 from"."tracking": [{"class": "RJMainViewController"."event": {
                "rj_main_tracking": [
                    "tripTypeViewChangedWithIndex:"."tripLabClickWithLabKey:"]."user_fp_slide_click": "clickNavLeftBtn"."user_fp_reflocate_click": "clickLocationBtn"}}, {"class": "RJTripHistoryViewModel"."event": {
                "user_mytrip_show": "tableView:didSelectRowAtIndexPath:"}}, {"class": "RJTripViewController"."event": {
                "rj_trip_tracking": "callServiceEvent"}}}]Copy the code

To put it simply, the buried point needs to be manually written in the method to record the buried point value. Now, the method of AOP is used to physically isolate the buried point code and business code, so as to avoid the intrusion of buried point logic and pollution of business logic. Buried points include fixed buried points and scenarioized buried points that require logical judgment. The fixed buried points are shown as follows

{
    "class": "RJTripHistoryViewModel"."event": {
        "user_mytrip_show": "tableView:didSelectRowAtIndexPath:"}}Copy the code

RJTripHistoryViewModel as the class name, tableView: didSelectRowAtIndexPath: for hook methods in the class, while the user_mytrip_show is specific buried point value, Namely the tableView: when RJTripHistoryViewModel didSelectRowAtIndexPath: method execution time record user_mytrip_show buried point value

{
    "class": "RJTripViewController"."event": {
        "rj_trip_tracking": "callServiceEvent"}},Copy the code

For scenarioized burying points, an IMPL class needs to be provided to provide the appropriate logical judgment. For example, rj_trip_Tracking is the implementation class of the scenario burying point in the configuration table above. In this class, the corresponding burying point value is returned based on the amount of state, that is, when the callServiceEvent method is executed, the rj_trip_tracking burying point impL class will be found. The buried point value returned by the class is taken to record the buried point. Note that the key value in the event can be used as either a buried point value or the class name of the IMPL. The buried point library will first determine whether the corresponding class exists. If so, it is considered as the IMPL implementation class and obtains the specific buried point value from that class. Otherwise, it is considered to be fixed buried point value

The class name and method name in the configuration table need to match, and the hook will match. If the corresponding method does not exist in the class, the assertion will be triggered automatically

Buried fixed point

For fixed buried points, simply log the buried points directly when the corresponding method executes, using Aspects to hook the specified classes and methods, as shown in the code below

[class aspect_hookSelector:sel withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> info) {
    [events enumerateObjectsUsingBlock:^(NSString *name, NSUInteger idx, BOOL *stop) {
        NSLog(@"<RJEventTracking> - %@", ename);
    }];
} error:&error];
Copy the code

In order to facilitate the detection of invalid buried points, the class and method of hook should be matched and verified. If there is no corresponding method in the class, the assertion will be thrown

+ (void)checkValidWithClass:(NSString *)class method:(NSString *)method {
    SEL sel       = NSSelectorFromString(method);
    Class c       = NSClassFromString(class);
    BOOL respond  = [c respondsToSelector:sel] || [c instancesRespondToSelector:sel];
    NSString *err = [NSString stringWithFormat:@"<RJEventTracking> - no specified method: %@ found on class: %@, please check", method, class];
    
    NSAssert(respond, err);
}
Copy the code

The scene is buried point

The burying point of the scene mainly refers to the situation of the same event but different burying points under various states or logic, for example, the operation of contacting customer service is the same. The burying point is set differently under various order types and order states. In this case, the buried point library is implemented by providing a protocol to the buried point IMPl class, which returns the corresponding buried point value depending on the logic

@protocol RJEventTracking <NSObject>

- (NSString *)trackingMethod:(NSString *)method instance:(id)instance arguments:(NSArray *)arguments;

@end
Copy the code

For example, the rj_trip_Tracking class needs to follow the RJEventTracking protocol and return a buried value based on the logic

The name of the buried implementation class needs to be the same as the key in the event in the buried configuration JSON, because the buried library implements plug-in buried rules by detecting the presence of a class with the same name. In addition, an IMPL can correspond to multiple method methods

State judgment

The buried point value is determined according to the state quantity. As an example of contacting customer service burials, to return the corresponding burials based on the order type and order status, first define the IMPL class of the same name in the JSON table and follow the RJEventTracking protocol

#import "RJEventTracking.h"
 
NS_ASSUME_NONNULL_BEGIN
 
@interface rj_trip_tracking : NSObject <RJEventTracking>
 
@end
 
NS_ASSUME_NONNULL_END
Copy the code

In. To implement a custom m file buried point method trackingMethod: the agreement of the instance: the arguments:

#import "rj_trip_tracking.h"
 
@implementation rj_trip_tracking
 
- (NSString *)trackingMethod:(NSString *)method instance:(id)instance arguments:(NSArray *)arguments {
    id dataManager        = [instance property:@"dataManager"];
    NSInteger orderStatus = [[dataManager property:@"orderStatus"] integerValue];
    NSInteger orderType   = [[dataManager property:@"orderType"] integerValue];
 
    if ([method isEqualToString:@"callServiceEvent"]) {
        if (orderType == 1) {       
            if (orderStatus == 1) { 
                return @"user_inbook_psgservice_click";
            } else if (orderStatus == 2) {
                return @"user_finishbook_psgservice_click"; }}else {
            return @"user_psgservice_click"; }}return nil;
}
 
@end
Copy the code

In the protocol method, you can get the current instance (in this case, RJTripViewController) and enter the parameter group. The type and status of the order are stored in the dataManager property in the RJTripViewController, so the property value can be obtained using the property: method wrapped in the burying library, and the corresponding burying point name can be returned based on the property value

@interface NSObject (RJEventTracking)
 
- (id)property:(NSString *)property;
 
@end
Copy the code

The implementation of reading the property value is

- (id)property:(NSString *)property {
    return [NSObject runMethodWithObject:self selector:property arguments:nil];
}
Copy the code

The principle is simple: encapsulate the getter method in the NSInvocation and invoke the return value

+ (id)runMethodWithObject:(id)object selector:(NSString *)selector arguments:(NSArray *)arguments {
    if(! object)return nil;
    
    if (arguments && [arguments isKindOfClass:NSArray.class] == NO) {
        arguments = @[arguments];
    }
    SEL sel = NSSelectorFromString(selector);
        
    NSMethodSignature *signature = [object methodSignatureForSelector:sel];
    if(! signature) {return nil;
    }
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    invocation.selector      = sel;
    invocation.arguments     = arguments;
    [invocation invokeWithTarget:object];
    
    return invocation.returnValue_obj;
}
Copy the code

The judge refs

You need to determine the buried point name based on the input parameter of the hook method set in JSON. For example, click all, In progress, to pay, to evaluate, completed and other menu items in the order list. The method to be hooked is tripLabClickWithLabKey: its parameter is UILabel. In the original code, we can determine which subitem is clicked by the tag of the Label. Similarly, we can also obtain the input parameter of the Label and judge accordingly. Since there is only one argument, you can simply take the first value of arguments

#import "rj_main_tracking.h"
#import <UIKit/UIKit.h>
 
static NSString *order_types[5] = { @"user_order_all_click"The @"user_order_ongoing_click"The @"user_order_unpay_click"The @"user_order_unmark_click"The @"user_order_finish_click" };
@implementation rj_main_tracking
 
- (NSString *)trackingMethod:(NSString *)method instance:(id)instance arguments:(NSArray *)arguments {
    if ([method isEqualToString:@"tripLabClickWithLabKey:"]) {
        UILabel *label = arguments[0];
        if(! label || label.tag > 4) {return nil;
        }
        return order_types[label.tag];
    } else if ([method isEqualToString:@"tripTypeViewChangedWithIndex:"]) {
        return @"xx_ryan_jin";
    }
}
 
@end
Copy the code

Hook method through AOP, you can get the current hook method of the corresponding instance object and input parameters, in the call protocol method, directly to the protocol implementation class

The method call

Similar to reading attribute values, it is also the case that the same event has different buried point names in different scenarios, but the amount of state obtained is not the current instance object, but the return value of a method. In this case, it can be realized through method call function provided by the buried point library

@interface NSObject (RJEventTracking)
 
- (id)performSelector:(NSString *)selector arguments:(nullable NSArray *)arguments;
 
@end
Copy the code

For example, get the view type of a page stored in a singleton object

[RJViewTypeModel sharedInstance].viewType
Copy the code

In this scenario, the corresponding buried point name is returned according to the type of viewType

- (NSString *)trackingMethod:(NSString *)method instance:(id)instance arguments:(NSArray *)arguments {
    NSString *labKey   = [instance property:@"labKey"];
    id viewTypeModel   = [NSClassFromString(@"RJViewTypeModel") performSelector:@"sharedInstance"
                                                                      arguments:nil];
    NSInteger viewType = [[viewTypeModel property:@"viewType"] integerValue];
     
    if (viewType == 0) { 
        if ([labKey isEqualToString:@"rj_view_begin_add"]) {
            return @"user_fp_book_on_click";
        }
        if ([labKey isEqualToString:@"rj_view_end_add"]) {
            return @"user_fp_book_off_click"; }}if (viewType == 1) { 
        if ([labKey isEqualToString:@"rj_view_begin_add"]) {
            return @"user_fr_on_click";
        }
        if ([labKey isEqualToString:@"rj_view_end_add"]) {
            return @"user_fr_off_click"; }}return nil;
}
Copy the code

logic

Need to add additional logic, such as the need to statistics the user to enter the order details page page viewing behavior, but the type of details page after the network request is needed to obtain, and the network request will trigger regularly, so buried of hook method to walk many times, the case, you need to add an attribute to identify whether recorded points. Therefore, the buried point library needs to provide the function of adding attributes dynamically

@interface NSObject (RJEventTracking)
 
- (id)extraProperty:(NSString *)property;
 
- (void)addExtraProperty:(NSString *)property defaultValue:(id)value;
 
@end
Copy the code

In the buried point implementation IMPL class, add additional attributes to indicate whether a buried point has been recorded

@implementation user_orderdetail_show
 
- (NSString *)trackingMethod:(NSString *)method instance:(id)instance arguments:(NSArray *)arguments {
    if ([instance extraProperty:@"isRecorded"]) {
        return nil;
    }
    [instance addExtraProperty:@"isRecorded" defaultValue:@(YES)];
     
    return @"user_orderdetail_show";
}
 
@end
Copy the code

Using addExtraProperty: defaultValue: to dynamically add to the current instance attributes, and extraProperty: method is used to retrieve an instance of an additional attributes. If isRecorded returns YES meaning the buried point has been recorded, nil is returned to ignore the buried point

The isRecorded attribute in the example above is added because of the requirements of the buried point and has nothing to do with the business logic. Therefore, it is more reasonable to add the isRecorded attribute in the plug-in IMPL class of the buried point to avoid affecting the business code

Adding properties dynamically to a buried library is also simple, using the Runtime objc_setAssociatedObject and objc_getAssociatedObject methods to bind properties to instance objects

- (id)extraProperty:(NSString *)property {
    return objc_getAssociatedObject(self, NSSelectorFromString(property));
}

- (void)addExtraProperty:(NSString *)property defaultValue:(id)value {
    objc_setAssociatedObject(self, NSSelectorFromString(property), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
Copy the code

Dynamic distributed

The server provides an interface for the buried JSON configuration table. The client obtains the latest buried point configuration table through the interface during each startup for dynamic delivery. After receiving the JSON, the client reads the buried point information and it takes effect

[RJEventTracking loadConfiguration:[[NSBundle mainBundle] pathForResource:@"RJUserTracking" ofType:@"json"]].Copy the code

The code read is as follows. The main logic is to traverse the classes and hook methods in the buried point, and detect whether the buried point is fixed or the scene buried point. For the case of the scene buried point, check whether there is a corresponding impL implementation class. Of course, you also need to check the validity of the JSON configuration table and whether each class matches the methods in it

+ (void)loadConfiguration:(NSString *)path {
    NSData *data = [NSData dataWithContentsOfFile:path];
    if(! data) {return;
    }
    NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil];
    NSString *version  = dict[@"version"];
    NSArray *ts        = dict[@"tracking"];
    [ts enumerateObjectsUsingBlock:^(NSDictionary *obj, NSUInteger idx, BOOL *stop) {
        Class class              = NSClassFromString(obj[@"class"]);
        NSDictionary *ed         = obj[@"event"];
        NSMutableDictionary *td  = [NSMutableDictionary dictionaryWithCapacity:0];
        [ed enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) {
            NSMutableArray *tArr = [NSMutableArray arrayWithCapacity:0];
            [tArr addObjectsFromArray:[obj isKindOfClass:[NSArray class]] ? obj : @[obj]];
            [tArr enumerateObjectsUsingBlock:^(NSString *m, NSUInteger idx, BOOL *stop) {
                if ([td.allKeys containsObject:m]) {
                    NSMutableArray *ms         = [td[m] mutableCopy];
                    if(! [ms containsObject:key]) [ms addObject:key]; td[m] = ms; }else{ td[m] = @[key]; }}]; }]; [td enumerateKeysAndObjectsUsingBlock:^(NSString *kmethod, NSArray <NSString *> *tArr, BOOL *stop) { SEL sel = NSSelectorFromString(kmethod); NSError *error = nil; [self checkValidWithClass:obj[@"class"] method:kmethod];
            [class aspect_hookSelector:sel withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> info) {
                [tArr enumerateObjectsUsingBlock:^(NSString *name, NSUInteger idx, BOOL *stop) {
                    NSString *ename       = name;
                    id<RJEventTracking> t = [NSClassFromString(name) new];
                    if (t && [t respondsToSelector:@selector(trackingMethod:instance:arguments:)]) {
                        ename = [t trackingMethod:kmethod instance:info.instance
                                                         arguments:info.arguments];
                    }
                    if ([ename length]) {
                        NSLog(@"<RJEventTracking> - %@", ename); }}]; } error:&error]; [self checkHookStatusWithClass:obj[@"class"] method:kmethod error:error];
        }];
    }];
}
Copy the code

Finally attached source code address: github.com/rjinxx/RJEv…

pod 'RJEventTracking'
Copy the code

If you have any questions or optimization suggestions during the process of using RJEventTracking, please leave a comment PR, thanks.