runtime无埋点实战

前言:书写,为了更好地思考。

说来惭愧,无埋点早在一年半之前就已经研究了,但是由于懒的原因一直没有写文章去分析,导致现在回过头来只知道自己详细研究过,但是要我复述具体怎么做,恐怕也不能描述清楚。今天我就根据代码来详细分析下runtime无埋点的实际使用方式,文末会附上Demo供大家参考,也希望各位多多指教。
首先我们来一张大致的分析图,明确我们的整体框架流程。


无埋点流程.png

一、创建工具类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、交换方法的实现。

+ (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,这里是为了避免源SEL没有实现IMP的情况
    BOOL didAddMethod = class_addMethod(class,originalSelector,
                                        method_getImplementation(swizzledMethod),
                                        method_getTypeEncoding(swizzledMethod));
    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(myviewWillDisappear:)];
  
    });
#endif
}
- (void)swizzledViewDidAppear:(BOOL)animated {
    [self swizzledViewDidAppear:animated];
    
#pragma mark 不能使用类别,由于界面可能是由多个类组成,或者能选出来它本身的类
    [[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获取UITabbarItem  可以直接遍历当前页面的所有控件,然后再找出按钮
            if ([NSStringFromClass(view.class) isEqualToString:@"UITabBar"]) {
                for (id object in [view subviews]) {
                    UIView * subview = (UIView *)object;
                    //NSLog(@"获取当前页面所有控件的名称:%@",subview);
                    for (id obj in [subview subviews]) {
                        UIView * litSubview = (UIView *)obj;
                    
#pragma mark 自定义TabBar,自定义的按钮都会有opaque属性,所以。。。。。。
                        
                        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);
                        
                        }
                        
#pragma mark 系统控件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"]){
#pragma mark 自定义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,获取坐标等属性。
#pragma mark 系统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];
    float Add_Y = 0;//用来计算相对于window的坐标
    if ([eventName isEqualToString:@"UITabBarButton"]) {
        Add_Y = KSCREEN_HEIGHT-49;
    }else if([eventName isEqualToString:@"UINavigationButton"]){
        Add_Y = 20;//手机状态栏的高度加上,获取的坐标是相对于UINavigationBar的坐标
    }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(s_hitTest:withEvent:)];
    });
}

//可以在这里实现监测Tabbar点击监测   事件的获取建立在点击的情况下
- (UIView *)s_hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    
    for (UIView *childView in self.subviews){
        
#pragma mark 判断每一个控件中的text值
    if (![childView isKindOfClass:NSClassFromString(@"UITabBarButton")]){
        
    if (!self.clipsToBounds && !self.hidden && self.alpha > 0) {
        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);
#pragma mark 进一步判别自定义控件。
            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);
                    
                    
                }
              }

#pragma mark 系统控件生成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;
}

最后奉上代码,由于是鄙人自己研究摸索的,所以内容难免有些疏漏或缺陷,万望大神指点。
Demo地址:https://github.com/caiqingchong/Runtime-SGK

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,864评论 6 494
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,175评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,401评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,170评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,276评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,364评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,401评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,179评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,604评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,902评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,070评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,751评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,380评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,077评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,312评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,924评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,957评论 2 351