在开发过程中,埋点可以解决两类问题:一是了解用户使用app的行为,二是降低分析线上问题的难度。目前iOS开发中常见的埋点方式主要有:代码埋点可视埋点无代码埋点埋点代码主要是通过手写代码埋点,可以非常准确的在需要埋点的代码中加入埋点代码,可以方便的记录变量当前环境值,方便调试,跟踪埋点内容,但存在开发工作量大,埋点代码到处都是,后期维护困难等问题。缺点:1.很明显,后期维护的时候会写疑生。2、复用性差,难以移植到其他项目。3.工作量大,写的会越来越多。问题,只能在后续版本中迭代5.如果统计项的名字改了,老APP版本还是会统计老页面名。优点:1.如果非要写一个其他统计做不到的优点,可以定义度高,统计代码想写到哪里(其实这些也可以在后面的方案中实现,但是实现起来稍微麻烦一点)费斯路)可视化埋点就是把新增和修改埋点的工作可视化,提高新增和维护埋点的体验。本方案具体步骤为:1.从后台获取需要统计的地方2.将需要统计的类的load方法挂钩到MethodSwizzing待统计的方法3.上传统计的事件到背景分析以UIViewController和UIControl为例,说明程序的思路。UIViewControllerPV统计,页面统计比较简单,使用MethodSwizzing钩子系统的viewDidLoad,可以直接通过页面名锁定页面显示代码如下:+(void)load{staticdispatch_once_tonceToken;dispatch_once(&onceToken,^{SELoriginalDidLoadSelector=@selector(viewDidLoad);SELswizzingDidLoadSelector=@selector(analytic_viewDidLoad);[MethodSwizzingToolswizzingForClass:[selfclass]originalSel:originalDidLoadSelectorswizzingSel:swizzingDidLoadSelector];});}-(void)analytic_viewDidLoadSelector]/viewDidLoad{[selfclass]Identifier[NSStringStringWithFormidentifier]:@"%@",[selfclass]];//通过当前类名NSDictionary*dic=[[[AnalyticToolshareInstance].dataobjectForKey:@"PAGE"]objectForKey:获取PAGEPV表中对应页面的pageid和pagename:标识符];if(dic){NSString*pageid=dic[@"screenData"][@"pageid"];NSString*pagename=dic[@"screenData"][@"pagename"];[AnalyticToolupLoadScreenName:pagenamewithScreenID:pageid];}}UIControl点击统计,主要是通过hooksendAction:to:forEvent:来实现的,其唯一标识我们使用targetname/selector/tag来标记,具体代码如下:+(void)load{staticdispatch_once_tonceToken;dispatch_once(&onceToken,^{SELoriginalSelector=@selector(sendAction:to:forEvent:);SELswizzingSelector=@selector(analytic_sendAction:to:forEvent:);[MethodSwizzingToolswizzingForClass:[selfclass]originalSel:originalSelectorswizzingSel:swizzingSelector];});}-(void)analytic_sendAction:(SEL)actionto:(id)targetforEvent:(UIEvent*)event{[selfanalytic_sendAction:actionto:targetforEvent:event];NSString*identifier=[NSStringstringWithFormat:@"%@/%@/%ld",[targetclass],NSStringFromSelector(action),self.tag];NSDictionary*dic=[[[AnalyticToolshareInstance].dataobjectForKey:@"ACTION"]objectForKey:identifier];if(dic){NSString*eventid=dic[@"ActionData"][@"eventid"];NSString*targetname=dic[@"ActionData"][@"target"];NSString*pageid=dic[@"ActionData"][@"pageid"];NSString*pagename=dic[@"ActionData";][@"pagename"];[AnalyticToolupLoadActionEventWithScreenName:pagenamewithScreenID:pageidwithTargetName:targetnamewithEventID:eventid];}}缺点:1.需要后台配合2.扩展性不是很高,因为后台下发的统计内容需要修改来做各版本统计扩展的优点:1.相比第一种方案,代码量少了很多。2、后台动态获取统计内容,方便在线修改,无需埋点。没必要埋点。更准确的说是“全埋点”,业务代码中不会出现埋点代码,便于管理和维护。它的缺点是埋点成本高,后期分析也比较复杂,加上view_path的不确定性。所以这个方案不能解决所有的埋点需求,但是对于大量的通用埋点需求可以节省大量的开发和维护成本。其中可视埋点和无埋点属于非侵入式埋点方案,因为它们不需要在工程代码中编写埋点代码。因此,采用这种非侵入式埋点方案,既可以保证埋点统一维护,又可以实现与工程代码的解耦。接下来,通过今天的文章,我们来分析下非侵入式埋点方案的实现。我们都知道iOS开发中最常见的三个埋点就是页面进入次数、页面停留时间、点击事件的埋点。对于这三种常见的情况,我们可以利用runtime方法替换技术插入埋点代码,实现埋点方法的非侵入式。具体实现方法是:先写一个替换runtime方法的类ViewHook,添加替换方法hookClass:fromSelector:toSelector,代码如下:#import"ViewHook.h"#import@implementationViewHook+(void)hookClass:(Class)classObjectfromSelector:(SEL)fromSelectortoSelector:(SEL)toSelector{Classclass=classObject;//获取被替换类的实例方法MethodfromMethod=class_getInstanceMethod(class,fromSelector);//获取被替换类MethodtoMethod的实例方法=class_getInstanceMethod(class,toSelector);//Class_addMethod返回成功,说明被替换的方法还没有执行,接下来会先通过class_addMethod方法执行;如果返回失败,说明被替换的方法已经存在,可以直接交换IMP指针}}@end此方法使用运行时method_exchangeImplementations接口来交换方法的实现。当调用原始方法时,它会被挂钩以执行指定的方法。页面进入次数和页面停留时间需要埋没UIViewController的生命周期。可以通过以下代码创建一个UIViewController的Category:@implementationUIViewController(logger)+(void)load{staticdispatch_once_tonceToken;dispatch_once(&onceToken,^{//通过@selector获取被替换和替换方法的SEL,传入作为ViewHook的参数:hookClass:fromSelector:toSelectorSELfromSelectorAppear=@selector(viewWillAppear:);SELtoSelectorAppear=@selector(hook_viewWillAppear:);[ViewHookhookClass:selffromSelector:fromSelectorAppeartoAppeartoAppear:toSelect];SELfromSelectorDisappear=@selector(viewWillDisappear:);SELtoSelectorDisappear=@selector(hook_viewWillDisappear:);[ViewHookhookClass:selffromSelector:fromSelectorDisappeartoSelector:toSelectorDisappear];});}-(void)先hook_viewOL/WillAppear)插入代码:(BO,然后执行原viewWillAppear方法[selfinsertToViewWillAppear];[selfhook_viewWillAppear:animated];}-(void)hook_viewWillDisappear:(BOOL)animated{//执行插入的代码,再执行原来的viewWillDi消失方法[selfinsertToViewWillDisappear];[selfhook_viewWillDisappear:animated];}-(void)insertToViewWillAppear{//ViewWillAppear时埋日志[[[[SMLoggercreate]message:[NSStringstringWithFormat:@"%@Appear",NSStringFromClass([selfclass])]]classify:ProjectClassifyOperation]save];}-(void)insertToViewWillDisappear{//ViewWillDisappear时埋日志[[[[SMLoggercreate]message:[NSStringstringWithFormat:@"%@Disappear",NSStringFromClass([selfclass])]]classify:ProjectClassifyOperation]save];}@end可以看到原来Category使用+load()方法中的ViewHook进行方法替换,并执行替换方法中需要嵌入的方法[selfinsertToViewWillAppear]这样,每个UIViewController生命周期都会在到达ViewWillAppear时执行insertToViewWillAppear方法。那么,我们如何区分不同的UIViewController呢?我通常使用NSStringFromClass([selfclass])方法来获取类名。这样我就可以通过类名来区分不同的UIViewControllers了。对于点击事件,我们也可以通过运行时方法替换的方式进行非侵入式的埋点。这里的主要工作是找到这个点击事件的方法sendAction:to:forEvent:,然后在+load()方法中使用ViewHook将其替换为你定义的方法。完整的代码实现如下:+(void)load{staticdispatch_once_tonceToken;dispatch_once(&onceToken,^{//通过@selector获取替换和替换方法的SEL,将SEL作为ViewHook的参数传入:hookClass:fromeSelector:toSelectorfromSelector=@selector(sendAction:to:forEvent:);SELtoSelector=@selector(hook_sendAction:to:forEvent:);[ViewHookhookClass:selffromSelector:fromSelectortoSelector:toSelector];});}-(void)hook_sendAction:(SEL)actionto:(id)targetforEvent:(UIEvent*)event{[selfinsertToSendAction:actionto:targetforEvent:event];[selfhook_sendAction:actionto:targetforEvent:event];}-(void)insertToSendAction:(SEL)actionto:(id)targetforEvent:(UIEvent*)event{//日志记录]message:[NSStringstringWithFormat:@"%@%@",targetName,actionString]]save];}}与UIViewCont不同roller生命周期埋点,UIButton在一个视图类中可能有多个不同的继承类,同一个UIButton的子类在不同的视图类的埋点也要区分。因此,我们需要将“动作选择器名称NSStringFromSelector(action)”+“视图类名称NSStringFromClass([targetclass])”组合起来,形成埋点记录的唯一标识。除了UIViewController和UIButton控件,Cocoa框架的其他控件都可以使用该方法进行非侵入式埋入。以Cocoa框架中最复杂的UITableView控件为例,可以使用hooksetDelegate方法实现非侵入式埋点。另外,对于Cocoa框架中的手势事件(GestureEvent),我们也可以使用钩子initWithTarget:action:方法来实现非侵入式的埋点。事件的唯一标识符被运行时方法取代。我们可以hook所有的Objective-C方法,可以说是面面俱到,可以帮助我们解决大部分的埋点问题。但是,该方案的准确性不够高,无法区分不同视图树节点中的同一类。例如,视图中同一个UIButton的不同实例仅通过“动作选择器名称”+“视图类名称”的组合是无法区分的。这时候我们就需要一个唯一的标识来区分不同的事件。接下来,我将告诉您如何制定这个唯一标识符。这时候我首先想到的是这个问题是否可以通过view-level的路径来解决。因为每个页面都有视图树结构,我们可以通过视图的父视图和子视图的属性来还原每个页面的视图树。视图树的顶层是UIWindow,每个视图都是树的一个子节点。如下图:一个视图下的子节点可能是同一个视图的不同实例,比如上图中UIView视图节点下的两个UIButton是同一个类的不同实例,所以单独的viewtree不能唯一标识view的ID。那么,在这种情况下,我们应该如何区分不同的观点呢?这时候我们就想到了索引:每个子视图在父视图中都会有自己的索引,所以如果加上这个索引,每个视图的ID都是唯一的。接下来的问题是视图层级路径的唯一标识加上父视图中的索引是否可以覆盖所有情况?当然不是。我们还需要考虑像UITableViewCell这样具有可重用机制的视图。页面滚动的时候cell会被不断的复用,所以索引方式还是没用。但问题并非无法解决。UITableViewCell需要用到indexPath,里面包含section和row的值。因此,我们可以通过indexPath来判断每个Cell的唯一性。除了UITableViewCell的情况,UIAlertController也比较特殊。它的特殊性在于视图层级是不固定的,因为它可能出现在任何页面中。但是,我们都知道它的功能区分往往是由弹窗的内容来决定的,所以它的唯一标识可以由内容来决定。此外,需要特殊处理的情况较多,但我们总能通过一些方法判断其唯一性,在此不再一一列举。这个想法是想办法找出元素之间的不同因素,然后将它们组合起来形成一个可以与其他元素区分开来的标志。除了上面提到的特殊情况,还有一种情况让我们很难获得准确的唯一标识。如果视图层级会在运行时发生变化,比如在执行insertSubView:atIndex:、removeFromSuperView等方法时,我们将无法获得唯一标识。即使我们只截取部分路径,也不能保证后面更新代码的时候这部分不会被移动。即使在运行时不会修改视图层级,如果以后需要频繁迭代页面更新,视图唯一标识也需要同步更新和维护。此类问题不易解决,唯一事件标识的准确性也难以保证。这也是各个公司难以通过运行时方法替换来全面铺开非侵入式嵌入点的原因。虽然无创埋点无法覆盖所有情况,全面铺开难度较大,但无创埋点还是解决了大部分埋点需求,节省了大量的人工成本。最好的解决方案总是针对不同的场景。一个创业团队初期我们是不可能选择方案3的结构的,所以对于你来说,如果没有后端业务同学的支持,你就得选择目前最适合你的方案,选项1可能真的是最适合你的选项,至少可以完成统计要求,虽然有点累。但在合适的时候,在不同的选择之间切换才是成长的体现。一开始,如果你的团队给了你资源和时间来改进这个模块,如果你把它做得更好,那一定是一件很酷的事情。参考资料1.网易HubbleData无痕埋点SDK实现2.iOS无埋点数据SDK实践之路3.美团前端无痕埋点解决方案4.微信读书团队Aspects基本原理5.杂谈iOS管理【本文为栏目组织“AiChinaTech”微信公众号(id:tech-AI)的原创文章】】点此查看更多本作者好文