美文网首页
iOS - Runtime 无埋点实现

iOS - Runtime 无埋点实现

作者: GSNICE | 来源:发表于2020-03-28 11:32 被阅读0次
    导引

    一、创建工具类 NSObject+Swizzling

    创建工具类,里面包含以下四个方法,这样可以针对不同的需求进行处理,这里主要使用方法的交换。

    NSObject+Swizzling.h

    #import <Foundation/Foundation.h>
    #import <objc/runtime.h>
    @interface NSObject (Swizzling)
    
    // 公用的交换方法
    + (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector;
    
    // 获取对象的所有属性
    + (NSArray *)getAllProperties;
    
    // 获取对象的所有方法
    + (NSArray *)getAllMethods;
    
    // 获取对象的所有属性和属性内容
    + (NSDictionary *)getAllPropertiesAndVaules;
    

    要交换方法,分三步走:

    1. 获取原有方法。
    2. 创建替换新方法。
    3. 交换方法的实现。

    NSObject+Swizzling.m

    + (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector{
        Class class = [self class];
        // 原有方法
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        // 替换原有方法的新方法
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        // 先尝试给原 SEL 添加 IMP
        BOOL didAddMethod = class_addMethod(class,originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        // 这里是为了避免原 SEL 没有实现 IMP 的情况
        if (didAddMethod) {
            // 添加成功:说明原 SEL 没有实现 IMP,将原 SEL 的 IMP 替换为交换 SEL 的 IMP
            class_replaceMethod(class,swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            // 添加失败:说明原 SEL 已经有 IMP,直接将两个 SEL 的 IMP 交换即可
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    }
    

    二、遍历当前页面的事件类控件

    重点解释:在 Objective-C 中,运行时会自动调用每个类的两个方法。+load 会在类初始加载时调用,+initialize 会在第一次调用类的类方法或实例方法之前被调用。这两个方法是可选的,且只有在实现了它们时才会被调用。由于 methodswizzling 会影响到类的全局状态,因此要尽量避免在并发处理中出现竞争的情况。+load 能保证在类的初始化过程中被加载,并保证这种改变应用级别的行为的一致性。相比之下,+initialize 在其执行时不提供这种保证。事实上,如果在应用中没给这个类发送消息,则它可能永远不会被调用。

    + (void)load {
    #ifdef DEBUG
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            [self methodSwizzlingWithOriginalSelector:@selector(viewDidAppear:) bySwizzledSelector:@selector(swizzledViewDidAppear:)];
            [self methodSwizzlingWithOriginalSelector:@selector(viewWillDisappear:) bySwizzledSelector:@selector(swizzledViewWillDisappear:)];
        });
    #endif
    }
    
    - (void)swizzledViewDidAppear:(BOOL)animated {
        //  为保证原有的 viewDidAppear 执行,由于进行了方法的交换,此处并非会形成循环调用
        [self swizzledViewDidAppear:animated];
        
        // 不能使用类别,由于界面可能是由多个类组成,或者能选出来它本身的类
        [[NSUserDefaults standardUserDefaults] setObject:NSStringFromClass([self class]) forKey:@"className"];
        [[NSUserDefaults standardUserDefaults] synchronize];
    
        // 遍历出导航栏和 tabbar,再次进行遍历,看一下是否能够遍历出来控件
        for (id object in [self.view subviews]) {
            if ([object isKindOfClass:[UIView class]]) {
                // 对 object 进行了判断,它一定是 UIView 或其子类
                UIView * view = (UIView *)object;
                //  找到 UITabBar
                if ([NSStringFromClass(view.class) isEqualToString:@"UITabBar"]) {
                    // 遍历 UITabBar 获取 UITabbarItem 可以直接遍历当前页面的所有控件,然后再找出按钮
                    for (id object in [view subviews]) {
                        UIView * subview = (UIView *)object;
                        // NSLog(@"获取当前页面所有控件的名称:%@",subview);
                        for (id obj in [subview subviews]) {
                            UIView * litSubview = (UIView *)obj;
                        
                            // 自定义 TabBar
                            if (litSubview.opaque == NO || litSubview.opaque == YES) {
                                // 在这里也要遍历一下它的 text 尽量获取
                                NSString *litSubText = [UIEventAttributes getEventText:litSubview];
                                NSMutableDictionary *dic = [UIEventAttributes getEventAttributes:litSubview andUI:@"UITabBarButton"];
                                
                                // 这个方法用来生成相应事件的 ID
                                [UIEventAttributes getControllerName:NSStringFromClass(litSubview.superview.class) eventText:litSubText eventUI:@"UITabBarButton" indexForView:[NSString stringWithFormat:@"%ld",litSubview.tag]];
    
                                // NSLog(@"UITabBarButton 的坐标值为:%@",dic);
                            }
    
                            // 系统控件 UITabBarButton
                            if([NSStringFromClass(subview.class) isEqualToString:@"UITabBarButton"]){
                                // 查看余数,如果不为零则说明还有一个
                                float x = subview.frame.origin.x;
                                float w = subview.frame.size.width;
                                // 由此可以得出每一个 tabbar 的索引值
                                int tabIndex = x/w;
                                // 判断当为 UITabBar 时,不用类别作为生成事件ID的参数。
                               NSString *tabBarID = [UIEventAttributes getControllerName:NSStringFromClass(subview.superview.class) eventText:@"UITabBar" eventUI:@"UITabBarButton" indexForView:[NSString stringWithFormat:@"%d",tabIndex]];
                               NSLog(@"UITabBarButton 获取ID:%@---------%@",tabBarID,subview);
                            }
                        }
                    }
                }
     
                // 遍历获取导航栏 UINavigationBar 获取按钮上传数据 UIButtonLabel
                if([NSStringFromClass(view.class) isEqualToString:@"UINavigationBar"]){
                    // 自定义 UINavigationBar 类型,特别要注意自定义控件的实现方式,要涵盖大多数自定义控件的实现方法
                    for (id object in [view subviews]) {
                        UIView * subview = (UIView *)object;
                        // 自定义的 Nav 要进一步遍历控件,找出按钮
                        for (id obj in [subview subviews]) {
                            UIView * litSubview = (UIView *)obj;
                            NSString *text = [[NSString alloc] init];
                            // 剥出来按钮信息,并得出坐标
                            if ([NSStringFromClass(litSubview.class) isEqualToString:@"UIButton"]) {
                                // 获取父视图的坐标,获取在 window 上的坐标
                                NSMutableDictionary *dic = [UIEventAttributes getEventAttributes:litSubview andUI:@"UIButton"];
      
                                float Super_X = litSubview.superview.frame.origin.x;
                                float Super_Y = litSubview.superview.frame.origin.y;
                                float but_x = [[dic objectForKey:@"b_x"] floatValue]+Super_X;
                                float but_y = [[dic objectForKey:@"b_y"] floatValue]+Super_Y;
                                [dic setObject:[NSString stringWithFormat:@"%f",but_x] forKey:@"b_x"];
                                [dic setObject:[NSString stringWithFormat:@"%f",but_y] forKey:@"b_y"];
                               //  获取事件的名称
                                text = [UIEventAttributes getEventText:litSubview];
                            
                                // 开始生成 ID
                                NSString *butID =  [UIEventAttributes getControllerName:NSStringFromClass(litSubview.superview.class) eventText:text eventUI:@"UIButton" indexForView:[NSString stringWithFormat:@"1%ld",litSubview.tag]];
                                // NSLog(@"UINavigationBar 中 [litSubview subviews] 所有控件信息:%@-------ButtonText:%@-------butID:%@",litSubview,text,butID);
                            }
                        }
                   
                        // 应该直接遍历上面所有的控件信息,找出所有的可点击控件。并生成 ID,获取坐标等属性。
                        // 系统 UINavigationBar 类型
                        if ([NSStringFromClass(subview.class) isEqualToString:@"UINavigationButton"]) {
                            NSString *className = [[NSUserDefaults standardUserDefaults] objectForKey:@"className"];
                            //NSLog(@"~~~~~~~~~~~~~~~~~~~~~~~:%@",className);
                            /*
                             为了区分左边和右边按钮,我们手动设置 left、right 以及索引值。这里看开发者理解,也不必全部都设置成这个样子。100 为手动判断。
                             */
                            if (subview.frame.origin.x <100) {
                                NSString *eventID = [UIEventAttributes getControllerName:className eventText:@"left" eventUI:NSStringFromClass(subview.class) indexForView:@"1"];
                            // NSLog(@"~~~~~~~~~~~左边的按钮,索引设置为1~~~~~~~ID:%@",eventID);
                            }else{
                                
                                NSString *eventID = [UIEventAttributes getControllerName:className eventText:@"right" eventUI:NSStringFromClass(subview.class) indexForView:@"2"];
                                // NSLog(@"-----------右边的按钮,索引设置为2~~~~~~~ID:%@",eventID);
                            }
                        }
                    }
                }
             }
        }
    }
    

    三、获取事件的属性

    属性包含控件的 frame、透明度、是否 hidden、按钮的 Label 等信息。此处为我们生成事件 UI 的唯一 ID 提供数据。

    + (NSMutableDictionary *)getEventAttributes:(UIView *)view andUI:(NSString *)eventName{
        //  要返回的信息字典
        NSMutableDictionary *mdic = [NSMutableDictionary dictionaryWithCapacity:10];
        // 用来计算相对于 window 的坐标
        float Add_Y = 0;
        if ([eventName isEqualToString:@"UITabBarButton"]) {
            Add_Y = KSCREEN_HEIGHT - 49;
        }else if([eventName isEqualToString:@"UINavigationButton"]){
            // 手机状态栏的高度加上,获取的坐标是相对于 UINavigationBar 的坐标
            Add_Y = 20; 
        }else if([eventName isEqualToString:@"UIButton"]){
            // 如果有按钮的父视图,要加上父视图的坐标保证准确性
            Add_Y = 0;
        }
        
        NSString *B_X = [NSString stringWithFormat:@"%.1f",view.frame.origin.x];
        NSString *B_Y = [NSString stringWithFormat:@"%.1f",view.frame.origin.y+Add_Y];
        
        NSString *B_W = [NSString stringWithFormat:@"%.1f",view.frame.size.width];
        NSString *B_H = [NSString stringWithFormat:@"%.1f",view.frame.size.height];
        NSString *B_A = [NSString stringWithFormat:@"%.2f",view.alpha];
        NSString *B_O = (view.opaque||!view.hidden)?@"YES":@"NO";
        
        [mdic setObject:B_X forKey:@"b_x"];
        [mdic setObject:B_Y forKey:@"b_y"];
        [mdic setObject:B_W forKey:@"b_w"];
        [mdic setObject:B_H forKey:@"b_h"];
        [mdic setObject:B_A forKey:@"b_a"];
        [mdic setObject:B_O forKey:@"b_o"];
        
        return mdic;
    }
    
    // 返回事件名称,这个只是针对 UIButton
    + (NSString *)getEventText:(UIView *)view{
        NSString *eventText = [[NSString alloc]init];
        
        if ([NSStringFromClass(view.class) isEqualToString:@"UIButtonLabel"]) {
            NSArray *arr = [NSArray arrayWithObject:view];
            
            NSString *UIButtonLabel = [NSString stringWithFormat:@"%@",arr[0]];
            
            NSArray *zomeArr = [UIButtonLabel componentsSeparatedByString:@"'"];
            eventText = zomeArr[1];
        }else{
            for (id object in [view subviews]) {
                UIView * subview = (UIView *)object;
                // 测试如果没有 text 的话根本就不会进入下面的判断
                if ([NSStringFromClass(subview.class) isEqualToString:@"UIButtonLabel"]) {
                    NSArray *arr = [NSArray arrayWithObject:subview];
                    
                    NSString *UIButtonLabel = [NSString stringWithFormat:@"%@",arr[0]];
                    
                    NSArray *zomeArr = [UIButtonLabel componentsSeparatedByString:@"'"];
                    eventText = zomeArr[1];
                }
            }
            // 在没有 text 的时候设置返回 text 名称(也可以不设置)
            if (eventText == nil || [eventText isEqualToString:@""]) {
                eventText = @"无名事件";
            }
        }
            return eventText;
    }
    

    四、监控事件的点击

    我们监控 UIButton、UINavigationButton、UITabBarButton 的事件点击。同时存储数据,等到 SDK 设定的时机再发送数据。我们以 tabbar 的点击事件监控为例,通过交换 hitTest:withEvent: 事件,我们对点击的事件做出相应的处理。主要针对自定义和系统的 TabBar 分别进行处理,防止出现遗漏的问题。

    // 获取 Tabbar 点击事件监控。不能在按钮类别中单独的获取事件,这样会导致数据出现问题
    + (void)load{
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            [self methodSwizzlingWithOriginalSelector:@selector(hitTest:withEvent:) bySwizzledSelector:@selector(swizzledHitTest:withEvent:)];
        });
    }
    
    // 可以在这里实现监测 Tabbar 点击监测,事件的获取建立在点击的情况下
    - (UIView *)swizzledHitTest:(CGPoint)point withEvent:(UIEvent *)event {
        
        for (UIView *childView in self.subviews){
            
            // 判断每一个控件中的 text 值
            if (![childView isKindOfClass:NSClassFromString(@"UITabBarButton")]){
                //  判断是否可以接收事件:self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01(不可以接收事件)
                if (!self.clipsToBounds && self.userInteractionEnabled && !self.hidden && self.alpha > 0.01) {
                    UIView *result = [super hitTest:point withEvent:event];
            
                    // NSLog(@"点击的按钮的按钮:%@",result);
       
                    if (result) {
                        // 在这里可以获知点击的是第几个 tabbar,上传数据,以供判断,上传坐标数据
                        float x = result.frame.origin.x;
                        float w = result.frame.size.width;
            
                        int tabIndex = x/w;
                        NSLog(@"~~~~~~~~~~~~~~~~~~~~~~~~~~~收到的控件 result:%d",tabIndex);
                        // 进一步判别自定义控件。
                        for (id obj in [result subviews]) {
                            UIView * litSubview = (UIView *)obj;
    
                            if (litSubview.opaque == NO || litSubview.opaque == YES) {
                        
                                // 在这里也要遍历一下它的 text 尽量获取
                                NSString *litSubText = [UIEventAttributes getEventText:litSubview];
                                NSString *litSubID = [UIEventAttributes getControllerName:NSStringFromClass(result.superview.class) eventText:litSubText eventUI:@"UITabBarButton" indexForView:[NSString stringWithFormat:@"%ld",result.tag]];
                                NSLog(@"点击 UITabBarButton 的按钮 litSubview:%@",litSubText);
                            }
                        }
    
                        // 系统控件生成 ID 规则
                        // NSString *tabBarID = [UIEventAttributes getControllerName:NSStringFromClass(result.superview.class) eventText:@"UITabBar" eventUI:@"UITabBarButton" indexForView:[NSString stringWithFormat:@"%d",tabIndex]];
                        // NSLog(@"---------~~~~~~~~~~~~~~~~~~~~~~~~~~点击后获取的 UITabBarButton:%@-------------%@",result,tabBarID);
                        return result;
                    }
                }
            }
        }
        return nil;
    }
    

    相关文章

      网友评论

          本文标题:iOS - Runtime 无埋点实现

          本文链接:https://www.haomeiwen.com/subject/pbjjuhtx.html