OC是一種動態性比較強的語言,所有的函數調用都是基於消息機制;簡介參照:
文章目錄
1. isa指針
1.1 簡述
***注意:以下的分析都是基於arm64***
isa
在前面介紹過,實例對象可以通過 isa
找到類對象,類對象通過isa可以找到原類對象;在arm64後isa並不直接是Class類型,而是union
,同時用位域位域(w3c)來存儲更多信息(struct test{uintptr_r nonpointer :1; }
),
1.2 在看isa之前先熟悉兩個知識點位域
和共用體union
char _bool;//將所有BOOL值存儲到一個字節中
-(void)setTall:(BOOL)tall{
if(tall){
_bool |= (1<<1);
}else{
_bool &= ~(1<<1);
}
}
-(void)setRich:(BOOL)rich{
if(rich){
_bool |= (1<<0);
}else{
_bool &= ~(1<<0);
}
}
- (BOOL)isRich{
return !!(_bool&(1<<0));//最高位存儲rich
}
- (BOOL)isTall{
return !!(_bool&(1<<1));//第二位存儲Tall
}
接下來看下位域中怎麼實現:
@interface LYMPerson()
{
// 位域
struct {
char tall : 1;
char rich : 1;
} _bool;
}
@end
@implementation LYMPerson
- (void)setTall:(BOOL)tall
{
_bool.tall = tall;
}
- (BOOL)isTall
{
return !!_bool.tall;
}
- (void)setRich:(BOOL)rich
{
_bool.rich = rich;
}
- (BOOL)isRich
{
return !!_bool.rich;
}
@end
//簡化的源碼
union isa_t //共用體
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;//存放所有的數據
struct {//位域
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
};
};
1.3 isa
結構體的成員的含義:
- 結構體基本成員介紹
nonpointer |
has_assoc |
---|---|
0 代表普通指針,存儲着Class、Meta-Class對象的內存地址;1 代表優化過,使用位域存儲更多的信息 | 是否設置過關聯對象,如果沒有,釋放時更快 |
has_cxx_dtor |
shiftcls |
是否有c++的析構函數(cxx_destruct),如果沒有,釋放時會更快 | 存儲着Class,Meta-Class對象的內存地址信息 |
magic |
weakly_referenced |
用於在調試時分辨對象是否未完成初始化 | 是否有被弱引用指向如果沒有,釋放速度會更快 |
deallocating |
has_sidetable_rc |
對象是否正在釋放 | 裏面存儲的值的引用計數器減1 |
extra_rc |
|
引用計數器是否過大無法存儲在isa中,如果爲1那麼引用計數會存儲在一個叫SideTable的類屬性中 |
- 下面來驗證一下
加上weak和關聯屬性後的值
#import "ViewController.h"
#import <objc/message.h>
#import <objc/runtime.h>
@interface LYMAnimal:NSObject
@property(nonatomic,assign,readwrite) NSInteger age;
@end
@implementation LYMAnimal
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
LYMAnimal *animal = [[LYMAnimal alloc]init];
__weak LYMAnimal *weakSelf = animal;
objc_setAssociatedObject(animal, @"name", @"haha", OBJC_ASSOCIATION_COPY);
id va = objc_getAssociatedObject(animal, @"name");
NSLog(@"%@ %@",animal,va);
}
@end
通過對比兩張圖可以看出,加了weak和關聯對象後相應的位的值都有改變,weakly_refrenced
的變成了1,has_assoc
對應的位變成了1,這與表格的描述剛好符合;
1.4 isa擴展
- 在之前
實例對象/類對象
通過isa找到類對象/原類對象
前都必須的&ISA_MASK
;而在源碼中的定義爲ISA_MASK 0x0000000ffffffff8ULL
,在聯合體中shiftcls
存儲真實地址只佔33位,因此&ISA_MASK
剛好可以取出類對象或者原類對象的真實地址;同時也可以知道類對象或者原類對象的最後三位爲零,通過一段代碼驗證一下:
- 從上圖的輸出可知最後一位全是0和8,而16進制的0和8 對應的二進制是 0000和1000,那麼可以驗證
實例對象和類對象、原類對象的內存地址最後三位都是0
。
Class
- 照例看下圖:
- 下面詳細介紹下這個結構體的成員:
class_rw_t
:可讀可寫,裏面的methods和propeties及protocols都是二位數組,而ro指向的結構體class_ro_t
(只讀)裏面的baseMethodList
…是一維的,其中baseMethodList
中是method_t
類型;
method_t
:結構體包含 IMP imp
,指向函數地址的指針;SEL name
函數名;const chat *type;
返回值類型,參數類型;
-
IMP代表函數的具體實現,
typedef id _Nullable (*IMP)(id __Nonnull,SEL _nonnull ,...)
; -
SEL代表方法/函數名,也叫做選擇器,底層結構和char類似,可以通過
@selector()、sel_registerName()
獲得,不同類中相同名字的方法,所對應的方法選擇器是相同的,它的底層是:
typedef struct objc_selector *SEL
-
types 包含了函數的返回值,參數的編碼信息;
v@:
:v
代表返回值是void,@
代表參數id類型,:
代表參數sel,@encode()
指令,可以將具體的類型表示成字符串編碼,蘋果在其官方文檔中也有說明;這裏列舉部分如下圖:
3. 方法緩存(cache_t
)
- 在之前研究
objc_class
結構體的時候,我們知道在Class
結構體內部有一個方法緩存結構體(cache_t cache
),使用散列表
來緩存使用過的方法來提高方法的查找速度;
- 散列表:是一個長度不固定的列表,當長度不夠存儲的時候會重新創建後將原來的釋放掉,主要是通過一定的算法將數據存入內存列表的指定索引位置,查找的時候根據同一算法算出的索引直接去列表中取出,當只有一個數據需要存儲的時候,算出的索引在什麼位置就存儲在什麼索引位置,該索引以外的其他索引存儲爲
NULL
;因此可以說散列表犧牲了部分內存換取查找速度;oc中如果算出的索引相同繼續比較key,如果key不相同則將值減1,依次查找直到找到; - 順序:實例對象isa–> 類對象–>類對象的cache中查找,沒有–>methodList中找,找到返回,同時加入cache中
如果methodList找沒有找到–>superclass找到父類–>cache中查找–>找到返回,同時緩存到類對象的cache中
如果在父類的cache找沒有找到–>methodList中查找–>找到返回,同時緩存到類對象的cache中; 即:如下圖
2. objc_msgSend(id,SEL);
OC中的方法調用
2.1 簡述
OC中的方法調用都是基於消息發送即:objc_msgSend(id,SEL);
,看一個簡單的示例:
LYMAnimal *animal = [[LYMAnimal alloc]init];
[animal callEat];
在執行xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-12.0.0 main.m
的c++文件裏最後裏(約32000多行)如下:
#pragma clang assume_nonnull end
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_v7_xv8f6lp50hbcxftgjf7yb_sw0000gn_T_main_773e9c_mi_0);
LYMAnimal *animal = ((LYMAnimal *(*)(id, SEL))(void *)objc_msgSend)((id)((LYMAnimal *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LYMAnimal"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)animal, sel_registerName("callEat"));
}
return 0;
}
名詞: receiver
從上述代碼可以看到在main函數裏[animal callEat];
最終轉換成了objc_msgSend
;objc_msgSend在蘋果開源的運行時代碼裏是以彙編實現的,因爲這部分代碼肯定是調用頻率非常的高;
2.2 執行階段:消息發送
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
MESSENGER_START
//x0寄存器:消息接收者,判斷消息接受者是不是(<0)空,是則跳轉到LNilOrTagged 然後LReturnZero最後返回(ret)
cmp x0, #0 // nil check and tagged pointer check
b.le LNilOrTagged // (MSB tagged pointer looks negative) //進行判斷如果上述成立就跳轉
ldr x13, [x0] // x13 = isa
and x16, x13, #ISA_MASK // x16 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached //這裏開始查找緩存
LNilOrTagged:
b.eq LReturnZero // nil check
// tagged
mov x10, #0xf000000000000000
cmp x0, x10
b.hs LExtTag
adrp x10, _objc_debug_taggedpointer_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
ubfx x11, x0, #60, #4
ldr x16, [x10, x11, LSL #3]
b LGetIsaDone
LExtTag:
// ext tagged
adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
ubfx x11, x0, #52, #8
ldr x16, [x10, x11, LSL #3]
b LGetIsaDone
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
MESSENGER_END_NIL
ret //相當於return
END_ENTRY _objc_msgSend
首先會判斷消息接受者(receiver)是不是空的,空的直接返回;如果不是空則在緩存中查找CacheLookup
:
.macro CacheLookup
// x1 = SEL, x16 = isa
ldp x10, x11, [x16, #CACHE] // x10 = buckets, x11 = occupied|mask
and w12, w1, w11 // x12 = _cmd & mask
add x12, x10, x12, LSL #4 // x12 = buckets + ((_cmd & mask)<<4)
ldp x9, x17, [x12] // {x9, x17} = *bucket
1: cmp x9, x1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: x12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp x12, x10 // wrap if bucket == buckets
b.eq 3f
ldp x9, x17, [x12, #-16]! // {x9, x17} = *--bucket
b 1b // loop
3: // wrap: x12 = first bucket, w11 = mask
add x12, x12, w11, UXTW #4 // x12 = buckets+(mask<<4)
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
ldp x9, x17, [x12] // {x9, x17} = *bucket
1: cmp x9, x1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp 查找到返回IMP
2: // not hit: x12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp x12, x10 // wrap if bucket == buckets
b.eq 3f
ldp x9, x17, [x12, #-16]! // {x9, x17} = *--bucket
b 1b // loop
3: // double wrap
JumpMiss $0//緩存中沒有找到跳轉
.endmacro
在上述彙編中進行查找,沒有找到跳轉到JumpMiss
.macro JumpMiss
.if $0 == GETIMP
b LGetImpMiss
.elseif $0 == NORMAL
b __objc_msgSend_uncached//這個是一個C語言的函數__class_lookupMethodAndLoadCache3
.elseif $0 == LOOKUP
b __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
接着便是在這個函數中執行:
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)//彙編代碼比c代碼多一個"_"
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
在lookUpImpOrForward
中會重新查找一次緩存,因爲這裏有可能動態添加方法;這個方法也是最核心的方法:
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// Optimistic cache lookup
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
// runtimeLock is held during isRealized and isInitialized checking
// to prevent races against concurrent realization.
// runtimeLock is held during method search to make
// method-lookup + cache-fill atomic with respect to method addition.
// Otherwise, a category could be added but ignored indefinitely because
// the cache was re-filled with the old value after the cache flush on
// behalf of the category.
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();
}
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();
// Try this class's cache.
imp = cache_getImp(cls, sel);//會再次去緩存中查找
if (imp) goto done;
// Try this class's method lists. 去方法列表中查找
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
// Try superclass caches and method lists. 去父類的緩存和方法列表中查找
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// Found the method in a superclass. Cache it in this class.
log_and_fill_cache(cls, imp, sel, inst, curClass);//父類中找到後緩存到當前類中
goto done;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
}
// Superclass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
// No implementation found. Try method resolver once.
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;
}
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlockRead();
return imp;
}
通過上述代碼可知:方法調用時候,底層轉換成消息發送(obj_msgSend),首先如果有緩存則在緩存中查找,緩存中沒有則在方法列表中查找,在方法列表中進行查找的時候會執行一次緩存查找避免該過程中動態加入的方法導致的問題;如果方法列表找那個也沒有則去父類的緩存和方法列表中查找;
2.3 執行階段:動態方法解析 (dynamic method resolution)
在其父類中還找不到方法且父類沒有父類,那麼就會進入動態方法解析,這裏會調用+(BOOL)resolveInstanceMethod:(SEL)sel
,這裏可以動態添一次方法,然後會重新開始一次方法查找:
2.3.1 實例方法動態添加
在動態添加之前會判斷是不是已經動態添加過,如果添加過就不會添加;就是直接到消息轉發階段
void callEat(id self,SEL _cmd){
NSLog(@"callEat 動態添加");
}
@implementation LYMAnimal
//-(void)callEat{
// NSLog(@"eat======");
//}
struct method_t {
SEL sel;
char *types;
IMP imp;
};
//-(void)otherCall{
// NSLog(@"%s",__func__);
//}
//+(BOOL)resolveInstanceMethod:(SEL)sel{
// if (sel == @selector(callEat)) {
// struct method_t *meth = (struct method_t *)class_getInstanceMethod(self, @selector(otherCall));
// //動態添加方法
// class_addMethod(self, sel, meth->imp, meth->types);
// return YES;
// }
// return [super resolveInstanceMethod:sel];
//}
+(BOOL)resolveInstanceMethod:(SEL)sel{//對象方法
if (sel == @selector(callEat)) {
//動態添加方法
class_addMethod(self, sel, (IMP)callEat, "v16@0:8");
return YES;
}
return [super resolveInstanceMethod:sel];
}
@end
2.3.2 類方法動態添加:
+(BOOL)resolveClassMethod:(SEL)sel{
if (sel == @selector(callEat1)) {
//動態添加方法
class_addMethod(object_getClass(self), sel, (IMP)callEat, "v16@0:8");
return YES;
}
return [super resolveInstanceMethod:sel];
}
如果這一步沒有處理,那麼就會到消息轉發;
2.4 執行階段:消息轉發
消息轉發有三個階段,各個階段關係如圖(圖來自:<<Effective-Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法>>):
-(id)forwardingTargetForSelector:(SEL)aSelector
先看一個經典錯誤:
消息轉發部分源碼是沒有開源,但是從上圖可以知道:
- 先調用
__forwarding__
然後在該方法裏會調用forwardingTargetForSelector
,該方法裏可以返回能處理該消息的對象 - 如果上面的方法返回nil,則會調用methodSignatureForSelector
補充一個知識點:NSInvocation
封裝了一個方法調用,包括方法調用者,方法名,方法參數,更改其target屬性可以修改其方法調用者;
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if (aSelector == @selector(callEat)) {
return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];//這裏不是空的就會調用forwardInvocation
}
return [super methodSignatureForSelector:aSelector];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation{
}
注意:類對象消息轉發對應的方法:
//+(id)forwardingTargetForSelector:(SEL)aSelector
+(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if (aSelector == @selector(callEat1)) {
return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
}
return [super methodSignatureForSelector:aSelector];
}
//NSInvocation 封裝了一個方法調用,包括方法調用者,方法名,參數,返回值
+(void)forwardInvocation:(NSInvocation *)anInvocation{
}
對應輸出:
2.4 最後
應用來自《Effective-Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法》的一段總結:
接收者在每一步中均有機會處理消息。步驟越往後,處理消息的代價就越大。最好能在第一步就處理完,這樣的話,運行期系統就可以將此方法緩存起來了。如果這個類的實例稍後還收到同名選擇子,那麼根本無須啓動消息轉發流程。若想在第三步裏把消息轉給備援的接收者,那還不如把轉發操作提前到第二步。因爲第三步只是修改了調用目標,這項改動放在第二步執行會更爲簡單,不然的話,還得創建並處理完整的NSInvocation。
3. super關鍵字
3.1 一個網上經典的面試題:
NSLog(@"self class = %@",[self class]);
NSLog(@"self superclass = %@",[self superclass]);
NSLog(@"super class = %@",[super class]);
NSLog(@"super superclass =%@",[super superclass]);
輸出結果是:
super是一個編譯器標示,告訴方法調用時候,從父類的方法中去找方法的實現,但是消息的接收者還是自己(子類對象);
其詳細的底層實現解釋請參考:iOS-底層原理17
3.2 底層分析
先看一下轉換成c++文件中對應的結構:
struct __rw_objc_super {
struct objc_object *object;
struct objc_object *superClass;
__rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {}
};
// @implementation LYMWorker
static void _I_LYMWorker_eat(LYMWorker * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("LYMWorker"))}, sel_registerName("eat"));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_v7_xv8f6lp50hbcxftgjf7yb_sw0000gn_T_LYMWorker_b54bca_mi_0,__func__);
}
// @end
簡化一下函數裏的方法調用就是:objc_msgSendSuper(rw_objc_super,@selector(eat))
;結構體objc_super在objc源碼裏定義爲:
struct objc_super{//這裏是簡化的寫法
id receiver;
Class super_class;
}
objc_msgSendSuper參數的註釋裏解釋,super_class
只是告訴查找方法實現
的時候是直接從父類的類對象開始查找;方法的接收者還是LYMWorker
的實例;這裏有總結兩點:
- 上述方法中調用的class方法在NSObject中實現,不論從哪個開始查找最終的實現都是在NSObject中的
- class只是告訴類型是什麼,類型至於方法的調用者有關(也就是
消息接收者
);因此[super class]
只是告訴編譯器從父類開始查找,所以最終返回的類型還是LYMWorker
;