首先,歸納下Runtime的幾個使用場景。
- 做用戶埋點統(tǒng)計
- 處理異常崩潰(NSDictionary, NSMutableDictionary, NSArray, NSMutableArray 的處理)
- 按鈕最小點擊區(qū)設置
- 按鈕重復點擊設置
- 手勢的重復點擊處理
- UIButton點擊事件帶多參數(shù)
- MJRefresh封裝
- 服務端控制頁面跳轉(zhuǎn)
- 字典轉(zhuǎn)模型
一 用戶埋點
在做app運營的時候, 我們經(jīng)常會需要接入一些第三方做統(tǒng)計, 例如友盟統(tǒng)計,google統(tǒng)計等。 例如外面需要統(tǒng)計某個頁面用戶停留的時長, 統(tǒng)計某個頁面的展示次數(shù)。 通常我們的做法是 : 需要統(tǒng)計A頁面停留時長的時候,我們再A頁面出現(xiàn)(appear)的時候記錄一個時間戳,頁面消失(dispear)的時候用當前時間戳與之前的時間戳求出時間間隔,然后上報到分析平臺。 如果統(tǒng)計頁面展示次數(shù), 就在每次頁面出現(xiàn)時調(diào)用統(tǒng)計方法。 這樣做的壞處是 代碼侵入性太強,維護性與易讀性都不太好。 假設以后要改需求, 就要進入到代碼所在處進行修改。 又或者別人接手你的代碼, 根本不知道已經(jīng)做了哪些埋點, 需求改來改去,時間久了, 項目中全都是垃圾代碼。
此時,為了優(yōu)化統(tǒng)計, 我們使用 Hook (鉤子)的思想, 例如Runtime的 Method sweezing(方法交換)去攔截系統(tǒng)方法來實現(xiàn)共計。
首先,我們寫一個集成NSObject的工具類,實現(xiàn)方法交換
#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方法中把系統(tǒng)方法替換掉:
#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
{
//頁面出現(xiàn)
[self user_viewWillAppear:animated];
}
-(void)user_viewWillDisappear:(BOOL)animated
{
//頁面消失
[self user_viewWillDisappear:animated];
}
@end
此時還有個問題, 首先你可能并不想對每個頁面進行統(tǒng)計, 但是又不想每次添加一個統(tǒng)計就加一個if判斷。 這個時候我們就在Xcode中加入一張plist表, plist表里面記錄我們所需統(tǒng)計的信息

此時,我們只需要在hook的方法中去實現(xiàn)統(tǒng)計邏輯
-(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
{
//頁面停留時間統(tǒng)計
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];
}
這樣的話,以后做頁面時長或者頁面展示的統(tǒng)計,就只需要維護這個plist表就行了,不需要具體改動代碼。
點擊事件統(tǒng)計:
與VC的統(tǒng)計類似, 也是利用catagory + hook的思想來實現(xiàn), 我們可以添加一個UIControl的分類。但是具體需要hook UIControl的哪個方法那 ? 點擊進入UIControl的api, 我們很容易發(fā)現(xiàn)需要Hook的方法
- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;
接著我們在UIControl的分類中實現(xiàn)方法的交互
@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的統(tǒng)計所需的參數(shù)就可以了

利用Runtime做用戶埋點的就說這么多, 文章只提供思路, 具體plist的結(jié)構,或者代碼細節(jié)根據(jù)情況自己做實現(xiàn)就行了。另外, 由于需求變動的原因,造成代碼與配置表不匹配(例如可能會出現(xiàn)某個method名字被改變 )從而造成埋點統(tǒng)計失敗, 建議寫一個單元測試對Plist進行測試,思路: 在單元測試中我們首先讀取plist配置文件,遍歷所有的頁面。在一個頁面內(nèi)遍歷所有的ControlEventIDs,對每個響應函數(shù)名進行respondsToSelector:判斷。 這樣可以有效減少埋點失效問題。
二 處理異常崩潰(NSDictionary, NSMutableDictionary, NSArray, NSMutableArray 的處理)
在開發(fā)過程中, 有時候會出現(xiàn)set object for key的時候 object為Nil或者Key為Nil, 又或者初始化array, dic的時候由于數(shù)據(jù)個數(shù)與指定的長度不一致造成崩潰。 此時利用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
三 按鈕最小點擊區(qū)設置
按鈕太不好點中了,點擊好幾次才點擊到”, 測試經(jīng)常會有這樣的抱怨, 但是此時按鈕圖片本身設計就很小。 此時,例如Runtime進行點擊區(qū)放大, 是個挺好的解決版本
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];
}
五 手勢的重復點擊處理
手勢重復點擊有個誤區(qū): 不能通過攔截 addTarget:(id)target action:(SEL)action 這個方法來實現(xiàn),因為這個方法是是添加方法,即使我們交換了,在執(zhí)行的時候并沒有什么變化的。正確的做法是添加一個timeInterval,然后在代理里面根據(jù)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點擊帶多參數(shù)
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這個工具,大部分用法都每次出現(xiàn)tabview初始化后, 都初始化出來一個 mj_header, mj_footer, 并且設置 header與footer后, 把mj_header與mj_footer復制給tableview.mj_header, tableview.mj_footer. 每次去重復創(chuàng)建Header, Footer, 這個是不能容忍的。 我們知道tableview和collectionView都是繼承自scrollView,那么我們可以在 scrollView的分類里面添加一些方法,那么我們在以后使用的時候,就不需要一遍一遍的重復寫無用代碼了,只需要調(diào)用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:@"暫無更多數(shù)據(jù)" forState:MJRefreshStateNoMoreData];
[footer setTitle:@"" forState:MJRefreshStateIdle];
self.mj_footer.ignoredScrollViewContentInsetBottom = 44;
self.mj_footer = footer;
}
}
/**
開啟頭部刷新
*/
- (void)headerBeginRefreshing{
[self.mj_header beginRefreshing];
}
/**
沒有更多數(shù)據(jù)
*/
- (void)footerNoMoreData{
[self.mj_footer setState:MJRefreshStateNoMoreData];
}
/**
結(jié)束刷新
*/
- (void)endRefresh{
if (self.mj_header) {
[self.mj_header endRefreshing];
}
if (self.mj_footer) {
[self.mj_footer endRefreshing];
}
}
八 服務端控制頁面跳轉(zhuǎn)
項目開發(fā)中,我們可能會有這樣的需求: 根據(jù)服務端推送過來的數(shù)據(jù)規(guī)則,跳轉(zhuǎn)到對應的控制器。 之前我們的做法是這樣的: 前端與服務端定義好規(guī)則, 例如服務端推送 Push/Live/WatchLive/12, Push: push方式跳轉(zhuǎn) , Live指的直播模塊, WatchLive指的看直播的功能, 12指的房間號, 也就是跳轉(zhuǎn)到12號主播間。 但是這么做壞處就是,必須提前與服務端約定好協(xié)議, 每次運營如果加一個新的跳轉(zhuǎn), 移動端需要改代碼,重新上線。擴展性很低。
其實利用Runtime完全可以寫成通用的方式來實現(xiàn)跳轉(zhuǎn)。例如外面與服務端定義好推送規(guī)則后,服務端推送過來的數(shù)據(jù)如下:
// 這個規(guī)則肯定事先跟服務端溝通好,跳轉(zhuǎn)對應的界面需要對應的參數(shù)
NSDictionary *userInfo = @{
@"class": @"LiveViewController", //VC的名字
@"property": @{
@"ID": @"123", //參數(shù)名字為 ID , value為 123
@"type": @"12" //type為附加信息, 根據(jù)實際情況定義
}
};
接著我們利用Runtime進行跳轉(zhuǎn)
// 類名
NSString *class =[NSString stringWithFormat:@"%@", params[@"class"]];
const char *className = [class cStringUsingEncoding:NSASCIIStringEncoding];
// 從一個字串返回一個類
Class newClass = objc_getClass(className);
if (!newClass)
{
return; //推送的class不存在
}
// 創(chuàng)建對象
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];
// 跳轉(zhuǎn)到對應的控制器
[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];
// 屬性名轉(zhuǎn)成字符串
NSString *propertyName = [[NSString alloc] initWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
// 判斷該屬性是否存在
if ([propertyName isEqualToString:verifyPropertyName]) {
free(properties);
return YES;
}
}
free(properties);
return NO;
}
九 字典轉(zhuǎn)模型
獲取屬性的列表的方法是字典轉(zhuǎn)模型的比較核心的方法。常見的字典轉(zhuǎn)模型的三方有 MJExtension, YYModel, JsonModel等, 翻看其源碼, 都會發(fā)現(xiàn) Ivar *class_copyIvarList(Class cls, unsigned int *outCount)的使用
MJExtension核心代碼摘錄

YYModel核心代碼摘錄

JsonModel json字典轉(zhuǎn)model 摘錄

基本上主流的json 轉(zhuǎn)model 都少不了,使用運行時動態(tài)獲取屬性的屬性名的方法,來進行字典轉(zhuǎn)模型替換,字典轉(zhuǎn)模型效率最高的(耗時最短的)的是KVC,其他的字典轉(zhuǎn)模型是在KVC 的key 和Value 做處理,動態(tài)的獲取json 中的key 和value ,當然轉(zhuǎn)換的過程中,第三方框架需要做一些判空啊,鑲嵌的邏輯處理, 再進行KVC 轉(zhuǎn)模型.這句代碼 [xx setValue:value forKey:key];無論JsonModle,YYKIt,MJextension 都少不了[xx setValue:value forKey:key];這句代碼的,不信可以去搜,這是字典轉(zhuǎn)模型的核心方法,