參考 Objective-C Runtime 1小時入門教程 Objective-C特性:Runtime
Objc 物件的今生今世 神經病院Objective-C Runtime入院第一天——isa和Class 深入解析 ObjC 中方法的結構
iOS黑魔法-Method Swizzling 玉令天下:Objective-C Method Swizzling
####例項物件結構 id就是一個指向類例項的指標
typedef struct objc_object *id;
struct objc_object {
isa_t _Nonnull isa OBJC_ISA_AVAILABILITY;
};
複製程式碼
arm64 架構中的 isa_t 結構體
#define ISA_MASK 0x0000000ffffffff8ULL
#define ISA_MAGIC_MASK 0x000003f000000001ULL
#define ISA_MAGIC_VALUE 0x000001a000000001ULL
#define RC_ONE (1ULL<<45)
#define RC_HALF (1ULL<<18)
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
struct {
uintptr_t indexed : 1; // 0 表示普通的 isa 指標,1 表示使用優化,儲存引用計數
uintptr_t has_assoc : 1; // 表示該物件是否包含 associated object,如果沒有,則析構時會更快
uintptr_t has_cxx_dtor : 1; // 表示該物件是否有 C++ 或 ARC 的解構函式,如果沒有,則析構時更快
uintptr_t shiftcls : 33; // 類的指標
uintptr_t magic : 6; // 固定值為 0xd2,用於在除錯時分辨物件是否未完成初始化。
uintptr_t weakly_referenced : 1; // 表示該物件是否有過 weak 物件,如果沒有,則析構時更快
uintptr_t deallocating : 1; // 表示該物件是否正在析構
uintptr_t has_sidetable_rc : 1; // 表示該物件的引用計數值是否過大無法儲存在 isa 指標
uintptr_t extra_rc : 19; // 儲存引用計數值減一後的結果
};
};
複製程式碼
####類結構 類其實也是物件,叫做類物件
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();
}
........
}
複製程式碼

#####class_rw_t 執行期拷貝class_ro_t中的部分資訊存入此結構體中,並存放執行期新增的資訊
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
Class firstSubclass;
Class nextSiblingClass;
};
複製程式碼
#####class_ro_t 記錄編譯期就已經確定的資訊
struct class_ro_t {
const char * name; // 類名
uint32_t reserved; // 預留欄位
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
property_list_t *baseProperties;
const ivar_list_t * ivars;
const uint8_t * ivarLayout;
const uint8_t * weakIvarLayout;
};
複製程式碼
baseMethodList、baseProtocols、baseProperties,ivars編譯器確定好的方法、協議、屬性、成員變數
######flags:各種資訊合集

######instanceStart、instanceSize 1、instanceStart之所以等於8,是因為每個物件的isa佔用了前8個位元組。 2、instanceSize = isa + 3個ivar,$6的size只有1,但是為了對齊,也佔用了8 繼承體系就在父類上面加
// ZNObjectFather,三個成員變數
instanceStart = 8
instanceSize = 32 (instanceStart + 3個ivar)
// ZNObjectSon,也有三個成員變數
instanceStart = 32
instanceSize = 56 (instanceStart + 3個ivar)
複製程式碼
size_t objSize = class_getInstanceSize([ZNObjectFather class]);// 32
size_t objSize2 = class_getInstanceSize([ZNObjectSon class]);// 56
複製程式碼
######ivarLayout和 weakIvarLayout:成員變數的strong與weak資訊 1、ivarLayout = "\x01",表示在先有0個弱屬性,接著有1個連續的強屬性。若之後沒有強屬性了,則忽略後面的弱屬性 2、weakIvarLayout = "\x11",表示先有1個強屬性,然後才有1個連續的弱屬性,若之後沒有弱屬性了,則忽略後面的強屬性
const uint8_t *
class_getIvarLayout(Class cls)
{
if (cls) return cls->data()->ro->ivarLayout;
else return nil;
}
const uint8_t *
class_getWeakIvarLayout(Class cls)
{
if (cls) return cls->data()->ro->weakIvarLayout;
else return nil;
}
複製程式碼
@interface BBObject : NSObject
{
NSString *name1;
__weak NSString *name2;
NSString *name3;
NSString *name4;
NSString *name5;
NSString *name6;
__weak NSString *name7;
NSString *name8;
__weak NSString *name9;
}
const uint8_t *ivarLayout = class_getIvarLayout([BBObject class]);
const uint8_t *weakIvarLayout = class_getWeakIvarLayout([BBObject class]);
// ivarLayout=\x01\x14\x11
// weakIvarLayout =\x11\x31\x11
複製程式碼
ivarLayout=\x01\x14\x11


######例項物件的isa的isa...是什麼?各層isa是什麼? 舉例子: BBObj *obj = [BBObj new];
1、obj->isa是一個objc_class結構物件,存放在普通成員變數、動態方法(“-”開頭的方法)、isa(metaclass)、super_class
2、obj->isa->isa也是一個objc_class結構物件,叫做元類metaclass,存放著static型別的成員變數與static型別的方法(“+”開頭的方法)
3、obj->isa->super_class也是一個objc_class結構物件:父類例項物件
4、obj->isa->isa->isa(metaclass->isa)是NSObject類物件
5、 obj->isa->isa->super_class(metaclass->super_class)是父類metaclass物件
Student *stu = [[Student alloc]init];
NSLog(@"Student's class is %@", [stu class]);
NSLog(@"Student's meta class is %@", object_getClass([stu class]));
NSLog(@"Student's meta class's superclass is %@", object_getClass(object_getClass([stu class])));
Class currentClass = [Student class];
for (int i = 1; i < 5; i++)
{
NSLog(@"Following the isa pointer %d times gives %p %@", i, currentClass,currentClass);
currentClass = object_getClass(currentClass);
}
NSLog(@"NSObject's class is %p", [NSObject class]);
NSLog(@"NSObject's meta class is %p", object_getClass([NSObject class]));
複製程式碼
輸出如下:
Student's class is Student
Student's meta class is Student
Student's meta class's superclass is NSObject
Following the isa pointer 1 times gives 0x100004d90 Student
Following the isa pointer 2 times gives 0x100004d68 Student
Following the isa pointer 3 times gives 0x7fffba0b20f0 NSObject
Following the isa pointer 4 times gives 0x7fffba0b20f0 NSObject
NSObject's class is 0x7fffba0b2140
NSObject's meta class is 0x7fffba0b20f0
複製程式碼

#####objc_ivar_list *ivars是什麼? objc_ivar_list其實就是一個連結串列,儲存多個objc_ivar
struct objc_ivar_list {
int ivar_count;
#ifdef __LP64__
int space;
#endif
/* variable length structure */
struct objc_ivar ivar_list[1];
}
複製程式碼
objc_ivar是什麼?
objc_ivar結構體儲存類的單個成員變數資訊
typedef struct objc_ivar *Ivar;
struct objc_ivar {
char *ivar_name; // 變數名
char *ivar_type; // 變數型別
int ivar_offset; // 基地址偏移位元組
#ifdef __LP64__
int space; // 佔用空間
#endif
}
複製程式碼
######使用物件成員變數流程 呼叫 +alloc 方法來初始化一個物件時,也僅僅在記憶體中生成了一個objc_object結構體,並根據其instanceSize來分配空間,將其isa指標指向所屬的類。 類的成員變數ivar_t儲存在class_ro_t中的ivar_list_t * ivars中, 其中offset 是成員變數相對於物件記憶體地址的偏移量,正是通過它來完成變數定址。 當我們使用物件的成員變數時,如 myObject.var ,編譯器會將其轉化為object_getInstanceVariable(myObject, 'var', **value) 找到其ivar_t結構體ivar,然後呼叫object_getIvar(myObject, ivar)來獲取成員變數的記憶體地址。其計算公式如下:
id *location = (id *)((char *)obj + ivar_offset); 基於此,雖然多個物件的isa指標指向同一個objc_class,但由於物件的記憶體地址不一樣,所以它們的例項變數儲存位置也不一樣,從而實現物件與類之間的多對一關係。
#####objc_method_list是什麼? objc_method_list是一個連結串列,儲存多個objc_method,而objc_method結構體儲存類的某個方法的資訊
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}
// 表示類中的某個方法
typedef struct objc_method *Method;
struct objc_method {
SEL method_name; // 方法名
char *method_types; // 方法型別
IMP method_imp; // 方法實現
}
複製程式碼
######呼叫物件方法: 當obj_object接收到訊息後,通過其isa指標找到對應的objc_class,objc_class又通過其data() 方法,查詢class_rw_t的methods列表。
SEL是什麼?
SEL是selector在Objective-C中的表示型別
typedef struct objc_selector *SEL;
struct objc_selector {
char *name; // 名稱
char *types; // 型別
};
複製程式碼
#####IMP是什麼? IMP本質上就是一個函式指標,指向方法的實現(方法的程式碼)
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id (*IMP)(id, SEL, ...);
#endif
複製程式碼
#####Cache是什麼? Cache其實就是一個儲存Method的連結串列,主要是為了優化方法呼叫的效能。
typedef struct objc_cache *Cache OBJC2_UNAVAILABLE;
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
/*
當物件receiver呼叫方法message時,
1、先在Cache查詢IMP,找到返回IMP
2、如果沒有找到,再在類的methodLists中查詢,
3、如果沒有找到,就在super_class父類重複(1、2),
4、如果找到把方法加入receiver類中的Cache、返回IMP
複製程式碼
類的相關操作函式有如下: 1、iOS Class結構分析 2、Objective-C Runtime 執行時之一:類與物件 ####三、Objective-C的訊息傳遞 #####1、基本訊息傳遞 物件呼叫方法叫做傳送訊息。在編譯時,程式的原始碼就會從物件傳送訊息轉換成Runtime的objc_msgSend函式呼叫。
例如某例項變數receiver實現某一個方法oneMethod
[receiver oneMethod];
Runtime會將其轉成類似這樣的程式碼
objc_msgSend(receiver, selector);
複製程式碼
具體會轉換成什麼程式碼呢?Runtime會根據型別自動轉換成下列某一個函式:
1、objc_msgSend:普通的訊息都會通過該函式傳送
2、objc_msgSend_stret:訊息中有資料結構作為返回值(不是簡單值)時,通過此函式傳送和接收返回值
3、objc_msgSendSuper:和objc_msgSend類似,這裡把訊息傳送給父類的例項
4、objc_msgSendSuper_stret:和objc_msgSend_stret類似,這裡把訊息傳送給父類的例項並接收返回值
複製程式碼
#####2、objc_msgSend函式的呼叫過程: 第一步:檢測這個selector是不是要忽略的。
第二步:檢測這個target是不是nil物件。nil物件傳送任何一個訊息都會被忽略掉。
第三步: 1、先在Cache查詢IMP,找到返回IMP 2、如果沒有找到,再在類的methodLists中查詢, 3、如果沒有找到,就在super_class父類重複(1、2), 4、如果找到把方法加入Cache、返回IMP
第四步:前三步都找不到就會進入動態方法解析(看下文)。
#####3、訊息轉發及動態解析方法 當一個物件能接收一個訊息時,會走正常的方法呼叫流程。但如果一個物件無法接收一個訊息時,就會走訊息轉發機制。
訊息轉發機制基本上分為三個步驟:

######(1)動態增加方法階段:方法目的是為了給類利用 class_addMethod 新增方法的機會,如下:
//類方法
+(BOOL)resolveClassMethod:(SEL)sel {
// 寫法與下面resolveInstanceMethod類似
}
//例項方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(XXXX)) {
//const char *types:
//"v@:" 返回值void型別的方法,沒有引數傳入。
//"i@:" 返回值int型別的方法,沒有引數傳入。
//"i@:@" 返回值int型別的方法,又一個引數傳入。
//"s@:@" 返回值string型別的方法,又一個引數傳入。
class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(AAA)), "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
複製程式碼
表示方法的引數和返回值,詳情請參考Type Encodings
######(2)物件轉發階段:詢問是否把訊息給其他接收者處理(單一),返回id是執行者(非self非nil) 如下:
- (id)forwardingTargetForSelector:(SEL)aSelector
{
MessageForwarding *obj=[MessageForwarding new];// 訊息執行者
if ([obj respondsToSelector:aSelector]) {
return obj;
}
return [super forwardingTargetForSelector:aSelector];
}
複製程式碼
######(3)NSInvocation執行階段:
//首先呼叫methodSignatureForSelector:方法來獲取函式的引數和返回值,如果返回為nil,程式會Crash掉,並丟擲unrecognized selector sent to instance異常資訊
// methodSignatureForSelector例項方法;instanceMethodSignatureForSelector類方法
//如果返回一個函式簽名,系統就會建立一個NSInvocation物件並呼叫-forwardInvocation:方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
if (!methodSignature) {
methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
}
return methodSignature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
MessageForwarding *messageForwarding1 = [MessageForwarding new];
if ([messageForwarding1 respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:messageForwarding1];
}
//可以多個物件,區別於第二個,步驟越往後,處理訊息的代價越大,到最後一個階段時,都建立了 NSInvocation 物件了
MessageForwarding *messageForwarding2 = [MessageForwarding new];
if ([messageForwarding2 respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:messageForwarding2];
}
}
複製程式碼
######(4)以上三點都不處理,就doesNotRecognizeSelector 只是程式主動丟擲一個-[類 XXX方法]: unrecognized selector sent to instance不能識別方法的異常

lookUpImpOrForward
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver) {
Class curClass;
IMP imp = nil;
Method meth;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// 檢查是否新增快取鎖,如果沒有進行快取查詢。
// 查到便返回IMP指標
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
// 通過呼叫realizeClass方法,分配可讀寫`class_rw_t`的空間
if (!cls->isRealized()) {
rwlock_writer_t lock(runtimeLock);
realizeClass(cls);
}
// 倘若未進行初始化,則初始化
if (initialize && !cls->isInitialized()) {
_class_initialize (_class_getNonMetaClass(cls, inst));
}
// 保證方法查詢,並進行快取填充(cache-fill)
retry:
runtimeLock.read();
// 是否忽略GC垃圾回收機制(僅用在macOS中)
if (ignoreSelector(sel)) {
imp = _objc_ignored_method;
cache_fill(cls, sel, imp, inst);
goto done;
}
// 當前類的快取列表中進行查詢
imp = cache_getImp(cls, sel);
if (imp) goto done;
// 從類的方法列表中進行查詢
meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
// 從父類中迴圈遍歷
curClass = cls;
while ((curClass = curClass->superclass)) {
// 父類的快取列表中查詢
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// 如果在父類中發現方法,則填充到該類快取列表
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
break;
}
}
// 從父類的方法列表中查詢
meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
// 進入method resolve過程
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
// 呼叫_class_resolveMethod,解析沒有實現的方法
_class_resolveMethod(cls, sel, inst);
// 進行二次嘗試
triedResolver = YES;
goto retry;
}
// 沒有找到方法,啟動訊息轉發
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlockRead();
return imp;
}
複製程式碼
從類的方法列表中進行查詢
static method_t *getMethodNoSuper_nolock(Class cls, SEL sel) {
runtimeLock.assertLocked();
// 遍歷所在類的methods,這裡的methods是List鏈式型別,裡面存放的都是指標
for (auto mlists = cls->data()->methods.beginLists(), end = cls->data()->methods.endLists(); mlists != end; ++mlists) {
method_t *m = search_method_list(*mlists, sel);
if (m) return m;
}
return nil;
}
複製程式碼
呼叫_class_resolveMethod,嘗試類是否現實了resolveInstanceMethod或resolveClassMethod
void _class_resolveMethod(Class cls, SEL sel, id inst) {
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
// 針對於物件方法的操作
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
// 針對於類方法的操作
_class_resolveClassMethod(cls, sel, inst);
// 再次啟動查詢,並且判斷是否擁有快取中訊息標記_objc_msgForward_impcache
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) {
// 說明可能不是 metaclass 的方法實現,當做物件方法嘗試
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
複製程式碼
####四、self與super
- self是類的一個隱藏引數,每個方法的實現的第一個引數即為self。
- super並不是隱藏引數,它實際上只是一個”編譯器標示符”,它負責告訴編譯器,當呼叫方法時,去呼叫父類的方法,而不是本類中的方法。
下面的程式碼分別輸出什麼?
@implementation Son : Father
- (id)init
{
self = [super init];
if (self)
{
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
複製程式碼
結果:輸出兩個Son
解析 當呼叫[self class]方法時,會轉化為objc_msgSend函式。 當呼叫[super class]方法時,會轉化為objc_msgSendSuper。
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Son"))}, sel_registerName("class"));
複製程式碼
簡化
__rw_objc_super objc_super = (__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Son"))}
class_getSuperclass(objc_super,sel_registerName("class"))
複製程式碼
objc_super *super是什麼?
struct objc_super {
__unsafe_unretained id receiver;
__unsafe_unretained Class super_class;
};
複製程式碼
在objc_msgSendSuper方法中,第一個引數是一個objc_super的結構體,這個結構體裡面有兩個變數,一個是接收訊息的receiver,一個是當前類的父類super_class。
objc_msgSendSuper的工作原理應該是這樣的: 從objc_super結構體指向的superClass父類的方法列表開始查詢selector,找到後以objc_super的receiver(就是self)去呼叫父類的這個selector。注意,最後的呼叫者是self,而不是super_class!
####五、Method Swizzling 比較簡單、常用的方式,方案A
void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector) {
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// originalMethod 已經存在 class_addMethod 方法就會失敗
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
// 方法存在就替換掉,如果不存在就直接新增
if (didAddMethod) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
複製程式碼
1、RSSwizzle 被很多人推薦,它用很複雜的方式解決了 What are the Dangers of Method Swizzling in Objective C? 中提到的一系列問題。不過引入它還是有一些成本的,建議在本文列舉的那些極端特殊情況下才使用它,畢竟方案 A 已經能 Cover 到大部分情況了。
2、JRSwizzle 嘗試解決在不同平臺和系統版本上的 Method Swizzling 與類繼承關係的衝突。對各平臺低版本系統相容性較強。