Commit 42658103 authored by 乔金柱's avatar 乔金柱

Merge branch 'ajp/7.27.0_expo' into 'master'

add Exposure

See merge request !54
parents b2404a0e ed064ed2
PODS:
- GMCache (1.0.1):
- TMCache (= 2.1.0)
- GMPhobos (2.0.3):
- GMPhobos (2.0.4):
- GMCache
- MagicalRecord
- MJExtension
......@@ -28,7 +28,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
GMCache: b78d8e46db864405e91d226ce640cc80d966c611
GMPhobos: 6bd14d7b98841b6a3e35c5aa8b1f091ad07bbbf7
GMPhobos: fd6f7e453e71fb7f51d8c5601e605c110ddfda92
MagicalRecord: 53bed74b4323b930992a725be713e53b37d19755
MJExtension: 635f2c663dcb1bf76fa4b715b2570a5710aec545
TMCache: 95ebcc9b3c7e90fb5fd8fc3036cba3aa781c9bed
......
......@@ -23,7 +23,7 @@ Pod::Spec.new do |s|
s.source = { :git => "git@git.wanmeizhensuo.com:gengmeiios/GMPhobos.git", :tag => s.version.to_s }
s.platform = :ios, '8.0'
s.source_files = 'GMPhobos/Classes/*.{h,m}'
s.source_files = 'GMPhobos/Classes/**/*'
s.dependency 'GMCache'
s.dependency 'MJExtension'
s.dependency 'MagicalRecord'
......
//
// GMExposureManager.h
// Gengmei
//
// Created by Mikasa on 2019/6/21.
// Copyright © 2019 更美互动信息科技有限公司. All rights reserved.
//
#import "UIView+Tracker.h"
#import "UIScrollView+Tracker.h"
#import "UIViewController+Tracker.h"
#import "NSObject+Tracker.h"
/**
* 精准曝光
* 开始曝光时机:1、页面出现 2、退到后台
*/
typedef NS_ENUM(NSInteger, GMExpoUploadMode) {
GMExpoUploadModeImmediate = 0,// 立即上报
GMExpoUploadModePage = 1,// 按页面上报
GMExpoUploadModeApp = 2 // 按App上报
};// 精准曝光数据上报模式
typedef NS_ENUM(NSInteger, GMViewTrackerType) {
GMViewTrackerTypeUIViewSetHidden = 0,
GMViewTrackerTypeUIViewSetAlpha = 1,
GMViewTrackerTypeUIViewDidMoveToWindow = 2,
GMViewTrackerTypeUIScrollContentOffset = 3,
GMViewTrackerTypeUIScrollDragging = 4,
GMViewTrackerTypeUIViewSetExposure = 5,
GMViewTrackerTypeUIViewEnterForeground = 6,
GMViewTrackerTypeUIViewDidAppear = 7,
GMViewTrackerTypeUIViewDidDisappear = 8,
GMViewTrackerTypeUIViewWillResignActive = 9
}; // 记录方式
@interface GMExposureManager : NSObject
/// 设置上报数据地址
@property (nonatomic, copy) NSString *uploadExposureAPI;
/**
* 精准曝光数据上报模式
* 默认为 GMExpoUploadModePage
*/
@property (nonatomic, assign) GMExpoUploadMode uploadMode;
/** view出现的屏幕的占比 默认0.01f */
@property (nonatomic, assign) CGFloat exposureDimThreshold;// view visble in size > exposureDimThreshold
/** 曝光最少时长 默认0.01s (目前暂未使用)*/
@property (nonatomic, assign) NSTimeInterval exposureMinDuration;// view duration > exposureMinDuration
@property (nonatomic, assign) BOOL inBackground;// 当前App是否进入后台
@property (nonatomic, assign) BOOL ignoreTrackModel;// 是否需要忽略滚动模式
/**
GMExposureManager
@return sharedManager
*/
+ (instancetype)sharedManager;
#pragma mark - Deal Tracker
/**
* 结束所有页面的曝光并上报
* 会先将所有页面上正在曝光的View置为end,再进行数据上传
*/
- (void)endExpoTrcakerForAllPage;
/**
* 结束指定页面的曝光并上报
* 会先将指定页面上正在曝光的View置为end,再进行数据上传
* 多用于下拉刷新前需要曝光当前的vc的数据
@param pageCtrl 指定页面
*/
- (void)endExpoTrcakerForPageCtrl:(UIViewController *)pageCtrl;
#pragma mark - Deal Data
/**
上传指定页面的曝光数据
@param pageName 指定页面
*/
- (void)uploadExposureDataForPage:(UIViewController *)pageCtrl;
/**
上传所有页面的曝光数据
*/
- (void)uploadAllExposureData;
// 存储曝光数据
- (void)startStoreExposureData;
/**
清理指定页面的曝光数据
@param pageName 指定页面
*/
- (void)clearExposureDataForPage:(UIViewController *)pageCtrl;
// 清理当前数据
- (void)clearAllExposureData;
/**
当前View上是否有需要精准曝光的数据
@param view View
@return yes/no
*/
+ (BOOL)haveExposureForView:(UIView *)view;
#pragma mark - View Visible/UnVisibel
/**
获取当前View的可见状态
@param view view
@param 可见状态
*/
+ (BOOL)isViewVisible:(UIView *)view;
/**
设置当前view是否可见状态
@param view view
@param recursive 是否递归子view
@param ignoreTracking 是否忽略手势滚动时追踪
*/
+ (void)fetchViewForVisibleState:(UIView *)view trackerType:(GMViewTrackerType)trackerType recursive:(BOOL)recursive;
/** 设置view的曝光状态
* view 可见 && 状态none或者end——> 开始曝光
* view 可见 && 状态start——> 正在曝光,不做处理
* view 不可见 && 状态start ——> 结束曝光
* view 不可见 && 状态none或者end ——> 不做处理
*/
- (void)exposureStatusForView:(UIView *)view inPageCtrl:(UIViewController *)inPageCtrl;
@end
//
// GMExposureManager.m
// Gengmei
//
// Created by Mikasa on 2019/6/21.
// Copyright © 2019 更美互动信息科技有限公司. All rights reserved.
//
#import "GMExposureManager.h"
#import "GMExposureModel.h"
#import "UIResponder+PhobosPV.h"
#import "Phobos.h"
#import "PhobosUtil.h"
#import <MJExtension/MJExtension.h>
@interface GMExposureManager ()
/// 曝光页面
@property (nonatomic, strong) NSMutableArray *pageArrayM;
@property (nonatomic, strong) NSMutableDictionary *exposureDictM;// 存储开始进入曝光的数据
@property (nonatomic, strong) NSMutableDictionary *exposureViewDictM;// 存储开始进入曝光的View
@property (nonatomic, strong) NSMutableDictionary *exposurePageDictM;//存储页面上已经曝光过的View
@end
@implementation GMExposureManager
#pragma mark - init Method
+ (instancetype)sharedManager {
static dispatch_once_t onceToken;
static id _instance;
dispatch_once(&onceToken, ^{
_instance = [[GMExposureManager alloc] init];
});
return _instance;
}
- (instancetype)init {
if (self =[super init]) {
_pageArrayM = [NSMutableArray array];
_exposureDictM = [NSMutableDictionary dictionary];
_exposurePageDictM = [NSMutableDictionary dictionary];
_exposureViewDictM = [NSMutableDictionary dictionary];
_uploadMode = GMExpoUploadModePage;
_exposureDimThreshold = 0.01f;
_exposureMinDuration = 0.01f;
[self addObserverMethod];
}
return self;
}
#pragma mark - Observer Method
- (void)addObserverMethod {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleNotificaion:) name:UIApplicationDidEnterBackgroundNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleNotificaion:) name:UIApplicationWillEnterForegroundNotification object:nil];
}
- (void)removeObserverMethod {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIApplicationDidEnterBackgroundNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIApplicationWillEnterForegroundNotification object:nil];
}
// 处理通知事件
- (void)handleNotificaion:(NSNotification *)notific {
NSString *notifName = notific.name;
// 进入后台 结束曝光 + 上报所有数据
if ([notifName isEqualToString:UIApplicationDidEnterBackgroundNotification] ) {
[GMExposureManager sharedManager].inBackground = YES;
// 同步所有页面数据并上传
[[GMExposureManager sharedManager] endExpoTrcakerForAllPage];
}
// 回到前台 记录数据
if ([notifName isEqualToString:UIApplicationWillEnterForegroundNotification] ) {
[GMExposureManager sharedManager].inBackground = NO;
// 取出临时存储的需要曝光的页面,进行曝光
for (NSString *pageKey in self.pageArrayM) {
UIViewController *pageCtrl = [GMExposureManager expoPageCtrl:pageKey];
if (pageCtrl && [pageCtrl isKindOfClass:[UIViewController class]]) {
[GMExposureManager fetchViewForVisibleState:pageCtrl.view trackerType:GMViewTrackerTypeUIViewEnterForeground recursive:YES];
}
}
[self.pageArrayM removeAllObjects];
}
}
#pragma mark - Tracker Method
// 结束所有页面的曝光并上报
- (void)endExpoTrcakerForAllPage {
// 将所有正在曝光的View的状态变更为结束曝光
for (UIView *view in self.exposureViewDictM.allValues) {
view.expoStatus = GMViewExpoStatusEnd;
}
[self.exposureViewDictM removeAllObjects];
for (NSString *viewKey in self.exposureDictM.allKeys) {
// 将正在曝光的数据直接更改状态为结束曝光
NSDictionary *dict = self.exposureDictM[viewKey];
NSString *page = [[viewKey componentsSeparatedByString:@"/"] firstObject];
UIViewController *pageCtrl = [GMExposureManager expoPageCtrl:page];
[self storeExposureData:dict pageCtrl:pageCtrl];
}
[self.exposureDictM removeAllObjects];
// 将之前曝光的页面临时存储起来,确保下次从后台回来页面会立即开启曝光
[self.pageArrayM addObjectsFromArray:self.exposurePageDictM.allKeys];
[self uploadAllExposureData];
}
/**
1.结束指定页面的曝光并上报
2.多用于下拉刷新前需要曝光当前的vc的数据
@param pageCtrl 指定页面
*/
- (void)endExpoTrcakerForPageCtrl:(UIViewController *)pageCtrl {
// 在结束之前获取一次
[GMExposureManager fetchViewForVisibleState:pageCtrl.view trackerType:GMViewTrackerTypeUIViewDidDisappear recursive:YES];
NSString *page = [NSString stringWithFormat:@"%p",pageCtrl];
// View
NSMutableDictionary *dictViewM = [NSMutableDictionary dictionaryWithDictionary:self.exposureViewDictM];
for (NSString *viewKey in self.exposureViewDictM.allKeys) {
if ([viewKey hasPrefix:page]) {
UIView *view = self.exposureViewDictM[viewKey];
view.expoStatus = GMViewExpoStatusEnd;
[dictViewM removeObjectForKey:viewKey];
}
}
self.exposureViewDictM = dictViewM;
// data
NSMutableDictionary *dictDataM = [NSMutableDictionary dictionaryWithDictionary:self.exposureDictM];
for (NSString *viewKey in self.exposureDictM.allKeys) {
if ([viewKey hasPrefix:page]) {
NSDictionary *dict = self.exposureDictM[viewKey];
[self storeExposureData:dict pageCtrl:pageCtrl];
[dictDataM removeObjectForKey:viewKey];
}
}
self.exposureDictM = dictDataM;
[self uploadExposureDataForPage:pageCtrl];;
}
#pragma mark - Deal Data
// 存储曝光数据
- (void)startStoreExposureData {
// 当前曝光View 上数据曝光情况
NSMutableDictionary *dictDataM = [NSMutableDictionary dictionaryWithDictionary:self.exposureDictM];
for (NSString *key in dictDataM.allKeys) {
UIView *exposureView = self.exposureViewDictM[key];
// 当前view已经结束曝光 记录曝光数据---等待上传
if (!exposureView) {
NSString *page = [[key componentsSeparatedByString:@"/"] firstObject];
UIViewController *pageCtrl = [GMExposureManager expoPageCtrl:page];
[self storeExposureData:dictDataM[key] pageCtrl:pageCtrl];
[self.exposureDictM removeObjectForKey:key];
}
}
// 当前曝光数据是否仍在曝光-> 无存储曝光数据
NSMutableDictionary *dictViewM = [NSMutableDictionary dictionaryWithDictionary:self.exposureViewDictM];
for (NSString *key in dictViewM.allKeys) {
NSDictionary *dataDict = self.exposureDictM[key];
if (!dataDict) {
UIView *view = dictViewM[key];
[self storeStartExposureData:view pageCtrl:view.pageCtrl];
}
}
}
// 存储开始曝光数据
- (void)storeStartExposureData:(UIView *)startView pageCtrl:(UIViewController *)pageCtrl {
NSMutableDictionary *exposureDictM = [GMExposureManager sharedManager].exposureDictM;
startView.inTime = [PhobosUtil currentTime];
startView.expoStatus = GMViewExpoStatusStart;
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:startView.exposure];
// 安卓没有这个功能 暂时屏蔽
//[dict setObject:SafeString([PhobosUtil currentTime]) forKey:@"inTime"];
// 获取卡片位置
[dict setObject:startView.relative_position?:@"" forKey:@"relative_position"];
[dict setObject:startView.absolute_position?:@"" forKey:@"absolute_position"];
if (startView.extra_param && startView.extra_param.allKeys.count > 0) {
[dict addEntriesFromDictionary:startView.extra_param];
}
[exposureDictM setObject:dict forKey:[GMExposureManager exposureKeyForData:startView]];
}
/**
按指定页面存储曝光数据
@param view 曝光数据
@param pageName 指定页面
*/
- (void)storeExposureData:(NSDictionary *)expoDict pageCtrl:(UIViewController *)pageCtrl {
// 卡片曝光持续时间超出曝光最少时长才进行记录
/*
NSInteger inTime = [expoDict[@"inTime"] integerValue];
NSInteger duration = [view.outTime integerValue] - [view.inTime integerValue];
if (!duration) {
return;
}*/
// 将曝光数据所需要的某些参数从Page中取出
NSString *pageKey = [GMExposureManager exposureKeyForPage:pageCtrl];
GMExposureItemModel *itemModel = [self.exposurePageDictM objectForKey:pageKey];
if (!itemModel) {
itemModel = [GMExposureItemModel new];
}
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:expoDict];
/* 安卓没有此功能 暂屏蔽
if (![dict[@"outTime"] isNonEmpty]) {
[dict setObject:SafeString([PhobosUtil currentTime]) forKey:@"outTime"];
}*/
[itemModel.exposure_cards addObject:dict];
[self.exposurePageDictM setObject:itemModel forKey:pageKey];
}
// 同步Item数据
- (void)synchronizeViewItemData:(UIView *)view {
NSMutableDictionary *dict = [self.exposureDictM objectForKey:[GMExposureManager exposureKeyForData:view]];
if (dict) {
if (![PhobosUtil isNonEmpty:dict[@"relative_position"]]|| ![PhobosUtil isNonEmpty:dict[@"absolute_position"]]) {
[view fechExposurePositionForView];
[dict setObject:view.relative_position?:@"" forKey:@"relative_position"];
[dict setObject:view.absolute_position?:@"" forKey:@"absolute_position"];
if (view.extra_param && view.extra_param.allKeys.count > 0) {
[dict addEntriesFromDictionary:view.extra_param];
}
[self.exposureDictM setObject:dict forKey:[GMExposureManager exposureKeyForData:view]];
}
}
}
// 进行数据同步
- (void)synchronizeUploadItem:(GMExposureItemModel *)itemModel withViewPageData:(UIViewController *)pageCtrl {
if (!itemModel || !pageCtrl) {
return;
}
itemModel.tab_name = pageCtrl.tabName?:@"";
itemModel.page_name = pageCtrl.pageName?:@"";
itemModel.business_id = pageCtrl.businessId?:@"";
itemModel.referrer = pageCtrl.referer?:@"";
itemModel.referrerId = pageCtrl.referrerId?:@"";
itemModel.up_slide_times = pageCtrl.up_slide_times;
itemModel.down_slide_times = pageCtrl.down_slide_times;
itemModel.up_loading_times = pageCtrl.up_loading_times;
itemModel.down_loading_times = pageCtrl.down_loading_times;
[itemModel.exposure_cards sortUsingComparator:^NSComparisonResult(NSDictionary *obj1, NSDictionary *obj2){
return [obj1[@"absolute_position"] integerValue] > [obj2[@"absolute_position"] integerValue];
}];
NSDictionary *dic = [itemModel mj_keyValues];
NSMutableDictionary *dicM = [NSMutableDictionary dictionaryWithDictionary:dic];
[dicM addEntriesFromDictionary:pageCtrl.extra_param];
[Phobos track:@"page_precise_exposure" attributes:dicM sendNow:YES currentAPI:self.uploadExposureAPI];
}
/**
上传指定页面的曝光数据
@param pageName 指定页面
*/
- (void)uploadExposureDataForPage:(UIViewController *)pageCtrl {
GMExposureItemModel *itemModel = [self.exposurePageDictM objectForKey:[GMExposureManager exposureKeyForPage:pageCtrl]];
if (!itemModel.exposure_cards || itemModel.exposure_cards.count == 0) {
return;
}
[self synchronizeUploadItem:itemModel withViewPageData:pageCtrl];
// 上传成功后进行数据清理
[self clearExposureDataForPage:pageCtrl];
}
/**
上传所有页面的曝光数据
*/
- (void)uploadAllExposureData {
// 同步所有页面数据并上传
__weak __typeof(self)weakSelf = self;
NSDictionary *pageDictM = [GMExposureManager sharedManager].exposurePageDictM;
[pageDictM enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
NSString *pageName = key;
UIViewController *pageCtrl = [GMExposureManager expoPageCtrl:pageName];
[weakSelf uploadExposureDataForPage:pageCtrl];
}];
[self clearAllExposureData];
}
/**
清理指定页面的曝光数据
@param pageName 指定页面
*/
- (void)clearExposureDataForPage:(UIViewController *)pageCtrl {
pageCtrl.up_loading_times = 0;
pageCtrl.down_loading_times = 0;
pageCtrl.up_slide_times = 0;
pageCtrl.down_slide_times = 0;
pageCtrl.extra_param = [NSDictionary dictionary];
NSString *pageName = [GMExposureManager exposureKeyForPage:pageCtrl];
// 清理指定页面存储view
NSMutableDictionary *dictViewM = [NSMutableDictionary dictionaryWithDictionary:self.exposureViewDictM];
for (NSString *viewKey in self.exposureViewDictM.allKeys) {
if ([viewKey hasPrefix:pageName]) {
[dictViewM removeObjectForKey:viewKey];
}
}
self.exposureViewDictM = dictViewM;
// 清理指定页面的数据
NSMutableDictionary *dicDataM = [NSMutableDictionary dictionaryWithDictionary:self.exposureDictM];
for (NSString *viewKey in self.exposureDictM.allKeys) {
if ([viewKey hasPrefix:pageName]) {
[dicDataM removeObjectForKey:viewKey];
}
}
[GMExposureManager sharedManager].exposureDictM = dicDataM;
[[GMExposureManager sharedManager].exposurePageDictM removeObjectForKey:pageName];
}
/**
清理所有页面的曝光数据
*/
- (void)clearAllExposureData {
[[GMExposureManager sharedManager].exposurePageDictM removeAllObjects];
[[GMExposureManager sharedManager].exposureDictM removeAllObjects];
[[GMExposureManager sharedManager].exposureViewDictM removeAllObjects];
}
#pragma mark - Verify ViewExposure
/**
当前View上是否有需要精准曝光的数据
@param view View
@return yes/no
*/
+ (BOOL)haveExposureForView:(UIView *)view {
if (view.exposure && view.exposure.allKeys.count > 0) {
return YES;
}
return NO;
}
/**
是否是需要精准曝光的View
@param view view
@return yes/no
*/
+ (BOOL)isTargetViewForExposure:(UIView *)view {
// NSLog(@"%@--%@",view,view.pageCtrl);
return view.pageCtrl.needExpo;
}
/**
是否需要忽略UITrackingRunLoopMode模式下的记录
@param ignoreTracking 是否需要忽略
@return 模式下的记录
*/
+ (BOOL)isNeedIgnoreTrackExposure:(BOOL)ignoreTracking {
NSRunLoopMode mode = [NSRunLoop currentRunLoop].currentMode;
if (ignoreTracking && [mode isEqualToString:UITrackingRunLoopMode]) {
return YES;
}
return NO;
}
/**
设置当前view是否可见状态
@param view view
@param recursive 是否递归子view
*/
+ (void)fetchViewForVisibleState:(UIView *)view trackerType:(GMViewTrackerType)trackerType recursive:(BOOL)recursive {
BOOL ignoreTracking = (trackerType == GMViewTrackerTypeUIScrollDragging || trackerType == GMViewTrackerTypeUIViewEnterForeground || trackerType == GMViewTrackerTypeUIViewDidAppear || trackerType == GMViewTrackerTypeUIViewDidDisappear || trackerType == GMViewTrackerTypeUIViewWillResignActive)?NO:YES;
if ([GMExposureManager haveExposureForView:view] && [GMExposureManager isTargetViewForExposure:view]) {
view.visibleView = [self isViewVisible:view];
[view fechExposurePositionForView];
if (![GMExposureManager isNeedIgnoreTrackExposure:ignoreTracking]) {
/*
if (trackerType == GMViewTrackerTypeUIViewSetExposure &&
![GMExposureManager sharedManager].inBackground) {
view.visibleView = YES;
}*/
[[GMExposureManager sharedManager] exposureStatusForView:view inPageCtrl:view.pageCtrl];
} else {
[[GMExposureManager sharedManager] synchronizeViewItemData:view];
}
}
if (recursive) {
for (UIView *subview in view.subviews) {
if (!subview.pageCtrl) {
subview.pageCtrl = view.pageCtrl;
}
[GMExposureManager fetchViewForVisibleState:subview trackerType:trackerType recursive:recursive];
}
}
if (![GMExposureManager isNeedIgnoreTrackExposure:ignoreTracking] && (!recursive || view.subviews.count == 0)) {
[[GMExposureManager sharedManager] startStoreExposureData];
}
}
/**
判断当前view是否可见
@param view view
@return yes/no
*/
+ (BOOL)isViewVisible:(UIView *)view {
// NSLog(@"%@:%@:%d:%d:%f",view,view.window ,view.hidden,view.layer.hidden,view.alpha);
// 可见
if (!view.window || view.hidden || view.layer.hidden || !view.alpha) {
return NO;
}
// App进入后台,当前view均置为不可见
if ([GMExposureManager sharedManager].inBackground) {
return NO;
}
UIView *current = view;
while ([current isKindOfClass:[UIView class]]) {
if (current.alpha <= 0 || current.hidden) {
return NO;
}
current = current.superview;
}
// 判断当前View 与 view所在window是否存在交集
CGRect viewRectInWindow = [view convertRect:view.bounds toView:view.window];
BOOL isIntersects = CGRectIntersectsRect(view.window.bounds, viewRectInWindow);
if (isIntersects) {
// 获取View 与 view所在window的相交区域
CGRect intersectRect = CGRectIntersection(view.window.bounds, viewRectInWindow);
if (intersectRect.size.width != 0.f && intersectRect.size.height != 0.f) {
// size > exposureDimThreshold 视为可见
CGFloat dimThreshold = [GMExposureManager sharedManager].exposureDimThreshold;
if (intersectRect.size.width / viewRectInWindow.size.width > dimThreshold &&
intersectRect.size.height / viewRectInWindow.size.height > dimThreshold) {
return YES;
}
}
}
return NO;
}
#pragma mark - View Visible/UnVisibel
/** 设置view的曝光状态
* view 可见 && 状态none或者end——> 开始曝光
* view 可见 && 状态start——> 正在曝光,不做处理
* view 不可见 && 状态start ——> 结束曝光
* view 不可见 && 状态none或者end ——> 不做处理
*/
- (void)exposureStatusForView:(UIView *)view inPageCtrl:(UIViewController *)inPageCtrl {
// GMViewExpoStatus status = view.expoStatus;
if (view.visibleView) {
// 开始曝光
[self view:view startVisibleInPageCtrl:inPageCtrl];
} else {
// 结束曝光
[self view:view endVisibleInPageCtrl:inPageCtrl];
}
}
/**
view 在当前页面开始可见(开始曝光)
@param view 目标view
@param inPageCtrl inPageViewContrl 当前页面
*/
- (void)view:(UIView *)startView startVisibleInPageCtrl:(UIViewController *)inPageCtrl {
// 无状态进入页面直接开启曝光
NSString *viewKey = [GMExposureManager exposureKeyForData:startView];
UIView *view = [self.exposureViewDictM objectForKey:viewKey];
if (!view) {
startView.expoStatus = GMViewExpoStatusStart;
[self.exposureViewDictM setObject:startView forKey:viewKey];
}
}
/**
view在当前页面结束可见(曝光结束)
@param view 目标view
@param inPageViewContrl 指定页面
*/
- (void)view:(UIView *)endView endVisibleInPageCtrl:(UIViewController *)inPageCtrl {
if ([self.exposureViewDictM.allValues containsObject:endView]) {
NSArray *keys = [self.exposureViewDictM allKeysForObject:endView];
for (NSString *key in keys) {
[self.exposureViewDictM removeObjectForKey:key];
}
}
}
#pragma mark - ExposureKey
/**
* 获取曝光View存储Key(pageCtrl地址/inTime/view)
* pageCtrl地址:当前view所在页面的地址,方便后续根据页面就能删除
* inTime:当前开始曝光时间
* view地址:view
* /:后续可根据/进行切分
@param view View
@return 当前曝光View pageCtrl地址/inTime/view的地址为key
*/
+ (NSString *)exposureKeyForData:(UIView *)view {
return [NSString stringWithFormat:@"%p/%p",view.pageCtrl,view.exposure];
}
/**
获取页面key存储的曝光数据
@param pageCtrl pageCtrl
@return 当前页面地址
*/
+ (NSString *)exposureKeyForPage:(UIViewController *)pageCtrl {
return [NSString stringWithFormat:@"%p",pageCtrl];
}
/// 根据view的地址获取当前的page地址
/// @param viewKey viewKey
+ (UIViewController *)expoViewKeyForPageCtrl:(NSString *)viewKey {
NSString *page = [[viewKey componentsSeparatedByString:@"/"] firstObject];
return [GMExposureManager expoPageCtrl:page];
}
/**
根据页面地址获取页面对象
@param pageAddress 页面地址
@return 页面对象
*/
+ (UIViewController *)expoPageCtrl:(NSString *)pageAddress {
uintptr_t hex = strtoull(pageAddress.UTF8String, NULL, 0);
id gotcha = (__bridge id)(void *)hex;
UIViewController *pageCtrl = (UIViewController *)gotcha;
return pageCtrl;
}
- (void)dealloc {
[self removeObserverMethod];
}
@end
//
// GMExposureModel.h
// Gengmei
//
// Created by Mikasa on 2019/6/25.
// Copyright © 2019 更美互动信息科技有限公司. All rights reserved.
//
// 基于页面的精准曝光Item
@interface GMExposureItemModel : NSObject
/** 记录上滑总次数 */
@property (nonatomic, assign) NSInteger up_slide_times;
/** 记录下滑总次数 */
@property (nonatomic, assign) NSInteger down_slide_times;
/** 记录向上加载总次数 */
@property (nonatomic, assign) NSInteger up_loading_times;
/** 记录向下加载总次数 */
@property (nonatomic, assign) NSInteger down_loading_times;
/** 当前tab_name */
@property (nonatomic, copy) NSString *tab_name;
/** 当前page_name */
@property (nonatomic, copy) NSString *page_name;
/** 当前business_id */
@property (nonatomic, copy) NSString *business_id;
/** 当前referrer */
@property (nonatomic, copy) NSString *referrer;
/** 当前referrer */
@property (nonatomic, copy) NSString *referrerId;
/** 当前filter_f */
@property (nonatomic, copy) NSString *filter_f;
/** 曝光卡片数据 */
@property (nonatomic, strong) NSMutableArray *exposure_cards;
/** 是否精准曝光 */
@property (nonatomic, copy) NSString *is_exposure;
@end
//
// GMExposureModel.m
// Gengmei
//
// Created by Mikasa on 2019/6/25.
// Copyright © 2019 更美互动信息科技有限公司. All rights reserved.
//
#import "GMExposureModel.h"
@implementation GMExposureItemModel
- (instancetype)init {
if (self = [super init]) {
_exposure_cards = [NSMutableArray array];
_is_exposure = @"1";
}
return self;
}
@end
//
// GMExposureProtocol.h
// Gengmei
//
// Created by Mikasa on 2019/6/21.
// Copyright © 2019 更美互动信息科技有限公司. All rights reserved.
//
typedef NS_ENUM(NSInteger, GMViewExpoStatus) {
GMViewExpoStatusNone = 0,// 无状态
GMViewExpoStatusStart = 1,// 开始曝光
GMViewExpoStatusIng = 2,// 正在曝光(暂未使用,暂时先保留)
GMViewExpoStatusEnd = 3 // 结束曝光
};
@protocol GMViewExposureProtocol <NSObject>
@property (nonatomic, weak) UIViewController *pageCtrl;// 当前viewz所在页面
/** view别名 */
@property (nonatomic, copy) NSString *viewName;
/** view别名 */
@property (nonatomic, copy) NSString *inTime;
/** view别名 */
@property (nonatomic, copy) NSString *outTime;
/** 绝对位置 */
@property (nonatomic, copy) NSString *absolute_position;
/** 相对位置(相对屏幕所在位置)*/
@property (nonatomic, copy) NSString *relative_position;
/** 额外参数*/
@property (nonatomic, strong) NSDictionary *extra_param;
/** 曝光状态 */
@property (nonatomic, assign) GMViewExpoStatus expoStatus;
@end
@protocol GMPageExposureProtocol <NSObject>
/**
* 是否获取到数据立即发送,YES的话不会进行去重逻辑,需要数据去重
*/
@property (nonatomic, assign) BOOL needImmediatelySend;
@property (nonatomic, assign) BOOL needExpo;// 页面需要精准曝光
/** 记录上滑总次数 */
@property (nonatomic, assign) NSInteger up_slide_times;
/** 记录下滑总次数 */
@property (nonatomic, assign) NSInteger down_slide_times;
/** 记录向上加载总次数 */
@property (nonatomic, assign) NSInteger up_loading_times;
/** 记录向下加载总次数 */
@property (nonatomic, assign) NSInteger down_loading_times;
/** 额外参数 */
@property (nonatomic, strong) NSDictionary *extra_param;
@end
@protocol GMExposureProtocol <NSObject>
/** 精准曝光数据 */
@property (nonatomic, strong) NSDictionary *exposure;
@end
//
// GMTrackerProtocol.h
// Gengmei
//
// Created by MoMo on 2019/8/15.
// Copyright © 2019 更美互动信息科技有限公司. All rights reserved.
//
//#import "NSString+Tools.h"
#ifndef GMTrackerProtocol_h
#define GMTrackerProtocol_h
#define kCpcReferer @"cpc_referer"
#define kIsCpc @"is_cpc"
// 页面CPC
typedef NS_ENUM(NSInteger, GMCpcReferer) {
GMCpcNone = 9999 // None
};
@protocol GMTrackerProtocol <NSObject>
//
///** 页面名称 */
//@property (nonatomic, copy) NSString *page_name;
///** 页面业务ID */
//@property (nonatomic, copy) NSString *business_id;
///** 当前页面tab名称 */
//@property (nonatomic, copy) NSString *tab_name;
///** 上一个页面的tab名称 */
//@property (nonatomic, copy) NSString *referrer_tab_name;
///** 上一个页面的名称 */
//@property (nonatomic, copy) NSString *referrer;
///** 上一个页面的业务ID */
//@property (nonatomic, copy) NSString *referrer_id;
@end
@protocol GMCpcProtocol <NSObject>
/** cpc扣费 */
@property (nonatomic, assign) GMCpcReferer cpc_referer;
/** 是否cpc卡片 */
@property (nonatomic, assign) BOOL is_cpc;
@end
#endif /* GMTrackerProtocol_h */
//
// GMHookTool.h
// Gengmei
//
// Created by Mikasa on 2019/6/19.
// Copyright © 2019 更美互动信息科技有限公司. All rights reserved.
//
#define MethodSwizzlerReplacement(returntype, selftype, ...) ^ returntype (__unsafe_unretained selftype self, ##__VA_ARGS__)
#define MethodSwizzlerReplacementProviderBlock ^ id (IMP original, __unsafe_unretained Class swizzledClass, SEL _cmd)
#define MethodSwizzlerOriginalImplementation(functype, ...) do{\
if (original)\
((functype)original)(self, _cmd, ##__VA_ARGS__);\
}while(0);
typedef id (^MethodSwizzlerProvider)(IMP original, __unsafe_unretained Class swizzledClass, SEL selector);
OBJC_EXTERN BOOL deswizzleAll(void);
@interface NSObject (SwizzleMethod)
/**
swizzleClassMethod
@param selector swizzleClass selector
@param replacementProvider replacementProvider
*/
+ (void)swizzleClassMethod:(SEL)selector withReplacement:(MethodSwizzlerProvider)replacementProvider;
/**
swizzleInstanceMethod
@param selector swizzleInstanceMethod selector
@param replacementProvider replacementProvider
*/
+ (void)swizzleInstanceMethod:(SEL)selector withReplacement:(MethodSwizzlerProvider)replacementProvider;
/**
Swizzle the specified instance method with another selector
@param selector Selector of the method to swizzle.
@param swizzledSelector Selector of the new method will be swizzled.
*/
+ (void)swizzleInstanceMethod:(SEL)selector withSelector:(SEL)swizzledSelector;
/**
Swizzle the specified class method with another selector
@param selector Selector of the method to swizzle.
@param swizzledSelector Selector of the new method will be swizzled.
*/
+ (void)swizzleClassMethod:(SEL)selector withSelector:(SEL)swizzledSelector;
/**
Swizzle the specified instance method with another swizzledClass swizzledMethod
@param originalClass originalClass
@param originalSel originalSel
@param swizzledClass swizzledClass
@param swizzledSelector swizzledSelector
@param noneSel noneSel
*/
+ (void)swizzelInstanceMethodForClass:(Class)originalClass originalSel:(SEL)originalSel swizzledClass:(Class)swizzledClass swizzledSelector:(SEL)swizzledSelector noneSel:(SEL)noneSel;
@end
@interface NSObject (DeSwizzleMethod)
/**
Restore the specified class method by removing all swizzles.
@param selector Selector of the swizzled method.
@return \c YES if the method was successfully restored, \c NO if the method has never been swizzled.
*/
+ (BOOL)deswizzleClassMethod:(SEL)selector;
/**
Restore the specified class method by removing all swizzles.
@param selector Selector of the swizzled method.
@return \c YES if the method was successfully restored, \c NO if the method has never been swizzled.
*/
+ (BOOL)deswizzleInstanceMethod:(SEL)selector;
/**
Restore all swizzled class methods.
@return \c YES if the method was successfully restored, \c NO if no method has never been swizzled
*/
+ (BOOL)deswizzleAllClassMethods;
/**
Restore all swizzled instance methods.
@return \c YES if the method was successfully restored, \c NO if no method has never been swizzled.
*/
+ (BOOL)deswizzleAllInstanceMethods;
/**
Restore all swizzled class and instance methods.
@return \c YES if the method was successfully restored, \c NO if no method has never been swizzled.
*/
+ (BOOL)deswizzleAllMethods;
@end
@interface GMHookTool : NSObject
//判断页面是否实现了某个sel
+ (BOOL)isContainSel:(SEL)sel inClass:(Class)className;
@end
@interface GMWeakObject : NSObject
@property (nonatomic, weak) id obj;
- (instancetype)initWithObj:(id)obj;
@end
//
// GMHookTool.m
// Gengmei
//
// Created by Mikasa on 2019/6/19.
// Copyright © 2019 更美互动信息科技有限公司. All rights reserved.
//
#import "NSObject+Swizzle.h"
#import <objc/runtime.h>
#import <libkern/OSAtomic.h>
static OSSpinLock lock = OS_SPINLOCK_INIT;
static NSMutableDictionary *originalClassMethods;
static NSMutableDictionary *originalInstanceMethods;
#pragma mark - Global Method
NS_INLINE void classSwizzleMethod(Class cls, Method method, IMP newImp) {
if (!class_addMethod(cls, method_getName(method), newImp, method_getTypeEncoding(method))) {
// class already has implementation, swizzle it instead
method_setImplementation(method, newImp);
}
}
NS_INLINE IMP originalClassMethodImplementation(__unsafe_unretained Class class, SEL selector, BOOL fetchOnly) {
if (!originalClassMethods) {
originalClassMethods = [[NSMutableDictionary alloc] init];
}
NSString *classKey = NSStringFromClass(class);
NSString *selectorKey = NSStringFromSelector(selector);
NSMutableDictionary *classSwizzles = originalClassMethods[classKey];
NSValue *pointerValue = classSwizzles[selectorKey];
if (!classSwizzles) {
classSwizzles = [NSMutableDictionary dictionary];
originalClassMethods[classKey] = classSwizzles;
}
IMP orig = NULL;
if (pointerValue) {
orig = [pointerValue pointerValue];
if (fetchOnly) {
if (classSwizzles.count == 1) {
[originalClassMethods removeObjectForKey:classKey];
}
else {
[classSwizzles removeObjectForKey:selectorKey];
}
}
}
else if (!fetchOnly) {
orig = (IMP)[class methodForSelector:selector];
classSwizzles[selectorKey] = [NSValue valueWithPointer:orig];
}
if (classSwizzles.count == 0) {
[originalClassMethods removeObjectForKey:classKey];
}
if (originalClassMethods.count == 0) {
originalClassMethods = nil;
}
return orig;
}
NS_INLINE IMP originalInstanceMethodImplementation(__unsafe_unretained Class class, SEL selector, BOOL fetchOnly) {
if (!originalInstanceMethods) {
originalInstanceMethods = [[NSMutableDictionary alloc] init];
}
NSString *classKey = NSStringFromClass(class);
NSString *selectorKey = NSStringFromSelector(selector);
NSMutableDictionary *classSwizzles = originalInstanceMethods[classKey];
NSValue *pointerValue = classSwizzles[selectorKey];
if (!classSwizzles) {
classSwizzles = [NSMutableDictionary dictionary];
originalInstanceMethods[classKey] = classSwizzles;
}
IMP orig = NULL;
if (pointerValue) {
orig = [pointerValue pointerValue];
if (fetchOnly) {
[classSwizzles removeObjectForKey:selectorKey];
if (classSwizzles.count == 0) {
[originalInstanceMethods removeObjectForKey:classKey];
}
}
} else if (!fetchOnly) {
orig = (IMP)[class instanceMethodForSelector:selector];
classSwizzles[selectorKey] = [NSValue valueWithPointer:orig];
}
if (classSwizzles.count == 0) {
[originalInstanceMethods removeObjectForKey:classKey];
}
if (originalInstanceMethods.count == 0) {
originalInstanceMethods = nil;
}
return orig;
}
#pragma mark - Deswizzling Global Swizzles
NS_INLINE BOOL deswizzleClassMethod(__unsafe_unretained Class class, SEL selector) {
OSSpinLockLock(&lock);
IMP originalIMP = originalClassMethodImplementation(class, selector, YES);
if (originalIMP) {
method_setImplementation(class_getClassMethod(class, selector), (IMP)originalIMP);
OSSpinLockUnlock(&lock);
return YES;
} else {
OSSpinLockUnlock(&lock);
return NO;
}
}
NS_INLINE BOOL deswizzleInstanceMethod(__unsafe_unretained Class class, SEL selector) {
OSSpinLockLock(&lock);
IMP originalIMP = originalInstanceMethodImplementation(class, selector, YES);
if (originalIMP) {
method_setImplementation(class_getInstanceMethod(class, selector), (IMP)originalIMP);
OSSpinLockUnlock(&lock);
return YES;
} else {
OSSpinLockUnlock(&lock);
return NO;
}
}
NS_INLINE BOOL deswizzleAllClassMethods(__unsafe_unretained Class class) {
OSSpinLockLock(&lock);
BOOL success = NO;
NSDictionary *d = [originalClassMethods[NSStringFromClass(class)] copy];
for (NSString *sel in d) {
OSSpinLockUnlock(&lock);
if (deswizzleClassMethod(class, NSSelectorFromString(sel))) {
success = YES;
}
OSSpinLockLock(&lock);
}
OSSpinLockUnlock(&lock);
return success;
}
NS_INLINE BOOL deswizzleAllInstanceMethods(__unsafe_unretained Class class) {
OSSpinLockLock(&lock);
BOOL success = NO;
NSDictionary *d = [originalInstanceMethods[NSStringFromClass(class)] copy];
for (NSString *sel in d) {
OSSpinLockUnlock(&lock);
if (deswizzleInstanceMethod(class, NSSelectorFromString(sel))) {
success = YES;
}
OSSpinLockLock(&lock);
}
OSSpinLockUnlock(&lock);
return success;
}
BOOL deswizzleAll(void) {
BOOL success = NO;
OSSpinLockLock(&lock);
NSDictionary *d = originalClassMethods.copy;
for (NSString *classKey in d) {
OSSpinLockUnlock(&lock);
BOOL ok = [NSClassFromString(classKey) deswizzleAllMethods];
OSSpinLockLock(&lock);
if (success != ok) {
success = YES;
}
}
NSDictionary *d1 = originalInstanceMethods.copy;
for (NSString *classKey in d1) {
OSSpinLockUnlock(&lock);
BOOL ok = [NSClassFromString(classKey) deswizzleAllMethods];
OSSpinLockLock(&lock);
if (success != ok) {
success = YES;
}
}
OSSpinLockUnlock(&lock);
return success;
return YES;
}
#pragma mark - Global Swizzle Method
NS_INLINE void swizzleClassMethod(__unsafe_unretained Class class, SEL selector, MethodSwizzlerProvider replacement) {
OSSpinLockLock(&lock);
Method originalMethod = class_getClassMethod(class, selector);
IMP orig = originalClassMethodImplementation(class, selector, NO);
id replaceBlock = replacement(orig, class, selector);
Class meta = object_getClass(class);
classSwizzleMethod(meta, originalMethod, imp_implementationWithBlock(replaceBlock));
OSSpinLockUnlock(&lock);
}
NS_INLINE void swizzleInstanceMethod(__unsafe_unretained Class class, SEL selector, MethodSwizzlerProvider replacement) {
OSSpinLockLock(&lock);
Method originalMethod = class_getInstanceMethod(class, selector);
IMP orig = originalInstanceMethodImplementation(class, selector, NO);
id replaceBlock = replacement(orig, class, selector);
IMP replace = imp_implementationWithBlock(replaceBlock);
classSwizzleMethod(class, originalMethod, replace);
OSSpinLockUnlock(&lock);
}
@implementation NSObject (SwizzleMethod)
+ (void)swizzleClassMethod:(SEL)selector withReplacement:(MethodSwizzlerProvider)replacementProvider {
swizzleClassMethod(self, selector, replacementProvider);
}
+ (void)swizzleInstanceMethod:(SEL)selector withReplacement:(MethodSwizzlerProvider)replacementProvider {
swizzleInstanceMethod(self, selector, replacementProvider);
}
+ (void)swizzleInstanceMethod:(SEL)originalSelector withSelector:(SEL)swizzledSelector {
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
BOOL didAddMethod = class_addMethod(self,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(self,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
+ (void)swizzelInstanceMethodForClass:(Class)originalClass originalSel:(SEL)originalSel swizzledClass:(Class)swizzledClass swizzledSelector:(SEL)swizzledSelector noneSel:(SEL)noneSel {
Method originalMethod = class_getInstanceMethod(originalClass, originalSel);
Method swizzingMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);
// 如果没有实现 delegate 方法,则手动动态添加
if (!originalMethod) {
Method noneMethod = class_getInstanceMethod(swizzledClass, noneSel);
BOOL didAddNoneMethod = class_addMethod(originalClass,
originalSel,
method_getImplementation(noneMethod),
method_getTypeEncoding(noneMethod));
if (didAddNoneMethod) {
// NSLog(@"******** 没有实现 (%@) 方法,手动添加成功!!(%@)",NSStringFromSelector(originalSel),NSStringFromClass(originalClass));
// originalMethod = class_getInstanceMethod(originalClass, originalSel);
}
return;
}
// 向实现 delegate 的类中添加新的方法
BOOL didAddMethod = class_addMethod(originalClass,
swizzledSelector,
method_getImplementation(swizzingMethod),
method_getTypeEncoding(swizzingMethod));
if (didAddMethod) {
// 添加成功
// NSLog(@"******** 实现了 (%@) 方法并成功 Hook 为 --> (%@)",NSStringFromSelector(originalSel) ,NSStringFromSelector(swizzledSelector));
// 重新拿到添加被添加的 method,这里是关键(注意这里 originalClass, 不 replacedClass), 因为替换的方法已经添加到原类中了, 应该交换原类中的两个方法
Method newMethod = class_getInstanceMethod(originalClass, swizzledSelector);
// 实现交换
method_exchangeImplementations(originalMethod, newMethod);
} else {
// 添加失败,则说明已经 hook 过该类的 delegate 方法,防止多次交换。
// NSLog(@"******** 已替换过,避免多次替换 --> (%@)",NSStringFromClass(originalClass));
}
}
+ (void)swizzleClassMethod:(SEL)originalSelector withSelector:(SEL)swizzledSelector {
Method originalMethod = class_getClassMethod(self, originalSelector);
Method swizzledMethod = class_getClassMethod(self, swizzledSelector);
BOOL didAddMethod =
class_addMethod(self,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(self,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
@end
@implementation NSObject (DeSwizzleMethod)
+ (BOOL)deswizzleClassMethod:(SEL)selector {
return deswizzleClassMethod(self, selector);
}
+ (BOOL)deswizzleInstanceMethod:(SEL)selector {
return deswizzleInstanceMethod(self, selector);
}
+ (BOOL)deswizzleAllClassMethods {
return deswizzleAllClassMethods(self);
}
+ (BOOL)deswizzleAllInstanceMethods {
return deswizzleAllInstanceMethods(self);
}
+ (BOOL)deswizzleAllMethods {
BOOL c = [self deswizzleAllClassMethods];
BOOL i = [self deswizzleAllInstanceMethods];
return (c || i);
}
@end
@implementation GMHookTool
//判断页面是否实现了某个sel
+ (BOOL)isContainSel:(SEL)sel inClass:(Class)className {
unsigned int count;
Method *methodList = class_copyMethodList(className,&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;
}
@end
@implementation GMWeakObject
- (instancetype)initWithObj:(id)obj {
if (self = [super init]) {
_obj = obj;
}
return self;
}
@end
//
// NSObject+Tracker.h
// GMPhobos
//
// Created by Mikasa on 2020/5/9.
//
#import <Foundation/Foundation.h>
#import "GMExposureProtocol.h"
#import "GMTrackerProtocol.h"
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (Tracker)<GMCpcProtocol, GMExposureProtocol>
/// 获取通用数据
/// @param responder 当前responder
+ (NSDictionary *)trackerPageParam:(UIResponder *)responder;
/// 进行数据同步,将当前response 上通用数据同步给toResponse
/// @param response 将当前UIResponder 上通用数据同步
/// @param toResponse toResponse
- (void)synchronizePVData:(UIResponder *)response
toResponse:(UIResponder *)toResponse;
@end
NS_ASSUME_NONNULL_END
//
// NSObject+Tracker.m
// GMPhobos
//
// Created by Mikasa on 2020/5/9.
//
#import "NSObject+Tracker.h"
#import "UIResponder+PhobosPV.h"
#import <objc/runtime.h>
@implementation NSObject (Tracker)
/// 获取通用数据
/// @param responder 当前responder
+ (NSDictionary *)trackerPageParam:(UIResponder *)responder {
NSArray *referrerLink = responder.referrerLink;
NSDictionary *params = @{@"page_name" : responder.pageName?:@"",
@"business_id" : responder.businessId?:@"",
@"tab_name" : responder.tabName?:@"",
@"referrer_tab_name" : responder.referrerTabName?:@"",
@"referrer_id" : responder.referrerId?:@"",
@"referrer" : responder.referer?:@""};
NSMutableDictionary *paramsM = [NSMutableDictionary dictionaryWithDictionary:params];
if (referrerLink.count) {
[paramsM addEntriesFromDictionary:@{@"referrer_link" : referrerLink}];
};
return paramsM;
}
/// 进行数据同步
/// 将当前UIResponder 上通用数据同步给 response
/// @param response 需要进行同步的response
- (void)synchronizePVData:(UIResponder *)response toResponse:(UIResponder *)toResponse {
if (!toResponse || !response) {
return;
}
toResponse.pageName = response.pageName?:@"";
toResponse.tabName = response.tabName?:@"";
toResponse.referrerTabName = response.referrerTabName?:@"";
toResponse.businessId = response.businessId?:@"";
toResponse.referer = response.referer?:@"";
toResponse.referrerId = response.referrerId?:@"";
toResponse.referrerLink = response.referrerLink;
}
#pragma mark - Exposure
- (void)setExposure:(NSDictionary *)exposure {
objc_setAssociatedObject(self, @selector(exposure), exposure, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSDictionary *)exposure {
return objc_getAssociatedObject(self, @selector(exposure));
}
@end
//
// UIScrollView+Tracker.h
// Gengmei
//
// Created by Mikasa on 2019/6/19.
// Copyright © 2019 更美互动信息科技有限公司. All rights reserved.
//
@interface UIScrollView (Tracker)
@property (nonatomic, assign) CGFloat preContentOffsetY;// 记录历史滚动的ContentOffset的Y值
/// 计算当前列表上滑/下拉次数,自动进行+1
- (void)calculateUpOrDownTimes;
@end
@interface UITableView (Tracker)
@end
@interface UICollectionView (Tracker)
@end
//
// UIScrollView+Tracker.m
// Gengmei
//
// Created by Mikasa on 2019/6/19.
// Copyright © 2019 更美互动信息科技有限公司. All rights reserved.
//
#import "UIScrollView+Tracker.h"
#import "UIViewController+Tracker.h"
#import "NSObject+Swizzle.h"
#import "GMExposureManager.h"
#import <objc/runtime.h>
@implementation UIScrollView (Tracker)
- (void)calculateUpOrDownTimes {
if (self.preContentOffsetY == self.contentOffset.y) {
return;
}
// 判断滚动方向
if (self.preContentOffsetY < self.contentOffset.y) {
self.pageCtrl.up_slide_times++;
} else {
self.pageCtrl.down_slide_times++;
}
}
#pragma mark - Add Method
- (void)setPreContentOffsetY:(CGFloat)preContentOffsetY {
objc_setAssociatedObject(self, @selector(preContentOffsetY), @(preContentOffsetY), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (CGFloat)preContentOffsetY {
return [objc_getAssociatedObject(self, @selector(preContentOffsetY)) floatValue];
}
@end
@implementation UITableView (Tracker)
+ (void)load {
[self swizzleInstanceMethod:@selector(reloadData) withSelector:@selector(swizzle_reloadData)];
}
- (void)swizzle_reloadData {
[self swizzle_reloadData];
if (self.pageCtrl.needExpo) {
__weak typeof(self)weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[GMExposureManager fetchViewForVisibleState:weakSelf
trackerType:GMViewTrackerTypeUIViewSetExposure recursive:YES];
});
}
}
@end
@implementation UICollectionView (Tracker)
+ (void)load {
[self swizzleInstanceMethod:@selector(reloadData) withSelector:@selector(swizzle_reloadData)];
}
- (void)swizzle_reloadData {
[self swizzle_reloadData];
if (self.pageCtrl.needExpo) {
__weak typeof(self)weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[GMExposureManager fetchViewForVisibleState:weakSelf
trackerType:GMViewTrackerTypeUIViewSetExposure recursive:YES];
});
}
}
@end
//
// UIView+Tracker.h
// Gengmei
//
// Created by Mikasa on 2019/6/21.
// Copyright © 2019 更美互动信息科技有限公司. All rights reserved.
//
#import "GMExposureProtocol.h"
#import "GMTrackerProtocol.h"
@interface UIView (Tracker)<GMCpcProtocol, GMViewExposureProtocol>
@property (nonatomic, assign) BOOL visibleView;// 是否是可见View
#pragma mark - View Method
/**
获取指定View所在的Controller
@param view 指定View
@return 当前所在Controller
*/
+ (UIViewController *)currentControllerForView:(UIView *)view;
/// 判断当前view是否处于屏幕上
/// @param view 指定view
+ (BOOL)viewIsVisible:(UIView *)view;
/**
获取曝光卡片所在的位置
*/
- (void)fechExposurePositionForView;
- (void)synchronizePageTimesToOther:(UIView *)otherView;
@end
//
// UIView+Tracker.m
// Gengmei
//
// Created by Mikasa on 2019/6/21.
// Copyright © 2019 更美互动信息科技有限公司. All rights reserved.
//
#import "UIView+Tracker.h"
#import "UIViewController+Tracker.h"
#import "NSObject+Swizzle.h"
#import "PhobosUtil.h"
#import "GMExposureModel.h"
#import <objc/runtime.h>
@implementation UIView (Tracker)
#pragma mark - View Method
/**
获取指定View所在的Controller
@param view 指定View
@return 当前所在Controller
*/
+ (UIViewController *)currentControllerForView:(UIView *)view {
UIViewController *currentCtrl;
UIResponder *responder = [view.superview nextResponder];
while (responder && ![responder isKindOfClass:[UIViewController class]]) {
responder = [responder nextResponder];
}
currentCtrl = (UIViewController *)responder;
return currentCtrl;
}
/**
获取曝光卡片所在的位置
*/
- (void)fechExposurePositionForView {
UIView *view = self;
NSInteger absolute_position = [PhobosUtil isNonEmpty:self.absolute_position]?self.absolute_position.integerValue : 0;
UIView *current = (UIView *)view;
UIView *tempView = view;
while ([current isKindOfClass:[UIView class]] && (current != view.pageCtrl.view)) {
if ([current isKindOfClass:[UITableViewCell class]]) {
tempView = current;
}
if ([current isKindOfClass:[UICollectionViewCell class]]) {
tempView = current;
}
if ([current isKindOfClass:[UITableView class]]) {
UITableView *tableView = (UITableView *)current;
UITableViewCell *cell = (UITableViewCell *)tempView;
if ([tableView.visibleCells containsObject:cell]) {
NSIndexPath *indexPath = [tableView indexPathForCell:cell];
if (indexPath) {
// 当有多个分组时,将position转化为绝对路径的position,一维数组
if (tempView == view) {
absolute_position = indexPath.row;
for (NSInteger i = 0; i < indexPath.section; i++) {
absolute_position += [tableView numberOfRowsInSection:i];
}
}
NSInteger relative_position = 0;
if ([[tableView visibleCells] containsObject:cell]) {
relative_position = [[tableView visibleCells] indexOfObject:cell];
view.relative_position = [NSString stringWithFormat:@"%zd",relative_position];
break;
}
}
}
}
if ([current isKindOfClass:[UICollectionView class]]) {
UICollectionView *collectionView = (UICollectionView *)current;
UICollectionViewCell *cell = (UICollectionViewCell *)tempView;
if ([collectionView.visibleCells containsObject:cell]) {
NSIndexPath *indexPath = [collectionView indexPathForCell:cell];
if (indexPath) {
// 当有多个分组时,将position转化为绝对路径的position,一维数组
if (tempView == view) {
absolute_position = indexPath.row;
for (NSInteger i = 0; i < indexPath.section; i++) {
absolute_position += [collectionView numberOfItemsInSection:i];
}
}
NSInteger relative_position = 0;
// UICollectionViewCell
if ([[collectionView visibleCells] containsObject:cell]) {
relative_position = [[collectionView visibleCells] indexOfObject:cell];
view.relative_position = [NSString stringWithFormat:@"%zd",relative_position];
break;
//NSLog(@"%f---%f",relative_position,absolute_position);
}
}
}
}
current = (UITableViewCell *)current.superview;
}
view.absolute_position = [NSString stringWithFormat:@"%zd",absolute_position];
}
// 将当前UIScrollView数据同步给另外一个otherScrollView
- (void)synchronizePageTimesToOther:(UIView *)otherView {
otherView.pageCtrl.up_slide_times = self.pageCtrl.up_slide_times;
otherView.pageCtrl.down_slide_times = self.pageCtrl.down_slide_times;
otherView.pageCtrl.up_loading_times = self.pageCtrl.up_loading_times;
otherView.pageCtrl.down_loading_times = self.pageCtrl.down_loading_times;
}
/// 判断当前view是否处于屏幕上
/// @param view 指定view
+ (BOOL)viewIsVisible:(UIView *)view {
// 可见
if (!view.window || view.hidden || view.layer.hidden || !view.alpha) {
return NO;
}
UIView *current = view;
while ([current isKindOfClass:[UIView class]]) {
if (current.alpha <= 0 || current.hidden) {
return NO;
}
current = current.superview;
}
// 判断当前View 与 view所在window是否存在交集
CGRect viewRectInWindow = [view convertRect:view.bounds toView:view.window];
BOOL isIntersects = CGRectIntersectsRect(view.window.bounds, viewRectInWindow);
if (isIntersects) {
// 获取View 与 view所在window的相交区域
CGRect intersectRect = CGRectIntersection(view.window.bounds, viewRectInWindow);
if (intersectRect.size.width != 0.f && intersectRect.size.height != 0.f) {
return YES;
}
}
return NO;
}
#pragma mark - GMViewExposureProtocol
- (void)setVisibleView:(BOOL)visibleView {
objc_setAssociatedObject(self, @selector(visibleView), @(visibleView), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)visibleView {
return [objc_getAssociatedObject(self, @selector(visibleView)) boolValue];
}
- (void)setInTime:(NSString *)inTime {
objc_setAssociatedObject(self, @selector(inTime), inTime, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)inTime {
return objc_getAssociatedObject(self, @selector(inTime));
}
- (void)setOutTime:(NSString *)outTime {
objc_setAssociatedObject(self, @selector(outTime), outTime, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)outTime {
return objc_getAssociatedObject(self, @selector(outTime));
}
- (void)setAbsolute_position:(NSString *)absolute_position {
objc_setAssociatedObject(self, @selector(absolute_position), absolute_position, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)absolute_position {
return objc_getAssociatedObject(self, @selector(absolute_position));
}
- (void)setRelative_position:(NSString *)relative_position {
objc_setAssociatedObject(self, @selector(relative_position), relative_position, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)relative_position {
return objc_getAssociatedObject(self, @selector(relative_position));
}
- (void)setPageCtrl:(UIViewController *)pageCtrl {
GMWeakObject *obj = [[GMWeakObject alloc] initWithObj:pageCtrl];
objc_setAssociatedObject(self, @selector(pageCtrl), obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UIViewController *)pageCtrl {
GMWeakObject *weakObj = objc_getAssociatedObject(self, @selector(pageCtrl));
if (!weakObj.obj) {
weakObj.obj = [UIView currentControllerForView:self];
}
return weakObj.obj;
}
- (void)setViewName:(NSString *)viewName {
objc_setAssociatedObject(self, @selector(viewName), viewName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)viewName {
return objc_getAssociatedObject(self, @selector(viewName));
}
- (void)setExpoStatus:(GMViewExpoStatus)expoStatus {
objc_setAssociatedObject(self, @selector(expoStatus), @(expoStatus), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if (expoStatus == GMViewExpoStatusStart) {
} else if (expoStatus == GMViewExpoStatusEnd) {
self.inTime = @"";
self.outTime = @"";
self.relative_position = @"";
self.absolute_position = @"";
}
}
- (GMViewExpoStatus)expoStatus {
return [objc_getAssociatedObject(self, @selector(expoStatus)) integerValue];
}
- (void)setExtra_param:(NSDictionary *)extra_param {
objc_setAssociatedObject(self, @selector(extra_param), extra_param, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSDictionary *)extra_param {
return objc_getAssociatedObject(self, @selector(extra_param));
}
@end
//
// UIViewController+Tracker.h
// Gengmei
//
// Created by Mikasa on 2019/6/25.
// Copyright © 2019 更美互动信息科技有限公司. All rights reserved.
//
#import "GMExposureProtocol.h"
#import "GMTrackerProtocol.h"
@interface UIViewController (Tracker)<GMPageExposureProtocol,GMCpcProtocol,UIScrollViewDelegate>
@end
//
// UIViewController+Tracker.m
// Gengmei
//
// Created by Mikasa on 2019/6/25.
// Copyright © 2019 更美互动信息科技有限公司. All rights reserved.
//
#import "UIViewController+Tracker.h"
#import "GMExposureManager.h"
#import "NSObject+Swizzle.h"
#import <objc/runtime.h>
@implementation UIViewController (Tracker)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self startViewPageExpoTracker];
});
}
+ (void)startViewPageExpoTracker {
[self swizzleInstanceMethod:@selector(viewDidLoad) withSelector:@selector(swizzle_viewDidLoad)];
[self swizzleInstanceMethod:@selector(viewDidAppear:) withSelector:@selector(swizzle_viewDidAppear:)];
[self swizzleInstanceMethod:@selector(viewDidDisappear:) withSelector:@selector(swizzle_viewDidDisappear:)];
}
- (void)swizzle_viewDidLoad {
[self swizzle_viewDidLoad];
if (self.needExpo) {
self.view.pageCtrl = self;
}
}
- (void)swizzle_viewDidAppear:(BOOL)animated {
[self swizzle_viewDidAppear:animated];
if (self.needExpo) {
self.view.pageCtrl = self;
[GMExposureManager fetchViewForVisibleState:self.view trackerType:GMViewTrackerTypeUIViewDidAppear recursive:YES];
if (self.needImmediatelySend) {
[GMExposureManager.sharedManager uploadExposureDataForPage:self];
}
}
}
- (void)swizzle_viewDidDisappear:(BOOL)animated {
if (self.needExpo) {
[[GMExposureManager sharedManager] endExpoTrcakerForPageCtrl:self];
}
[self swizzle_viewDidDisappear:animated];
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
if (scrollView.pageCtrl.needExpo) {
scrollView.preContentOffsetY = scrollView.contentOffset.y;
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
if (scrollView.pageCtrl.needExpo && !decelerate) {
[scrollView calculateUpOrDownTimes];
[GMExposureManager fetchViewForVisibleState:scrollView trackerType:GMViewTrackerTypeUIScrollDragging recursive:YES];
if (self.needImmediatelySend) {
[GMExposureManager.sharedManager endExpoTrcakerForPageCtrl:self];
}
}
//NSLog(@"%@:%zd--%zd",scrollView.pageCtrl,scrollView.pageCtrl.down_slide_times,scrollView.pageCtrl.up_slide_times);
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
if (scrollView.pageCtrl.needExpo) {
[scrollView calculateUpOrDownTimes];
[GMExposureManager fetchViewForVisibleState:scrollView trackerType:GMViewTrackerTypeUIScrollDragging recursive:YES];
if (self.needImmediatelySend) {
[GMExposureManager.sharedManager endExpoTrcakerForPageCtrl:self];
}
}
}
#pragma mark - GMPageExposureProtocol
- (void)setNeedImmediatelySend:(BOOL)needImmediatelySend {
objc_setAssociatedObject(self, @selector(needImmediatelySend), @(needImmediatelySend), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)needImmediatelySend {
return [objc_getAssociatedObject(self, @selector(needImmediatelySend)) boolValue];
}
- (void)setNeedExpo:(BOOL)needExpo {
objc_setAssociatedObject(self, @selector(needExpo), @(needExpo), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)needExpo {
return [objc_getAssociatedObject(self, @selector(needExpo)) boolValue];
}
- (void)setUp_slide_times:(NSInteger)up_slide_times {
objc_setAssociatedObject(self, @selector(up_slide_times), @(up_slide_times), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSInteger)up_slide_times {
return [objc_getAssociatedObject(self, @selector(up_slide_times)) integerValue];
}
- (void)setDown_slide_times:(NSInteger)down_slide_times {
objc_setAssociatedObject(self, @selector(down_slide_times), @(down_slide_times), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSInteger)down_slide_times {
return [objc_getAssociatedObject(self, @selector(down_slide_times)) integerValue];
}
- (void)setUp_loading_times:(NSInteger)up_loading_times {
objc_setAssociatedObject(self, @selector(up_loading_times), @(up_loading_times), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSInteger)up_loading_times {
return [objc_getAssociatedObject(self, @selector(up_loading_times)) integerValue];
}
- (void)setDown_loading_times:(NSInteger)down_loading_times {
objc_setAssociatedObject(self, @selector(down_loading_times), @(down_loading_times), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSInteger)down_loading_times {
return [objc_getAssociatedObject(self, @selector(down_loading_times)) integerValue];
}
- (void)setExtra_param:(NSDictionary *)extra_param {
objc_setAssociatedObject(self, @selector(extra_param), extra_param, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSDictionary *)extra_param {
return objc_getAssociatedObject(self, @selector(extra_param));
}
@end
......@@ -11,12 +11,7 @@
#import "PhobosConfig.h"
#import "Phobos.h"
#import "PhobosUtil.h"
#ifdef APPSTORE
#define GMExactExposureApi @"https://log.igengmei.com/log/precise_exposure"
#else
#define GMExactExposureApi @"http://log.test.igengmei.com/log/precise_exposure"
#endif
#import "GMExposureManager.h"
@implementation PhobosSendManager
......@@ -44,7 +39,7 @@
}
}];
}
NSString *exposureAPI = GMExactExposureApi;
NSString *exposureAPI = [GMExposureManager sharedManager].uploadExposureAPI;
NSArray *exposureArray = [GMCache fetchObjectAtDocumentPathWithkey:[PhobosUtil MD5String:exposureAPI]];
if (exposureArray.count > 0) {
/** 获取非灰度下发送失败的埋点,进行发送 */
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment