目錄:
- isa存儲(chǔ)信息分析
- Class的內(nèi)部結(jié)構(gòu)、method_t、cache
- objc_msgSend底層調(diào)用流程
- super
- Runtime-API
- Runloop
一. Runtime
1. isa存儲(chǔ)信息分析
isa指針
isa指針,在arm64架構(gòu)之前,isa就是一個(gè)普通的指針,的確存儲(chǔ)著類對(duì)象、元類對(duì)象的內(nèi)存地址(實(shí)例對(duì)象的isa&ISA_MASK得到類對(duì)象的地址值,類對(duì)象的isa&ISA_MASK得到元類對(duì)象的地址值),從arm64架構(gòu)開始,對(duì)isa進(jìn)行了優(yōu)化,變成了一個(gè)共用體(union)結(jié)構(gòu),還使用位域來存儲(chǔ)更多的信息。為什么使用共用體?
union isa_t
{
Class cls;
uintptr_t bits;
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
//0,代表普通的指針,存儲(chǔ)著Class、Meta-Class對(duì)象的內(nèi)存地址
//1,代表優(yōu)化過,使用位域存儲(chǔ)更多的信息
uintptr_t nonpointer : 1;
//是否有設(shè)置過關(guān)聯(lián)對(duì)象,如果沒有,釋放時(shí)會(huì)更快
uintptr_t has_assoc : 1;
//是否有C++的析構(gòu)函數(shù)(.cxx_destruct),如果沒有,釋放時(shí)會(huì)更快
uintptr_t has_cxx_dtor : 1;
//存儲(chǔ)著Class、Meta-Class對(duì)象的內(nèi)存地址信息
uintptr_t shiftcls : 33;
//用于在調(diào)試時(shí)分辨對(duì)象是否未完成初始化
uintptr_t magic : 6;
//是否有被弱引用指向過,如果沒有,釋放時(shí)會(huì)更快
uintptr_t weakly_referenced : 1;
//對(duì)象是否正在釋放
uintptr_t deallocating : 1;
//引用計(jì)數(shù)是否過大無(wú)法存儲(chǔ)在isa中
//如果為1,那么引用計(jì)數(shù)會(huì)存儲(chǔ)在一個(gè)叫SideTable的結(jié)構(gòu)體的refcnts成員中,refcnts是個(gè)散列表
uintptr_t has_sidetable_rc : 1;
//里面存儲(chǔ)的值是引用計(jì)數(shù)減1
uintptr_t extra_rc : 19;
};
};
通過共用體將bits和結(jié)構(gòu)體結(jié)合起來,而且自始至終一直都在操作bits,沒有動(dòng)結(jié)構(gòu)體,結(jié)構(gòu)體僅僅是為了可讀性,所以不會(huì)影響bits里面的值,刪除這個(gè)結(jié)構(gòu)體也不影響。這種方式就是巧妙的利用共用體,達(dá)到了代碼可讀性的目的。
2. Class的內(nèi)部結(jié)構(gòu)、method_t、cache
- 關(guān)于method_t
method_t是對(duì)方法\函數(shù)的封裝,源碼如下:
struct method_t {
SEL name; //函數(shù)名(選擇器)
const char *types; //返回值類型、參數(shù)類型的編碼
IMP imp; //指向函數(shù)的指針(函數(shù)地址)
};
一個(gè)method_t就需要上面三個(gè)東西就夠了,一個(gè)method_t就是一個(gè)方法。
關(guān)于SEL name;
① SEL代表方法\函數(shù)名,一般叫做選擇器,底層結(jié)構(gòu)跟char *類似
② 可以通過@selector()和sel_registerName()獲得
③ 可以通過NSStringFromSelector()和sel_getName()轉(zhuǎn)成字符串
④ 不同類中相同名字的方法,所對(duì)應(yīng)的方法選擇器是相同的const char *types;
types包含了函數(shù)返回值類型、參數(shù)類型的編碼。IMP imp;
IMP代表函數(shù)的具體實(shí)現(xiàn),就是指向函數(shù)的地址
- 查找方法的過程
這里用對(duì)象方法解釋,因?yàn)閷?duì)象方法和類方法其實(shí)是一樣的,就是放的位置不一樣。
① 當(dāng)某個(gè)對(duì)象調(diào)用某個(gè)方法,先根據(jù)isa找到當(dāng)前類對(duì)象,在當(dāng)前類對(duì)象的cache里面查找方法,如果查到就調(diào)用方法,查不到就去當(dāng)前類對(duì)象的methods數(shù)組里面查找方法,如果查到就調(diào)用方法,并把方法緩存到cache里面。
② 如果當(dāng)前類對(duì)象沒查到,就根據(jù)superclass查找父類,同樣先查找父類的cache,如果查到就調(diào)用方法,然后把父類cache里面的方法緩存到當(dāng)前類對(duì)象的cache里面,如果父類的cache里面沒查到,就去父類的methods數(shù)組里面查找方法,如果查到就調(diào)用這個(gè)方法并把父類的這個(gè)方法緩存到當(dāng)前類對(duì)象的cache里面。
③ 如果父類也沒有這個(gè)方法,再查找基類,同樣先查找基類的cache再查找基類的methods數(shù)組,如果基類有這個(gè)方法就調(diào)用這個(gè)方法并把基類的這個(gè)方法放到當(dāng)前類對(duì)象的cache緩存里面,這樣下次再次調(diào)用這個(gè)方法就直接在當(dāng)前類對(duì)象的cache里面取了,就不用遍歷methods數(shù)組了。
博客地址:Runtime2-Class的內(nèi)部結(jié)構(gòu)、method_t、cache
3. objc_msgSend底層調(diào)用流程
- objc_msgSend的執(zhí)行流程可以分為3大階段
① 消息發(fā)送:就是根據(jù)isa、superclass尋找方法
② 動(dòng)態(tài)方法解析:允許開發(fā)者動(dòng)態(tài)創(chuàng)建新的方法
③ 消息轉(zhuǎn)發(fā):轉(zhuǎn)發(fā)給另外一個(gè)對(duì)象調(diào)用這個(gè)方法
objc_msgSend內(nèi)部的這三個(gè)階段經(jīng)歷完還找不到方法就報(bào)錯(cuò):unrecognized selector sent to instance/class。



說一下消息轉(zhuǎn)發(fā)流程
當(dāng)消息發(fā)送和動(dòng)態(tài)方法解析都沒找到方法就會(huì)進(jìn)入消息轉(zhuǎn)發(fā)階段:
① 首先會(huì)調(diào)用+或-開頭的forwardingTargetForSelector方法,如果這個(gè)方法返回值不為空,就給返回值發(fā)送SEL消息:objc_msgSend(返回值, SEL)。
② 如果這個(gè)方法的返回值為空,就會(huì)調(diào)用+或-開頭的methodSignatureForSelector方法,如果這個(gè)方法返回值不為空,就會(huì)再調(diào)用+或-開頭的forwardInvocation方法,我們可以在forwardInvocation里面方法做任何我們想做的事。
③ 如果這個(gè)方法的返回值為空,就會(huì)調(diào)用doesNotRecognizeSelector,報(bào)錯(cuò)unrecognized selector sent to instance/class。@synthesize自動(dòng)生成_age成員變量、setter和getter的實(shí)現(xiàn),@dynamic不自動(dòng)生成_age成員變量、setter和getter的實(shí)現(xiàn),正好是反過來的
博客地址:Runtime3-objc_msgSend底層調(diào)用流程
4. super
[super message]的底層實(shí)現(xiàn)
① 消息接收者仍然是子類對(duì)象
② 從父類開始查找方法的實(shí)現(xiàn)如何降低unrecognized selector sent to instance/class崩潰?說一下思路
項(xiàng)目中我們可以給NSObject添加分類,實(shí)現(xiàn)forwardInvocation方法,在這里收集信息,然后上傳到服務(wù)器。這里只是簡(jiǎn)單提個(gè)思路,其實(shí)NSProxy這個(gè)類是專門用來做消息轉(zhuǎn)發(fā)的,以后再說。
博客地址:super
5. Runtime-API
交換方法實(shí)現(xiàn)在開發(fā)中經(jīng)常使用,但是實(shí)際上我們使用最多的是交換系統(tǒng)或者第三方框架的方法。
- 如何攔截所有按鈕的點(diǎn)擊事件?
UIButton繼承于UIControl,UIControl有一個(gè)sendAction:to:forEvent:方法,每當(dāng)觸發(fā)一個(gè)事件就會(huì)調(diào)用這個(gè)方法,所以我們可以給UIControl添加分類,在分類中交換這個(gè)方法的實(shí)現(xiàn):
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// hook:鉤子函數(shù)
Method method1 = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
Method method2 = class_getInstanceMethod(self, @selector(mj_sendAction:to:forEvent:));
method_exchangeImplementations(method1, method2);
});
}
- (void)mj_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
NSLog(@"%@-%@-%@", self, target, NSStringFromSelector(action));
// 調(diào)用系統(tǒng)原來的實(shí)現(xiàn)
// 因?yàn)榉椒ㄒ呀?jīng)交換了,所以其實(shí)是調(diào)用sendAction:to:forEvent:
[self mj_sendAction:action to:target forEvent:event];
//攔截按鈕事件
if ([self isKindOfClass:[UIButton class]]) {
// 攔截了所有按鈕的事件
}
}
上面交換方法也叫鉤子函數(shù),利用鉤子函數(shù)就實(shí)現(xiàn)了攔截所有UIButton的點(diǎn)擊事件。
為什么上面要加個(gè)dispatch_once?
按理說load方法只會(huì)調(diào)用一次,萬(wàn)一別人主動(dòng)調(diào)用了load方法那不就調(diào)用兩次了嗎,這樣方法就交換兩次了和沒交換一樣,所以加個(gè)dispatch_once。交換方法實(shí)現(xiàn)的原理是什么?
method_exchangeImplementations方法是傳入兩個(gè)Method,以前我們講過Method的內(nèi)部結(jié)構(gòu),其實(shí)交換方法實(shí)現(xiàn)就是把Method里面的IMP交換了。對(duì)于交換方法,如果這個(gè)方法有緩存,怎么辦?
其實(shí),調(diào)用method_exchangeImplementations函數(shù)會(huì)清空緩存,這樣就保證了交換方法之后調(diào)用方法不會(huì)出錯(cuò)。如何預(yù)防數(shù)組添加nil崩潰?
我們可以交換insertObject:atIndex:方法,因?yàn)闊o(wú)論調(diào)用addObject:還是調(diào)用insertObject:atIndex:最后都會(huì)調(diào)用insertObject:atIndex:方法。給NSMutableArray添加分類,實(shí)現(xiàn)如下代碼:
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 類簇:NSString、NSArray、NSDictionary,真實(shí)類型是其他類型
Class cls = NSClassFromString(@"__NSArrayM");
Method method1 = class_getInstanceMethod(cls, @selector(insertObject:atIndex:));
Method method2 = class_getInstanceMethod(cls, @selector(mj_insertObject:atIndex:));
method_exchangeImplementations(method1, method2);
});
}
- (void)mj_insertObject:(id)anObject atIndex:(NSUInteger)index
{
if (anObject == nil) return;
[self mj_insertObject:anObject atIndex:index];
}
- 如何預(yù)防字典key傳入nil崩潰?
給NSMutableDictionary添加分類,交換setObject:forKeyedSubscript:方法,如下:
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = NSClassFromString(@"__NSDictionaryM");
Method method1 = class_getInstanceMethod(cls, @selector(setObject:forKeyedSubscript:));
Method method2 = class_getInstanceMethod(cls, @selector(mj_setObject:forKeyedSubscript:));
method_exchangeImplementations(method1, method2);
Class cls2 = NSClassFromString(@"__NSDictionaryI");
Method method3 = class_getInstanceMethod(cls2, @selector(objectForKeyedSubscript:));
Method method4 = class_getInstanceMethod(cls2, @selector(mj_objectForKeyedSubscript:));
method_exchangeImplementations(method3, method4);
});
}
- (void)mj_setObject:(id)obj forKeyedSubscript:(id<NSCopying>)key
{
if (!key) return;
[self mj_setObject:obj forKeyedSubscript:key];
}
- (id)mj_objectForKeyedSubscript:(id)key
{
if (!key) return nil;
return [self mj_objectForKeyedSubscript:key];
}
什么是Runtime?平時(shí)項(xiàng)目中有用過么?
① OC是一門動(dòng)態(tài)性比較強(qiáng)的編程語(yǔ)言,允許很多操作推遲到程序運(yùn)行時(shí)再進(jìn)行。
② OC的動(dòng)態(tài)性就是由Runtime來支撐和實(shí)現(xiàn)的,Runtime是一套C語(yǔ)言的API,封裝了很多動(dòng)態(tài)性相關(guān)的函數(shù)。
③ 平時(shí)編寫的OC代碼,底層都是轉(zhuǎn)換成了RuntimeAPI進(jìn)行調(diào)用。Runtime具體應(yīng)用在哪里?
① 利用關(guān)聯(lián)對(duì)象(AssociatedObject)給分類添加屬性
② 遍歷類的所有成員變量(修改textfield的占位文字顏色、字典轉(zhuǎn)模型、自動(dòng)歸檔解檔)
③ 交換方法實(shí)現(xiàn)(交換系統(tǒng)的方法)
④ 利用消息轉(zhuǎn)發(fā)機(jī)制解決方法找不到的異常問題
......
博客地址:Runtime-API
二. Runloop
什么是Runloop?
顧名思義,Runloop就是運(yùn)行循環(huán),就是在程序運(yùn)行過程中循環(huán)做一些事情。RunLoop的基本作用
① 保持程序的持續(xù)運(yùn)行
② 處理App中的各種事件(比如觸摸事件、定時(shí)器事件等)
③ 節(jié)省CPU資源,提高程序性能:該做事時(shí)做事,該休息時(shí)休息兩套R(shí)unLoop API
① NSRunLoop和CFRunLoopRef都代表著RunLoop對(duì)象
② NSRunLoop是基于CFRunLoopRef的一層OC包裝
③ CFRunLoopRef是開源的:Core Foundation源碼RunLoop與線程的關(guān)系
① 每條線程都有唯一的一個(gè)與之對(duì)應(yīng)的RunLoop對(duì)象,主線程的RunLoop已經(jīng)自動(dòng)獲取(創(chuàng)建),子線程默認(rèn)沒有開啟RunLoop,RunLoop會(huì)在線程結(jié)束時(shí)銷毀。
② RunLoop是懶加載的,線程剛創(chuàng)建時(shí)并沒有RunLoop對(duì)象,RunLoop會(huì)在第一次獲取它時(shí)創(chuàng)建。
③ 主線程幾乎所有的事情都是交給了runloop去做,比如UI界面的刷新、點(diǎn)擊時(shí)間的處理、performSelector等等
④ RunLoop保存在一個(gè)全局的Dictionary里,線程作為key,RunLoop作為value。Core Foundation中關(guān)于RunLoop的5個(gè)類的關(guān)系
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
CFRunLoopRef是指向__CFRunLoop結(jié)構(gòu)體的指針,找到__CFRunLoop結(jié)構(gòu)體源碼:
struct __CFRunLoop {
......
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;//當(dāng)前模式
CFMutableSetRef _modes; //是個(gè)集合,無(wú)序的,里面裝的是CFRunLoopModeRef類型的對(duì)象
......
};
__CFRunLoop里面有個(gè)_modes,它是個(gè)集合,里面裝的是一堆CFRunLoopModeRef類型的對(duì)象,當(dāng)前的模式是_currentMode。
進(jìn)入CFRunLoopModeRef:
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
......
CFStringRef _name;//名稱
CFMutableSetRef _sources0;//里面裝的是CFRunLoopSourceRef類型的對(duì)象
CFMutableSetRef _sources1;//里面裝的是CFRunLoopSourceRef類型的對(duì)象
CFMutableArrayRef _observers;//里面裝的是CFRunLoopObserverRef類型的對(duì)象
CFMutableArrayRef _timers;//里面裝的是CFRunLoopTimerRef類型的對(duì)象
......
};
CFRunLoopRef里面有個(gè)_modes集合,里面裝好多CFRunLoopModeRef類型的模式,_currentMode是當(dāng)前模式。
模式里面有name,_sources0、_sources1集合(里面裝的是CFRunLoopSourceRef類型的東西),_observers數(shù)組(里面裝的是CFRunLoopObserverRef類型的東西),_timers數(shù)組(里面裝的是CFRunLoopTimerRef類型的東西)。
如下圖所示:

① CFRunLoopModeRef代表RunLoop的運(yùn)行模式。
② 一個(gè)RunLoop包含若干個(gè)Mode,每個(gè)Mode又包含若干個(gè)Source0/Source1/Timer/Observer。
③ RunLoop啟動(dòng)時(shí)只能選擇其中一個(gè)Mode,作為currentMode,如果需要切換Mode,只能退出當(dāng)前Loop,再重新選擇一個(gè)Mode進(jìn)入。
④ 不同組的Source0/Source1/Timer/Observer能分隔開來,互不影響。
⑤ 如果Mode里沒有任何Source0/Source1/Timer/Observer,RunLoop會(huì)立馬退出。
⑥ 其中Timer是定時(shí)器,平時(shí)創(chuàng)建的一些定時(shí)器都放在這里,Observer是監(jiān)聽器,source0/Source1是事件,比如點(diǎn)擊事件、performSelector等等。
為什么多種模式要分開呢?
比如scrollView滾動(dòng)的時(shí)候讓它切換到滾動(dòng)模式,那么在滾動(dòng)模式下,scrollView就專心處理滾動(dòng)相關(guān)的就可以了,以前模式下的事情就不處理了。如果不滾動(dòng),在正常模式下,就專心處理正常模式下的事情就好了,這樣可以做到流暢不卡頓。常見的兩種Mode
① kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默認(rèn)Mode,通常主線程是在這個(gè)Mode下運(yùn)行。
② UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動(dòng),保證界面滑動(dòng)時(shí)不受其他 Mode 影響。Source0、Source1、Timers、Observers分別代表什么呢?
Source0:觸摸事件處理、performSelector:onThread:。
Source1:基于Port(端口)的線程間通信、系統(tǒng)事件捕捉 (比如點(diǎn)擊事件,通過Source1捕捉,然后包裝成Source0進(jìn)行處理)。
Timers:NSTimer、performSelector:withObject:afterDelay:(底層就是NSTimer)。
Observers:用于監(jiān)聽RunLoop的狀態(tài)、UI刷新(BeforeWaiting)、Autorelease pool(BeforeWaiting)。
比如,點(diǎn)擊界面空白就是Source0事件。關(guān)于Observers監(jiān)聽UI刷新(BeforeWaiting),self.view.backgroundColor = [UIColor redColor];這句代碼并不是立馬執(zhí)行,Observers會(huì)先記下來,當(dāng)Observers監(jiān)聽到RunLoop將要睡覺啦,就在RunLoop將要睡覺之前執(zhí)行(刷新UI)。同理Autorelease pool也是一樣,當(dāng)Observers監(jiān)聽到RunLoop將要睡覺啦,就在RunLoop睡覺之前釋放對(duì)象。
- RunLoop有幾種狀態(tài)?
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //即將進(jìn)入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), //即將處理Timer
kCFRunLoopBeforeSources = (1UL << 2), //即將處理Source
kCFRunLoopBeforeWaiting = (1UL << 5), //即將進(jìn)入休眠
kCFRunLoopAfterWaiting = (1UL << 6), //即將從休眠中喚醒
kCFRunLoopExit = (1UL << 7), //即將退出RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU //所有狀態(tài)
};
如何監(jiān)聽RunLoop的狀態(tài)?
Observers數(shù)組里面有系統(tǒng)創(chuàng)建的一些Observer,用于監(jiān)聽RunLoop狀態(tài)進(jìn)行UI刷新、Autorelease pool等,如果我們自己想監(jiān)聽RunLoop狀態(tài)肯定要自己創(chuàng)建Observer。
首先使用CFRunLoopObserverCreate創(chuàng)建observer,然后再用CFRunLoopAddObserver添加Observer到RunLoop中。RunLoop運(yùn)行流程圖

- 下面代碼是誰(shuí)處理?
一般情況下GCD的東西是GCD來處理的,不會(huì)交給RunLoop。GCD是GCD,RunLoop是RunLoop,他們互不干擾,但是有一種情況下GCD是交給RunLoop處理的,如下:
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 子線程處理一些邏輯
// 回到主線程去刷新UI界面
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"11111111111"); // 斷點(diǎn)
});
});
}
當(dāng)我們?cè)谧泳€程處理一些邏輯然后回到主線程去刷新UI界面,這種情況就會(huì)交給RunLoop去處理GCD相關(guān)的東西,然后再回到GCD。
RunLoop線程休眠的實(shí)現(xiàn)原理?
線程休眠就是因?yàn)橛脩魬B(tài)和內(nèi)核態(tài)的切換。RunLoop在實(shí)際開發(fā)中的應(yīng)用有哪些?
① 解決NSTimer在滑動(dòng)時(shí)停止工作的問題
② 控制線程生命周期(線程?;睿?br> ③ 監(jiān)控應(yīng)用卡頓
④ 性能優(yōu)化NSRunLoopCommonModes是什么?
NSRunLoopCommonModes并不是一個(gè)真的模式,它只是一個(gè)標(biāo)記,定時(shí)器能在_commonModes數(shù)組中存放的模式下工作。timer 與 runloop 的關(guān)系?
① RunLoop對(duì)象里面有個(gè)_modes數(shù)組,里面放一堆模式,模式里面會(huì)放timer,如果timer被標(biāo)記為commonModes,那么timer就能在_commonModes數(shù)組中存放的模式下工作,能在commonModes“模式”下工作的東西都會(huì)被添加到_commonModeItems數(shù)組里中。
② 如果線程休眠了,timer也可以喚醒休眠的RunLoop。runloop 是怎么響應(yīng)用戶操作的,具體流程是什么樣的?
當(dāng)用戶有個(gè)點(diǎn)擊事件,這個(gè)系統(tǒng)事件會(huì)先被Source1捕捉,Source1捕捉之后會(huì)包裝成事件隊(duì)列(EventQuene),再放到Source0里面進(jìn)行處理,然后RunLoop循環(huán)再處理Source0里面的事件。