iOS - Runtime 無埋點實現(xiàn)

導引

一、創(chuàng)建工具類 NSObject+Swizzling

創(chuàng)建工具類,里面包含以下四個方法,這樣可以針對不同的需求進行處理,這里主要使用方法的交換。

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;

// 獲取對象的所有屬性和屬性內(nèi)容
+ (NSDictionary *)getAllPropertiesAndVaules;

要交換方法,分三步走:

  1. 獲取原有方法。
  2. 創(chuàng)建替換新方法。
  3. 交換方法的實現(xiàn)。

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 沒有實現(xiàn) IMP 的情況
    if (didAddMethod) {
        // 添加成功:說明原 SEL 沒有實現(xiàn) IMP,將原 SEL 的 IMP 替換為交換 SEL 的 IMP
        class_replaceMethod(class,swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else {
        // 添加失敗:說明原 SEL 已經(jīng)有 IMP,直接將兩個 SEL 的 IMP 交換即可
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

二、遍歷當前頁面的事件類控件

重點解釋:在 Objective-C 中,運行時會自動調(diào)用每個類的兩個方法。+load 會在類初始加載時調(diào)用,+initialize 會在第一次調(diào)用類的類方法或?qū)嵗椒ㄖ氨徽{(diào)用。這兩個方法是可選的,且只有在實現(xiàn)了它們時才會被調(diào)用。由于 methodswizzling 會影響到類的全局狀態(tài),因此要盡量避免在并發(fā)處理中出現(xiàn)競爭的情況。+load 能保證在類的初始化過程中被加載,并保證這種改變應用級別的行為的一致性。相比之下,+initialize 在其執(zhí)行時不提供這種保證。事實上,如果在應用中沒給這個類發(fā)送消息,則它可能永遠不會被調(diào)用。

+ (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 執(zhí)行,由于進行了方法的交換,此處并非會形成循環(huán)調(diào)用
    [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);
                        }

                        // 系統(tǒng)控件 UITabBarButton
                        if([NSStringFromClass(subview.class) isEqualToString:@"UITabBarButton"]){
                            // 查看余數(shù),如果不為零則說明還有一個
                            float x = subview.frame.origin.x;
                            float w = subview.frame.size.width;
                            // 由此可以得出每一個 tabbar 的索引值
                            int tabIndex = x/w;
                            // 判斷當為 UITabBar 時,不用類別作為生成事件ID的參數(shù)。
                           NSString *tabBarID = [UIEventAttributes getControllerName:NSStringFromClass(subview.superview.class) eventText:@"UITabBar" eventUI:@"UITabBarButton" indexForView:[NSString stringWithFormat:@"%d",tabIndex]];
                           NSLog(@"UITabBarButton 獲取ID:%@---------%@",tabBarID,subview);
                        }
                    }
                }
            }
 
            // 遍歷獲取導航欄 UINavigationBar 獲取按鈕上傳數(shù)據(jù) UIButtonLabel
            if([NSStringFromClass(view.class) isEqualToString:@"UINavigationBar"]){
                // 自定義 UINavigationBar 類型,特別要注意自定義控件的實現(xiàn)方式,要涵蓋大多數(shù)自定義控件的實現(xiàn)方法
                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,獲取坐標等屬性。
                    // 系統(tǒng) UINavigationBar 類型
                    if ([NSStringFromClass(subview.class) isEqualToString:@"UINavigationButton"]) {
                        NSString *className = [[NSUserDefaults standardUserDefaults] objectForKey:@"className"];
                        //NSLog(@"~~~~~~~~~~~~~~~~~~~~~~~:%@",className);
                        /*
                         為了區(qū)分左邊和右邊按鈕,我們手動設置 left、right 以及索引值。這里看開發(fā)者理解,也不必全部都設置成這個樣子。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 提供數(shù)據(jù)。

+ (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"]){
        // 手機狀態(tài)欄的高度加上,獲取的坐標是相對于 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;
}

四、監(jiān)控事件的點擊

我們監(jiān)控 UIButton、UINavigationButton、UITabBarButton 的事件點擊。同時存儲數(shù)據(jù),等到 SDK 設定的時機再發(fā)送數(shù)據(jù)。我們以 tabbar 的點擊事件監(jiān)控為例,通過交換 hitTest:withEvent: 事件,我們對點擊的事件做出相應的處理。主要針對自定義和系統(tǒng)的 TabBar 分別進行處理,防止出現(xiàn)遺漏的問題。

// 獲取 Tabbar 點擊事件監(jiān)控。不能在按鈕類別中單獨的獲取事件,這樣會導致數(shù)據(jù)出現(xiàn)問題
+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self methodSwizzlingWithOriginalSelector:@selector(hitTest:withEvent:) bySwizzledSelector:@selector(swizzledHitTest:withEvent:)];
    });
}

// 可以在這里實現(xiàn)監(jiān)測 Tabbar 點擊監(jiān)測,事件的獲取建立在點擊的情況下
- (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,上傳數(shù)據(jù),以供判斷,上傳坐標數(shù)據(jù)
                    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);
                        }
                    }

                    // 系統(tǒng)控件生成 ID 規(guī)則
                    // 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;
}
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 前言:書寫,為了更好地思考。 說來慚愧,無埋點早在一年半之前就已經(jīng)研究了,但是由于懶的原因一直沒有寫文章去分析,導...
    woniu閱讀 477評論 0 2
  • 繼上Runtime梳理(四) 通過前面的學習,我們了解到Objective-C的動態(tài)特性:Objective-C不...
    小名一峰閱讀 848評論 0 3
  • 面向?qū)ο蟮娜筇匦裕悍庋b、繼承、多態(tài) OC內(nèi)存管理 _strong 引用計數(shù)器來控制對象的生命周期。 _weak...
    運氣不夠技術湊閱讀 1,222評論 0 10
  • Method Swizzling 是什么 Method Swizzling是objective-c中的黑魔法,算是...
    進擊的阿牛哥閱讀 1,967評論 0 6
  • 文中的實驗代碼我放在了這個項目中。 以下內(nèi)容是我通過整理[這篇博客] (http://yulingtianxia....
    茗涙閱讀 1,028評論 0 6

友情鏈接更多精彩內(nèi)容