本文屬筆記性質,主要針對本身理解不太透徹的地方進行記錄。ios
推薦系統直接學習小碼哥iOS底層原理班---MJ老師的課確實不錯,強推一波。緩存
isa做爲runtime底層中最經常使用的一個數據結構。bash
在arm64架構以前,isa就是一個普通的指針,存儲着Class、Meta-Class對象的內存地址數據結構
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
複製代碼
從arm64架構開始,對isa進行了優化,變成了一個共用體(union)結構,使用位運算來得到更多的信息,但依舊是8位字符。架構
共用體與結構體相似,但內部全部成員將會共用(首元素的)內存。對整塊內存經過位運算來進行擴展操做。ide
union isa_t
{
Class cls;
uintptr_t bits; //決定了共用體所佔的內存大小。可使用位運算
# if __arm64__ //位運算用掩碼 bits & MASK \\ bits | MSAK
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
// 位域負責可讀性展現.從右向左
struct {
uintptr_t nonpointer : 1; //佔1位
uintptr_t has_assoc : 1;//佔1位
uintptr_t has_cxx_dtor : 1;//佔1位
//因爲這種運算除了中間33位都是0,因此arm64下,全部類對象地址的64位中除了這33位必定都是0
uintptr_t shiftcls : 33; //佔33位 //class對象的地址 bits&ISA_MASK
uintptr_t magic : 6;//佔6位
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
.....
};
複製代碼
存放類信息的可讀寫列表函數
基本介紹能夠查閱OC對象本質中關於class_wr_t
的部分。post
有一些東西不過重要的東西能夠補充:學習
從源碼上看,類在初始化時最開始的data() -> ro_t
,在初始化過程當中纔將data() -> rw_t
而且將ro_t
賦值給rw_t -> ro_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; }
};
};
複製代碼
方法名,基本等同於C語言字符串 char*
typedef struct objc_selector *SEL;
複製代碼
不一樣類中的同名方法,其SEL相同
函數返回值,參數類型
能夠經過@encode(type)函數查看類型對應的編碼
// "i24@0:8i16f20" 數字表明參數/返回值所佔字節
// 0id 8SEL 16int 20float == 24
- (int)test:(int)age height:(float)height;
複製代碼
須要注意的是,在構建方法簽名(NSMethodSignature
)時,即便簡化成i@:if
也是沒有問題的。
Class內部結構中有個方法緩存(cache_t),用散列表(哈希表)來緩存
曾經調用過
的方法,能夠提升方法的查找速度
struct cache_t {
struct bucket_t *_buckets; //散列表
mask_t _mask; //散列表長度-1
mask_t _occupied; //已緩存的方法數量
}
複製代碼
struct bucket_t {
cache_key_t _key; //SEL
IMP _imp; //IMP
}
複製代碼
本類 -> 父類
在其中任一位置查找到IMP,都將會寫入本類(在查找到的那一刻,直接經過cls
寫入)緩存中。(父類自己則不會被緩存)
//LLDB中
p (IMP)0x0000123 能夠打印內存地址所指的方法
複製代碼
OC的消息機制。內部會經歷消息發送、動態方法解析、消息轉發三個階段
OC中的方法調用,其實都是轉換爲objc_msgSend函數的調用
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *person = [[MJPerson alloc] init];
[person personTest];
// 在編譯是將會轉化成
objc_msgSend(person, @selector(personTest));
}
return 0;
}
複製代碼
objc_msgSend底層,由彙編實現
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
MESSENGER_START
//x0:寄存器 消息接收者 receiver
cmp x0, #0 // nil check and tagged pointer check
//一旦消息接收者爲0 跳轉(b.le)到 LNilOrTagged
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:
//返回0
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
END_ENTRY _objc_msgSend
複製代碼
objc_msgSend的消息機制,(在查找方法時)能夠分爲三個階段:
消息發送、動態方法解析、消息轉發。最後崩潰unrecognized selector sent to instance
在本類以及父類中查找方法
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
....
....
....
retry:
runtimeLock.assertReading();
// 查找本類緩存
imp = cache_getImp(cls, sel);
if (imp) goto done;
// 查找本類方法列表
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
// 去查找父類
{
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; } } } .... .... .... } 複製代碼
幾個源碼方面的點
在未查找到緩存後進入正常代碼lookUpImpOrForward
進行方法查找以及後續階段操做。
第一次是在彙編中直接查找緩存,第二次是在lookUpImpOrForward
方法中查找本類re_t以前再查找一次。
已經排序的,二分查找。沒有排序的,遍歷查找
只要查詢到方法存在,就會寫入調用者類緩存。
全部父類查找完畢仍未找到方法。進入動態方法解析。
若是未查找到指定方法,runtime容許開發者進行一次方法動態添加。
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
...
...
...
// 進行一次動態解析`triedResolver`
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
//內部會根據cls是不是元類調用不一樣的邏輯
_class_resolveMethod(cls, sel, inst);
runtimeLock.read();
// 不會進行緩存
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
//從新走一遍方法查找,但若是尚未也不會再次被動態解析了。
goto retry;
}
// No implementation found, and method resolver didn't help. // Use forwarding. //消息轉發 .... .... done: runtimeLock.unlockRead(); return imp; } 複製代碼
resolveInstanceMethod
的返回值其實沒什麼用runtime裏只作了打印返回值的工做而已
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time. // +resolveInstanceMethod adds to self a.k.a. cls IMP imp = lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/); if (resolved && PrintResolving) { if (imp) { _objc_inform("RESOLVE: method %c[%s %s] " "dynamically resolved to %p", cls->isMetaClass() ? '+' : '-', cls->nameForLogging(), sel_getName(sel), imp); } else { // Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
複製代碼
若是經歷動態解析依舊沒有肯定方法。runtime容許開發者從新指定target,甚至修改方法調用的各類參數再次調用。
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
....
....
....
//查找失敗,動態解析失敗
//進行消息轉發.內部由彙編實現
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlockRead();
return imp;
}
複製代碼
若是forwardingTargetForSelector
指定了一個新的target
,會對其調用objc_msgSend,從新走一遍以前的邏輯。
有一種簡便的調用方式,不須要本身編寫type
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector == @selector(test:)) {
// return [NSMethodSignature signatureWithObjCTypes:"v20@0:8i16"];
// return [NSMethodSignature signatureWithObjCTypes:"i@:i"];
return [[[MJCat alloc] init] methodSignatureForSelector:aSelector];
}
return [super methodSignatureForSelector:aSelector];
}
複製代碼
若是一個方法調用能夠進入forwardingTargetForSelector
,那麼你能夠對他進行任何操做。即便不invoke
也不會出現崩潰。
int main(int argc, const char * argv[]) {
@autoreleasepool {
[MJPerson test];
}
return 0;
}
//由於是objc_msgSend是對MJPerson對象發送的消息,因此只有+方法才能被調用
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector == @selector(test)) return [NSMethodSignature signatureWithObjCTypes:"v@:"];
return [super methodSignatureForSelector:aSelector];
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation
{
NSLog(@"1123");
}
複製代碼
只要將target對象修改爲實例對象便可
objc_msgSend並不區分類方法與對象方法,兩者只有消息接受者的區別而已。
+ (id)forwardingTargetForSelector:(SEL)aSelector
{
if (aSelector == @selector(test)) return [[MJCat alloc] init];
return [super forwardingTargetForSelector:aSelector];
}
複製代碼
super
@implementation MJStudent
- (void)run {
[super run];
}
/cpp
struct objc_super {
__unsafe_unretained _Nonnull id receiver; // 消息接收者
__unsafe_unretained _Nonnull Class super_class; // 消息接收者的父類
};
static void _I_MJStudent_run(MJStudent * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MJStudent"))}, sel_registerName("run"));
//化簡一下
objc_msgSendSuper((__rw_objc_super){
(id)self,
(id)class_getSuperclass(objc_getClass("MJStudent"))},
sel_registerName("run"));
//繼續化簡
struct objc_super arg = {self, [MJPerson class]};
objc_msgSendSuper(arg, @selector(run));
}
複製代碼
可見在進行super調用時,使用的是objc_msgSendSuper
方法。在objc_super
結構體內部的receiver
依舊是self,只是在查找IMP時,從父類開始查找IMP實現而已。
對象方法是比對
[self class]
,類方法是比對object_getClass(self)
因此在對類對象使用isMemberOfClass
或者isKindOfClass
時,須要對元類進行對比。 不過對於元類NSObject,其父類是NSObject因此會有點不一樣結果。
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass((id)self) == cls;
}
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
@end
複製代碼
結果以下:
// 這句代碼的方法調用者不論是哪一個類(只要是NSObject體系下的),都返回YES
NSLog(@"%d", [NSObject isKindOfClass:[NSObject class]]); // 1
NSLog(@"%d", [NSObject isMemberOfClass:[NSObject class]]); // 0
NSLog(@"%d", [MJPerson isKindOfClass:[MJPerson class]]); // 0
NSLog(@"%d", [MJPerson isMemberOfClass:[MJPerson class]]); // 0
複製代碼
交換兩個方法的IMP,而且清空cache
void method_exchangeImplementations(Method m1, Method m2)
{
if (!m1 || !m2) return;
rwlock_writer_t lock(runtimeLock);
IMP m1_imp = m1->imp;
m1->imp = m2->imp;
m2->imp = m1_imp;
// RR/AWZ updates are slow because class is unknown
// Cache updates are slow because class is unknown
// fixme build list of classes whose Methods are known externally?
flushCaches(nil);//清空類對象的方法緩存
updateCustomRR_AWZ(nil, m1);
updateCustomRR_AWZ(nil, m2);
}
複製代碼
如今的LLVM編譯器在將代碼轉成彙編以前,再也不由CPP進行過分。而改成經過一種LLVM編譯器專屬的語言。
可使用如下命令行指令生成中間代碼
// 不須要指定架構
clang -emit-llvm -S main.m
複製代碼
以後會生成一個.ll文件。
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
[super forwardInvocation:anInvocation];
int a = 10;
int b = 20;
int c = a + b;
test(c);
}
define internal void @"\01-[MJPerson forwardInvocation:]"(%0*, i8*, %1*) #1 {
%4 = alloca %0*, align 8
%5 = alloca i8*, align 8
%6 = alloca %1*, align 8
%7 = alloca %struct._objc_super, align 8
%8 = alloca i32, align 4
%9 = alloca i32, align 4
%10 = alloca i32, align 4
store %0* %0, %0** %4, align 8
store i8* %1, i8** %5, align 8
store %1* %2, %1** %6, align 8
%11 = load %0*, %0** %4, align 8
%12 = load %1*, %1** %6, align 8
%13 = bitcast %0* %11 to i8*
%14 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %7, i32 0, i32 0
store i8* %13, i8** %14, align 8
%15 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_SUP_REFS_$_", align 8
%16 = bitcast %struct._class_t* %15 to i8*
%17 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %7, i32 0, i32 1
store i8* %16, i8** %17, align 8
%18 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !8
call void bitcast (i8* (%struct._objc_super*, i8*, ...)* @objc_msgSendSuper2 to void (%struct._objc_super*, i8*, %1*)*)(%struct._objc_super* %7, i8* %18, %1* %12)
store i32 10, i32* %8, align 4
store i32 20, i32* %9, align 4
%19 = load i32, i32* %8, align 4
%20 = load i32, i32* %9, align 4
%21 = add nsw i32 %19, %20
store i32 %21, i32* %10, align 4
%22 = load i32, i32* %10, align 4
call void @test(i32 %22)
ret void
}
複製代碼
一些語法
@ - 全局變量
% - 局部變量
alloca - 在當前執行的函數的堆棧幀中分配內存,當該函數返回到其調用者時,將自動釋放內存
i32 - 32位4字節的整數
align - 對齊
load - 讀出,store 寫入
icmp - 兩個整數值比較,返回布爾值
br - 選擇分支,根據條件來轉向label,不根據條件跳轉的話相似 goto
label - 代碼標籤
call - 調用函數
複製代碼
MJPerson *person = [[MJPerson alloc] init];
object_setIvar(person, nameIvar, @"123");
//先將10,轉化爲指針類變量指向10這個值。再將指針轉化成id
object_setIvar(person, ageIvar, (__bridge id)(void *)10);
NSLog(@"%@ %d", person.name, person.age);
複製代碼