1、背景
稀里哗啦一大段
2、主要功能划分
从整个流程来说,我把他划分为下面几个主要的功能,事件拦截
、viewPath获取
、数据上报
、圈选功能
,并在文章中会对每个功能进行比较详细的解析和代码粘贴。
3、事件拦截
3.0、runtime核心功能
这里用到runtime的添加方法
,交换方法
+(void)swizzingForClass:(Class)cls originalSel:(SEL)originalSelector swizzingSel:(SEL)swizzingSelector{
//添加交换实例方法
Class class = cls;
//添加交换类方法
//Class class = objc_getMetaClass(object_getClassName(cls));;
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzingMethod = class_getInstanceMethod(class, swizzingSelector);
BOOL addMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzingMethod),
method_getTypeEncoding(swizzingMethod));
//如果添加成功交换,交换实现
if (addMethod) {
class_replaceMethod(class,
swizzingSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
}else{
method_exchangeImplementations(originalMethod, swizzingMethod);
}
}
注意:添加实例方法和添加类方法有少许的区别,在使用是需要更具具体的场景进行处理。具体原理可点击这里查看。
3.1、页面拦截
创建UIViewController的Category,在此对生命周期的方法进行交换。
+(void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalDidLoadSelector = @selector(viewDidLoad);
SEL swizzingDidLoadSelector = @selector(user_viewDidLoad);
[WMMethodSwizzingTool swizzingForClass:[self class] originalSel:originalDidLoadSelector swizzingSel:swizzingDidLoadSelector];
});
}
-(void)user_viewDidLoad
{
[self user_viewDidLoad];
//TODO:数据上传代码
}
小插曲:原本想对控制器的dealloc
方法也行统一处理,但是在完成后发现和某个第三方有问题,在双击输入框是出现crash,所以先不对这个进行拦截。
3.2、按钮拦截
对于系统的按钮可直接对创建UIControl的Category分类,并对sendAction:to:forEvent:
方法进行拦截。
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalSelector = @selector(sendAction:to:forEvent:);
SEL swizzingSelector = @selector(wm_sendAction:to:forEvent:);
[WMMethodSwizzingTool swizzingForClass:[self class] originalSel:originalSelector swizzingSel:swizzingSelector];
});
}
- (void)wm_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
[self wm_sendAction:action to:target forEvent:event];
//TODO:数据上传(GIO过滤)
}
注意:由于原先项目中集成了GIO统计一直还在用着,点击方法中会拦截到GIO的growingHookTouch_xxxx
方法,导致数据的多次上传,所以在这边对GIO的方法进行过滤掉。
if ([NSStringFromSelector(action) hasPrefix:@"growingHookTouch"])return;
3.3、手势拦截
确实在项目中使用点击手势的地方远比直接使用按钮的地方多,由于这次埋点只对点击事件处理所以也只UITapGestureRecognizer
创建Category
。
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[WMMethodSwizzingTool swizzingForClass:[self class] originalSel:@selector(initWithTarget:action:) swizzingSel:@selector(vi_initWithTarget:action:)];
});
}
- (instancetype)vi_initWithTarget:(nullable id)target action:(nullable SEL)action
{
UITapGestureRecognizer *selfGestureRecognizer = [self vi_initWithTarget:target action:action];
if (!target && !action) {
return selfGestureRecognizer;
}
if ([target isKindOfClass:[UIScrollView class]]) {
return selfGestureRecognizer;
}
Class class = [target class];
SEL sel = action;
//创建一个新的方法 方法名为 sel_name
NSString * sel_name = [NSString stringWithFormat:@"wm_%s_%@", class_getName([target class]),NSStringFromSelector(action)];
SEL sel_ = NSSelectorFromString(sel_name);
//添加一个方法 参数:相应手势的类,添加的方法名,实现方法的函数 responseUser_gesture
BOOL isAddMethod = class_addMethod(class,
sel_,
method_getImplementation(class_getInstanceMethod([self class], @selector(responseUser_gesture:))),
nil);
self.methodName = NSStringFromSelector(action);
//方法添加成功,原先的方法实现 action -> 新的方法实现 responseUser_gesture。
if (isAddMethod) {
Method selMethod = class_getInstanceMethod(class, sel);
Method sel_Method = class_getInstanceMethod(class, sel_);
method_exchangeImplementations(selMethod, sel_Method);
}
return selfGestureRecognizer;
}
-(void)responseUser_gesture:(UITapGestureRecognizer *)gesture
{
NSString * identifier = [NSString stringWithFormat:@"wm_%s_%@", class_getName([self class]),gesture.methodName];
//调用原方法
SEL sel = NSSelectorFromString(identifier);
if ([self respondsToSelector:sel]) {
IMP imp = [self methodForSelector:sel];
void (*func)(id, SEL,id) = (void *)imp;
func(self, sel,gesture);
}
}
//TODO:数据上报
解析:这边做了两部处理,有区别于按钮点击事件,按钮是直接在触发点击事件消息转发方法拦截,直接能到触发的点。而这边手势是在创建手势是,对点击事件要再度处理。
第一步:在初始化方法中拿到实现方法
action
,并动态创建一个方法和原本的action
进行交换。
第二步:在交互的实现中实现原先的action
,然后在做数据上报处理。
小插曲:最开始想着对手势的拦截就直接对UITapGestureRecognizer
进行处理,在拦截里面过其他的过滤,但后来发现是在太多系统的手势,导致一些手势直接失效,最后改成这样。
3.4、列表拦截
对UITableView和UICollectionView的处理是对delegate进行处理,过程类似于手势。
+(void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalAppearSelector = @selector(setDelegate:);
SEL swizzingAppearSelector = @selector(wm_collection_setDelegate:);
[WMMethodSwizzingTool swizzingForClass:[self class] originalSel:originalAppearSelector swizzingSel:swizzingAppearSelector];
});
}
-(void)wm_tableView_setDelegate:(id)delegate
{
[self wm_tableView_setDelegate:delegate];
SEL sel = @selector(tableView:didSelectRowAtIndexPath:);
SEL sel_ = NSSelectorFromString([NSString stringWithFormat:@"%@_%@_%ld", NSStringFromClass([delegate class]), NSStringFromClass([self class]),(long)self.tag]);
//因为 tableView:didSelectRowAtIndexPath:方法是optional的,所以没有实现的时候直接return
if (![self isContainSel:sel inClass:[delegate class]]) {
return;
}
BOOL addsuccess = class_addMethod([delegate class],
sel_,
method_getImplementation(class_getInstanceMethod([self class], @selector(user_tableView:didSelectRowAtIndexPath:))),
nil);
//如果添加成功了就直接交换实现, 如果没有添加成功,说明之前已经添加过并交换过实现了
if (addsuccess) {
Method selMethod = class_getInstanceMethod([delegate class], sel);
Method sel_Method = class_getInstanceMethod([delegate class], sel_);
method_exchangeImplementations(selMethod, sel_Method);
}
}
- (void)user_collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath;
{
SEL sel = NSSelectorFromString([NSString stringWithFormat:@"%@_%@_%ld", NSStringFromClass([self class]), NSStringFromClass([collectionView class]), (long)collectionView.tag]);
if ([self respondsToSelector:sel]) {
IMP imp = [self methodForSelector:sel];
void (*func)(id, SEL,id,id) = (void *)imp;
func(self, sel,collectionView,indexPath);
}
//TODO:数据上报
}
//判断页面是否实现了某个sel
- (BOOL)isContainSel:(SEL)sel inClass:(Class)class {
unsigned int count;
Method *methodList = class_copyMethodList(class,&count);
for (int i = 0; i < count; i++) {
Method method = methodList[i];
NSString *tempMethodString = [NSString stringWithUTF8String:sel_getName(method_getName(method))];
if ([tempMethodString isEqualToString:NSStringFromSelector(sel)]) {
return YES;
}
}
return NO;
}
解析:实现思路和手势的一样,不过多书写。
3.5、Alert拦截
Alert的拦截是直接对UIAlertAction
点击的按钮进行拦截。
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[WMMethodSwizzingTool swizzingClassForClass:[self class] originalSel:@selector(actionWithTitle:style:handler:) swizzingSel:@selector(wm_actionWithTitle:style:handler:)];
});
}
+ (instancetype)wm_actionWithTitle:(nullable NSString *)title style:(UIAlertActionStyle)style handler:(void (^ __nullable)(UIAlertAction *action))handler{
void (^handlerBlock)(UIAlertAction *action) = ^(UIAlertAction *action){
if (handler) {
handler(action);
}
//TODO:数据是否上报
}
UIAlertAction *alterAction = [UIAlertAction wm_actionWithTitle:title style:style handler:handlerBlock];
}
注意:这边有点不一样
1.这边交换的类方法(上面也写过区别)。
2..这里的点击是block回调,所以创建了一个中间block进行处理。
3..数据上报这块,直接给到取消
、确认
是完全没有意义的,所以给UIAlertAction
添加了个属性,记录这个弹框的更多信息,
已定位业务。
UIAlertController
给UIAlertAction
添加的属性赋值。(UIAlertAction
添加属性方法略)
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[WMMethodSwizzingTool swizzingForClass:[self class] originalSel:@selector(addAction:) swizzingSel:@selector(wm_addAction:)];
});
}
- (void)wm_addAction:(UIAlertAction *)action {
[self wm_addAction:action];
action.alertControllerActionPath = [NSString stringWithFormat:@"%@/%@",self.title,self.message];
}
小总结:
这期拦截代码到这里基本上就没了。其中完成了:
1.进页面有统一的地方得到当前的控制器。
2.点击(按钮,手势)有统一的响应方法的地方。
3.列表点击有统一响应的地方。
4.弹框有统一的响应,并能拿到弹框信息。
4、ViewPath获取
先放出ViewPath格式:
普通路径:
WMMineViewController[0]/UIView[0]/UITableView[0]/UIView[1]/WMMineTopInfoView[0]/UIView[0]
复杂路径:
WMHomePageViewController[0]/UIView[0]/UICollectionView[0]/WMHomePageBannerCell#[1,0]/UIView[0]/SDCycleScrollView[0]/UICollectionView[0]/SDCollectionViewCell#[0,1]
ViewPath是每个组件的唯一路径,大数据通过ViewPath来确定当前点击的是什么(圈选来告诉这个ViewPath是什么),然后进行数据分析。
直接上代码:
4.1、第一步
UIView的Category,获取某个view在同一级别的深度,上面路劲中的[0]
。
- (NSString *)obtainSameSuperViewSameClassViewTreeIndexPath
{
NSString *classStr = NSStringFromClass([self class]);
//cell的子view
//UITableView 特殊的superview (UITableViewContentView)
//UICollectionViewCell
BOOL shouldUseSuperView =
([classStr isEqualToString:@"UITableViewCellContentView"]) ||
([[self.superview class] isKindOfClass:[UITableViewCell class]])||
([[self.superview class] isKindOfClass:[UICollectionViewCell class]]);
if (shouldUseSuperView) {
return [self obtainIndexPathByView:self.superview];
}else {
return [self obtainIndexPathByView:self];
}
}
- (NSString *)obtainIndexPathByView:(UIView *)view
{
// NSInteger viewTreeNodeDepth = NSIntegerMin;//所有类型 深度
NSInteger sameViewTreeNodeDepth = -1;//相同类型 深度(默认-1)
NSString *classStr = NSStringFromClass([view class]);
NSMutableArray *sameClassArr = [[NSMutableArray alloc]init];
//所处父view的全部subviews根节点深度
for (NSInteger index =0; index < view.superview.subviews.count; index ++) {
//同类型
if ([classStr isEqualToString:NSStringFromClass([view.superview.subviews[index] class])]){
[sameClassArr addObject:view.superview.subviews[index]];
}
}
//所处父view的同类型subviews根节点深度
for (NSInteger index =0; index < sameClassArr.count; index ++) {
if (view == sameClassArr[index]) {
sameViewTreeNodeDepth = index;
break;
}
}
return [NSString stringWithFormat:@"%ld",sameViewTreeNodeDepth];
}
这里是做同类控件的深度。
4.2、第二步
UIResponder的Category,通过响应链获取完整的路径。
- (NSString *)generateViewPath
{
NSString *spointViewPath;
if ([self isKindOfClass:[UIView class]]) {
UIView *view1 = (id)self;
NSMutableString *str = [NSMutableString string];
str = [[NSStringFromClass(view1.class) stringByAppendingFormat:@"%@",str] mutableCopy];
//将viewPath放入 accessibilityIdentifier ,如果存在直接返回,优化性能。
if (view1.accessibilityIdentifier) {
return view1.accessibilityIdentifier;
}else{
[str appendFormat:@"%@",[self getIndexPathForView:view1]];
}
UIView *view = (id)self;
while (view.nextResponder) {
if ([view.class isSubclassOfClass:[UIViewController class]]) {
break;
}
if ([view isMemberOfClass:[MMPopupWindow class]]) {
spointViewPath = [NSString stringWithFormat:@"%@",[(MMPopupView *)view class]];
break;
}
str = [[@"/" stringByAppendingFormat:@"%@",str] mutableCopy];
view = (id)view.nextResponder;
NSString *sameViewTreeNode1 = @"[0]";
if ([view isKindOfClass:[UIView class]]) {
sameViewTreeNode1 = [self getIndexPathForView:view];
}
str = [[sameViewTreeNode1 stringByAppendingString:str] mutableCopy];
str = [[NSStringFromClass(view.class) stringByAppendingFormat:@"%@",str] mutableCopy];
}
spointViewPath = [NSString stringWithFormat:@"%@",str];
view1.accessibilityIdentifier = spointViewPath;
}
return spointViewPath;
}
- (NSString *)getIndexPathForView:(UIView *)cellView {
NSString *cellIndexPath = [NSString string];
if ([cellView.superview isKindOfClass:[UICollectionView class]]&&[self isKindOfClass:[UICollectionViewCell class]]) {
UICollectionView *collectionView = (UICollectionView *)cellView.superview;
NSIndexPath *indexPath = [collectionView indexPathForCell:(UICollectionViewCell *)cellView];
cellIndexPath = [NSString stringWithFormat:@"#[%ld,%ld]",(long)indexPath.section,(long)indexPath.row];
}else if ([cellView.superview isKindOfClass:[UITableView class]]&&[self isKindOfClass:[UITableViewCell class]]) {
UITableView *tableView = (UITableView *)cellView.superview;
NSIndexPath *indexPath = [tableView indexPathForCell:(UITableViewCell *)cellView];
cellIndexPath = [NSString stringWithFormat:@"#[%ld,%ld]",(long)indexPath.section,(long)indexPath.row];
}else{
cellIndexPath = [NSString stringWithFormat:@"[%@]",[cellView obtainSameSuperViewSameClassViewTreeIndexPath]];
}
return cellIndexPath;
}
为了方面后面的圈选统一,在这边直接在cell的后面添加了所在位置,就不必再各个上传数据的地方在拼接上去。
5、圈选功能
圈选代码太多详细内容查看demo
这里只提示一些注意点。
1.圈选得到的路径和上传得到的路径必须一致。
2.圈选根据要求只对能响应事件的控件进行圈选。
3.圈选的内容可能没有事件但能响应事件也能圈选。
4.一些第三方轮播库的index并不确定,需要组件里面页码实现的规则进行特殊计算。
5.出现圈选icon可通过扫scheme二维码实现或项目中隐蔽的入口。
6、数据上传
数据上传这块更具自己服务所需数据处理就好,总结一下几点。
1.网络这块直接通过AFN再次封装,不使用项目中现有的减少依赖。
2.上传的数据模型和服务约定就好。
7、总结
以上能实现基本的实时埋点和实时上传的功能,也是目前公司项目做得第一期所有功能。感谢网络上许多文章,后续有更新再补充,希望对你有帮助,谢谢阅读。
项目完整demo