美文网首页iOS底层收集
iOS进阶-15 切面编程AOP+埋点

iOS进阶-15 切面编程AOP+埋点

作者: ricefun | 来源:发表于2020-03-11 15:35 被阅读0次

    Aspects

    Aspects这个第三方相信大家都有所运用,简单讲Aspects是利用切面编程的思想去hook的实例方法,然后返回一个可操作的block;下面我将针对Aspects的一些重要的源码进行解释;

    基于NSObject的分类

    Aspects是NSObject的分类。基于NSObject当然是基于万物皆为对象,分类的话可以在不破坏源代码的基础上实现功能。

    @interface NSObject (Aspects)
    

    aspect_add()

    static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
        //空值判断
        NSCParameterAssert(self);
        NSCParameterAssert(selector);
        NSCParameterAssert(block);
        
        //__block 修饰,用于blockl内部可以操作该对象
        __block AspectIdentifier *identifier = nil;
        //aspect_performLocked 内部是一把os_unfair_lock,用于保证block内部的线程安全
        aspect_performLocked(^{
            //内部判断对象,方法,options,是否满足条件
            if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
                //使用关联对象给当前self添加一个AspectsContainer对象,内部是保存3不同操作时机(AspectPositionAfter,AspectPositionInstead,AspectPositionBefore)的AspectIdentifier对象的集合的容器
                AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
                //封装selector 对象(self)options block error的一个对象,方便加入aspectContainer容器中
                identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
                if (identifier) {
                    //加入到aspectContainer容器
                    [aspectContainer addAspect:identifier withOptions:options];
    
                    // Modify the class to allow message interception.
                    //内部使用class_replaceMethod()去 hook原来的实例方法;
                    aspect_prepareClassAndHookSelector(self, selector, error);
                }
            }
        });
        return identifier;
    }
    

    AspectIdentifier

    • 封装 selector 对象(self) options block
    • @property (nonatomic, weak) id object;是一个弱引用,防止循环引用
    + (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error {
        NSCParameterAssert(block);
        NSCParameterAssert(selector);
        NSMethodSignature *blockSignature = aspect_blockMethodSignature(block, error); // TODO: check signature compatibility, etc.
        if (!aspect_isCompatibleBlockSignature(blockSignature, object, selector, error)) {
            return nil;
        }
        
        AspectIdentifier *identifier = nil;
        if (blockSignature) {
            identifier = [AspectIdentifier new];
            identifier.selector = selector;
            identifier.block = block;
            identifier.blockSignature = blockSignature;
            identifier.options = options;
            identifier.object = object; // weak
        }
        return identifier;
    }
    

    aspect_blockMethodSignature获取block方法签名

    由block(block_layout).desc内存偏移得到signature

    static NSMethodSignature *aspect_blockMethodSignature(id block, NSError **error) {
        AspectBlockRef layout = (__bridge void *)block;
        if (!(layout->flags & AspectBlockFlagsHasSignature)) {
            NSString *description = [NSString stringWithFormat:@"The block %@ doesn't contain a type signature.", block];
            AspectError(AspectErrorMissingBlockSignature, description);
            return nil;
        }
        void *desc = layout->descriptor;
        desc += 2 * sizeof(unsigned long int);
        if (layout->flags & AspectBlockFlagsHasCopyDisposeHelpers) {
            desc += 2 * sizeof(void *);
        }
        if (!desc) {
            NSString *description = [NSString stringWithFormat:@"The block %@ doesn't has a type signature.", block];
            AspectError(AspectErrorMissingBlockSignature, description);
            return nil;
        }
        const char *signature = (*(const char **)desc);
        return [NSMethodSignature signatureWithObjCTypes:signature];
    }
    

    只对实例方法进行hook

    NSMethodSignature *methodSignature = [[object class] instanceMethodSignatureForSelector:selector];

    static BOOL aspect_isCompatibleBlockSignature(NSMethodSignature *blockSignature, id object, SEL selector, NSError **error) {
        NSCParameterAssert(blockSignature);
        NSCParameterAssert(object);
        NSCParameterAssert(selector);
    
        BOOL signaturesMatch = YES;
        NSMethodSignature *methodSignature = [[object class] instanceMethodSignatureForSelector:selector];
       ...
       ...
       ...
        return YES;
    }
    

    根据options的类型放入集合

    根据options的类型放入三个不同的数组中

    - (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)options {
        NSParameterAssert(aspect);
        NSUInteger position = options&AspectPositionFilter;
        switch (position) {
            case AspectPositionBefore:  self.beforeAspects  = [(self.beforeAspects ?:@[]) arrayByAddingObject:aspect]; break;
            case AspectPositionInstead: self.insteadAspects = [(self.insteadAspects?:@[]) arrayByAddingObject:aspect]; break;
            case AspectPositionAfter:   self.afterAspects   = [(self.afterAspects  ?:@[]) arrayByAddingObject:aspect]; break;
        }
    }
    

    切面编程

    像Aspects这种,对原生代码不会去破坏,有很容易进行统一的修改,并且业务代码被统一放在了一个block中,这种编程就是切面编程AOP;

    切面编程的运用

    • 参数校验:网络请求前的参数校验,返回数据格式的校验等
    • 无痕埋点:统一处理埋点,降低代码耦合度
    • 页面统计:帮助统计页面的访问量
    • 事物处理:拦截指定事件,添加出发事件
    • 异常处理:发生异常时,可以捕获异常进行处理
    • 热修复:AOP可以在在方法的前后直接替换另一行代码,根据这个思想我们可以实现bug修复
    • 剥离非业务逻辑代码:一些非业务逻辑的代码可以不放在原生代码中

    埋点

    针对特定用户或时间进行捕获、处理和发送的相关技术及实行过程即为埋点。比如用户某个button的点击次数,观看某个视频的时长等等。而在代码层,埋点的实质即监听软件运行过程中的事件,对需要关注的事件进行判断和捕获。

    埋点种类

    • 代码埋点:用预先写好的代码埋在事件交互的代码中发送数据。
    • 可视化埋点:用可视化的方法来代替代码埋点把代码和业务逻辑分开。举个例子,像游戏公司现在开发会把资源文件和配置信息和代码分开,用户更新游戏只要下载资源文件和配置就可以了。代表:MixPanel
    • 无侵入埋点:和可视化埋点类似,二者的区别是可视化埋点通过界面配置来决定要统计的事件来源,而无埋点是经可能把所有能收集的数据全部收集一遍,再通过后台配置要留下哪些统计分析。代表:sensorsdata

    MixPanel

    原理:

    • 通过手势请求web创建连接(匹配设备)
    • 连接成功后将信息发送给web
    • 手机将截图发送给服务器用于投屏
    • 生成websocket长连接,用于持续投屏
    • 将web上的操作返回给手机,打印日志

    crash 捕获

    • FOUNDATION_EXPORT void NSSetUncaughtExceptionHandler(NSUncaughtExceptionHandler * _Nullable);使用系统接口去捕获崩溃信息
    #import "LGUncaughtExceptionHandle.h"
    #import <SCLAlertView.h>
    #import <UIKit/UIKit.h>
    #include <libkern/OSAtomic.h>
    #include <execinfo.h>
    #include <stdatomic.h>
    
    NSString * const LGUncaughtExceptionHandlerSignalExceptionName = @"LGUncaughtExceptionHandlerSignalExceptionName";
    NSString * const LGUncaughtExceptionHandlerSignalExceptionReason = @"LGUncaughtExceptionHandlerSignalExceptionReason";
    NSString * const LGUncaughtExceptionHandlerSignalKey = @"LGUncaughtExceptionHandlerSignalKey";
    NSString * const LGUncaughtExceptionHandlerAddressesKey = @"LGUncaughtExceptionHandlerAddressesKey";
    NSString * const LGUncaughtExceptionHandlerFileKey = @"LGUncaughtExceptionHandlerFileKey";
    NSString * const LGUncaughtExceptionHandlerCallStackSymbolsKey = @"LGUncaughtExceptionHandlerCallStackSymbolsKey";
    
    atomic_int      LGUncaughtExceptionCount = 0;
    const int32_t   LGUncaughtExceptionMaximum = 8;
    const NSInteger LGUncaughtExceptionHandlerSkipAddressCount = 4;
    const NSInteger LGUncaughtExceptionHandlerReportAddressCount = 5;
    
    @implementation LGUncaughtExceptionHandle
    
    /// Exception
    void LGExceptionHandlers(NSException *exception) {
        NSLog(@"%s",__func__);
        
        // 收集 - 上传
        int32_t exceptionCount = atomic_fetch_add_explicit(&LGUncaughtExceptionCount,1,memory_order_relaxed);
        if (exceptionCount > LGUncaughtExceptionMaximum) {
            return;
        }
        // 获取堆栈信息 - model 编程思想
        NSArray *callStack = [LGUncaughtExceptionHandle lg_backtrace];
        NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:[exception userInfo]];
        [userInfo setObject:exception.name forKey:LGUncaughtExceptionHandlerSignalExceptionName];
        [userInfo setObject:exception.reason forKey:LGUncaughtExceptionHandlerSignalExceptionReason];
        [userInfo setObject:callStack forKey:LGUncaughtExceptionHandlerAddressesKey];
        [userInfo setObject:exception.callStackSymbols forKey:LGUncaughtExceptionHandlerCallStackSymbolsKey];
        [userInfo setObject:@"LGException" forKey:LGUncaughtExceptionHandlerFileKey];
        
        [[[LGUncaughtExceptionHandle alloc] init]
         performSelectorOnMainThread:@selector(lg_handleException:)
         withObject: [NSException exceptionWithName:[exception name] reason:[exception reason] userInfo:userInfo] waitUntilDone:YES];
    }
    
    + (void)installUncaughtSignalExceptionHandler{
        NSSetUncaughtExceptionHandler(&LGExceptionHandlers);
    }
    
    - (void)lg_handleException:(NSException *)exception{
        NSDictionary *dict = [exception userInfo];
        [self saveCrash:exception file:[dict objectForKey:LGUncaughtExceptionHandlerFileKey]];
        
        // 网络上传 - flush
        // 用户奔溃
        // runloop 起死回生
        CFRunLoopRef runloop = CFRunLoopGetCurrent();
        // 跑圈依赖 - mode
        CFArrayRef allmodes  = CFRunLoopCopyAllModes(runloop);
        
        SCLAlertView *alert = [[SCLAlertView alloc] initWithNewWindowWidth:300.0f];
        [alert addButton:@"请你奔溃" actionBlock:^{
            self.dismissed = YES;
        }];
        [alert showSuccess:exception.name subTitle:exception.reason closeButtonTitle:nil duration:0.0f];
        
        // 起死回生
        while (!self.dismissed) {
            for (NSString *mode in (__bridge NSArray *)allmodes) {
                CFRunLoopRunInMode((CFStringRef)mode, 0.0001, false);
            }
        }
        CFRelease(runloop);
    }
    
    /// 保存奔溃信息或者上传
    - (void)saveCrash:(NSException *)exception file:(NSString *)file{
        NSArray *stackArray = [[exception userInfo] objectForKey:LGUncaughtExceptionHandlerCallStackSymbolsKey];// 异常的堆栈信息
        NSString *reason = [exception reason];// 出现异常的原因
        NSString *name = [exception name];// 异常名称
        
        // 或者直接用代码,输入这个崩溃信息,以便在console中进一步分析错误原因
        // NSLog(@"crash: %@", exception);
        
        NSString * _libPath  = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:file];
        
        if (![[NSFileManager defaultManager] fileExistsAtPath:_libPath]){
            [[NSFileManager defaultManager] createDirectoryAtPath:_libPath withIntermediateDirectories:YES attributes:nil error:nil];
        }
        
        NSDate *dat = [NSDate dateWithTimeIntervalSinceNow:0];
        NSTimeInterval a=[dat timeIntervalSince1970];
        NSString *timeString = [NSString stringWithFormat:@"%f", a];
        
        NSString * savePath = [_libPath stringByAppendingFormat:@"/error%@.log",timeString];
        
        NSString *exceptionInfo = [NSString stringWithFormat:@"Exception reason:%@\nException name:%@\nException stack:%@",name, reason, stackArray];
        
        BOOL sucess = [exceptionInfo writeToFile:savePath atomically:YES encoding:NSUTF8StringEncoding error:nil];
        
        NSLog(@"保存崩溃日志 sucess:%d,%@",sucess,savePath);
        
    }
    
    /// 获取函数堆栈信息
    + (NSArray *)lg_backtrace{
        
        void* callstack[128];
        int frames = backtrace(callstack, 128);//用于获取当前线程的函数调用堆栈,返回实际获取的指针个数
        char **strs = backtrace_symbols(callstack, frames);//从backtrace函数获取的信息转化为一个字符串数组
        int i;
        NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];
        for (i = LGUncaughtExceptionHandlerSkipAddressCount;
             i < LGUncaughtExceptionHandlerSkipAddressCount+LGUncaughtExceptionHandlerReportAddressCount;
             i++)
        {
            [backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
        }
        free(strs);
        return backtrace;
    }
    @end
    

    相关文章

      网友评论

        本文标题:iOS进阶-15 切面编程AOP+埋点

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