Runtime原始碼解析和實戰使用
文章目錄
Runtime-原始碼分析
1.類的初始化 在外部是如何實現的?
2.初始化過程中runtime 起到了什麼作用?
類的結構體
類是繼承於物件的:
struct objc_class : objc_object { // Class ISA; Class superclass; cache_t cache; // formerly cache pointer and vtable class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags class_rw_t *data() { return bits.data(); } ...... }
在objc_class
中有定義了三個變數 ,superclass
是一個objc_class的結構體,指向的本類的父類的objc_class
結構體。cache
用來處理已經呼叫方法的快取。 class_data_bits_t
是objc_class 的關鍵,很多變數都是根據 bits
來實現的。
物件的初始化
在物件初始化的時候,一般都會呼叫 alloc+init 方法進行例項化,或者通過new 方法。
- 第一步:呼叫系統的alloc 方法 或者new 方法(其中`new`方法直接呼叫的`callAlloc init`)
+ (instancetype)alloc OBJC_SWIFT_UNAVAILABLE("use object initializers instead"); +(id)alloc{ return _objc_rootAlloc(self); }
- 第二步: runtime 內部實現呼叫objc_rootAlloc 方法
// Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
- 第三步: callAlloc 方法實現,解析:
callAlloc 方法在建立物件的地方有兩種方式,一種是通過calloc
開闢記憶體,然後通過obj->initInstanceIsa(cls, dtor)
函式初始化這塊記憶體。 第二種是直接調class_createInstance
函式,由內部實現初始化邏輯 ;
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false) {
if (fastpath(cls->canAllocFast())) {
bool dtor = cls->hasCxxDtor();
id obj = (id)calloc(1, cls->bits.fastInstanceSize());
if (slowpath(!obj)) return callBadAllocHandler(cls);
obj->initInstanceIsa(cls, dtor);
return obj;
} else {
id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls); return obj;
}
}
但是在最新的objc-723
中,呼叫canAllocFast()
函式直接返回false ,所以只會執行上面所述的第二個else
程式碼塊。
bool canAllocFast(){
return false;
}
初始化的程式碼最終會呼叫到 _class_createInstanceFromZone
函式,這個函式是初始化的關鍵程式碼。然後通過instanceSize 函式返回的 size
,並通過calloc
函式分配記憶體,初始化isa_t
指標。
size_t size = cls->instanceSize(extraBytes);
obj->initIsa(cls);
訊息的傳送機制
在OC 中方法呼叫時通過Runtime 來實現的,runtime 進行方法呼叫本質上是傳送訊息,通過objc_msgSend()
函式來進行訊息的傳送
[MyClass classMethod]
在runtime執行時被轉換為 ((void ()(id, SEL))(void )objc_msgSend)((id)objc_getClass("MyClass"), sel_registerName("classMethod"));
相關的demo可見我個人的github,在目錄檔案中,我將main.m進行了OC->C的轉換:RuntimeDemo
上述的方法可以理解為 向一個objc_class傳送了一個SEL 。
OC中每一個Method
的結構體如下:
struct objc_method {
SEL _Nonnull method_name
char * _Nullable method_types
IMP _Nonnull method_imp
}
在新的objc_runtime_new.h
中objc_method
已經沒有使用了,使用的是如下的結構體,其引入的方式也發生了改變,不是直接定義在objc_class
類中,而是通過getLoadMethod
方法來實現間接的呼叫。
struct method_t {
SEL name;
const char *types;
IMP 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; }
};
};
objc_msgSend
就是通過SEL
來進行遍歷查詢的,如果兩個類定義了相同名稱的方法,它們的SEL
就是一樣的。
objc_method
中具體引數解析如下:
SEL
指的就是第一步中解析方法呼叫得到的sel_registerName(“methodName”)
的返回值。method_types
指的是返回值的型別和引數。以返回值為開始,依次把引數拼接在後面,型別對應表格連結[TYPE EDCODING]。(聯想一哈,這個東西也是類似於property_gerAttrubute一樣,有對應的型別關係,某個字元意味著某種型別)IMP_Method
引數 是一個函式指標,指向objc_method所對應的實現部分。
objc_msgSend 工作原理
當一個物件被建立,系統會為通過上述的callalloc
函式分配一個記憶體size
並給他初始化一個isa
指標,可以通過指標訪問其類物件,並且通過對類物件訪問其所有繼承者鏈中的類。
-
objc_msgSend 底層實現沒有完全的暴露出來,但是通過原始碼中的
objc-msg-simulator-x86_64.s
的第672行程式碼開始可以看到部分實現,也可以通過Xcode
斷點來檢視執行的堆疊資訊。其實現原理主要是通過2個方法來完成,首先是CacheLookup
方法,在快取中沒有存在的情況下會去執行__objc_msgSend_uncached
的MethodTable
查詢SEL
GetIsaCheckNil NORMAL // r10 = self->isa, or return zero CacheLookup NORMAL, CALL // calls IMP on success GetIsaSupport NORMAL NilTestReturnZero NORMAL // cache miss: go search the method lists LCacheMiss: // isa still in r10 MESSENGER_END_SLOW jmp __objc_msgSend_uncached END_ENTRY _objc_msgSend
__objc_msgSend_uncached
方法查詢STATIC_ENTRY __objc_msgSend_uncached UNWIND __objc_msgSend_uncached, FrameWithNoSaves // THIS IS NOT A CALLABLE C FUNCTION // Out-of-band x16 is the class to search MethodTableLookup br x17 END_ENTRY __objc_msgSend_uncached STATIC_ENTRY __objc_msgLookup_uncached UNWIND __objc_msgLookup_uncached, FrameWithNoSaves // THIS IS NOT A CALLABLE C FUNCTION // Out-of-band x16 is the class to search MethodTableLookup ret
-
在執行
MethodTableLookup
方法時其中呼叫到了__class_lookupMethodAndLoadCache3
去找到需要的Class
引數和SEL
,內部實現找IMP
的是操作 方法是lookUpImpOrForward
。 -
當物件接受到訊息時,runtime會沿著訊息函式的
isa
查詢對應的類物件,然後是先在objc_cache
中去查詢當前的SEL
的快取,如果快取中存在SEL
,就直接返回該IMP
也就是該實現方法的指標。 -
如果cache 中不存在快取,需要先判斷該類是否已經被建立,如果沒有,則將類例項化,第一次呼叫當前類的話,執行
initialized
程式碼,再開始讀取這個類的快取,還是沒有的情況下才在method list 中查詢方法selector。本類如果沒有,就會到父類的method list中去查詢快取和method list 中的SEL
,直到NSObject
類 。
//如果快取在就直接返回
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
runtimeLock.read();
// 看看類有沒有被初始化,沒有初始化就直接初始化
if (!cls->isRealized()) {
// Drop the read-lock and acquire the write-lock.
// realizeClass() checks isRealized() again to prevent
// a race while the lock is down.
runtimeLock.unlockRead();
runtimeLock.write();
realizeClass(cls);
runtimeLock.unlockWrite();
runtimeLock.read();
}
//走一遍 initialized 方法
if (initialize && !cls->isInitialized()) {
runtimeLock.unlockRead();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.read();
// If sel == initialize, _class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}
retry:
runtimeLock.assertReading();
4.如果在類的繼承體系中都沒有找到SEL
,則會進行動態訊息解析,給自己保留處理找不到方法的機會,
// 沒有找到該方法,會執行下面的分解方法
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
runtimeLock.read();
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
goto retry;
}
其中_class_resolveMethod
的原始碼解析為:
if(!cls->isMetaClass()){
_class_resolveInstanceMethod(cls, sel, inst);
}else{
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
- 動態訊息解析如果沒有做出響應,則進入動態訊息轉發階段,如果還沒有人響應,就會觸發
doesNotRecognizeSelector
此時可以在動態訊息轉發階段做一些處理,否則就會Crash
.
訊息轉發機制
訊息轉發機制的實現:
首先從上述的_class_resolveMethod
可以方法可以看到,在找不到相關實現方法的時候,最終執行的都是_class_resolveInstanceMethod
方法,那我們就從這個方法來進行剖析。
_class_resolveMethod
方法實現所屬類動態方法的解析,其中主要的函式_class_resolveInstanceMethod
方法本質還是給指定類傳送一個objc_msgSend
訊息。經過各層級查詢後還是沒有,就會返回nil。但是iOS提供了使用者處理返回nil 後會出現閃退的方案,也就是resolveInstanceMethod
方法,從 option 鍵檢視描述可以得到其內部實現的是addMethod
方法。- 在物件所屬類不能動態新增方法後,
runtime
又提供了其他物件可以處理這個未知的SEL
的方法,相關方法宣告如下:
- (id)forwardingTargetForSelector:(SEL)aSelector;
- 在上述2種方法都沒有被實現的情況下,就只剩下最後一次機會,那就是訊息重定向。這個時間
runtime
會把SEL
封裝成NSInvocation
物件,然後呼叫:
- (void)forwardInvocation: (NSInvocation*)invocation;
如果這個類不能處理,就會呼叫其父類,知道NSObject
也沒有找到這個方法就會報錯doesNotRecognizeSelector
丟擲異常,並且閃退.
上述的訊息動態轉發是要人為的去實現,如果沒實現在動態轉發,在執行到動態解析之後就會發生閃退。
實戰使用
Runtime 為類別動態新增屬性
思考:
- 類的屬性是怎麼實現的?
- 在類別中新增屬性為什麼會不成功?
- 使用動態時實現在類別中新增屬性的原理是什麼?
類的屬性實現原理
- 在類中使用@property,系統會自動生成帶__ 的成員變數,和該變數的Setter 和getter 方法。 也就是意味著 一個成員屬性(property) 就相當於 成員變數+setter+getter 方法
unsigned int invarCount = 0;
Ivar *invars = class_copyIvarList([Human class], &invarCount);
for (NSInteger i =0; i<invarCount; i++) {
Ivar ivar = invars[i];
NSLog(@"獲取到的成員變數%s",ivar_getName(ivar));
}
unsigned int outCount = 0;
Method *method = class_copyMethodList([Human class], &outCount);
for (NSInteger i= 0; i< outCount; i++) {
NSLog(@"method%s", sel_getName(method_getName(method[i]))); // 4
}
objc_property_t *propertys = class_copyPropertyList([Human class], &outCount);
for (unsigned int i = 0; i<outCount; i++) {
objc_property_t property = propertys[i];
NSString *propertyName = [[NSString alloc]initWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
NSLog(@"propertyName%@",propertyName);
}
解析:使用property 會自動生成成員變數和getter、setter 函式
類別中直接新增屬性剖析
- 在類的類別中新增屬性,系統不會生成該屬性的 成員變數+Setter+getter 方法
@interface UIImage (SubImage)
@property(nonatomic,strong)NSString *imageString;
@end
輸出log 為: 獲取到的成員變數_imageRef
獲取到的成員變數_scale
獲取到的成員變數_imageFlags
獲取到的成員變數_flipsForRightToLeftLayoutDirection
獲取到的成員變數_traitCollection
獲取到的成員變數_vectorImageSupport
獲取到的成員變數_imageAsset
獲取到的成員變數_alignmentRectInsets
解析:category 它是在執行期決議的。 因為在執行期即編譯完成後,物件的記憶體佈局已經確定。
使用runtime 為類別新增屬性
思考:
1.runtime 為什麼能給類別新增屬性?
2.runtime 實現給類別新增屬性的原理是什麼?
在OC 中,類別即category 也是一個結構體categroy_t
,具體的定義如下:
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
從上述的程式碼我們不難看出category 是帶有協議、例項方法、類方法的引數的,而在runtime 進行初始化即呼叫objc_init
的時候,最後會有呼叫_dyld_objc_notify_register(&map_images, load_images, unmap_image)
。在其內部有一個_read_images
的操作會去取出當前類對應的category 陣列,並將其中的每個category_t
物件取出,最終執行addUnattachedCategoryForClass
函式新增到category 雜湊表中。然後通過remethodizeClass
方法來新增到指定的Class
上。
具體原始碼見:
objc-runtime-new.mm
- 在給類別新增屬性的時候需要通過runtime 來為該屬性手動實現getter 和setter 方法
//實現程式碼的getter 方法
-(NSTimeInterval)timeInterval{
return [objc_getAssociatedObject(self, _cmd)doubleValue];
}
實現程式碼的setter 方法
-(void)setTimeInterval:(NSTimeInterval)timeInterval{
objc_setAssociatedObject(self, @selector(timeInterval), @(timeInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
解析:通過runtime 可以對類的屬性動態繫結 生成getter 和setter 方法,從category的結構體中可以看到,但是卻無法生存例項變數。
參考資料:
Runtime 實現方法交換
Method Swizzle 實現的原理
在現實開發中,我們會遇到一些需求,比如為防止按鈕重複點選、檢測所有介面執行viewdidload
、之類的操作時,Method 可以起到很不錯的效果。這中程式設計方式也是屬於 面向切向程式設計(AOP
)的一種實現方式。在iOS
中一個典型的第三方框架 Aspects。
-
在上面曾講到過
objc_method
中SEL
就代表著方法名稱,IMP
代表著對應的實現方法。所以我們可以講 Methodswizzle 做的就是將兩個方法的SEL
和IMP
進行對應的交換。
如圖所示,對應的SEL 是指向對應的IMP 的,方法交換要實現的就是把SEL 指向的IMP 方法進行交換。
-
在實現把對應的方法進行交換時,我們通常會在一個類的類別中來實現,在
load
方法中執行所要交換的兩個方法。 因為load
方法在程式執行時就被呼叫載入到記憶體中了,有關load
和initialize
之間的差別其中有一點就是呼叫時機,load 在這個類中只會被呼叫一次 ,而initialize 在第一次傳送訊息的時候才會呼叫。所以在load
中來實現方法交換會更加的合適。
Methoad swizzle 實現程式碼
- 首先要獲取到當前用於交換的方法 。runtime 提供了2種形式來獲取:
1.獲取Method ,交換的方法為例項方法
class_getInstanceMethod([self Class],SEL oldSel);
2.獲取Method ,交換的方法為類方法
class_getClassMethod([self Class],SEL oldSel);
- 對需要進行交換的方法進行驗證,保證該類實現了這個方法,而不是他的父類實現了,這樣就達不到想要的交換本類方法的效果。
class_addMethod
在runtime 內部實現註釋為 :
新增一個新的方法到指定類,併為其指定方法名和實現方法(即SEL
和IMP
)。在新增成功時,返回YES
,否則NO
,該方法將新增超類的實現覆蓋,但是不會替換此類中的已經存在的實現方法,要更改現有實現方法,請使用method_setImplementation
.
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)
在返回失敗的時候,說明該方法本身就已經存在,只要執行交換操作就可以了,否則就執行replace 操作。
- 實現方法之間的交換
method_exchangeImplementations(oldMethod, newMethod);
注意:在使用方法交換的時候要記得使用 單例,為了避免出現第一次交換之後,第二次又給換回來的情況。