最近公司項(xiàng)目做用戶大數(shù)據(jù)信息采集,需要采集App端在頁(yè)面切換/交互操作的時(shí)候需要統(tǒng)計(jì)頁(yè)面顯示/頁(yè)面消失事件到腥,要給后端統(tǒng)計(jì)系統(tǒng)發(fā)送采集logs記錄朵逝。
但項(xiàng)目中ViewController之前沒(méi)有做任何繼承關(guān)系(無(wú)基類Controller),所以在幾十上百個(gè)Controller的項(xiàng)目里乡范,一個(gè)一個(gè)添加耗時(shí)不說(shuō)配名,也不利于后期維護(hù),那也是不推薦使用的蠢方法晋辆。
1渠脉、利用Objective-C 中的對(duì)象繼承
繼承在面向?qū)ο箝_(kāi)發(fā)中是非常常用的。
優(yōu)點(diǎn):繼承可以實(shí)現(xiàn)代碼的復(fù)用瓶佳,減少代碼冗余芋膘。將所有重復(fù)的內(nèi)容合并在一起,可以使代碼有效率涩哟,簡(jiǎn)潔索赏,才意味著是一個(gè)成功的架構(gòu)。否則贴彼,修改代碼時(shí)需要修改多處潜腻,就很容易出錯(cuò)。
缺點(diǎn):繼承造成類與類之間耦合性太強(qiáng)器仗。
現(xiàn)在項(xiàng)目工程中都會(huì)有一個(gè)BaseViewController融涣,所有新建的ViewController都繼承BaseViewController,通過(guò)往BaseViewController中添加一些公共方法/屬性可以被他們的子類所調(diào)用精钮;這是統(tǒng)一工程中所有視圖控制器樣式的一個(gè)主要途徑威鹿。
如果項(xiàng)目中沒(méi)有ViewController基類的話,重新創(chuàng)建一個(gè)基類也不難轨香。
新建一個(gè)BaseViewController忽你,在所需采集統(tǒng)計(jì)的地方寫(xiě)公共方法;
-
處理項(xiàng)目ViewController的繼承關(guān)系臂容,將項(xiàng)目中默認(rèn)繼承ViewController的替換繼承BaseViewController科雳;
左側(cè)邊欄搜索替換
: UIViewController
,替換成: XXBaseViewController
這種方法比較考驗(yàn)?zāi)托暮图?xì)心脓杉,更換了默認(rèn)的繼承關(guān)系糟秘。會(huì)更改很多類,侵入性雖說(shuō)很大球散。但考慮到以后的維護(hù)成本尿赚,在控制器類還不龐大的情況下,建議使用此種方法。不能完成的是原項(xiàng)目已經(jīng)有繼承關(guān)系了凌净,但繼承關(guān)系比較負(fù)責(zé)悲龟。這時(shí)候就要更加小心(除了處理可能多個(gè)父類,還要處理沒(méi)繼承的控制器)泻蚊,相當(dāng)于給他們?cè)炝艘粋€(gè)祖宗
~
2躲舌、利用Category和Runtime交換系統(tǒng)方法并添加方法(系統(tǒng)主動(dòng)調(diào)用load)
load函數(shù)調(diào)用特點(diǎn)如下:
???當(dāng)類被引用進(jìn)項(xiàng)目的時(shí)候就會(huì)執(zhí)行l(wèi)oad函數(shù)(在main函數(shù)開(kāi)始執(zhí)行之前),與這個(gè)類是否被用到無(wú)關(guān),每個(gè)類的load函數(shù)只會(huì)自動(dòng)調(diào)用一次.由于load函數(shù)是系統(tǒng)自動(dòng)加載的丑婿,因此不需要調(diào)用父類的load函數(shù)性雄,否則父類的load函數(shù)會(huì)多次執(zhí)行。
- 1羹奉、當(dāng)父類和子類都實(shí)現(xiàn)load函數(shù)時(shí),父類的load方法執(zhí)行順序要優(yōu)先于子類秒旋;
- 2、當(dāng)子類未實(shí)現(xiàn)load方法時(shí),不會(huì)調(diào)用父類load方法诀拭;
- 3迁筛、類中的load方法執(zhí)行順序要優(yōu)先于類別(Category);
- 4耕挨、當(dāng)有多個(gè)類別(Category)都實(shí)現(xiàn)了load方法,這幾個(gè)load方法都會(huì)執(zhí)行,但執(zhí)行順序不確定(其執(zhí)行順序與類別在Compile Sources中出現(xiàn)的順序一致)细卧;
- 5、當(dāng)然當(dāng)有多個(gè)不同的類的時(shí)候,每個(gè)類load 執(zhí)行順序與其在Compile Sources出現(xiàn)的順序一致筒占;
- 新建一個(gè)UIViewController 的category,引人runtime頭文件
#import <objc/runtime.h>
- 重寫(xiě)load方法
+ (void)load {
[super load];
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 交換VC頁(yè)面完全顯示方法
__mmc_tracer_swizzleMethod([self class], @selector(viewDidAppear:), @selector(__mmc_tracer_viewDidAppear:));
// 交換VC頁(yè)面完全消失方法
__mmc_tracer_swizzleMethod([self class], @selector(viewDidDisappear:), @selector(__mmc_tracer_viewDidDisappear:));
});
}
- 使用runtime實(shí)現(xiàn)方法交換
void __mmc_tracer_swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector){
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod =
class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
- 替換的交換方法中添加自己需要的采集方法(
注意:要調(diào)用自己的方法一次翰苫,因?yàn)樽约阂惨獙?shí)現(xiàn)被交換之前方法的內(nèi)部實(shí)現(xiàn))止邮,但要注意忽略系統(tǒng)控制器類的方法。
/** 只忽略了部分常用系統(tǒng)原生contrlloer
* 通過(guò)繼承父類來(lái)實(shí)現(xiàn) 相對(duì)于hook來(lái)說(shuō) 是較為準(zhǔn)確的,因?yàn)樾枰唤y(tǒng)計(jì)的頁(yè)面都是繼承于這個(gè)父類的控制器,而其他的如UINavigationController,系統(tǒng)自帶的UIAlertController等則不會(huì)誤入統(tǒng)計(jì)數(shù)據(jù)當(dāng)中
*/
+ (BOOL)isIgnoreSystemViewController:(id)instance {
return
[instance isKindOfClass:[UITabBarController class]] ||
[instance isKindOfClass:[UINavigationController class]] ||
[instance isKindOfClass:[UISearchController class]] ||
[instance isKindOfClass:[UIAlertController class]] ||
[instance isKindOfClass:[UISearchController class]] ||
[instance isKindOfClass:[UIActivityViewController class]];
}
- (void)__mmc_tracer_viewDidAppear:(BOOL)animated {
[self __mmc_tracer_viewDidAppear:animated]; //由于方法已經(jīng)被交換,這里調(diào)用的實(shí)際上是viewDidAppear:方法
if ([UIViewController isIgnoreSystemViewController:self]) {
//TODO: 實(shí)現(xiàn)頁(yè)面顯示的打點(diǎn)采集方法
}
}
- (void)__mmc_tracer_viewDidDisappear:(BOOL)animated {
[self __mmc_tracer_viewDidDisappear:animated]; //由于方法已經(jīng)被交換,這里調(diào)用的實(shí)際上是viewDidDisappear:方法
if ([UIViewController isIgnoreSystemViewController:self]) {
//TODO: 實(shí)現(xiàn)頁(yè)面消失的打點(diǎn)采集方法
}
}
3奏窑、利用Aspects實(shí)行方法hook (被動(dòng)調(diào)用导披,類似通知監(jiān)聽(tīng))
Aspects是AOP(面向切面編程)思想在iOS下OC的實(shí)現(xiàn)。Aspects可以用于hook函數(shù)埃唯,讓函數(shù)執(zhí)行一些副操作撩匕。為嵌入不同函數(shù)中的功能相同的操作,每類功能相同的操作可以抽取出一個(gè)切面墨叛。
- OOP針對(duì)業(yè)務(wù)處理過(guò)程的實(shí)體及其屬性和行為進(jìn)行抽象封裝止毕,以獲得更加清晰高效的邏輯單元?jiǎng)澐郑?/li>
- AOP則是針對(duì)業(yè)務(wù)處理過(guò)程中的切面進(jìn)行提取,它所面對(duì)的是處理過(guò)程中的某個(gè)步驟或階段巍实,以獲得邏輯過(guò)程中各部分之間低耦合性的隔離效果滓技。
核心原理:當(dāng)被 hook 的 selector 被執(zhí)行的時(shí)候,首先根據(jù) selector找到了
objc_msgForward / _objc_msgForward_stret
,而這個(gè)會(huì)觸發(fā)消息轉(zhuǎn)發(fā)棚潦,從而進(jìn)入forwardInvocation
令漂。同時(shí)由于forwardInvocation
的指向也被修改了,因此會(huì)轉(zhuǎn)入新的forwardInvocation
函數(shù),在里面執(zhí)行需要嵌入的附加代碼叠必,完成之后荚孵,再轉(zhuǎn)回原來(lái)的 IMP。
- 寫(xiě)一個(gè)內(nèi)部私有方法hook所有UIViewController的所有實(shí)例的顯示與消失方法纬朝,在采集類初始化的時(shí)候調(diào)用(建議:初始化方法中應(yīng)該有一個(gè)是否由該采集SDK主動(dòng)采集頁(yè)面顯示與隱藏的
BOOL
參數(shù)收叶,在該參數(shù)下初始化為最佳時(shí)機(jī))。
/** 只忽略了部分常用系統(tǒng)原生contrlloer
* 通過(guò)繼承父類來(lái)實(shí)現(xiàn) 相對(duì)于hook來(lái)說(shuō) 是較為準(zhǔn)確的,因?yàn)樾枰唤y(tǒng)計(jì)的頁(yè)面都是繼承于這個(gè)父類的控制器,而其他的如UINavigationController,系統(tǒng)自帶的UIAlertController等則不會(huì)誤入統(tǒng)計(jì)數(shù)據(jù)當(dāng)中
*/
- (BOOL)isIgnoreSystemViewController:(id)instance {
return
[instance isKindOfClass:[UITabBarController class]] ||
[instance isKindOfClass:[UINavigationController class]] ||
[instance isKindOfClass:[UISearchController class]] ||
[instance isKindOfClass:[UIAlertController class]] ||
[instance isKindOfClass:[UISearchController class]] ||
[instance isKindOfClass:[UIActivityViewController class]];
}
- (void)hookAllViewControllerAppearAndDisappear {
/// hook 控制器的顯示和消失 分別打log
// 顯示
[UIViewController aspect_hookSelector:@selector(viewDidAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) {
if (![self isIgnoreSystemViewController:aspectInfo.instance]) {
// 不是忽略VC則采集
[[self class] addBeginLogPageView:NSStringFromClass([aspectInfo.instance class])];
}
} error:NULL];
// 消失
[UIViewController aspect_hookSelector:@selector(viewDidDisappear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) {
if (![self isIgnoreSystemViewController:aspectInfo.instance]) {
// 不是忽略VC則采集
[[self class] addEndLogPageView:NSStringFromClass([aspectInfo.instance class])];
}
} error:NULL];
}
hook方案有一個(gè)好處就是可以避免代碼入侵共苛,做到更加廣泛的通用性判没。通過(guò)swizzling我們可以將原method與自己加入的method相結(jié)合,即不需要在原有工程中加入代碼隅茎,又能做到全局覆蓋澄峰。
三種方案對(duì)比:
1、通過(guò)繼承父類來(lái)實(shí)現(xiàn)辟犀,相對(duì)于hook來(lái)說(shuō)俏竞,是較為準(zhǔn)確的。因?yàn)樾枰唤y(tǒng)計(jì)的頁(yè)面都是繼承于這個(gè)父類的控制器堂竟,而其他的如UINavigationController魂毁、UIAlertController等則不會(huì)誤入統(tǒng)計(jì)數(shù)據(jù)當(dāng)中。
2出嘹、上面提到 hook方案是通過(guò)hook UIViewController
viewDidLoad/viewDidAppear
等方法席楚,而這些方法實(shí)際上每個(gè)Controller 都會(huì)調(diào)用,那么就會(huì)出現(xiàn)不該出現(xiàn)的Controller 也出現(xiàn)在這里(如上面說(shuō)到的UINavigationController
和UIAlertController
)疚漆。但hook方案一個(gè)比較好的特點(diǎn)是無(wú)代碼入侵酣胀,在不修改項(xiàng)目代碼的前提下完成工作。? 3娶聘、兩種hook的對(duì)比:分類的方法只要分類被load則就開(kāi)始hook, 時(shí)機(jī)并不能自己控制闻镶,而且也不能自己開(kāi)關(guān)控制是否hook或者終止hook操作。一個(gè)由程序員主動(dòng)調(diào)用(Aspects)丸升,一個(gè)由系統(tǒng)調(diào)用(分類)铆农。由于可控性最后還是選擇了
Aspects
來(lái)進(jìn)行hook。???但要做的是通用的大數(shù)據(jù)采集類狡耻,以后公司內(nèi)部所有App都可能會(huì)用到墩剖,在不知道是哪個(gè)App有什么Controller的情況下,hook顯然成了最好的方法夷狰。當(dāng)然要注意篩選掉系統(tǒng)的Controller岭皂,避免重復(fù)采集無(wú)用的數(shù)據(jù)。 如果要做內(nèi)部封裝的話顯然Aspects hook的方式好一點(diǎn)沼头,不然的話你就要暴露出API提供給分類爷绘,或者將分類寫(xiě)入封裝類內(nèi)部书劝。這樣代碼比較長(zhǎng)不利于后期維護(hù)。還有最重要的一點(diǎn)就是:使用
Aspects
可以留一個(gè)開(kāi)關(guān)給外部土至,是否需要sdk幫助采集所有界面的出現(xiàn)和消失购对,或者交給使用者自己采集界面信息。