AI智能
改变未来

iOS – Runtime 无埋点实现

导引

一、创建工具类 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 添加 IMPBOOL didAddMethod = class_addMethod(class,originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));// 这里是为了避免原 SEL 没有实现 IMP 的情况if (didAddMethod) {// 添加成功:说明原 SEL 没有实现 IMP,将原 SEL 的 IMP 替换为交换 SEL 的 IMPclass_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 DEBUGstatic 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;//  找到 UITabBarif ([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;// 自定义 TabBarif (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);}// 系统控件 UITabBarButtonif([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 获取按钮上传数据 UIButtonLabelif([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];// 开始生成 IDNSString *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;}
  • 点赞
  • 收藏
  • 分享
  • 文章举报

GS-NICE发布了177 篇原创文章 · 获赞 0 · 访问量 2717私信关注

赞(0) 打赏
未经允许不得转载:爱站程序员基地 » iOS – Runtime 无埋点实现