日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Runtime底层原理--动态方法解析、消息转发源码分析

發布時間:2024/9/30 编程问答 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Runtime底层原理--动态方法解析、消息转发源码分析 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

了解了Runtime函數含義,我們就可以直接使用Runtime的API了,那接下來繼續探究Runtime的源碼,經過源碼分析來更加深刻的了解Runtime原理。

開發應用

都知道Runtime很重要,但是有很多小伙伴根本不了解,或者只是知道可以替換方法啊、可以交換兩個方法的調用,項目中也用不到,
從進入iOS開始,寫了無數個[[objc alloc] init],這個到底在干嘛?初始化和init?alloc和init到底做了什么?

通過匯編查看方法調用
Person *person = [Person alloc];Person *person1 = [person init];Person *person2 = [person init];NSLog(@"%p-----%p------%p", person, person1, person2);

這里會輸出什么呢?

0x10102e1a0-----0x10102e1a0------0x10102e1a0

來,讓我們斷點看下,alloc和init是怎么調用的

我們看到調用alloc和init都調起了objc_msgSend,接下來跟著符號斷點走

進入libobjc庫的dylib之后走+[NSObject alloc]方法,指針調起_objc_rootAlloc,進入_objc_rootAlloc方法,繼續調起callAlloc,通過寄存器,可以看到alloc已經通過類創建實例對象

init按照同樣方法 依然可以通過匯編看出方法調用順序,可以用真機進行測試并打印

通過編譯C++

當新的對象被創建時,其內存同時被分配,實例變量也同時被初始化。對象的第一個實例變量是一個指向該對象的類結構的指針,叫做 isa。通過該指針,對象可以訪問它對應的類以及相應的父類。在 Objective-C 運行時系統中對象需要有 isa 指針,我們一般創建的從 NSObject 或者 NSProxy 繼承的對象都自動包括 isa 變量。接下來看下對象被創建的過程
首先,我們通過clang命令

$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o testMain.c++

也可以用clang -rewrite-objc main.m -o test.c++命令,只不過會有很多警告、代碼會更長(大概9萬多行)。
編譯main函數中的OC代碼為C++代碼

int main(int argc, const char * argv[]) {@autoreleasepool {Person *p = [[Person alloc] init];[p run];}return 0; }

編譯后多一個testMain.c++文件,打開后在代碼最后面會發現我們的main函數

int main(int argc, const char * argv[]) {/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("run"));}return 0; }

可以看出,我們的方法調用會編譯成objc_msgSend,

由此還會發現對象的本質其實就是一個結構體

下層通訊(通過源碼查看objc_msgSend內部實現)

首先我們到蘋果open source官網下載最新源碼

方法調用的時候,會發送objc_msgSend消息,objc_msgSend會根據sel找到函數實現的指針imp,進而執行函數,那sel是如何找到imp的呢?
objc_msgSend在發送消息時候根據sel查找imp有兩種方式

  • 快速(通過匯編的緩存快速查找)
  • 慢速(C配合C++、匯編一起查找)
    先看下objc_class

bits中包含各種數據,cache(每個類都有一個)用來存儲方法select和imp,select和imp會以哈希表形式存在
objc_msgSend在快速查找的時候,就是通過匯編查找objc_class中的cache,如果找到則直接返回,否則通過C的lookup,找到后再存入cache

匯編部分快速查找

首先調用objc_msgSend會走到ENTRY

先判斷p0檢查是否為空和tagged pointer(特殊類型)判斷,調用LNilOrTagged進行isa處理,通過isa找到相應類class,最后調用LGetIsaDone來執行CacheLookup在緩存中查找imp,如果查找到直接調起imp否則調起objc_msgSend_uncached,objc_msgSend_uncached有兩種情況

首先,第一個是CacheHit,直接調起imp,第二個是CheckMiss,之后調用objc_msgSend_uncached,第三個就是add,下面是CacheHit和CheckMiss的宏

那如果在緩存中沒有查找到imp,調起objc_msgSend_uncached,在方法列表中找到imp之后再TailCallFunctionPointer調起imp

STATIC_ENTRY __objc_msgSend_uncachedUNWIND __objc_msgSend_uncached, FrameWithNoSaves// THIS IS NOT A CALLABLE C FUNCTION// Out-of-band p16 is the class to searchMethodTableLookup // 方法列表中找到impTailCallFunctionPointer x17

重點:MethodTableLookup是怎么操作的

小知識點:通過method list查找method,下面是method_t的結構,method其實是一個哈希表,sel和imp是鍵值對

struct method_t {SEL name;const char *types; // 參數類型MethodListIMP imp;struct SortBySELAddress :public std::binary_function<const method_t&,const method_t&, bool>{bool operator() (const method_t& lhs,const method_t& rhs){ return lhs.name < rhs.name; }}; };

進入MethodTableLookup之后,調起了__class_lookupMethodAndLoadCache3,如下圖

__class_lookupMethodAndLoadCache3是C方法,再次進入_class_lookupMethodAndLoadCache3方法,注意,因為這里由匯編跳轉到C,所以要全局搜索_class_lookupMethodAndLoadCache3,要刪去一個"_",下面是_class_lookupMethodAndLoadCache3函數

/*********************************************************************** * _class_lookupMethodAndLoadCache. * Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp(). * This lookup avoids optimistic cache scan because the dispatcher * already tried that. **********************************************************************/ IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls) {return lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/); }
C/C++部分查找

調起lookUpImpOrForward,因為當前cls對象已經經過匯編編譯到結構,有了isa,并且在cache中沒有找到,所以這里的initialize為YES,cache為NO,resolver為YES

進入lookUpImpOrForward,這里再次判斷是否存在cache,如果有則直接快速查找,但是這里是NO,所以不會走。接下來走checkIsKnownClass判斷是否是已經聲明的類,如果沒有則報錯"Attempt to use unknown class %p.",之后走realizeClass判斷是否已經實現,如果就相應賦值data。

data賦值后走_class_initialize初始化cls,接下來開始retry操作。
前方高能
再次進行cache_getImp,why?并發啊,還有重映射(在初始化init的時候有個remap(class)第一次通過匯編找不到,但是在加載類的時候對當前類進行重映射)

接下來開始先在自己的class_rw_t的methods中根據sel查找方法返回method_t

如果拿到Method后保存到緩存中,保證以后調用可以直接走匯編的CacheHit快速查找,如果拿不到則繼續從父類開始查找,直到找到NSObject(因為NSObject的父類為nil),如果找到imp則一樣保存在緩存中,如果到最后還是沒有查找到,則進入動態方法解析。

動態方法解析

如果前面一系列操作還是沒有找到方法,那么就會進行動態方法解析,動態方法解析只執行一次

首先執行_class_resolveMethod,這里會執行+resolveClassMethod 或者 +resolveInstanceMethod。

先判斷當前cls是否為元類,如果是元類則執行_class_resolveClassMethod,再執行_class_resolveInstanceMethod,如果不是元類則直接執行_class_resolveInstanceMethod,_class_resolveInstanceMethod內部調用objc_msgSend實現消息發送,對cls發送了SEL_resolveInstanceMethod類型的消息,所以在方法中會走到resolveInstanceMethod方法。

為什么元類最后也執行了_class_resolveInstanceMethod方法呢?因為類方法以實例對象的形態存在元類里面,比如類方法中沒有找到方法,會去元類中查找,元類中沒有再繼續去根元類中查找,最后會查到NSObject。

代碼示例:

.h實現

- (void)run; + (void)eat;

.m實現(沒有實現-run方法和+eat方法)

- (void)walk {NSLog(@"%s",__func__); } + (void)drink {NSLog(@"%s",__func__); }// .m沒有實現,并且父類也沒有,那么我們就開啟動態方法解析 //- (void)walk{ // NSLog(@"%s",__func__); //} //+ (void)drink{ // NSLog(@"%s",__func__); //}#pragma mark - 動態方法解析+ (BOOL)resolveInstanceMethod:(SEL)sel{if (sel == @selector(run)) {// 我們動態解析我們的 對象方法NSLog(@"對象方法解析走這里");SEL walkSEL = @selector(walk);Method readM= class_getInstanceMethod(self, walkSEL);IMP readImp = method_getImplementation(readM);const char *type = method_getTypeEncoding(readM);return class_addMethod(self, sel, readImp, type);}return [super resolveInstanceMethod:sel]; }+ (BOOL)resolveClassMethod:(SEL)sel{if (sel == @selector(eat)) {// 我們動態解析我們的 對象方法NSLog(@"類方法解析走這里");SEL drinkSEL = @selector(drink);// 類方法就存在我們的元類的方法列表// 類 類犯法// 元類 對象實例方法// Method hellowordM1= class_getClassMethod(self, hellowordSEL);Method drinkM= class_getInstanceMethod(object_getClass(self), drinkSEL);IMP drinkImp = method_getImplementation(drinkM);const char *type = method_getTypeEncoding(drinkM);NSLog(@"%s",type);return class_addMethod(object_getClass(self), sel, drinkImp, type);}return [super resolveClassMethod:sel]; }
消息轉發

經歷了動態方法決議還沒有找到,會進入蘋果尚未開源的消息轉發,繼續查找方法,_objc_msgForward_impcache再次跨域到匯編。

走到__objc_msgForward_impcache后執行__objc_msgForward

沒有了源碼實現,但是我們可以通過instrumentObjcMessageSends函數來打印調用堆棧信息??梢赃M入instrumentObjcMessageSends內部看下具體實現。

先判斷了是否可以寫入日志信息等,接下來同步日志文件

所以我們每次運行會在/private/tmp文件下多一個msgSends-xxx文件,里面是所有調用過程

如果還沒有找到的話最后會報錯調用__objc_forward_handler

這也是我們在方法報錯的時候會報unrecognized selector sent to instance %p " "(no message forward handler is installed)"錯誤的原因,會提示出元類信息,+或者-方法,方法的名字還有SEL方法編號

代碼示例:
#pragma mark - 實例對象消息轉發- (id)forwardingTargetForSelector:(SEL)aSelector{NSLog(@"%s",__func__);// if (aSelector == @selector(run)) {// // 轉發給Student對象// return [Student new];// }return [super forwardingTargetForSelector:aSelector]; }- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{NSLog(@"%s",__func__);if (aSelector == @selector(run)) {// forwardingTargetForSelector 沒有實現,就只能方法簽名了return [NSMethodSignature signatureWithObjCTypes:"v@:@"];}return [super methodSignatureForSelector:aSelector]; }- (void)forwardInvocation:(NSInvocation *)anInvocation{NSLog(@"%s",__func__);NSLog(@"------%@-----",anInvocation);anInvocation.selector = @selector(walk);[anInvocation invoke]; }#pragma mark - 類消息轉發+ (id)forwardingTargetForSelector:(SEL)aSelector{NSLog(@"%s",__func__);return [super forwardingTargetForSelector:aSelector]; } //+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{NSLog(@"%s",__func__);if (aSelector == @selector(walk)) {return [NSMethodSignature signatureWithObjCTypes:"v@:@"];}return [super methodSignatureForSelector:aSelector]; }+ (void)forwardInvocation:(NSInvocation *)anInvocation{NSLog(@"%s",__func__);NSString *sto = @"奔跑吧";anInvocation.target = [Student class];[anInvocation setArgument:&sto atIndex:2];NSLog(@"%@",anInvocation.methodSignature);anInvocation.selector = @selector(run:);[anInvocation invoke]; }

現在我們應該也知道了為什么objc_msgSend的源碼用的匯編,因為匯編可以通過寄存器x0-x31來保留未知參數來跳轉到任意的指針,還有匯編更高效一點,而C滿足不了。

言而總之,總而言之

Runtime就是C、C++、匯編實現的一套API,給OC增加的一個運行時功能,也就是我們平時所說的運行時。
在運行工程時工程會被裝載到內存,來提供運行時功能。

該文章為記錄本人的學習路程,希望能夠幫助大家,也歡迎大家點贊留言交流!!!文章地址:https://www.jianshu.com/p/1ddd15e47343

總結

以上是生活随笔為你收集整理的Runtime底层原理--动态方法解析、消息转发源码分析的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。