導(dǎo)語:
CJMethodLog 對于Objective-C中的任意類、任意方法,均可實(shí)時根據(jù)用戶的操作行為,監(jiān)控還原對應(yīng)的函數(shù)調(diào)用日志,而且能夠自定義記錄當(dāng)前函數(shù)的參數(shù)類型、返回類型、執(zhí)行時間……
CJMethodLog
上一篇介紹了 Runtime 原理
CJMethodLog(一)Runtime原理:從監(jiān)控還原APP運(yùn)行的每一行代碼說起
這里就來講講 CJMethodLog 的具體實(shí)現(xiàn)。

上圖展示了CJMethodLog的文件結(jié)構(gòu),其中CJMethodLog.h CJMethodLog.m是核心部分
/**
* 初始化類名監(jiān)聽配置
* 注意!?。∷性O(shè)置的hook類不能存在繼承關(guān)系
*
* @param classNameList 需要hook的類名
* @param options 日志選項
* @param value 是否打印監(jiān)聽日志,(設(shè)置為YES,會輸出方法監(jiān)聽的log信息,該值只在 DEBUG 環(huán)境有效)
*/
+ (void)forwardingClasses:(NSArray <NSString *>*)classNameList logOptions:(CJLogOptions)options logEnabled:(BOOL)value;
/**
* 獲取日志文件
*
* @param finishBlock 獲取日志文件回調(diào)block
*/
+ (void)syncLogData:(SyncDataBlock)finishBlock;
/**
* 刪除日志數(shù)據(jù)
*/
+ (void)clearLogData;
CJMethodLog.h 暫時提供三個方法:初始化配置、獲取日志文件、刪除日志數(shù)據(jù)。
使用
在 main.m 文件中設(shè)置需要監(jiān)聽的類名配置,理論上任意時刻都可以重設(shè)監(jiān)聽配置,但不建議這么做??!因?yàn)槊看沃卦O(shè)監(jiān)聽配置都會修改監(jiān)聽類的方法鏈表(methodLists)中方法的IMP實(shí)現(xiàn),隨意修改可能會出現(xiàn)替換指定IMP的同時剛好調(diào)用了該IMP的實(shí)現(xiàn),造成不可預(yù)知錯誤。另外在 main.m中初始化配置可以確保所有的hook類都生效,比如如果你hook的是 AppDelegate 類。
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "CJMethodLog.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
/*
* 利用消息轉(zhuǎn)發(fā),hook指定類的調(diào)用方法
*/
[CJMethodLog forwardingClasses:@[
@"AppDelegate",
@"TestViewController"
]
logOptions:CJLogDefault
logEnabled:NO];
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
下圖展示了hook TestViewController類之后的函數(shù)調(diào)用情況:

日志格式說明:
- <TestViewController> begin: -clickManagerTest:
-- <TestViewController> begin: +managerTest
-- <TestViewController> finish: +managerTest ; time=0.000110
- <TestViewController> finish: -clickManagerTest: ; time=0.000416
- 最開始的
-表示函數(shù)調(diào)用層級; -
<TestViewController>表示當(dāng)前調(diào)用函數(shù)的類名; -
begin:finish:分別表示函數(shù)執(zhí)行起始階段(只會在設(shè)置了CJLogMethodTimer選項的時候出現(xiàn)); -
-clickManagerTest:表示執(zhí)行的是實(shí)例方法,+managerTest表示執(zhí)行的是類方法; -
time=0.000110表示函數(shù)耗時 - 之后會補(bǔ)充函數(shù)參數(shù)以及返回結(jié)果說明
日志數(shù)據(jù)
獲取日志數(shù)據(jù)使用+ (void)syncLogData:(SyncDataBlock)finishBlock ,你可以根據(jù)需要獲取。比如這里在app啟動的時候獲取,判斷當(dāng)數(shù)據(jù)量大于10*1024的時候上傳服務(wù)器并刪除客戶端數(shù)據(jù)。
- (void)applicationDidBecomeActive:(UIApplication *)application {
[CJMethodLog syncLogData:^void(NSData *logData) {
NSLog(@"CJMethodLog: logData = %@",@([logData length]));
if ([logData length] > 10*1024) {
// TODO: 上傳到服務(wù)器等自定義處理
// 刪除日志數(shù)據(jù)
[CJMethodLog clearLogData];
}
}];
}
其他
CJLogger.h CJLogger.mm 是日志數(shù)據(jù)存儲與獲取的實(shí)現(xiàn)類,這是一個由OC和C++混編實(shí)現(xiàn)的類。
CJMethodLog+CJMessage.h 是處理一些通用設(shè)置的分類。
實(shí)現(xiàn)原理
CJMethodLog 借助 Runtime 的消息轉(zhuǎn)發(fā)機(jī)制,在調(diào)用方法的時候主動觸發(fā)消息轉(zhuǎn)發(fā),然后在 - (void)forwardInvocation:(NSInvocation *)anInvocation 方法中還原當(dāng)前selector的實(shí)現(xiàn),同時記錄監(jiān)控日志信息。下面是具體的實(shí)現(xiàn)流程圖:
初始設(shè)置階段:

方法執(zhí)行階段:

一些關(guān)鍵點(diǎn)
-
默認(rèn)不 hook 系統(tǒng)類。CJMethodLog 理論上可以 hook 記錄任意類、任意類的任意方法的執(zhí)行日志,但實(shí)際上很少會這樣做;我們關(guān)注的應(yīng)該是基于具體的業(yè)務(wù)需求對應(yīng)到代碼層面是怎樣的函數(shù)執(zhí)行邏輯,而且這里的函數(shù)執(zhí)行邏輯信息一般只需要到達(dá)app層級的邏輯實(shí)現(xiàn)就可以了,沒必要更進(jìn)一步去關(guān)注系統(tǒng)層級的實(shí)現(xiàn)。當(dāng)然我也不介意你在初始階段設(shè)置指定系統(tǒng)類或三方類,分析其內(nèi)部的邏輯實(shí)現(xiàn),然后做些壞壞的事??。
附:設(shè)置不 hook 系統(tǒng)類的另一個原因是,hook 某個類,準(zhǔn)確的做法應(yīng)該要將該類的方法、它的父類的方法、父類的父類的方法……全都 hook 上,這樣才能完整的還原出它在整個生命周期內(nèi)的函數(shù)調(diào)用日志。那如果將父類方法也 hook,這時就需要一個出口了,不然會導(dǎo)致繼承鏈的判斷過長。我的做法是判斷當(dāng)父類為系統(tǒng)類時則停止。
其實(shí) hook 父類方法部分還未實(shí)現(xiàn),因?yàn)橛龅搅穗y題,具體后面會講到判斷是否為自定義類:
BOOL inMainBundle(Class hookClass) { NSBundle *currentBundle = [NSBundle bundleForClass:hookClass]; return [currentBundle.bundlePath hasPrefix:[NSBundle mainBundle].bundlePath]; } -
過濾屬性的setter和getter方法。屬性的setter和getter方法不hook,不然每調(diào)用一次
self.語法就產(chǎn)生一條監(jiān)控日志,造成太多沒必要的干擾信息。注意默認(rèn)過濾規(guī)則:getter方法(屬性名),setter方法(setXXX:),其他自定義的setter和getter方法無法過濾。+ (void)enumerateClassMethods:(Class)hookClass forwardMsg:(BOOL)forwardMsg logOptions:(CJLogOptions)options { NSString *hookClassName = NSStringFromClass(hookClass); // hookClass中已經(jīng)被hook過的方法 NSArray *hookClassMethodList = [_hookClassMethodDic objectForKey:hookClassName]; NSMutableArray *methodList = [NSMutableArray arrayWithArray:hookClassMethodList]; //屬性的 setter 與 getter 方法不hook NSMutableArray *propertyMethodList = [NSMutableArray array]; unsigned int propertyCount = 0; objc_property_t *properties = class_copyPropertyList(hookClass, &propertyCount); for (int i = 0; i < propertyCount; i++) { objc_property_t property = properties[i]; // getter 方法 NSString *propertyName = [[NSString alloc]initWithCString:property_getName(property) encoding:NSUTF8StringEncoding]; [propertyMethodList addObject:propertyName]; // setter 方法 NSString *firstCharacter = [propertyName substringToIndex:1]; firstCharacter = [firstCharacter uppercaseString]; NSString *endCharacter = [propertyName substringFromIndex:1]; NSMutableString *propertySetName = [[NSMutableString alloc]initWithString:@"set"]; [propertySetName appendString:firstCharacter]; [propertySetName appendString:endCharacter]; [propertySetName appendString:@":"]; [propertyMethodList addObject:propertySetName]; } unsigned int outCount; Method *methods = class_copyMethodList(hookClass,&outCount); for (int i = 0; i < outCount; i ++) { // Method tempMethod = *(methods + i); Method tempMethod = methods[i]; SEL selector = method_getName(tempMethod); BOOL needHook = YES; for (NSString *selStr in propertyMethodList) { SEL propertySel = NSSelectorFromString(selStr); if (sel_isEqual(selector, propertySel)) { needHook = NO; break; } } if (needHook) { if (forwardMsg) { /* * 方案一:利用消息轉(zhuǎn)發(fā),hook forwardInvocation: 方法 */ BOOL canHook = enableHook(tempMethod); if (canHook) { forwardInvocationReplaceMethod(hookClass, selector, options); } }else{ // char *returnType = method_copyReturnType(tempMethod); // /* // * 方案二:hook每一個方法(未實(shí)現(xiàn)) // */ // cjlHookMethod(hookClass, selector, returnType); // free(returnType); } [methodList addObject:NSStringFromSelector(selector)]; } } free(methods); [_hookedClassList addObject:hookClassName]; [_hookClassMethodDic setObject:methodList forKey:hookClassName]; } -
一些系統(tǒng)方法不應(yīng)該 hook。具體如下:
@[ /*UIViewController的:*/ @".cxx_destruct", @"dealloc", @"_isDeallocating", @"release", @"autorelease", @"retain", @"Retain", @"_tryRetain", @"copy", /*UIView的:*/ @"nsis_descriptionOfVariable:", /*NSObject的:*/ @"respondsToSelector:", @"class", @"allowsWeakReference", @"retainWeakReference", @"init", @"resolveInstanceMethod:", @"resolveClassMethod:", @"forwardingTargetForSelector:", @"methodSignatureForSelector:", @"forwardInvocation:", @"doesNotRecognizeSelector:", @"description", @"debugDescription", @"self", @"lockFocus", @"lockFocusIfCanDraw", @"lockFocusIfCanDraw" ]; CACurrentMediaTime() 。如果選擇了日志選項
CJLogMethodTimer,那么在計算函數(shù)執(zhí)行時間時采用CACurrentMediaTime()計算,CACurrentMediaTime()是基于內(nèi)建時鐘的,能夠更精確更原子化地測量,并且不會因?yàn)橥獠繒r間變化而變化(例如時區(qū)變化、夏時制、秒突變等),可以最小化的減少性能損耗。mmap。日志數(shù)據(jù)采用
mmap內(nèi)存映射的方式存儲,具體請看CJLogger.mm中的相關(guān)實(shí)現(xiàn)。
引用自—— 認(rèn)真分析mmap:是什么 為什么 怎么用
mmap是一種內(nèi)存映射文件的方法,即將一個文件或者其它對象映射到進(jìn)程的地址空間,實(shí)現(xiàn)文件磁盤地址和進(jìn)程虛擬地址空間中一段虛擬地址的一一對映關(guān)系。實(shí)現(xiàn)這樣的映射關(guān)系后,進(jìn)程就可以采用指針的方式讀寫操作這一段內(nèi)存,而系統(tǒng)會自動回寫臟頁面到對應(yīng)的文件磁盤上,即完成了對文件的操作而不必再調(diào)用read,write等系統(tǒng)調(diào)用函數(shù)。相反,內(nèi)核空間對這段區(qū)域的修改也直接反映用戶空間,從而可以實(shí)現(xiàn)不同進(jìn)程間的文件共享。
-
forwardInvocationReplaceMethod 這個方法是實(shí)現(xiàn) CJMethodLog 的核心部分:
BOOL forwardInvocationReplaceMethod(Class cls, SEL originSelector, CJLogOptions options) { Method originMethod = class_getInstanceMethod(cls, originSelector); if (originMethod == nil) { return NO; } const char *originTypes = method_getTypeEncoding(originMethod); IMP msgForwardIMP = _objc_msgForward; #if !defined(__arm64__) if (isStructType(originTypes)) { //Reference JSPatch: //In some cases that returns struct, we should use the '_stret' API: //http://sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html //NSMethodSignature knows the detail but has no API to return, we can only get the info from debugDescription. NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:originTypes]; if ([methodSignature.debugDescription rangeOfString:@"is special struct return? YES"].location != NSNotFound) { msgForwardIMP = (IMP)_objc_msgForward_stret; } } #endif IMP originIMP = method_getImplementation(originMethod); if (originIMP == nil || originIMP == msgForwardIMP) { return NO; } //添加一個新方法,該方法的IMP是原方法的IMP,并且在hook到的forwardInvocation里調(diào)用新方法 SEL newSelecotr = createNewSelector(originSelector); BOOL addSucess = class_addMethod(cls, newSelecotr, originIMP, originTypes); if (!addSucess) { NSString *str = NSStringFromSelector(newSelecotr); CJLNSLog(@"CJMethodLog: Class addMethod fail : %@,%@",cls,str); return NO; } //替換當(dāng)前方法的IMP為msgForwardIMP,從而在調(diào)用時候觸發(fā)消息轉(zhuǎn)發(fā) class_replaceMethod(cls, originSelector, msgForwardIMP, originTypes); Method forwardInvocationMethod = class_getInstanceMethod(cls, @selector(forwardInvocation:)); _VIMP originMethod_IMP = (_VIMP)method_getImplementation(forwardInvocationMethod); method_setImplementation(forwardInvocationMethod, imp_implementationWithBlock(^(id target, NSInvocation *invocation){ SEL originSelector = invocation.selector; BOOL isInstance = isInstanceType(target); Class targetClass = isInstance?[target class]:object_getClass(target); if (class_respondsToSelector(targetClass, originSelector)) { _CJDeep ++; NSString *originSelectorStr = NSStringFromSelector(originSelector); NSMutableString *methodlog = [[NSMutableString alloc]initWithCapacity:3]; for (NSInteger deepLevel = 0; deepLevel <= _CJDeep; deepLevel ++) { [methodlog appendString:@"-"]; } [methodlog appendFormat:@" <%@> ",targetClass]; CFTimeInterval startTimeInterval = 0; BOOL beginAndEnd = NO; if ((options & CJLogMethodTimer) || (options & CJLogMethodArgs)) { [methodlog appendFormat:@" begin: "]; if (options & CJLogMethodTimer) { startTimeInterval = CACurrentMediaTime(); } beginAndEnd = YES; } if (isInstance) { [methodlog appendFormat:@" -%@",originSelectorStr]; }else{ [methodlog appendFormat:@" +%@",originSelectorStr]; } if (options & CJLogMethodArgs) { NSDictionary *methodArguments = CJMethodArguments(invocation); NSArray *argumentArray = methodArguments[_CJMethodArgsListKey]; NSMutableString *argStr = [[NSMutableString alloc]initWithCapacity:3]; for (NSInteger i = 0; i < argumentArray.count; i++) { id arg = argumentArray[i]; if (i == 0) { [argStr appendFormat:@" ; args=[ argIndex:%@ argValue:%@",@(i),[arg description]]; }else{ [argStr appendFormat:@", argIndex:%@ argValue:%@",@(i),[arg description]]; } } if (argumentArray.count > 0) { [argStr appendString:@" ]"]; } [methodlog appendString:argStr]; } if (_logEnable) { CJLNSLog(@"%@",methodlog); } [_logger flushAllocationStack:[NSString stringWithFormat:@"%@\n",methodlog]]; [invocation setSelector:createNewSelector(originSelector)]; [invocation setTarget:target]; [invocation invoke]; if (beginAndEnd) { [methodlog setString:[methodlog stringByReplacingOccurrencesOfString:@"begin: " withString:@"finish:"]]; if (options & CJLogMethodTimer) { CFTimeInterval endTimeInterval = CACurrentMediaTime(); [methodlog appendFormat:@" ; time=%f",(endTimeInterval-startTimeInterval)]; } if (options & CJLogMethodReturnValue) { id returnValue = getReturnValue(invocation); [methodlog appendFormat:@" ; return= %@",[returnValue description]]; } if (_logEnable) { CJLNSLog(@"%@",methodlog); } [_logger flushAllocationStack:[NSString stringWithFormat:@"%@\n",methodlog]]; } _CJDeep --; } //如果target本身已經(jīng)實(shí)現(xiàn)了對無法執(zhí)行的方法的消息轉(zhuǎn)發(fā)(forwardInvocation:),則這里要還原其本來的實(shí)現(xiàn) else { originMethod_IMP(target,@selector(forwardInvocation:),invocation); } if (_CJDeep == -1) { if (_logEnable) { CJLNSLog(@"\n"); } [_logger flushAllocationStack:@"\n"]; } })); return YES; }
首先判斷SEL對應(yīng)的Method是否存在
然后獲取消息轉(zhuǎn)發(fā)IMP (
msgForwardIMP),注意_objc_msgForward與_objc_msgForward_stret的判斷判斷當(dāng)前方法的IMP(
originIMP)不為nil,并且不等于msgForwardIMP添加一個指定前綴(
"cjlMethod_")開頭的新方法,該方法的IMP是原方法的IMP替換當(dāng)前方法的IMP為
msgForwardIMP,從而在調(diào)用時候觸發(fā)消息轉(zhuǎn)發(fā)獲取記錄當(dāng)前class的
@selector(forwardInvocation:)對應(yīng)的IMP,然后重寫其IMP實(shí)現(xiàn),這里直接使用了imp_implementationWithBlock(id _Nonnull block)生成IMP-
調(diào)用方法,觸發(fā)消息轉(zhuǎn)發(fā),進(jìn)入
@selector(forwardInvocation:)對應(yīng)的IMP內(nèi)。
首先判斷當(dāng)前Class是否可執(zhí)行原來方法,這里注意一下實(shí)例方法以及類方法的判斷,如果是類方法,取的是object_getClass():SEL originSelector = invocation.selector; BOOL isInstance = isInstanceType(target); Class targetClass = isInstance?[target class]:object_getClass(target); if (class_respondsToSelector(targetClass, originSelector)) { //TODO:寫入日志以及還原原方法的執(zhí)行 } //如果target本身已經(jīng)實(shí)現(xiàn)了對無法執(zhí)行的方法的消息轉(zhuǎn)發(fā)(forwardInvocation:),則這里要還原其本來的實(shí)現(xiàn) else { originMethod_IMP(target,@selector(forwardInvocation:),invocation); }當(dāng)判斷到當(dāng)前方法需要執(zhí)行日志監(jiān)聽時,拼裝日志信息(
_CJDeep記錄了方法執(zhí)行的層級關(guān)系),然后執(zhí)行一次日志寫入操作:[_logger flushAllocationStack:[NSString stringWithFormat:@"%@\n",methodlog]];再接著還原實(shí)際調(diào)用方法的實(shí)現(xiàn):
[invocation setSelector:createNewSelector(originSelector)]; [invocation setTarget:target]; [invocation invoke];緊接著如果存在
CJLogMethodTimer選項,則計算當(dāng)前函數(shù)執(zhí)行時間,同時寫入日志信息。
最后將_CJDeep- 1,從而完成本次hook方法的執(zhí)行。
-
CJMethodLog 無法同時 hook super方法
是否還記得上篇文章講到,Objective-C中執(zhí)行方法,其實(shí)底層調(diào)用的是objc_msgSend(receiver,SEL);如果是super方法,會調(diào)用objc_msgSendSuper(objc_super,SEL),再往下會轉(zhuǎn)換成objc_msgSend(objc_super->receiver, SEL),此時的receiver和 objc_super->receiver 表示的是同一個接收者。
比如下面例子:- (void)viewDidLoad { [super viewDidLoad]; } 底層對應(yīng)的是 objc_msgSend(self,@selector(viewDidLoad)) objc_msgSend(objc_super->receiver, @selector(viewDidLoad))當(dāng)CJMethodLog都hook了父子類的 viewDidLoad 方法后,調(diào)用會觸發(fā)消息轉(zhuǎn)發(fā),最終由以下代碼還原其實(shí)現(xiàn)
[invocation setSelector:createNewSelector(@selector(viewDidLoad))]; [invocation setTarget:target]; [invocation invoke];子類方法 target=self,父類方法 target=objc_super->receiver,其中self = objc_super->receiver,到此為止你是否發(fā)現(xiàn)了問題所在?問題就是這里的方法調(diào)用其實(shí)是一個死循環(huán)!??!偽代碼如下:
objc_msgSend(receiver,@selector(viewDidLoad)) { objc_msgSend(receiver, @selector(viewDidLoad)); }要破解這一難題只能想辦法區(qū)分父子類調(diào)用方法時候的不同上下文,可惜這一塊我還沒找到好的解決方案。
更多
- 解決
selfsuper上下文調(diào)用的問題 - 歡迎各位大神
starissue,幫忙解決難題
項目地址: CJMethodLog