Jerry's Blog

Less is more, Live and learning

Hello,I'm an iOS developer in China.


iOS数据埋点方案

代码埋点

代码埋点是一种常规且直观的方案,需要开发人员在需要埋点的页面或点击事件的响应方法中注入埋点统计相关方法进行数据统计上报。也可以接入三方统计分析SDK来实现,比如友盟UMCAnalytics统计分析库。实现方式如下代码所示:

// 通过在HomeViewController 中的“页面已显示”和“页面已消失”两个回调中注入统计代码实现埋点
@implementation HomeViewController
//...other methods
- (void)viewDidAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    [WUserStatistics sendEventToServer:@"PAGE_EVENT_HOME_ENTER"];
}
 
- (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:animated];
    [WUserStatistics sendEventToServer:@"PAGE_EVENT_HOME_LEAVE"];
}
@end

这种埋点方式虽然实现起来简单,但是有很多缺点造成无法在实际项目中使用,如果需要埋点的页面很多,会增大工作量,代码侵入性太强,且不易于后期维护。

无埋点(无痕埋点)

通过Runtime的Method Swizzling 特性使用 AOP面向切面编程的思想 hook住系统关键方法,注入埋点代理实现统计上报功能。这种方式对项目没有侵入性,可以对某个控件进行全局埋点,使用更灵活。

比如我们要对某个页面的打开次数做统计,那么可以对UIViewController添加分类UIViewController+userStastistics来对控制器的viewWillAppear:方法进行hook,注入次数统计代码,代码示例如下:

// UIViewController 的分类
@implementation UIViewController (userStastistics)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(swiz_viewWillAppear:);
        // 使用hook工具(内部使用Method swizzing技术实现)交换两个方法的实现(IMP)
        [HookUtility swizzlingInClass:[self class] originalSelector:originalSelector swizzledSelector:swizzledSelector];
    });
}
#pragma mark - Method Swizzling
- (void)swiz_viewWillAppear:(BOOL)animated
{   
    // 注意:此行代码的调用实质上执行的是viewWillAppear: 的原生实现
    [self swiz_viewWillAppear:animated];
    
    // 添加埋点统计代码,用于记录页面打开次数
    Add your custom Analytics code.
}

@end

上述代码就可以实现无侵入性的数据埋点,但是这种方法会对所有UIViewController及其子类的viewWillAppear:方法进行hook代码注入,也就是说所有页面都会被统计进去,为了实现部分页面的统计,可以进行过滤,或使用白名单。

事件唯一ID的确定

在对事件进行上报的时候,需要对事件进行标记,我们可以创建一个plist配置表对每个事件定义一个唯一标识符、事件id、相关参数等信息,然后在埋点响应方法中通过获取事件发生的类名或方法名来确定事件,并上传到统计服务器。

AOP编程

Aspect Oriented Programming (AOP)是面向切面编程,利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。实现方式如下图所示:

img1

AOP的主要功能是:日志记录、性能统计、安全控制、事务处理、异常处理等。

主要的意图是:将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望将它们独立到非指导业务逻辑的方法中,进而改 变这些行为的时候不影响业务逻辑的代码。

可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。AOP是横向切入系统,提取各个模块可能都要重复操作的部分(如:权限检查,日志记录等等),是OOP的一个有效补充。

在iOS开发中,Aspects三方库就是基于AOP的思想来实现的,内部使用了ObjC动态特性Method swizzling Hook住原生方法注入block中的自定义代码。通过使用这个库,可以简单高效的实现无埋点数据统计。

无埋点采集

下面我们将介绍几种常用的UI控件的事件采集方式,分别可以这样实现:

UIViewController

主要是收集页面的生命周期,采用hook UIViewController的viewWillAppear:方法,将埋点代码插入到被替换的xxx_viewWillAppear:方法中,实现采集。

[HookUtility swizzlingInClass:[self class] originalSelector:@selector(viewWillAppear:)swizzledSelector:@selector(xxx_viewWillAppear:)];

UIControl

针对UIControl,HubbleData采用的是hook UIControl的sendAction:to:forEvent:方法。由官方文档可知,在UIControl执行对应的action时都会首先调用sendAction:to:forEvent:方法,HubbleData的实现如下:

[DASwizzler swizzleSendActionSelector:@selector(sendAction:to:forEvent:)
                              onClass:[UIControl class]
                            withBlock:executeBlock];

考虑到UIControl的子类较多,所以HubbleData选取了其中使用较多的几种进行了特殊的分析:主要是UITextField、UIButton和UISwitch,其余的暂时未做特殊分析。具体的埋点的采集设计为:无论是哪种UIControl,EventID均采用的是第三部分介绍的唯一标识字符串的SHA256编码值,但是相关采集properties有所差别。

UITextField

UITextField是UIControl的一个子类,由于UITextField涉及到用户的隐私比较多,比如用户名、密码、聊天文本等,所以HubbleData不会对此类的UITextField进行埋点的采集。

HubbleData主要采集的是UISearchBar中的UITextField,即UISearchBarTextField,并获取搜索的文本内容,这对于一些电商类的App来说,能够较好的分析用户感兴趣的商品等,这是作为HubbleData SDK无埋点的一个需求。

hook住sendAction:to:forEvent:后,如果对UISearchBarTextField的所有actions都进行hook的话,那么_searchFieldBeginEditing、_searchFieldEndEditing等所有的action发生的时候都会进行数据的采集,会采集到很多无用的信息,导致采集的数据混乱。HubbleData SDK只有当_searchFieldEndEditing action发生时才会进行埋点,收集的properties为:

(1) type 为UIControl采集的事件类型,这里设置为searchBarEvent;
(2) page 为当前页面的名称,用于前端显示用;
(3) searchText 为_searchFieldEndEditing发生时采集到搜索框的搜索文字(此字段不为空);

这样就能对搜索框进行无埋点采集,并能收集搜索的文本内容。此方法只是在_searchFieldEndEditing发生时采集数据,有可能该action执行时并未尽兴真正的搜索操作,可能会与业务数据库的数据有出入,但是也能够较为准确的分析用户感兴趣的搜索内容。

UIButton

UIButton是最常见的一种UIControl,由于UIButton在使用过程中会有多种状态,所有在记录的时候需要上报更详细的参数,可以添加titleColorimageNameframe等属性来做具体区分。

UISwitch

类似于UIButton,只不过这里要采集switchState,即当前的开关状态,具体的采集属性为:

(1) type 为UIControl采集的事件类型,这里设置为switchEvent;
(2) page 为当前页面的名称,用于前端显示用;
(3) switchState 为switch的开关状态;

UITableView和UICollectionView

针对UITableView和UICollectionView,HubbleData采用的是先hook UITableView和UICoolectionView的setDelegate:方法,然后找到对应的delegate,然后再hook delegate类中的tableView:didSelectRowAtIndexPath:方法和UICollectionView的collectionView:didSelectItemAtIndexPath:方法。这里以UITableView为例:

//先hook setDelegate:方法
[DASwizzler swizzleSelector:@selector(setDelegate:)
                    onClass:[UITableView class]
                  withBlock:executeSetDelegateBlock];

//再hook delegate的tableView:didSelectRowAtIndexPath:方法
void (^executeSetDelegateBlock)(id, SEL, id) = ^(id view, SEL command, id<UITableViewDelegate> delegate) {
        if ([delegate respondsToSelector:@selector(tableView:didSelectRowAtIndexPath:)]) {
            [DASwizzler swizzleSelector:@selector(tableView:didSelectRowAtIndexPath:)
                                onClass:[delegate class]
                              withBlock:executeBlock];
        }
    };

// "executeBlock" block 中插入埋点代码

EventID按照上述介绍的方法获取,只不过这里要注意的是,获取的并不是UITableView的唯一标识字符串而是对应的点击的cell的唯一标识字符串。采集的properties为:

(1) type UITableView采集的事件类型,这里设置为tableViewSelectEvent
(2) page 为当前页面的名称,用于前端显示用;
(3) section 为点击的cell所在的section
(4) row 为点击的cell所在的row

UIGestureRecognizer

在iOS开发中,经常会使用一些手势来处理一些点击的操作,所以也有必要对UIGestureRecognizer进行hook。HubbleData 并不是直接针对UIGestureRecognizer这个类进行hook,而是hook UIView类的addGestureRecognizer:方法,实现如下:

// hook addGestureRecognizer: 方法
[DASwizzler swizzleSelector:@selector(addGestureRecognizer:)
                    onClass:[UIView class]
                  withBlock:executeBlock];

// 执行block 判断手势类型,并插入响应埋点代码
void (^executeBlock)(id, SEL, id) = ^(id target, SEL command, id arg) {
        if ([arg isKindOfClass:[UITapGestureRecognizer class]] ||
            [arg isKindOfClass:[UILongPressGestureRecognizer class]]) {
            [arg addTarget:self action:@selector(da_autoEventAction:)];
            //在本类下添加一个action的实现
            ...........
        }
};

通过hook addGestureRecognizer:方法,可以得到该UIView所添加的UIGestureRecognizer,这里只对UITapGestureRecognizer和UILongPressGestureRecognizer进行处理,其他的手势暂未做处理。得到相应的UIGestureRecognizer,添加一个action,当该手势执行的时候,同样会执行该action,在action中执行埋点的操作。

这里获取的是UIGestureRecognizer所在的UIView的唯一标识标识字符串编码作为EventID,采集的属性为:

(1) type UIGestureRecognizer采集的事件类型,这里设置为gestureTapEvent
(2) page 为当前页面的名称,用于前端显示用;

参考资料

iOS数据埋点统计方案–陈满

iOS动态性可复用而且高度解耦的用户统计埋点实现

网易HubbleData无埋点SDK在iOS端的设计与实现

最近的文章

iOS页面卡顿及性能优化

卡顿产生的原因在屏幕成像的过程中,CPU和GPU的职责及CPU:负责对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)。GPU:负责变换、合成、纹理的渲染。CPU 把计算好的数据给 GPU,GPU 来渲染,渲染后的数据放在帧缓存(缓冲区,有两块缓冲区,前帧缓存和后帧缓存,协调使用,效率高)中。然后,视频控制器从缓冲区获取渲染后的数据显示在屏幕上。图像显示原理 引用YY大神对于图像显示原理的分析一帧(或一页)数据就是...…

iOS继续阅读
更早的文章

App常见崩溃分析

前言在iOS开发调试过程中,我们会遇到很多崩溃问题,比如数组越界、容器中插入nil、或调用不存在的方法时都会出现崩溃现象。那么,为了能更好的应对并避免这些常见崩溃问题的发生,就是我们亟待需要解决的问题。下面我们将分析iOS开发中常见的几个崩溃,并结合示例给出这类问题的解决方案。NSInvalidArgumentException从字面上来看,是无效的参数异常,但是触发这个异常的场景还有很多,最常见的就是在NSArray,NSDictionary容器中插入nil时发生,例如下面代码所示://...…

iOS继续阅读