首先,归纳下Runtime的几个使用场景。
- 做用户埋点统计
- 处理异常崩溃(NSDictionary, NSMutableDictionary, NSArray, NSMutableArray 的处理)
- 按钮最小点击区设置
- 按钮重复点击设置
- 手势的重复点击处理
- UIButton点击事件带多参数
- MJRefresh封装
- 服务端控制页面跳转
- 字典转模型
一 用户埋点
在做app运营的时候, 我们经常会需要接入一些第三方做统计, 例如友盟统计,google统计等。 例如外面需要统计某个页面用户停留的时长, 统计某个页面的展示次数。 通常我们的做法是 : 需要统计A页面停留时长的时候,我们再A页面出现(appear)的时候记录一个时间戳,页面消失(dispear)的时候用当前时间戳与之前的时间戳求出时间间隔,然后上报到分析平台。 如果统计页面展示次数, 就在每次页面出现时调用统计方法。 这样做的坏处是 代码侵入性太强,维护性与易读性都不太好。 假设以后要改需求, 就要进入到代码所在处进行修改。 又或者别人接手你的代码, 根本不知道已经做了哪些埋点, 需求改来改去,时间久了, 项目中全都是垃圾代码。
此时,为了优化统计, 我们使用 Hook (钩子)的思想, 例如Runtime的 Method sweezing(方法交换)去拦截系统方法来实现共计。
首先,我们写一个集成NSObject的工具类,实现方法交换
#import "HookTool.h"
#import <objc/runtime.h>
@implementation HookTool
+(void)swizzingForClass:(Class)cls originalSel:(SEL)originalSelector swizzingSel:(SEL)swizzingSelector
{
Class class = 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);
}
}
@end
接着,我们写一个UIViewController的分类, 在Load方法中把系统方法替换掉:
#import "UIViewController+actionAnalysis.h"
#import "HookTool.h"
#import "NSDate+Convenience.h"
@implementation UIViewController (actionAnalysis)
+(void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalAppearSelector = @selector(viewWillAppear:);
SEL swizzingAppearSelector = @selector(user_viewWillAppear:);
[HookTool swizzingForClass:[self class] originalSel:originalAppearSelector swizzingSel:swizzingAppearSelector];
SEL originalDisappearSelector = @selector(viewWillDisappear:);
SEL swizzingDisappearSelector = @selector(user_viewWillDisappear:);
[HookTool swizzingForClass:[self class] originalSel:originalDisappearSelector swizzingSel:swizzingDisappearSelector];
});
}
-(void)user_viewWillAppear:(BOOL)animated
{
//页面出现
[self user_viewWillAppear:animated];
}
-(void)user_viewWillDisappear:(BOOL)animated
{
//页面消失
[self user_viewWillDisappear:animated];
}
@end
此时还有个问题, 首先你可能并不想对每个页面进行统计, 但是又不想每次添加一个统计就加一个if判断。 这个时候我们就在Xcode中加入一张plist表, plist表里面记录我们所需统计的信息
image.png
此时,我们只需要在hook的方法中去实现统计逻辑
-(void)user_viewWillAppear:(BOOL)animated
{
NSDictionary * pageenter = [[HookTool getConfig] objectForKey:@"page_enter_anysis"];
if ([pageenter.allKeys containsObject:NSStringFromClass([self class])]) {
NSLog(@"%@ 页面展示", NSStringFromClass([self class]));
}
NSDictionary * pagetime = [[HookTool getConfig] objectForKey:@"page_time_anysis"];
if ([pagetime.allKeys containsObject:NSStringFromClass([self class])]) {
//此处用Userdefault存储只是因为方便书写, 实际用可以用一个单例去存储中间值
[[NSUserDefaults standardUserDefaults] setDouble:[[NSDate date] timeIntervalSince1970] * 1000 forKey:@"appeartime"];
}
[self user_viewWillAppear:animated];
}
-(void)user_viewWillDisappear:(BOOL)animated
{
//页面停留时间统计
NSDictionary * pagetime = [[HookTool getConfig] objectForKey:@"page_time_anysis"];
if ([pagetime.allKeys containsObject:NSStringFromClass([self class])]) {
double leaveTime = NSDate.currenMillisecondTimestamp - [[NSUserDefaults standardUserDefaults] doubleForKey:@"appeartime"];
NSLog(@"%@ 页面的停留时间为 %lf ms", [self class], leaveTime);
}
[self user_viewWillDisappear:animated];
}
这样的话,以后做页面时长或者页面展示的统计,就只需要维护这个plist表就行了,不需要具体改动代码。
点击事件统计:
与VC的统计类似, 也是利用catagory + hook的思想来实现, 我们可以添加一个UIControl的分类。但是具体需要hook UIControl的哪个方法那 ? 点击进入UIControl的api, 我们很容易发现需要Hook的方法
- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;
接着我们在UIControl的分类中实现方法的交互
@implementation UIControl (actionAnalysis)
+(void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalSelector = @selector(sendAction:to:forEvent:);
SEL swizzingSelector = @selector(user_sendAction:to:forEvent:);
[HookTool swizzingForClass:[self class] originalSel:originalSelector swizzingSel:swizzingSelector];
});
}
-(void)user_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
NSLog(@"\n***hook success.\n[1]action:%@\n[2]target:%@ \n[3]event:%ld", NSStringFromSelector(action), target, (long)event);
[self user_sendAction:action to:target forEvent:event];
}
同样的, 我们只需要在plist中添加click的统计所需的参数就可以了
image.png
利用Runtime做用户埋点的就说这么多, 文章只提供思路, 具体plist的结构,或者代码细节根据情况自己做实现就行了。另外, 由于需求变动的原因,造成代码与配置表不匹配(例如可能会出现某个method名字被改变 )从而造成埋点统计失败, 建议写一个单元测试对Plist进行测试,思路: 在单元测试中我们首先读取plist配置文件,遍历所有的页面。在一个页面内遍历所有的ControlEventIDs,对每个响应函数名进行respondsToSelector:判断。 这样可以有效减少埋点失效问题。
二 处理异常崩溃(NSDictionary, NSMutableDictionary, NSArray, NSMutableArray 的处理)
在开发过程中, 有时候会出现set object for key的时候 object为Nil或者Key为Nil, 又或者初始化array, dic的时候由于数据个数与指定的长度不一致造成崩溃。 此时利用runtime对异常情况进行捕捉,提前return或者抛弃多余的长度。
Dic:
#import "NSDictionary+Safe.h"
#import <objc/runtime.h>
@implementation NSDictionary (Safe)
+ (void)load {
Method originalMethod = class_getClassMethod(self, @selector(dictionaryWithObjects:forKeys:count:));
Method swizzledMethod = class_getClassMethod(self, @selector(na_dictionaryWithObjects:forKeys:count:));
method_exchangeImplementations(originalMethod, swizzledMethod);
}
+ (instancetype)na_dictionaryWithObjects:(const id [])objects forKeys:(const id <NSCopying> [])keys count:(NSUInteger)cnt {
id nObjects[cnt];
id nKeys[cnt];
int i=0, j=0;
for (; i<cnt && j<cnt; i++) {
if (objects[i] && keys[i]) {
nObjects[j] = objects[i];
nKeys[j] = keys[i];
j++;
}
}
return [self na_dictionaryWithObjects:nObjects forKeys:nKeys count:j];
}
@end
@implementation NSMutableDictionary (Safe)
+ (void)load {
Class dictCls = NSClassFromString(@"__NSDictionaryM");
Method originalMethod = class_getInstanceMethod(dictCls, @selector(setObject:forKey:));
Method swizzledMethod = class_getInstanceMethod(dictCls, @selector(na_setObject:forKey:));
method_exchangeImplementations(originalMethod, swizzledMethod);
}
- (void)na_setObject:(id)anObject forKey:(id<NSCopying>)aKey {
if (!anObject || !aKey)
return;
[self na_setObject:anObject forKey:aKey];
}
@end
array:
#import "NSArray+Safe.h"
#import <objc/runtime.h>
@implementation NSArray (Safe)
+ (void)load {
Method originalMethod = class_getClassMethod(self, @selector(arrayWithObjects:count:));
Method swizzledMethod = class_getClassMethod(self, @selector(na_arrayWithObjects:count:));
method_exchangeImplementations(originalMethod, swizzledMethod);
}
+ (instancetype)na_arrayWithObjects:(const id [])objects count:(NSUInteger)cnt {
id nObjects[cnt];
int i=0, j=0;
for (; i<cnt && j<cnt; i++) {
if (objects[i]) {
nObjects[j] = objects[i];
j++;
}
}
return [self na_arrayWithObjects:nObjects count:j];
}
@end
@implementation NSMutableArray (Safe)
+ (void)load {
Class arrayCls = NSClassFromString(@"__NSArrayM");
Method originalMethod1 = class_getInstanceMethod(arrayCls, @selector(insertObject:atIndex:));
Method swizzledMethod1 = class_getInstanceMethod(arrayCls, @selector(na_insertObject:atIndex:));
method_exchangeImplementations(originalMethod1, swizzledMethod1);
Method originalMethod2 = class_getInstanceMethod(arrayCls, @selector(setObject:atIndex:));
Method swizzledMethod2 = class_getInstanceMethod(arrayCls, @selector(na_setObject:atIndex:));
method_exchangeImplementations(originalMethod2, swizzledMethod2);
}
- (void)na_insertObject:(id)anObject atIndex:(NSUInteger)index {
if (!anObject)
return;
[self na_insertObject:anObject atIndex:index];
}
- (void)na_setObject:(id)anObject atIndex:(NSUInteger)index {
if (!anObject)
return;
[self na_setObject:anObject atIndex:index];
}
@end
三 按钮最小点击区设置
按钮太不好点中了,点击好几次才点击到”, 测试经常会有这样的抱怨, 但是此时按钮图片本身设计就很小。 此时,例如Runtime进行点击区放大, 是个挺好的解决版本
static const void *topNameKey = @"topNameKey";
static const void *rightNameKey = @"rightNameKey";
static const void *bottomNameKey = @"bottomNameKey";
static const void *leftNameKey = @"leftNameKey";
- (void)setEnlargeEdgeWithTop:(CGFloat)top right:(CGFloat)right bottom:(CGFloat)bottom left:(CGFloat)left{
objc_setAssociatedObject(self, &topNameKey, [NSNumber numberWithFloat:top], OBJC_ASSOCIATION_COPY_NONATOMIC);
objc_setAssociatedObject(self, &rightNameKey, [NSNumber numberWithFloat:right], OBJC_ASSOCIATION_COPY_NONATOMIC);
objc_setAssociatedObject(self, &bottomNameKey, [NSNumber numberWithFloat:bottom], OBJC_ASSOCIATION_COPY_NONATOMIC);
objc_setAssociatedObject(self, &leftNameKey, [NSNumber numberWithFloat:left], OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (CGRect)enlargedRect
{
NSNumber *topEdge = objc_getAssociatedObject(self, &topNameKey);
NSNumber *rightEdge = objc_getAssociatedObject(self, &rightNameKey);
NSNumber *bottomEdge = objc_getAssociatedObject(self, &bottomNameKey);
NSNumber *leftEdge = objc_getAssociatedObject(self, &leftNameKey);
if (topEdge && rightEdge && bottomEdge && leftEdge) {
return CGRectMake(self.bounds.origin.x - leftEdge.floatValue,
self.bounds.origin.y - topEdge.floatValue,
self.bounds.size.width + leftEdge.floatValue + rightEdge.floatValue,
self.bounds.size.height + topEdge.floatValue + bottomEdge.floatValue);
}
else
{
return self.bounds;
}
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
CGRect rect = [self enlargedRect];
if (CGRectEqualToRect(rect, self.bounds)) {
return [super hitTest:point withEvent:event];
}
return CGRectContainsPoint(rect, point) ? self : nil;
}
四 按钮的重复点击
这个就不多说了,详细大部分程序员都遇到过, 直接上代码
+ (void)load{
Method originalMethod = class_getInstanceMethod([self class], @selector(sendAction:to:forEvent:));
Method swizzledMethod = class_getInstanceMethod([self class], @selector(User_SendAction:to:forEvent:));
method_exchangeImplementations(originalMethod, swizzledMethod);
}
#pragma mark -- 时间间隔 --
static const void *ButtonDurationTime = @"ButtonDurationTime";
- (NSTimeInterval)durationTime{
NSNumber *number = objc_getAssociatedObject(self, &ButtonDurationTime);
return number.doubleValue;
}
- (void)setDurationTime:(NSTimeInterval)durationTime{
NSNumber *number = [NSNumber numberWithDouble:durationTime];
objc_setAssociatedObject(self, &ButtonDurationTime, number, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (void)User_SendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{
self.userInteractionEnabled = NO;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.durationTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.userInteractionEnabled = YES;
});
[self User_SendAction:action to:target forEvent:event];
}
五 手势的重复点击处理
手势重复点击有个误区: 不能通过拦截 addTarget:(id)target action:(SEL)action 这个方法来实现,因为这个方法是是添加方法,即使我们交换了,在执行的时候并没有什么变化的。正确的做法是添加一个timeInterval,然后在代理里面根据timeInterval设置UITapGestureRecognizer的enable属性
#import "UITapGestureRecognizer+LOOExtension.h"
#import <objc/runtime.h>
@interface UITapGestureRecognizer ()
///时间间隔
@property (nonatomic,assign) NSTimeInterval duration;
@end
static const void *UITapGestureRecognizerduration = @"GestureRecognizerduration";
@implementation UITapGestureRecognizer (LOOExtension)
#pragma mark - Getter Setter
- (NSTimeInterval)duration{
NSNumber *number = objc_getAssociatedObject(self, &UITapGestureRecognizerduration);
return number.doubleValue;
}
- (void)setDuration:(NSTimeInterval)duration{
NSNumber *number = [NSNumber numberWithDouble:duration];
objc_setAssociatedObject(self, &UITapGestureRecognizerduration, number, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
/**
添加点击事件
@param target taeget
@param action action
@param duration 时间间隔
*/
- (instancetype)initWithTarget:(id)target action:(SEL)action withDuration:(NSTimeInterval)duration{
self = [super init];
if (self) {
self.duration = duration;
self.delegate = self;
[self addTarget:target action:action];
}
return self;
}
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{
self.enabled = NO;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.duration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.enabled = YES;
});
return YES;
}
@end
六 UIButton点击带多参数
UIButton *btn = // create the button
objc_setAssociatedObject(btn, "firstObject", someObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC); //实际上就是KVC
objc_setAssociatedObject(btn, "secondObject", otherObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[btn addTarget:self action:@selector(click:) forControlEvents:UIControlEventTouchUpInside];
- (void)click:(UIButton *)sender
{
id first = objc_getAssociatedObject(btn, "firstObject"); //取参
id second = objc_setAssociatedObject(btn, "secondObject");
// etc.
}
这么使用runtime感觉有点鸡肋,至少在自己的iOS生涯中,没有必须需要这么做的时候。 其实写个子类,添加个Parameter属性岂不是更简单。
七 MJRefresh的封装
大部分程序员应该都用过MJRefresh这个工具,大部分用法都每次出现tabview初始化后, 都初始化出来一个 mj_header, mj_footer, 并且设置 header与footer后, 把mj_header与mj_footer复制给tableview.mj_header, tableview.mj_footer. 每次去重复创建Header, Footer, 这个是不能容忍的。 我们知道tableview和collectionView都是继承自scrollView,那么我们可以在 scrollView的分类里面添加一些方法,那么我们在以后使用的时候,就不需要一遍一遍的重复写无用代码了,只需要调用scrollView分类方法就可以了。
#import "UIScrollView+JHRefresh.h"
#import <MJRefresh.h>
@implementation UIScrollView (JHRefresh)
/**
添加刷新事件
@param headerBlock 头部刷新
@param footerBlock 底部刷新
*/
- (void)setRefreshWithHeaderBlock:(void(^)(void))headerBlock
footerBlock:(void(^)(void))footerBlock{
if (headerBlock) {
MJRefreshNormalHeader *header= [MJRefreshNormalHeader headerWithRefreshingBlock:^{
if (headerBlock) {
headerBlock();
}
}];
header.stateLabel.font = [UIFont systemFontOfSize:13];
header.lastUpdatedTimeLabel.font = [UIFont systemFontOfSize:13];
self.mj_header = header;
}
if (footerBlock) {
MJRefreshBackNormalFooter *footer = [MJRefreshBackNormalFooter footerWithRefreshingBlock:^{
footerBlock();
}];
footer.stateLabel.font = [UIFont systemFontOfSize:13];
[footer setTitle:@"暂无更多数据" forState:MJRefreshStateNoMoreData];
[footer setTitle:@"" forState:MJRefreshStateIdle];
self.mj_footer.ignoredScrollViewContentInsetBottom = 44;
self.mj_footer = footer;
}
}
/**
开启头部刷新
*/
- (void)headerBeginRefreshing{
[self.mj_header beginRefreshing];
}
/**
没有更多数据
*/
- (void)footerNoMoreData{
[self.mj_footer setState:MJRefreshStateNoMoreData];
}
/**
结束刷新
*/
- (void)endRefresh{
if (self.mj_header) {
[self.mj_header endRefreshing];
}
if (self.mj_footer) {
[self.mj_footer endRefreshing];
}
}
八 服务端控制页面跳转
项目开发中,我们可能会有这样的需求: 根据服务端推送过来的数据规则,跳转到对应的控制器。 之前我们的做法是这样的: 前端与服务端定义好规则, 例如服务端推送 Push/Live/WatchLive/12, Push: push方式跳转 , Live指的直播模块, WatchLive指的看直播的功能, 12指的房间号, 也就是跳转到12号主播间。 但是这么做坏处就是,必须提前与服务端约定好协议, 每次运营如果加一个新的跳转, 移动端需要改代码,重新上线。扩展性很低。
其实利用Runtime完全可以写成通用的方式来实现跳转。例如外面与服务端定义好推送规则后,服务端推送过来的数据如下:
// 这个规则肯定事先跟服务端沟通好,跳转对应的界面需要对应的参数
NSDictionary *userInfo = @{
@"class": @"LiveViewController", //VC的名字
@"property": @{
@"ID": @"123", //参数名字为 ID , value为 123
@"type": @"12" //type为附加信息, 根据实际情况定义
}
};
接着我们利用Runtime进行跳转
// 类名
NSString *class =[NSString stringWithFormat:@"%@", params[@"class"]];
const char *className = [class cStringUsingEncoding:NSASCIIStringEncoding];
// 从一个字串返回一个类
Class newClass = objc_getClass(className);
if (!newClass)
{
return; //推送的class不存在
}
// 创建对象
id instance = [[newClass alloc] init];
// 对该对象赋值属性
NSDictionary * propertys = params[@"property"];
[propertys enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
// 检测这个对象是否存在该属性
if ([self checkIsExistPropertyWithInstance:instance verifyPropertyName:key]) {
// 利用kvc赋值
[instance setValue:obj forKey:key];
}
}];
// 获取导航控制器
UITabBarController *tabVC = (UITabBarController *)self.window.rootViewController;
UINavigationController *pushClassStance = (UINavigationController *)tabVC.viewControllers[tabVC.selectedIndex];
// 跳转到对应的控制器
[pushClassStance pushViewController:instance animated:YES];
检测属性是否存在
- (BOOL)checkIsExistPropertyWithInstance:(id)instance verifyPropertyName:(NSString *)verifyPropertyName
{
unsigned int outCount, i;
// 获取对象里的属性列表
objc_property_t * properties = class_copyPropertyList([instance
class], &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property =properties[i];
// 属性名转成字符串
NSString *propertyName = [[NSString alloc] initWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
// 判断该属性是否存在
if ([propertyName isEqualToString:verifyPropertyName]) {
free(properties);
return YES;
}
}
free(properties);
return NO;
}
九 字典转模型
获取属性的列表的方法是字典转模型的比较核心的方法。常见的字典转模型的三方有 MJExtension, YYModel, JsonModel等, 翻看其源码, 都会发现 Ivar *class_copyIvarList(Class cls, unsigned int *outCount)的使用
MJExtension核心代码摘录
20180503143111683.png
YYModel核心代码摘录
20180503143407891.png
JsonModel json字典转model 摘录
20180503143454842.png
基本上主流的json 转model 都少不了,使用运行时动态获取属性的属性名的方法,来进行字典转模型替换,字典转模型效率最高的(耗时最短的)的是KVC,其他的字典转模型是在KVC 的key 和Value 做处理,动态的获取json 中的key 和value ,当然转换的过程中,第三方框架需要做一些判空啊,镶嵌的逻辑处理, 再进行KVC 转模型.这句代码 [xx setValue:value forKey:key];无论JsonModle,YYKIt,MJextension 都少不了[xx setValue:value forKey:key];这句代码的,不信可以去搜,这是字典转模型的核心方法,
网友评论