導語:
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

可以看出:
-
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 的定義

-
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
那么其結構如下:

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í)行代碼,驗證一下:

- 元類:
SonClassIsa和SonClassMetaClass是Son類的MetaClass(這里用了兩種不同的方式獲取MetaClass)FatherClassIsa是Father類的MetaClassNSObjectClassIsa是NSObject類的MetaClassMetaClass0是SonClassIsa的MetaClass,MetaClass1是FatherClassIsa的MetaClass,MetaClass2是NSObjectClassIsa的MetaClass;MetaClass0MetaClass1MetaClass2都指向NSObjectClassIsa本身(四者代表同一個對象)
- 繼承關系:
- 類:Son -> Father -> NSObject -> nil
- 元類: 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指針指向它所在的類。 -
self 與 super :
或許你已經(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結構體的構成如下:

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_msgSendSuper和objc_msgSendSuper_stret。發(fā)送給父類的消息會使用objc_msgSendSuper,如果方法的返回值是一個結構體(structures),那么就會使用objc_msgSendSuper_stret或者objc_msgSend_stret,其他的消息會使用objc_msgSend。消息傳遞的時候會先在接收者所屬類中的
cache中查找方法,如果找不到就在methodLists中查找,如果找到和選擇器名稱相符的方法就跳轉其實現(xiàn)代碼;如果都找不到,就在其父類找,如果該消息無法被該類、其父類、父類的父類……解讀,那就會進入消息轉發(fā)階段。
下面是以上三個方法調用時的流程圖:

消息轉發(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í)行流程如下:

到此為止已經(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)。
