Runtime
是什麼?Runtime
是一套有C
、C++
和彙編混合編寫的API
,爲OC
加入了面向對象以及運行時的功能。數組
運行時是指將數據類型的肯定有編譯時,推遲到了運行時。緩存
好比:在編譯時,只讀取macho
中的數據到ro
,而真正方法的讀取是在rw
中體現的,編譯好的ro
是沒法修改的,能夠經過運行時API
給編譯好的類能夠經過運行時添加方法
和屬性
。bash
能夠經過Runtime API
,能夠動態的添加屬性,交換方法,調用底層發送消息app
方法的本質是發送消息,發送消息會有一下幾個流程ide
(objc_msgSend)
,從cache_t中查找是否有緩存的IMP
。lookUpImpOrForward
resolveInstanceMethod
forwardingTargetForSelector
methodSignatureForSelector & forwardInvocation
SEL
是方法編號,在read_iamges
期間,就被編譯進了內存中的相關表中 IMP
就是咱們函數實現的指針,找IMP
,就是找函數實現的過程。函數
就好比:sel
至關於書本的目錄的標題,IMP
就至關於書本的頁碼,咱們首先知道咱們想看什麼,即SEL
,而後根據目錄找到對應的頁碼IMP
,而後翻到具體內容的一個過程。ui
ro
中,一旦編譯完成,就沒法修改。setter
和getter
,要手動添加。類拓展
添加實例變量關聯對象,類拓展在編譯的時候作爲類的一部分直接編譯到ro
中的,分類
添加實例變量,須要用關聯對象,本質是實例變量的值在關聯哈希表中的存儲和讀取。下面代碼怎麼打印:this
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]]; //
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]]; //
BOOL re3 = [(id)[LGPerson class] isKindOfClass:[LGPerson class]]; //
BOOL re4 = [(id)[LGPerson class] isMemberOfClass:[LGPerson class]]; //
NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);
BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]]; //
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]]; //
BOOL re7 = [(id)[LGPerson alloc] isKindOfClass:[LGPerson class]]; //
BOOL re8 = [(id)[LGPerson alloc] isMemberOfClass:[LGPerson class]]; //
NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
打印結果:1 0 0 0 1 1 1 1
複製代碼
經典isa
走位圖: atom
isKindOfClass
有一個繼承遞歸父類
的過程,有更多的容錯spa
isMemberOfClass
直接對比元類
和類
[self class]
本質就是發送消息objc_msgSend
,消息接受者是 self
, 方法編號:class
。
[super class]
本質是objc_msgSendSuper
,消息的接收者仍是self
, 方法編號:class
。只是objc_msgSendSuper
會更快 直接跳過 self
的查找
經過彙編
查看,當用__weak
去修飾一個對象的時候,底層會調用下面的方法:
id objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}
複製代碼
查看
storeWeak
方法
static id
storeWeak(id *location, objc_object *newObj)
{
assert(haveOld || haveNew);
if (!haveNew) assert(newObj == nil);
Class previouslyInitializedClass = nil;
id oldObj;
SideTable *oldTable;
SideTable *newTable;
// Acquire locks for old and new values.
// Order by lock address to prevent lock ordering problems.
// Retry if the old value changes underneath us.
retry:
if (haveOld) {
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
if (haveNew) {
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}
SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
if (haveOld && *location != oldObj) {
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
goto retry;
}
// Prevent a deadlock between the weak reference machinery
// and the +initialize machinery by ensuring that no
// weakly-referenced object has an un-+initialized isa.
if (haveNew && newObj) {
Class cls = newObj->getIsa();
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized())
{
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
class_initialize(cls, (id)newObj);
// If this class is finished with +initialize then we're good. // If this class is still running +initialize on this thread // (i.e. +initialize called storeWeak on an instance of itself) // then we may proceed but it will appear initializing and // not yet initialized to the check above. // Instead set previouslyInitializedClass to recognize it on retry. previouslyInitializedClass = cls; goto retry; } } // ✅Clean up old value, if any. if (haveOld) { weak_unregister_no_lock(&oldTable->weak_table, oldObj, location); } // ✅Assign new value, if any. if (haveNew) { newObj = (objc_object *) weak_register_no_lock(&newTable->weak_table, (id)newObj, location, crashIfDeallocating); // weak_register_no_lock returns nil if weak store should be rejected // Set is-weakly-referenced bit in refcount table. if (newObj && !newObj->isTaggedPointer()) { newObj->setWeaklyReferenced_nolock(); } // Do not set *location anywhere else. That would introduce a race. *location = (id)newObj; } else { // No new value. The storage is not changed. } SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable); return (id)newObj; } 複製代碼
在這個方法中,看到底層維護了一張散列表SideTable
,從SideTable
中找到維繫的一張weak_table
,而後判斷新值
和舊值
。
weak_unregister_no_lock
,最終在weak_resize
方法中static void weak_resize(weak_table_t *weak_table, size_t new_size)
{
size_t old_size = TABLE_SIZE(weak_table);
weak_entry_t *old_entries = weak_table->weak_entries;
weak_entry_t *new_entries = (weak_entry_t *)
calloc(new_size, sizeof(weak_entry_t));
weak_table->mask = new_size - 1;
weak_table->weak_entries = new_entries;
weak_table->max_hash_displacement = 0;
weak_table->num_entries = 0; // restored by weak_entry_insert below
if (old_entries) {
weak_entry_t *entry;
weak_entry_t *end = old_entries + old_size;
for (entry = old_entries; entry < end; entry++) {
if (entry->referent) {
//✅ entry加入到咱們的weak_table
weak_entry_insert(weak_table, entry);
}
}
free(old_entries);
}
}
複製代碼
經過
weak_entry_insert(weak_table, entry)
將修飾的對象,插入到eak_table
中。
weak_register_no_lock
,/**
* Registers a new (object, weak pointer) pair. Creates a new weak
* object entry if it does not exist.
*
* @param weak_table The global weak table.
* @param referent The object pointed to by the weak reference.
* @param referrer The weak pointer address.
*/
id
weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, bool crashIfDeallocating)
{
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;
if (!referent || referent->isTaggedPointer()) return referent_id;
// ensure that the referenced object is viable
bool deallocating;
if (!referent->ISA()->hasCustomRR()) {
deallocating = referent->rootIsDeallocating();
}
else {
BOOL (*allowsWeakReference)(objc_object *, SEL) =
(BOOL(*)(objc_object *, SEL))
object_getMethodImplementation((id)referent,
SEL_allowsWeakReference);
if ((IMP)allowsWeakReference == _objc_msgForward) {
return nil;
}
deallocating =
! (*allowsWeakReference)(referent, SEL_allowsWeakReference);
}
if (deallocating) {
if (crashIfDeallocating) {
_objc_fatal("Cannot form weak reference to instance (%p) of "
"class %s. It is possible that this object was "
"over-released, or is in the process of deallocation.",
(void*)referent, object_getClassName((id)referent));
} else {
return nil;
}
}
// now remember it and where it is being stored
weak_entry_t *entry;
// ✅根據referent 找到 entyry
if ((entry = weak_entry_for_referent(weak_table, referent))) {
append_referrer(entry, referrer);
}
else {
// ✅建立了這個數組 - 插入weak_table
weak_entry_t new_entry(referent, referrer);
weak_grow_maybe(weak_table);
weak_entry_insert(weak_table, &new_entry);
}
// Do not set *referrer. objc_storeWeak() requires that the
// value not change.
return referent_id;
}
複製代碼
經過referent
從 weak_table
中,找到entry
,判斷entry
是否存在(entry = weak_entry_for_referent(weak_table, referent))
),存在append_referrer
,將其添加到entry
中,不存在,則建立一個weak_entry_t
,而後四分之三擴容,而後插入到new_entry
中(weak_entry_insert(weak_table, &new_entry)
)。
總結:
__weak
底層維繫一張散列表SideTable
,SideTable
中維繫一張弱引用表weak_table
,在這張弱引用表中,有不少弱引用對象的實體weak_entry_t *entry
。
__weak
就是根據傳進來的弱引用對象,去weak_table
中找到對應的實體weak_entry_t *entry
,而後檢查是否須要擴容(3/4擴容
),而後拼接進行內部持有(new_referrers[i] = entry->inline_referrers[i]
)的過程,若是這個entry
不存在,則建立一下新的實體entry
,而後擴容,再插入weak_entry_insert
那麼,
__weak
爲何能夠自動置爲nil
?
查看dealloc
源碼,最終在objc_destructInstance
方法中,以下源碼:
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
// ✅關聯對象表
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj);
// ✅移除關聯對象表
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
複製代碼
進入obj->clearDeallocating()
方法
inline void
objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
sidetable_clearDeallocating();
}
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}
assert(!sidetable_present());
}
複製代碼
在clearDeallocating
通過判斷,最終都會進入到weak_clear_no_lock
方法中,
void
weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
objc_object *referent = (objc_object *)referent_id;
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
/// XXX shouldn't happen, but does with mismatched CF/objc //printf("XXX no entry for clear deallocating %p\n", referent); return; } // zero out references weak_referrer_t *referrers; size_t count; if (entry->out_of_line()) { referrers = entry->referrers; count = TABLE_SIZE(entry); } else { referrers = entry->inline_referrers; count = WEAK_INLINE_COUNT; } for (size_t i = 0; i < count; ++i) { objc_object **referrer = referrers[i]; if (referrer) { if (*referrer == referent) { *referrer = nil; } else if (*referrer) { _objc_inform("__weak variable at %p holds %p instead of %p. " "This is probably incorrect use of " "objc_storeWeak() and objc_loadWeak(). " "Break on objc_weak_error to debug.\n", referrer, (void*)*referrer, (void*)referent); objc_weak_error(); } } } weak_entry_remove(weak_table, entry); } 複製代碼
在weak_clear_no_lock
中直接將關聯對象的指針referrer
置爲nil
,因此當其釋放時,會自動設置爲空。
跟__weak
同樣,先經過彙編分析,找到底層調用的objc_storeStrong
方法,以下:
void
objc_storeStrong(id *location, id obj)
{
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}
複製代碼
先objc_retain(obj)
,在objc_release(prev)
,而objc_retain
方法和objc_release
方法底層都是發送retain
和release
消息。
在處理數組越界的時候,咱們很容易想到的就是經過Runtime
進行方法交換。
咱們一般會建立一個分類,以下:
#import "NSArray+LG.h"
#import "LGRuntimeTool.h"
#import <objc/runtime.h>
@implementation NSArray (LG)
+ (void)load{
[LGRuntimeTool lg_methodSwizzlingWithClass:objc_getClass("__NSArrayI") oriSEL:@selector(objectAtIndex:) swizzledSEL:@selector(lg_objectAtIndex:)];
[LGRuntimeTool lg_methodSwizzlingWithClass:objc_getClass("__NSArrayI") oriSEL:@selector(objectAtIndexedSubscript:) swizzledSEL:@selector(lg_objectAtIndexedSubscript:)];
}
// 交換的方法
- (id)lg_objectAtIndex:(NSUInteger)index{
if (index > self.count-1) {
NSLog(@"數組越界 -- ");
NSLog(@"取值越界了,記錄:%lu > %lu", (unsigned long)index, self.count - 1);
return nil;
}
return [self lg_objectAtIndex:index];
}
- (id)lg_objectAtIndexedSubscript:(NSUInteger)index {
if (index > self.count-1) {
NSLog(@"數組越界 -- ");
NSLog(@"取值越界了,記錄:%lu > %lu", (unsigned long)index, self.count - 1);
return nil;
}
return [self lg_objectAtIndexedSubscript:index];
}
#import "LGRuntimeTool.h"
#import <objc/runtime.h>
@implementation LGRuntimeTool
+ (void)lg_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"傳入的交換類不能爲空");
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
method_exchangeImplementations(oriMethod, swiMethod);
}
@end
複製代碼
對objectAtIndex
和lg_objectAtIndex
進行交換,當調用系統objectAtIndex
方法時,調用lg_objectAtIndex
方法,方便咱們在數組越界時的一些處理(好比防崩潰)
那麼假如咱們在添加了NSArray
的分類以後,在某個調用的地方,不當心調用了,NSArray
的load
方法,好比下面的代碼,會發生什麼呢?
self.dataArray = @[@"Hank",@"Cooci",@"Kody",@"CC"];
[NSArray load];
NSLog(@"%@",[self.dataArray objectAtIndex:4]);
複製代碼
經過驗證,上面的代碼,會崩潰,當再次調用load
方法時,會再次交換方法,調用系統的objectAtIndex
方法,而致使崩潰。
因此,咱們要在添加分類的load
方法中,使用單例,防止方法屢次重複交換
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[LGRuntimeTool lg_methodSwizzlingWithClass:objc_getClass("__NSArrayI") oriSEL:@selector(objectAtIndex:) swizzledSEL:@selector(lg_objectAtIndex:)];
[LGRuntimeTool lg_methodSwizzlingWithClass:objc_getClass("__NSArrayI") oriSEL:@selector(objectAtIndexedSubscript:) swizzledSEL:@selector(lg_objectAtIndexedSubscript:)];
});
}
複製代碼
那麼在平常的開發中,常常出現繼承關係,而在這個繼承關係中,常常出現子類繼承父類的方法,那麼在子類中交換繼承自父類的方法會出現什麼狀況呢?以下代碼:
// 父類
@interface LGPerson : NSObject
- (void)personInstanceMethod;
+ (void)personClassMethod;
@end
#import "LGPerson.h"
@implementation LGPerson
- (void)personInstanceMethod{
NSLog(@"person對象方法:%s",__func__);
}
+ (void)personClassMethod{
NSLog(@"person類方法:%s",__func__);
}
@end
// 子類
@interface LGStudent : LGPerson
@end
#import "LGStudent.h"
@implementation LGStudent
@end
複製代碼
上面代碼中LGStudent
繼承自LGPerson
,而LGPerson
中實現了personInstanceMethod
和personClassMethod
兩個方法,
在子類LGStudent
中,對personInstanceMethod
方法進行方法交換,以下:
@implementation LGStudent (LG)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[LGRuntimeTool lg_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
});
}
- (void)lg_studentInstanceMethod{
[self lg_studentInstanceMethod];
NSLog(@"LGStudent分類添加的lg對象方法:%s",__func__);
}
@end
+ (void)lg_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"傳入的交換類不能爲空");
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
method_exchangeImplementations(oriMethod, swiMethod);
}
複製代碼
這樣的方法交換,在咱們調用LGStudent
的時候,完美的實現了方法互換,那麼在咱們調用父類LGPerson
時,會有什麼問題呢?調用以下:
LGStudent *s = [[LGStudent alloc] init];
[s personInstanceMethod];
LGPerson *p = [[LGPerson alloc] init];
[p personInstanceMethod];
複製代碼
結果以下:
personInstanceMethod
時,出現了父類調用
lg_studentInstanceMethod
,而父類自己沒有這個方法,因此就崩潰了。
那麼,子類在交換繼承自父類而本身自己沒有重寫的方法時,應該怎麼作呢 ?
其實,咱們能夠在方法交換的時候作一個判斷,先嚐試添加一個方法,
+ (void)lg_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"傳入的交換類不能爲空");
// oriSEL personInstanceMethod
// swizzledSEL lg_studentInstanceMethod
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
// 嘗試添加
// ✅ 首先第一步:會先嚐試給本身添加要交換的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
/**
personInstanceMethod(sel) - lg_studentInstanceMethod(imp)
lg_studentInstanceMethod (swizzledSEL) - personInstanceMethod(imp)
*/
//oriSEL:personInstanceMethod
if (success) {// 本身沒有 - 交換 - 沒有父類進行處理 (重寫一個)
// ✅ 而後再將父類的IMP給swizzle personInstanceMethod(imp) -> swizzledSEL
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{ // 本身有
method_exchangeImplementations(oriMethod, swiMethod);
}
}
複製代碼
這樣就能夠完美的對子類的方法進行交換了,調用結果以下,
那麼,還有一個問題,假如某人交換了只有聲明並無實現的方法,上面的方式就會出現死循環,由於根本沒有父類方法的IMP
,因此,還要對其進行改造。
代碼以下:
+ (void)lg_bestMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"傳入的交換類不能爲空");
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
if (!oriMethod) {
// 在oriMethod爲nil時,替換後將swizzledSEL複製一個不作任何事的空實現,代碼以下:
class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
}
// 通常交換方法: 交換本身有的方法 -- 走下面 由於本身有意味添加方法失敗
// 交換本身沒有實現的方法:
// 首先第一步:會先嚐試給本身添加要交換的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
// 而後再將父類的IMP給swizzle personInstanceMethod(imp) -> swizzledSEL
//oriSEL:personInstanceMethod
BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (didAddMethod) {
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{
method_exchangeImplementations(oriMethod, swiMethod);
}
}
複製代碼
在方法交換的時候,先對被交換方法進行判斷,判斷這個方法是否實現,當方法未實現時,咱們手動添加一個空的方法實現,在這個空的方法實現中,咱們能夠作一些上傳等操做,來記錄收集crash
。
@interface LGPerson : NSObject
@property (nonatomic, copy)NSString *name;
@property (nonatomic, copy)NSString *subject;
@property (nonatomic)int age;
- (void)saySomething;
@end
@implementation LGPerson
- (void)saySomething{
NSLog(@"NB %s ",__func__);
}
@end
#import "ViewController.h"
#import <objc/runtime.h>
#import "LGPerson.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// NSString *tem = @"KC";
id pcls = [LGPerson class];
void *pp= &pcls;
[(__bridge id)pp saySomething];
// p -> LGPerson 實例對象
LGPerson *p = [LGPerson alloc];
[p saySomething];
}
複製代碼
運行打印結果以下,那麼爲何會這樣呢?
首先,指針p
指向的是LGPerson
的實例對象的存儲空間,而指針pcls
指向的是LGPerson
類對象的存儲空間。pp
指向的是指針pcls
的空間,而實例對象中的isa
也指向指針pcls
,因此pp
可以正常調用saySomething
方法
saySomething
方法進行修改,以下,並打印self.name
。- (void)saySomething{
NSLog(@"NB %s - %@",__func__,self.name);
}
複製代碼
經過調試,打印結果以下:
那麼爲何會打印<ViewController: 0x105105e10>
呢,接下來咱們把上面的// NSString *tem = @"KC"
註釋打開,再來看一下,這一次打印的是KC
。
那麼爲何會這樣呢?
由於棧的內存是連續的,而咱們的屬性的讀取是經過指針偏移來讀取的,而壓棧的示意以下:
上圖中pp
指向的是isa
,在經過指針偏移讀取self.name
時,恰好讀取到了tem
,因此打印了KC
,因此當沒有打開註釋時,打印的是ViewController
接着在saySomething
中打印subject
,打印出的是ViewController
那麼咱們接着再添加屬性
@property (nonatomic)NSString *age;
複製代碼
打印的話,就會出現野指針報錯
或者將age
的類型改爲int
,也會由於讀取不到內存,而報錯。