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

導語:

CJMethodLog 對于Objective-C中的任意類、任意方法,均可實時根據(jù)用戶的操作行為,監(jiān)控還原對應的函數(shù)調用日志,而且能夠自定義記錄當前函數(shù)的參數(shù)類型、返回類型、執(zhí)行時間……

背景說明

是否遇到過如此場景:對于項目中一些不是Crash的問題,由于缺乏log日志,排查起來很是麻煩;又或者對于一些特定設備、特定場景的問題,由于缺乏條件沒法重現(xiàn),最后只能不了了之。比如下面的例子:

你負責開發(fā)了一個考勤打卡頁面。某天你正當心情愉悅哼著小曲開始一天工作時,產(chǎn)品經(jīng)理突然闖入:又有用戶反饋明明打了卡卻提示曠工!并且那用戶未能提供當時的操作記錄,而且也無法再重現(xiàn)問題。無語的你只好再次默默過了一遍打卡代碼,沒有發(fā)現(xiàn)問題,然后求助后端同事查看接口調用日志,卻發(fā)現(xiàn)該用戶反饋問題的那一天根本就沒有相關的接口調用記錄……
——最后結論是用戶當時忘打卡了(但其實你卻不能十足確定打卡代碼沒有問題)

??那是否有這樣一種系統(tǒng):它能夠實時記錄用戶對APP的操作行為,并還原當前操作對應的運行代碼,然后將操作記錄寫入日志,生成一份詳細的函數(shù)調用堆棧日志。
??事實上,如果是只針對Crash錯誤,那么使用NSSetUncaughtExceptionHandler就完全可以收集到APP崩潰時刻的函數(shù)調用堆棧信息了(這點有時間我再起一篇文章說明)。
?? CJMethodLog就是這樣一個類庫:對于Objective-C中的任意類、任意方法,均可實時根據(jù)用戶的操作行為,監(jiān)控還原對應的函數(shù)調用日志,而且能夠自定義記錄當前函數(shù)的參數(shù)類型、返回類型、執(zhí)行時間等等。


實現(xiàn)原理

flag立的有點大??,但是如果你了解Objective-C運行時(Runtime)的語言特性,就知道理論上這是完全可以實現(xiàn)的。

一些基本概念

在xcode中用快捷鍵shift+cmd+O 分別搜索objc.h runtime.h NSObject.h

isa.jpg

可以看出:

  • NSObject是大部分Objective-C類繼承體系的基類,有一個私有成員變量isa
  • id實際上是一個objc_object結構體指針(這也是為什么使用id時不用再在前面加*的原因),id可以指向任何對象
  • objc_object是Objective-C對對象的定義, 其本質上是結構體對象,其中 isa是它唯一的私有成員變量,所有對象都有isa指針
  • Class是一個objc_class結構類型的指針
  • SEL(方法選擇器)是一個objc_selector結構類型的指針,其和C的函數(shù)指針有所不同,函數(shù)指針直接保存了方法的地址,而SEL只是方法編號,它是Objective-C在編譯時,根據(jù)方法名字生成的用來區(qū)分這個方法的唯一ID。兩個類之間,不管是父子類,還是沒有關系,只要方法名相同,那么它們的SEL就是一樣的。不同類的實例對象執(zhí)行相同的SEL時,會在各自的方法列表中根據(jù)SEL去尋找自己對應的IMP。
  • IMP是一個函數(shù)指針,這個被指向的函數(shù)包含一個接收消息的對象id(self 指針),調用方法SEL (方法選擇器),以及不定個數(shù)的方法參數(shù),并返回一個id。也就是說 IMP 是消息最終調用的執(zhí)行代碼,我們可以像在C語言里面一樣使用這個函數(shù)指針。

再看一下 objc_class 的定義

runtime.png

  • isa每個對象結構體的首個成員是Class類的變量,該變量定義了對象所屬的類,通常稱為isa指針
  • super_class父類,如果該類已經(jīng)是最頂層的根類,那么它為nil
  • name類名
  • version類的版本信息,默認為0
  • info供運行期使用的一些位標識
  • instance_size該類的實例變量大小
  • ivars用來存儲成員變量的數(shù)組
  • methodLists用來存儲當前類的方法鏈表
  • cache用來緩存用過的方法,提高查找性能
  • protocols協(xié)議鏈表

看到這里你可能有點暈了。id代表一個objc_object結構體指針,該結構體包含一個isa指針指向所屬的類;但是類里面又還有一個isa,那這個isa指向的又是誰呢?這里就引入了Meta Class(元類),Meta Class本身也是一個類,它跟其他類一樣也有自己的 isa 和 super_class 指針。

比如對于下面例子,類Son繼承自Father,Father繼承自NSObject

@interface Father : NSObject
+ (void)sendMessage;
@end
@implementation Father
+ (void)sendMessage {
    NSLog(@"sendMessage");
}
@end

@interface Son : Father
- (void)doSomething:(id)parameter;
- (void)somethingWrong;
@end
@implementation Son
- (void)doSomething:(id)parameter {
    NSLog(@"doSomething:");
}
@end

那么其結構如下:

MetaClass.png

Meta Class 結論:

  • 每個Class都有一個isa指針指向一個唯一的Meta Class
  • 每一個Meta Class的isa指針都指向最上層的Meta Class(圖中為NSObject的Meta Class)
  • 最上層的Meta Class的isa指針指向自己,從而形成一個回路
  • 每一個Meta Class的super class指針指向它原本Class的 Super Class對應的Meta Class。但是最上層的Meta Class的 Super Class指向NSObject Class本身
  • 最上層的NSObject Class的super class指向 nil

執(zhí)行代碼,驗證一下:

SonClass.png
  • 元類:
  1. SonClassIsaSonClassMetaClass是Son類的MetaClass(這里用了兩種不同的方式獲取MetaClass)
  2. FatherClassIsa是Father類的MetaClass
  3. NSObjectClassIsa是NSObject類的MetaClass
  4. MetaClass0SonClassIsa的MetaClass,MetaClass1FatherClassIsa的MetaClass,MetaClass2NSObjectClassIsa的MetaClass;MetaClass0 MetaClass1 MetaClass2都指向NSObjectClassIsa本身(四者代表同一個對象)
  • 繼承關系:
  1. 類:Son -> Father -> NSObject -> nil
  2. 元類: SonClassIsa -> FatherClassIsa -> NSObjectClassIsa -> NSObject -> nil
  • class 方法:
    再來看看 [self class]; 的低層實現(xiàn),下載Rumtime源碼,搜索NSObject.mm可以看到class方法的實現(xiàn)如下:

    class.png

    如果是類方法執(zhí)行 [self class];返回的是當前類本身;如果是實例方法執(zhí)行 [self class];,底層其實執(zhí)行的是object_getClass(id obj),最終返回的是實例對象的isa指針,這也驗證了前面的說法 —— 實例對象的isa指針指向它所在的類。

  • selfsuper
    或許你已經(jīng)注意到了,前面獲取super class不是調用方法[super class];那為什么不呢?事實上,如果你真的那樣調用,那么恭喜你!—— 準確掉坑里去了??!
    來看一下下面代碼,猜猜輸出啥:

      @implementation Son: Father
      - (instancetype)init {
          if ([super init]) {
              NSLog(@"self calss = %@",[self class]);
              NSLog(@"super calss = %@",[super class]);
          }
          return self;
      }
      @end
    

你以為是Son和Father,但其實輸出的都是Son ?。。?br> 要知道,在Objective-C中,self是一個隱藏參數(shù),它指向當前調用方法的對象(receiver);而super卻不是,它只是一個預編譯指令。調用super方法,底層執(zhí)行的是objc_msgSendSuper(下一節(jié)有詳細說明)

objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)

其中objc_super結構體的構成如下:

objc_super.png

receiver表示當前調用方法的對象,super_class表示其父類,objc_msgSendSuper的作用是先構建objc_super結構體,然后從結構體指向的父類開始尋找方法實現(xiàn),找不到再往上一級父類尋找。找到后最終會轉變?yōu)橐韵路椒ǎ?p>

objc_msgSend(objc_super->receiver, @selector(class))

這時候 self 和 objc_super->receiver 表示的是同一個接收者(receiver),同一個接收者執(zhí)行相同的方法@selector(class),那結果肯定是相同的。

消息傳遞

Objective-C中對于方法的調用,其實都是以消息的方式在傳遞。

Son *mySon = [Son new];
//調用方法
[mySon doSomething:@"parameter"];
[mySon performSelector:@selector(somethingWrong)];   
[Son sendMessage];

以上三個方法的調用,編譯器最終會將其轉換為C函數(shù):

objc_msgSend(mySon,@selector(doSomething:),@"parameter")
objc_msgSend(mySon,@selector(somethingWrong))
objc_msgSend(Son,@selector(sendMessage))

其中:

  • mySon叫做接收者(receiver)

  • @selector(doSomething:)叫做選擇器(selector),類型為SEL;選擇器和參數(shù)合起來成為消息(message)

  • 當對一個實例對象發(fā)送消息時,會在該實例對應的類里查找

  • 當對一個類發(fā)送消息時,會在該類的 Meta Class 里查找

  • objc_msgSend 是一個參數(shù)個數(shù)不定的函數(shù),它的作用是向一個實例類發(fā)送一個帶有簡單返回值的消息:

    objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
    

    另外還有:objc_msgSend_stret 、objc_msgSendSuperobjc_msgSendSuper_stret。發(fā)送給父類的消息會使用objc_msgSendSuper,如果方法的返回值是一個結構體(structures),那么就會使用objc_msgSendSuper_stret或者objc_msgSend_stret,其他的消息會使用objc_msgSend

    消息傳遞的時候會先在接收者所屬類中的cache 中查找方法,如果找不到就在methodLists 中查找,如果找到和選擇器名稱相符的方法就跳轉其實現(xiàn)代碼;如果都找不到,就在其父類找,如果該消息無法被該類、其父類、父類的父類……解讀,那就會進入消息轉發(fā)階段。

下面是以上三個方法調用時的流程圖:


消息傳遞.png
消息轉發(fā)

消息轉發(fā)是Objective-C運行時的一個重要特性,具體表現(xiàn)是當調用一個不存在的方法時,并不會立馬Crash,Runtime會有三次挽救的機會(準確的說是1次動態(tài)方法解析 + 1次快速消息轉發(fā) + 1次完整消息轉發(fā);)

調用階段 調用方法 備注
動態(tài)方法解析 +resolveInstanceMethod: (實例方法)

+resolveClassMethod:(類方法)
這里可以動態(tài)添加方法
快速消息轉發(fā)
(也叫備援接收者)
-forwardingTargetForSelector: 可以在此將消息轉發(fā)到指定對象,觸發(fā)新的消息傳遞
完整消息轉發(fā) -methodSignatureForSelector:

-forwardInvocation:
獲取簽名,并根據(jù)方法簽名包裝成的Invocation,對方法進行處理
消息處理失敗 -doesNotRecognizeSelector: 拋出異常

接上面,對于調用方法:

[mySon performSelector:@selector(somethingWrong)];

這是一個沒有實現(xiàn)的方法,最終會進入消息轉發(fā)階段,那么其完整的執(zhí)行流程如下:


objc_msgSend.png

到此為止已經(jīng)梳理完了Objective-C中完整的方法調用過程。細心的你可能會發(fā)現(xiàn)

- (id)forwardingTargetForSelector:(SEL)aSelector;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
- (void)doesNotRecognizeSelector:(SEL)aSelector;

這幾個方法都是實例方法,那是否意味著消息轉發(fā)只針對實例方法有效呢?答案是否定的!前面基本概念的介紹中提到,Classs的isa指針指向的是它的Meta Class,那么意味著一個Class的類方法會加入到它的Meta Class對應的methodLists中,所以你只需要在類中重寫下面的類方法,同樣可以實現(xiàn)對未知類方法的消息轉發(fā)。

+ (id)forwardingTargetForSelector:(SEL)aSelector;
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
+ (void)forwardInvocation:(NSInvocation *)anInvocation;
+ (void)doesNotRecognizeSelector:(SEL)aSelector

未完待續(xù)

上面已經(jīng)完整介紹了Objective-C中的消息傳遞以及消息轉發(fā)原理,回顧開篇提到的需求,聰明的你是否已經(jīng)get到了 CJMethodLog 的實現(xiàn)要點,這里先賣個關子。

本來是想只用一篇文章說完所有的,但是寫著寫著發(fā)現(xiàn)篇幅已經(jīng)不小了,所以另起一篇文章
CJMethodLog 二:從監(jiān)控還原APP運行的每一行代碼說起
來講解 CJMethodLog 的具體實現(xiàn)。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 轉至元數(shù)據(jù)結尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 2,072評論 0 9
  • Objective-C語言是一門動態(tài)語言,他將很多靜態(tài)語言在編譯和鏈接時期做的事情放到了運行時來處理。這種動態(tài)語言...
    tigger丨閱讀 1,601評論 0 8
  • 原文出處:南峰子的技術博客 Objective-C語言是一門動態(tài)語言,它將很多靜態(tài)語言在編譯和鏈接時期做的事放到了...
    _燴面_閱讀 1,425評論 1 5
  • 本文轉載自:http://southpeak.github.io/2014/10/25/objective-c-r...
    idiot_lin閱讀 1,032評論 0 4
  • 這篇文章完全是基于南峰子老師博客的轉載 這篇文章完全是基于南峰子老師博客的轉載 這篇文章完全是基于南峰子老師博客的...
    西木閱讀 30,892評論 33 466

友情鏈接更多精彩內容