CJMethodLog 二:從監(jiān)控還原APP運(yùn)行的每一行代碼說起

導(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)。

CJMethodLogSource.png

上圖展示了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)用情況:

CJMethodLog.gif

日志格式說明:

- <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è)置階段:

CJMethodLog設(shè)置.png

方法執(zhí)行階段:

CJMethodLog執(zhí)行.png

一些關(guān)鍵點(diǎn)

  1. 默認(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];
     }
    
  2. 過濾屬性的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];
     }
    
  3. 一些系統(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"
     ];  
    
  4. CACurrentMediaTime() 。如果選擇了日志選項CJLogMethodTimer,那么在計算函數(shù)執(zhí)行時間時采用 CACurrentMediaTime() 計算,CACurrentMediaTime()是基于內(nèi)建時鐘的,能夠更精確更原子化地測量,并且不會因?yàn)橥獠繒r間變化而變化(例如時區(qū)變化、夏時制、秒突變等),可以最小化的減少性能損耗。

  5. 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)程間的文件共享。

  1. 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í)行。

  1. 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)用方法時候的不同上下文,可惜這一塊我還沒找到好的解決方案。


更多

  • 解決self super 上下文調(diào)用的問題
  • 歡迎各位大神star issue,幫忙解決難題
    項目地址: CJMethodLog
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容