預(yù)計(jì)內(nèi)容:
1、Runloop
2腰池、autorelease
3尾组、事件傳遞 & 響應(yīng)鏈(不一樣的 super 寓意)
4忙芒、UIResponder 分類與線程保活
關(guān)于以上內(nèi)容:不總結(jié)不知道讳侨、一總結(jié)內(nèi)容還真不少呵萨。
0x01 Runloop 小節(jié)
一、概念
(略過(guò))
二跨跨、監(jiān)聽(tīng)
代碼先行:
// Runloop 監(jiān)聽(tīng)回調(diào)函數(shù)
void hgRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeWaiting");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting");
break;
case kCFRunLoopExit:
NSLog(@"kCFRunLoopExit");
break;
default:
break;
}
}
// 創(chuàng)建一個(gè) Runloop 狀態(tài)監(jiān)聽(tīng)
- (void)runLoopObserverCreate {
// 手動(dòng)創(chuàng)建一個(gè) observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, hgRunLoopObserverCallBack, NULL);
// 添加到 MainRunLoop 中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 釋放
CFRelease(observer);
}
以上的代碼還是比較簡(jiǎn)單的潮峦,但是五臟俱全,各個(gè)數(shù)據(jù)類型通過(guò)名字都能看出其意思歹叮。具體在項(xiàng)目中的使用可以參考 HGMonitorStuck 文件跑杭。 在開(kāi)發(fā)中可能比較關(guān)注的是 hgRunLoopObserverCallBack 中對(duì)所監(jiān)聽(tīng)到的不同狀態(tài)做不同的邏輯處理,關(guān)于 CFRunLoopActivity 的完整定義如下:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
這里需要注意的是這個(gè)枚舉類型是 *_OPTIONS 類型的咆耿。還有其它的數(shù)據(jù)類型,具體如下:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
這些具體的用法爹橱,直接搜索都有很多的萨螺,相關(guān)代碼,在這里:CF-1153.18.tar.gz
三愧驱、機(jī)制是什么
機(jī)制主要就是這張圖片慰技,就是運(yùn)行循環(huán),更多的信息可以看CF-1153.18.tar.gz
那么問(wèn)題來(lái)了:在 iOS 中哪里用到了 NSRunloop组砚,這個(gè)問(wèn)題的答案是:所有的地方都用到了吻商。比如將第二小節(jié)的代碼放到一個(gè)視圖控制器中調(diào)用,然后直接點(diǎn)擊屏幕糟红,都能監(jiān)聽(tīng)到其狀態(tài)的改變艾帐,這些東西都是很好理解的。
四盆偿、Runloop 總結(jié)
簡(jiǎn)單的介紹就到此結(jié)束了柒爸,上面除了代碼與圖片,貌似也沒(méi)有什么事扭。這種東西主要就是理解捎稚,要想理解那就看 CF-1153.18.tar.gz 中的代碼。更多的內(nèi)容求橄,比如與線程今野、定時(shí)器,還有其它的 Source0與 Source1都是什么關(guān)系罐农,這些網(wǎng)上隨便一搜都有条霜,但是盡量還是以看源代碼的邏輯為準(zhǔn)。
其實(shí)剛開(kāi)始想要介紹這個(gè) NSRunloop 的初衷是想直接介紹 autorelease 的啃匿,后來(lái)發(fā)現(xiàn)如果不先介紹 NSRunloop 的話蛔外,對(duì) autorelease 根本就解釋不通∏悖現(xiàn)在可以開(kāi)始對(duì) autorelease 小節(jié)的欣賞了。
0x02 autorelease 小節(jié)
一夹厌、概念
(略過(guò))
二豹爹、問(wèn)題
技術(shù)交流中可能會(huì)出現(xiàn)這樣的問(wèn)題:接收 autorelease 消息后的對(duì)象,是在什么時(shí)候被銷毀的矛纹?
關(guān)于這個(gè)問(wèn)題臂聋,是否會(huì)有小伙伴這樣考慮過(guò):這個(gè)是 MRC 環(huán)境的語(yǔ)法了,可以不用考慮或南。但是別忘了在 ARC 還有一個(gè)與之對(duì)應(yīng)的關(guān)鍵字是 __autoreleasing孩等,只能是理解了autorelease 之后就能能更好的理解 __autoreleasing。
知道這個(gè)問(wèn)題的答案很簡(jiǎn)單采够,但完全弄清楚這個(gè)問(wèn)題需要對(duì) ****autorelease 與 NSRunloop 要有深層次的理解肄方。
別人的答案:
1、autoreleasepool不是一個(gè)大棧蹬癌,是分一個(gè)一個(gè)固定大小的page权她,雙向鏈表連起來(lái)的。
2逝薪、在runLoop睡眠之前釋放(kCFRunLoopBeforeWaiting)隅要,也就是說(shuō):如果在主線程開(kāi)啟一個(gè)自動(dòng)釋放池,那么就會(huì)在主線程即將進(jìn)入休眠的時(shí)候清理一遍自動(dòng)釋放池董济。如果是在子線程添加的自動(dòng)釋放池步清,那么就在子線程即將進(jìn)入休眠的時(shí)候清理.線程的runloop下次再開(kāi)始運(yùn)行循環(huán)的時(shí)候再去創(chuàng)建這些池子中的對(duì)象及釋放池就可以了。
這些答案都是有道理的虏肾,但是如何去理解這些答案才是關(guān)鍵廓啊。比如:雙向鏈表結(jié)構(gòu)的自動(dòng)緩存池對(duì)一個(gè) autorelease 的對(duì)象有什么影響,與 NSRunloop 又有什么關(guān)系询微,上面提到 在主線程即將進(jìn)入休眠的時(shí)候清理一遍自動(dòng)釋放池 里面提到的清理一遍緩存池崖瞭,是如何清除的?等等一系列的問(wèn)題將在下一小節(jié)詳細(xì)介紹撑毛。
三书聚、詳細(xì)介紹
3.1 緩存池
其實(shí),我意思是介紹 autorelease 藻雌,但是如果不介紹 AutoreleasePool 那肯定是沒(méi)有意義的雌续。在現(xiàn)在的網(wǎng)上的技術(shù)文章,幾乎都是一上來(lái)就介紹重點(diǎn)胯杭,反正我 剛開(kāi)始的時(shí)候是沒(méi)有看懂驯杜。
先介紹在沒(méi)有 NSRunloop 的參與下的 autorelease。那么就直接創(chuàng)建一個(gè)沒(méi)有 Runloop 的項(xiàng)目(終端項(xiàng)目)做个,首先就切換成 MRC 環(huán)境鸽心。
3.1.1 自動(dòng)釋放池的簡(jiǎn)單認(rèn)識(shí)
剛創(chuàng)建的項(xiàng)目代碼是這樣的:
直接轉(zhuǎn)成 cpp 代碼看一下:
因?yàn)榇a很簡(jiǎn)單滚局,所以轉(zhuǎn)成的 cpp 代碼也很簡(jiǎn)單,但是在上面的代碼中有我們想要的關(guān)鍵代碼:
__AtAutoreleasePool __autoreleasepool;
通過(guò)仔細(xì)的觀察發(fā)現(xiàn)所謂的自動(dòng)釋放池顽频,變成了一個(gè)大括號(hào)與這個(gè)局部變量的初現(xiàn)了藤肢。再通過(guò)全文搜索 __AtAutoreleasePool ,找到了其定義:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
原來(lái) __AtAutoreleasePool 是一個(gè) C++ 類型的結(jié)構(gòu)體糯景,那么問(wèn)題來(lái)了:對(duì)應(yīng)這個(gè)結(jié)構(gòu)體嘁圈,僅僅申明一個(gè)局部變量就可以了?是的蟀淮,很容易看到在這個(gè)結(jié)構(gòu)體的定義中有一個(gè)成員變量最住、一個(gè)無(wú)參構(gòu)造函數(shù)以及虛構(gòu)虛構(gòu),并在無(wú)參構(gòu)造函數(shù)中通過(guò)另一個(gè)函數(shù)給成員變量賦值怠惶,在虛構(gòu)函數(shù)中將這個(gè)成員變量放到另一個(gè)函數(shù)中涨缚。
根據(jù) C++的語(yǔ)法,可以模擬一下這個(gè)自動(dòng)釋放池最終在內(nèi)存中的偽代碼甚疟,應(yīng)該是這樣的:
可以自行的分析一下仗岖,自動(dòng)緩存池嵌套的情況,分析都是一樣的览妖。
到這里,對(duì)一個(gè)自動(dòng)釋放池應(yīng)該有一個(gè)基本的了解了揽祥,在上面的介紹中認(rèn)識(shí)了三個(gè)東西:
1讽膏、__AtAutoreleasePool 數(shù)據(jù)結(jié)構(gòu)
2、objc_autoreleasePoolPush() 函數(shù)
3拄丰、objc_autoreleasePoolPop(atautoreleasepoolobj) 函數(shù)
關(guān)于上面的兩個(gè)陌生函數(shù)府树,大家可以到源代碼中查看。
關(guān)于看源代碼料按,最近很多的小伙伴都在使用源代碼來(lái)分析各種的技術(shù)點(diǎn)奄侠,我有一個(gè)建議:盡量使用最新的版本,前不久我一直使用的 723 的载矿,昨天發(fā)現(xiàn)已經(jīng)更新到 750了垄潮,這是因?yàn)閯偪吹叫』锇閭兊奈恼拢l(fā)現(xiàn)代碼不一樣闷盔,仔細(xì)看才知道版本不對(duì)弯洗,當(dāng)然不管版本怎么更新,流程肯定都是相似的逢勾。僅僅是建議牡整。
3.1.2 AutoreleasePoolPage
如果到源代碼中看了哪兩個(gè)陌生的函數(shù),會(huì)看到一個(gè)關(guān)鍵的數(shù)據(jù)結(jié)構(gòu) AutoreleasePoolPage溺拱,這是一個(gè) Class逃贝∫ゴ牵總之上面的兩個(gè)陌生函數(shù)實(shí)際調(diào)用的是AutoreleasePoolPage中的 Push() 與 pop 函數(shù)。
到這里可以理解成沐扳,自動(dòng)釋放池實(shí)際上就是在操作 AutoreleasePoolPage泥从。其實(shí)看到這里,如果到瀏覽器搜索一下 AutoreleasePoolPage 會(huì)發(fā)現(xiàn)迫皱,技術(shù)文章那是相當(dāng)?shù)呢S滿歉闰,但是很多的文章是直接介紹的,沒(méi)有像這篇文檔一樣還介紹了怎么找到這個(gè)數(shù)據(jù)結(jié)構(gòu)的卓起。
看到這里和敬,建議先到瀏覽器瀏覽一遍那些豐滿的文章,因?yàn)榻酉聛?lái)我就會(huì)直接介紹核心的東西了戏阅。也可以選擇在源代碼中按照這樣的順序全文搜索看一下:
1昼弟、objc_autoreleasePoolPush 與 objc_autoreleasePoolPop,主要看 NSObject.mm 文件中的奕筐。
2舱痘、AutoreleasePoolPage 中只要能看到下圖中的內(nèi)容就夠了。
3.1.3 自動(dòng)釋放池中的雙向鏈表
大概是這樣的:
是的离赫,具體的雙向鏈表大概就是這個(gè)樣子的了芭逝,每個(gè)節(jié)點(diǎn)就是一個(gè) Page,各個(gè) Page 就是通過(guò) parent 與 child 指針串起來(lái)的渊胸。
那么問(wèn)題來(lái)了:每個(gè) Page 的下半部分(大紅括號(hào))到底什么東西旬盯?可以在 AutoreleasePoolPage 中找到這樣的代碼:
由上圖,可以得出以下結(jié)論:
1翎猛、begin 返回的是 Page 成員變量所占的所有內(nèi)容地址的下一個(gè)指針位置胖翰。
2、end 就是整個(gè) Page 的末尾地址切厘。
那么就知道用大紅括號(hào)括起來(lái)的地方應(yīng)該是 SIZE - sizeof(*this) 了萨咳。關(guān)于 SIZE 的值,通過(guò)代碼的跟蹤疫稿,你會(huì)跟蹤到這個(gè)地方:
那么問(wèn)題又來(lái)了培他,這部分空間有什么作用?這部分就是為了在以后有對(duì)象發(fā)送 autorelease 的話而克,會(huì)將那個(gè)對(duì)象的地址直接依次的放入下面的地址中靶壮。可以先幻想一下將地址放到這里到底有什么作用员萍。
到這里腾降,對(duì)自動(dòng)釋放池的理解又進(jìn)了一公里了。
3.2 autorelease 后的變化
到這里碎绎,依舊直接到源代碼NSObject.mm 中看 autorelease 的實(shí)現(xiàn)螃壤。入口在這里:
看到了一個(gè)熟悉的東西:AutoreleasePoolPage抗果,在看的過(guò)程中也會(huì)發(fā)現(xiàn)只要是 isTaggedPointer() 為真的都直接 return 了,進(jìn)一步的證明 Tagged Pointer 的性能高是有一定的道理的奸晴,省去了很多的內(nèi)存操作冤馏。
看到上面的這張圖,要注意寄啼,最終調(diào)用的是 autorelease 函數(shù)逮光,還將當(dāng)前的 this 傳進(jìn)去了。依然一路的狂飆到這里:
這個(gè)方法墩划,大家猜都能猜到了涕刚,我大概的翻譯一下:
獲取當(dāng)前的 hotPage,如果這個(gè) page 有值并且不滿(full)的情況乙帮,直接添加(add)到這一頁(yè)杜漠,其次如果這個(gè)頁(yè)有值,那么就到 autoreleaseFullPage 中創(chuàng)建一個(gè)節(jié)點(diǎn)察净,然后串起來(lái)驾茴。否則通過(guò) autoreleaseNoPage 函數(shù)創(chuàng)建一個(gè)新的節(jié)點(diǎn)。
到這里氢卡,應(yīng)該完全的明白了上面的雙向鏈表的意思了锈至。總之在 autorelease 之后會(huì)將當(dāng)前的對(duì)象添加到一個(gè) Page中译秦,如果發(fā)現(xiàn)沒(méi)有任何的 Page裹赴,那么就創(chuàng)建第一個(gè) Page 節(jié)點(diǎn),如果有诀浪、但是已經(jīng)滿了,就創(chuàng)建下一個(gè) Page 節(jié)點(diǎn)延都,如果沒(méi)有滿雷猪、那么就直接添加到當(dāng)前的 Page 節(jié)點(diǎn)中。
3.2.1 _objc_autoreleasePoolPrint 打印看本質(zhì)
到這里可以借助一個(gè)私有函數(shù)來(lái)幫我們理解晰房,就是 _objc_autoreleasePoolPrint求摇,這個(gè)是私有函數(shù),可以直接使用殊者,但是需要在使用的地方這樣聲明一下:
extern void _objc_autoreleasePoolPrint(void);
然后這樣使用:
主要是看圖与境,上圖中可以看到當(dāng)前代碼之后內(nèi)存中自動(dòng)緩存池的數(shù)據(jù)布局,結(jié)合以上的介紹應(yīng)該會(huì)有一個(gè)比較綜合的認(rèn)識(shí)猖吴。從上圖中可以看出這個(gè)內(nèi)存布局摔刁,很清晰的就把每個(gè)自動(dòng)緩存池中的對(duì)象劃分了:
這個(gè)就代表開(kāi)始了,這個(gè)地方還是挺重要的海蔽,在上面瀏覽源代碼的過(guò)程中跳過(guò)了一個(gè)地方:
AutoreleasePoolPage中的 push 與 autorelease 中所做的事情貌似很像是共屈,僅僅是參數(shù)不同而已绑谣。到這里很輕松的就能想到,在 push 的時(shí)候是將 POOL_BOUNDARY 的東西弄進(jìn)去了拗引,autorelease 的時(shí)候是將當(dāng)前的對(duì)象弄進(jìn)去了借宵。POOL_BOUNDARY就是代表著上面每頁(yè)的開(kāi)始,關(guān)于POOL_BOUNDARY可以自行瀏覽矾削。再結(jié)合最最最開(kāi)始介紹的壤玫,在AutoreleasePoolPage中有一個(gè)成員變量 atautoreleasepoolobj ,這個(gè)變量是在自動(dòng)緩存池的開(kāi)始返回,然后是在自動(dòng)緩存池結(jié)束的地方放入到一個(gè)叫 Pop 的函數(shù)中萝勤,理解到這里似乎明白了 atautoreleasepoolobj 的用意了浅碾,估計(jì)在這個(gè) Pop 函數(shù)中拿到這個(gè)成員變量之后就是給當(dāng)前緩存池中的對(duì)象發(fā)送 release 的吧。為了證明這一點(diǎn)括改,再次看看這個(gè) pop 函數(shù),于是找到這個(gè)地方:
到了這里似乎明白了家坎,在 pop 中是通過(guò)逆向的遍歷一個(gè) Page 中的對(duì)象嘱能,然后發(fā)送 release 方法,一直遍歷到 POOL_BOUNDARY 的時(shí)候停止虱疏,因?yàn)檫@個(gè)地方是當(dāng)前 Page 的開(kāi)始惹骂。
這張圖片應(yīng)該還算形象,大紅箭頭表示 pop 以后 next 的移動(dòng)方向做瞪,向左的箭頭(除第三個(gè))代表一個(gè)緩存池的開(kāi)始位置对粪。當(dāng)?shù)谧罾锩娴木彺娉亟Y(jié)束的時(shí)候,會(huì)調(diào)用一下這個(gè)緩存池中的 pop装蓬,然后開(kāi)始通過(guò) next 逆向遍歷著拭,找到一個(gè) POOL_BOUNDARY 的時(shí)候就知道這個(gè)緩存池中的對(duì)象已經(jīng) release 結(jié)束了,當(dāng)最外層的緩存池結(jié)束的時(shí)候牍帚,同理儡遮。
看到這里,直接弄一個(gè)思考:
以上代碼后 log 打印順序是什么樣的暗赶?
name_11
name_10
中間的 @autoreleasepool 就要結(jié)束了
name_00
這樣對(duì)么鄙币?為什么不對(duì)?理解了這個(gè)問(wèn)題蹂随,那么上面的所有介紹應(yīng)該就已經(jīng)精通了十嘿。
3.2.2 autorelease 小節(jié)
到這里,在一個(gè)沒(méi)有 NSRunloop 的項(xiàng)目中對(duì) autorelease 就介紹結(jié)束了岳锁。如果沒(méi)有看明白的绩衷,那肯定是我的描述有問(wèn)題,只能是再看一遍或者多看源代碼。
3.3 有 Runloop 的 autorelease
在上面的介紹中唇聘,雖然只是一個(gè)簡(jiǎn)單代碼的介紹版姑,但是也差不多清楚了自動(dòng)釋放池(@autoreleasepool)的簡(jiǎn)單結(jié)構(gòu)以及 autorelease 消息發(fā)送之后的一些簡(jiǎn)單步驟。
在上面是在終端項(xiàng)目中使用了 兩個(gè) @autoreleasepool 來(lái)做介紹迟郎,在 @autoreleasepool 的開(kāi)始有一個(gè)Page 的 Push操作剥险,結(jié)束有一個(gè) Page 的 pop 操作。
那么換到 iOS 項(xiàng)目中怎么樣呢宪肖?首先需要清楚的一點(diǎn)是:iOS 的 App 是一直在 Runloop 中的表制,如果在不手動(dòng) @autoreleasepool {} 一個(gè)池子的話,那么整個(gè) APP 的運(yùn)行都是在 main 函數(shù)中的那個(gè)池子中控乾。在這里一定要注意 一個(gè)自動(dòng)釋放池 不等于一個(gè) AutoreleasePoolPage么介,這個(gè)僅僅是一個(gè) 池子中的其中一頁(yè)。從上面的介紹中蜕衡,不難看出也可以在不同的池子中共用一個(gè) AutoreleasePoolPage壤短。其實(shí)從源代碼中也可以看出,所有的池子都是在一個(gè)雙向鏈表中慨仿,只是不同的池子中的所有對(duì)象在鏈表中的不同地方久脯。
那么問(wèn)題來(lái)了:在 iOS 項(xiàng)目中,autorelease 對(duì)象在什么時(shí)候被釋放的镰吆?
關(guān)于這個(gè)問(wèn)題的答案帘撰,其實(shí)在網(wǎng)上都是使用上面的小節(jié)來(lái)回答的,說(shuō)到了雙鏈表万皿,也說(shuō)到了 AutoreleasePoolPage 的 Push 與 Pod摧找,但是到底執(zhí)行這些函數(shù)的時(shí)機(jī)是什么時(shí)候呢?有的說(shuō)是跳出離自己最近的大括號(hào)牢硅,這分明就是錯(cuò)誤的蹬耘,在上面的小節(jié)中已經(jīng)提到。也有說(shuō)是在運(yùn)行循環(huán)中减余,在睡眠之前清理了一遍婆赠,這個(gè)答案比較靠譜,單并不完整佳励。
先來(lái)看一個(gè)很經(jīng)典的現(xiàn)象吧:
看了這張圖,就知道了 NSRunloop 與 autorelease 的結(jié)合真的不簡(jiǎn)單蛆挫。
看到這個(gè)顯示赃承,已經(jīng)清楚的一個(gè)事實(shí)是:autorelease 對(duì)象的銷毀肯定是 AutoreleasePoolPage 中的 next 指針逆向移動(dòng)了。
接下來(lái)看一下在 主線程中的 Runloop 中都有什么信息悴侵,發(fā)現(xiàn)信息很多瞧剖,但是有一個(gè) callout 值得關(guān)注:_wrapRunLoopWithAutoreleasePoolHandler,可以看出這是在主線程中的一個(gè) observer 。關(guān)于這個(gè) observer 可以發(fā)現(xiàn) activities 的值由兩種: 0x1 與 0xa0抓于。通過(guò)對(duì) NSRunloop 的了解做粤,0x1 就是 kCFRunLoopEntry,0xa0 就是 kCFRunLoopBeforeWaiting | kCFRunLoopExit捉撮。
直接上一張 ibireme 大佬 的文章節(jié)選吧:
在那些年看到這么金典的內(nèi)容怕品,只能一臉的懵逼,死記硬背的記住了答案巾遭。
至此肉康、關(guān)于 autorelease 的全部?jī)?nèi)容已經(jīng)介紹完了。文檔有些凌亂灼舍,細(xì)心的看的話吼和,應(yīng)該也會(huì)有所收獲的,感興趣的話就回頭再看一遍骑素。
0x03 事件 & 事件傳遞 & 響應(yīng)鏈
一炫乓、認(rèn)識(shí) UIResponder
在 iOS 開(kāi)發(fā)中很少看到 UIResponder 這個(gè)東西,但是都清楚 AppDelegate 献丑、 UIView 與UIViewController 都是直接繼承自她末捣,凡是繼承自她的,都叫響應(yīng)者[對(duì)象]阳距,都能接收與處理事件塔粒。在接下來(lái)的介紹中,不會(huì)特意的提 UIResponder筐摘,但是應(yīng)該清楚的是 事件傳遞與響應(yīng)鏈?zhǔn)请x不開(kāi) UIResponder 的卒茬。
二、觸摸方法
提到觸摸方法咖熟,肯定能想到如下四個(gè)方法:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
以上的四個(gè)方法特別的簡(jiǎn)單圃酵,大概的調(diào)用流程如下:
其中 B 節(jié)點(diǎn),如果不移動(dòng)的話就不會(huì)觸發(fā)馍管。所以就有如下兩種可能:
1郭赐、A-[B]-C1
2、A-[B]-C2
在這里需要注意的是确沸,在同一次操作中捌锭,各個(gè)方法中的兩個(gè)參數(shù)值是一樣的。
2.1 參數(shù)介紹
touches
是一個(gè) UITouch 集合罗捎,這個(gè)集合的數(shù)量就是當(dāng)前的觸摸由多少個(gè)觸摸點(diǎn)(手指的個(gè)數(shù))观谦。
UIEvent
這個(gè)就是具體的事件,雖然 UIEvent 中的屬性不是太多桨菜,但是包括了第一個(gè)參數(shù)的信息(allTouches)豁状。還有兩個(gè)比較關(guān)鍵的屬性:
@property(nonatomic,readonly) UIEventType type NS_AVAILABLE_IOS(3_0);
@property(nonatomic,readonly) UIEventSubtype subtype NS_AVAILABLE_IOS(3_0);
這就是事件類型捉偏,當(dāng)然當(dāng)前文檔主要介紹的是 UIEventTypeTouches。
三泻红、事件傳遞
為了接下來(lái)的介紹夭禽,我準(zhǔn)備了一個(gè)這樣的項(xiàng)目:responder
從上圖中可以清楚的了解到這些自定義視圖的父子關(guān)系,這些自定義視圖都是直接繼承于 UIView 的谊路。都重寫(xiě)了 touchesBegan 方法讹躯,都是這樣的:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%s", __func__);
}
請(qǐng)不要問(wèn)為什么不弄一個(gè)基類,看到后面會(huì)明白的凶异。
現(xiàn)在可以順便亂點(diǎn)蜀撑,看看控制臺(tái)信息。主要關(guān)注圓圈圈區(qū)域:
經(jīng)過(guò)試驗(yàn)剩彬,應(yīng)該理解了大概的規(guī)律酷麦,可能比較難理解的就是 標(biāo)號(hào)為 4 的點(diǎn)擊。想必小伙伴們都會(huì)有自己的思考喉恋,這個(gè)簡(jiǎn)單的試驗(yàn)隱藏了 iOS 中的事件傳遞沃饶。接下來(lái)直接給出最終的解釋:
當(dāng)點(diǎn)擊屏幕的時(shí)候,首先是 UIApplication 收到這個(gè)事件轻黑,然后傳遞給 KeyWindow糊肤,然后通過(guò)控制器以及視圖相互的傳遞下去。對(duì)于父子控制器的順序是:父控制器傳給父控制器的視圖然后再傳給子控制器氓鄙。對(duì)于父子視圖的順序是:父視圖傳給子視圖馆揉。
比如點(diǎn)擊上面 標(biāo)號(hào) 2 的區(qū)域,其順序?yàn)椋篈ppDelegate --> UIApplication --> KeyWindow --> 控制器 --> HGVcView --> HGRedView抖拦。
同理 標(biāo)號(hào) 9 的順序?yàn)椋篈ppDelegate --> UIApplication --> KeyWindow --> 控制器 --> HGVcView --> HGGrayView --> HGYellowView.
那么問(wèn)題來(lái)了升酣,是所有的視圖都能傳遞事件么?不是的态罪,在以下的情況是不能傳遞事件的:
1噩茄、userInteractionEnabled = NO;
2、hidden = YES;
3复颈、alpha < 0.01
比如我將 HGGrayView 的 userInteractionEnabled = NO;绩聘,那么點(diǎn)擊標(biāo)號(hào)9的區(qū)域,事件是這樣的:AppDelegate --> UIApplication --> KeyWindow --> 控制器 --> HGVcView耗啦。 看完效果 記得將 HGGrayView 的 userInteractionEnabled = YES;
關(guān)于事件傳遞凿菩,還需要清楚的是,當(dāng)我點(diǎn)擊 標(biāo)號(hào)8 區(qū)域的時(shí)候帜讲,為什么是 HGGrayView 接收了事件呢蓄髓?首先要清楚一點(diǎn):在事件的傳遞過(guò)程中、這個(gè)傳遞對(duì)象是不處理事件的舒帮,除非找到了一個(gè)合適的響應(yīng)對(duì)象,也稱第一響應(yīng)者(First Responder)。那么什么樣的響應(yīng)者對(duì)象才算合適呢玩郊?需要滿足以下幾點(diǎn):
1肢执、這個(gè)響應(yīng)者對(duì)象已經(jīng)沒(méi)有可用的子響應(yīng)者對(duì)象
2、觸摸點(diǎn)在這個(gè)響應(yīng)者對(duì)象的空間范圍之類
到這里译红、關(guān)于事件的傳遞就簡(jiǎn)單的介紹結(jié)束了预茄。
看到這里,細(xì)心的小伙伴可能就會(huì)拋出一個(gè)疑問(wèn):如果將 HGGrayView 中的 touchesBegan 注釋之后侦厚,當(dāng)點(diǎn)擊 標(biāo)號(hào)8 區(qū)域的時(shí)候耻陕,盡然是 HGVcView 處理了這個(gè)事件,這是為什么呢刨沦?這個(gè)問(wèn)題的答案在下一小節(jié)诗宣,這個(gè)問(wèn)題已經(jīng)牽扯到了響應(yīng)鏈的概念。
四想诅、響應(yīng)鏈
在看響應(yīng)者鏈之前召庞,先來(lái)復(fù)習(xí)一下再 OC 中的 super 關(guān)鍵字吧。
復(fù)習(xí) super 中 来破。篮灼。。徘禁。诅诱。
復(fù)習(xí) super 中 。送朱。娘荡。。骤菠。
4.1 響應(yīng)鏈中的 super
當(dāng)你復(fù)習(xí)結(jié)束之后它改,想要問(wèn)問(wèn)你,上面提到touchesBegan商乎。央拖。。鹉戚。touchesEnded這些方法鲜戒,都是系統(tǒng)方法,按照道理來(lái)說(shuō)這種系統(tǒng)方法抹凳,蘋果是推薦在使用的時(shí)候調(diào)用 super 的遏餐。但是在開(kāi)發(fā)中,你有調(diào)用過(guò) super 么赢底?相反失都,一旦調(diào)用可能會(huì)出現(xiàn)一些莫名其妙的問(wèn)題柏蘑。
好的,接下來(lái)將 HGGrayView 中的 touchesBegan 打開(kāi)粹庞,并給 HGGrayView 添加一個(gè)自定義的父類 HGBaseView咳焚, 讓 HGGrayView 繼承于這個(gè) HGBaseView。在 HGBaseView 中重寫(xiě) touchesBegan 方法庞溜,然后在 HGGrayView 中調(diào)用 super革半。
最后你發(fā)現(xiàn)了什么?是不是 HGBaseView 中的 touchesBegan 根本沒(méi)有被調(diào)用流码,而是調(diào)用了 HGVcView 中的 touchesBegan又官、這就厲害了(畫(huà)線部分是我忘記將 HGGrayView 繼承于HGBaseView了,通過(guò) HGResponder 項(xiàng)目可以看出漫试,也許已經(jīng)有小伙伴發(fā)現(xiàn)了六敬。所以才沒(méi)有調(diào)用 HGBaseView 中的方法,實(shí)際上是會(huì)調(diào)用父類方法的商虐,會(huì)在父類方法中通過(guò) super 將事件傳給下一個(gè)響應(yīng)者對(duì)象)觉阅。通過(guò)對(duì)事件傳遞的理解,HGGrayView 的事件是由 HGVcView 傳遞來(lái)的秘车、差不多知道是怎么回事了典勇。
響應(yīng)鏈就是事件傳遞的反方向。比如在 HGYellowView 中的 touchesBegan 方法中添加如下代碼:
NSMutableString* stringM = [NSMutableString string];
UIResponder * r = self;
[stringM appendFormat:@"%@", NSStringFromClass(r.class)];
while (r) {
r = r.nextResponder;
[stringM appendFormat:@" --> %@", NSStringFromClass(r.class)];
}
NSLog(@"%@", stringM);
點(diǎn)擊 標(biāo)簽 9 區(qū)域的打印如下:
HGYellowView --> HGGrayView --> HGVcView --> ViewController --> UIWindow --> UIApplication --> AppDelegate --> (null)
到這里可以介紹在上面小節(jié)中提出的一個(gè)疑問(wèn):
如果將 HGGrayView 中的 touchesBegan 注釋之后叮趴,當(dāng)點(diǎn)擊標(biāo)號(hào)8 區(qū)域的時(shí)候割笙,盡然是 HGVcView 處理了這個(gè)事件,這是為什么呢眯亦?
這是因?yàn)樵谀J(rèn)的情況下 HGGrayView 中的事件通過(guò)響應(yīng)鏈傳給了 HGVcView伤溉,因?yàn)椴恢貙?xiě) touchesBegan 方法的話,默認(rèn)是這樣的:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
}
4.2 響應(yīng)鏈小節(jié)
其實(shí)對(duì)應(yīng)響應(yīng)鏈的介紹到這里已經(jīng)結(jié)束了妻率。
總之乱顾,要理解好響應(yīng)鏈,那么必須要理解好事件傳遞宫静。這兩者的區(qū)別是走净,一個(gè)是屬于尋找的過(guò)程,一個(gè)事件執(zhí)行的過(guò)程孤里。他們的方向正好是相反的伏伯。還有一點(diǎn)是要正確的理解在響應(yīng)鏈的過(guò)程中對(duì) super 關(guān)鍵字的理解,之所以在以后的技術(shù)交流中又能在 super 這個(gè)技術(shù)點(diǎn)上添點(diǎn)油加點(diǎn)醋了捌袜。
到這里也應(yīng)該也能解釋當(dāng)點(diǎn)擊標(biāo)簽4 的現(xiàn)象了说搅,其實(shí)這個(gè)問(wèn)題是我在 15年的時(shí)候親自遇到的一個(gè) BUG,具體的現(xiàn)象是有一個(gè)按鈕虏等,在有的設(shè)備上只能下半部分點(diǎn)擊有反應(yīng)弄唧,在按鈕的下半部分點(diǎn)擊是沒(méi)有問(wèn)題的适肠,就是因?yàn)樯习氩糠殖隽烁缚丶恕?br>
這里有一些的技術(shù)交流中會(huì)出現(xiàn)的問(wèn)題:
如何讓UIView處理事件的同時(shí)繼續(xù)傳遞事件.
如何判斷一個(gè)UIView是View Controller的Root View
這些答案就顯得 手一日 了。
關(guān)于事件傳遞與響應(yīng)鏈候引,在開(kāi)發(fā)中很有可能會(huì)遇到這兩個(gè)系統(tǒng)方法迂猴,在現(xiàn)有項(xiàng)目中都有使用,可以看一下:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
關(guān)于時(shí)間傳遞與響應(yīng)鏈的內(nèi)容背伴,還能想到的是手勢(shì),還有 UIButton 的 Action 峰髓,暫時(shí)感覺(jué)這些除了都是由觸摸發(fā)起的之外傻寂,貌似沒(méi)有多大的關(guān)系,畢竟屬于不同的機(jī)制(可能是我的理解還不夠深入)携兵。比如在 UIButton 中會(huì)遇到這些現(xiàn)象:
1疾掰、按鈕添加手勢(shì),那么 action 就得不到執(zhí)行徐紧。
2静檬、按鈕沒(méi)有添加手勢(shì),想要屏蔽 action 的話就可以重寫(xiě) touchesBegan 不調(diào)用 super并级。
反正就是各種的沖突拂檩,但是話又說(shuō)回來(lái),一般情況誰(shuí)會(huì)在同一個(gè)控件上搞這些事件呢嘲碧,除非像圖片瀏覽器稻励,可以有點(diǎn)擊、可以有雙擊愈涩、也可以有旋轉(zhuǎn)望抽,但是這些都是屬于手勢(shì)范疇。在 LongLong Ago履婉,還會(huì)出現(xiàn) UIButton 的 superView 添加手勢(shì)的情況會(huì)影響到 UIButton煤篙,但是現(xiàn)在蘋果在很久之前就已經(jīng)優(yōu)化。
當(dāng)然在一些特殊的場(chǎng)景毁腿,可能手勢(shì)與觸摸沖突了辑奈,蘋果也已經(jīng)考慮到?jīng)_突的情況,所以也提供了這樣的 delegate 方法:
// called before touchesBegan:withEvent: is called on the gesture recognizer for a new touch. return NO to prevent the gesture recognizer from seeing this touch
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;
五狸棍、官方參考
在網(wǎng)上有很多大佬都在使用這張圖片來(lái)對(duì)響應(yīng)鏈做介紹:
這個(gè)對(duì)響應(yīng)鏈的解釋算是最權(quán)威的了身害,在GitHub 找到了一 大佬 的翻譯 Event-Handling-Guide-for-iOS, 可以參考一下草戈。
這里有一個(gè)最新的官方介紹塌鸯,請(qǐng)看這張圖片與上面的圖片是一樣的:
仔細(xì)觀察,這張圖與上面的那張圖還是有區(qū)別的唐片,多了一個(gè) UIApplicationDelegate丙猬。難道在很久之前的響應(yīng)鏈直接到 UIApplication 就結(jié)束了涨颜??這個(gè)就不是太清楚了茧球,確實(shí)在之前大佬們的介紹中沒(méi)有提到 UIApplicationDelegate 的庭瑰。但是同時(shí)也要清楚一點(diǎn) UIApplicationDelegate 是 Delegate 過(guò)來(lái)的。
更多的詳細(xì)信息在這里 using_responders_and_the_responder_chain_to_handle_events
0x04 金典案例
在這里有兩個(gè)例子:線程鼻缆瘢活與UIResponder分類分別介紹了 NSRunloop 與 UIRespomder 的應(yīng)用弹灭。具體的代碼在這里:UIResponder
一、UIResponder分類
核心代碼:
/**
響應(yīng)鏈
@param eventName 事件名稱
@param eventInfo 事件信息
*/
- (void)routerEventWithName:(NSString *)eventName eventInfo:(NSDictionary *_Nullable)eventInfo {
[[self nextResponder] routerEventWithName:eventName eventInfo:eventInfo];
}
突然一看死循環(huán)揪垄、仔細(xì)一看很經(jīng)典穷吮。這也是有使用場(chǎng)景的,這個(gè)使用場(chǎng)景也可用通知來(lái)實(shí)現(xiàn)饥努。具體的使用捡鱼,可以自行品味。
二酷愧、線程奔菡活
我們知道,一個(gè)線程只要是任務(wù)結(jié)束了溶浴,那么當(dāng)前的線程馬上就退出了乍迄。如果有一批的任務(wù)需要在一個(gè)單獨(dú)的線程中處理,但是又不想每次使用都創(chuàng)建一個(gè)新的線程戳葵,那么久需要考慮將同一個(gè)線程一直處于活躍狀態(tài)而不退出就乓。這個(gè)功能主要就是結(jié)合 NSRunloop 來(lái)處理。
具體的代碼拱烁,在這里:
代碼不多生蚁,但是技術(shù)點(diǎn)還真不少,主要集中于這三條:
1戏自、NSThread 的使用
2邦投、NSRunloop 的 run 方法
3、waitUntilDone參數(shù)選擇
接下來(lái)一一介紹擅笔。
2.1 NSThread 的使用
現(xiàn)在使用的是 initWithBlock 方法 很像 NSTimer志衣,那么第一個(gè)問(wèn)題來(lái)了,這個(gè) block 中可否使用如下的代碼:
__strong typeof(weakSelf) self = weakSelf;
答案是不可以的猛们,即使這樣使用了念脯,貌似還是內(nèi)存泄漏了,這個(gè)具體的原因有待進(jìn)一步的研究弯淘,應(yīng)該是這個(gè) Block 的內(nèi)部實(shí)現(xiàn) 很強(qiáng)大绿店,所以只能使用 weakSelf。
第二個(gè)問(wèn)題是:是否可以使用 initWithTarget?
其實(shí)這個(gè)也要注意假勿,我在之前的文檔中也介紹過(guò)借嗽,這種方式會(huì)像 NSTimer 一樣導(dǎo)致指針循環(huán), 但是還不好解決的樣子。我使用了 YYWeakProxy 都不行转培。恶导。。浸须。惨寿。
這樣看來(lái),這個(gè) NSThread 確實(shí)不簡(jiǎn)單删窒,有待進(jìn)一步的研究缤沦。所以當(dāng)前的實(shí)現(xiàn)來(lái)看、只可以使用 initWithBlock + weak 的方式易稠。
2.2 NSRunloop 的 run 方法
是的, NSRunloop 有三個(gè)去激活的方式:
- (void)run;
- (void)runUntilDate:(NSDate *)limitDate;
- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
這里使用了 第三個(gè)方法包蓝,第二個(gè)方法不符合現(xiàn)在的場(chǎng)景驶社。那么第一個(gè)方法為什么不可以呢?這是因?yàn)榈谝粋€(gè)方法是永遠(yuǎn)停不下來(lái)的测萎,一旦 run 了亡电,那么這個(gè)線程就永遠(yuǎn)的保活了硅瞧,這個(gè)可以從蘋果對(duì)這個(gè)方法的注釋中可以看出份乒。所以只能放棄這種方式。
在看代碼的過(guò)程中腕唧,還有一個(gè)屬性 stopped或辖。關(guān)于這個(gè)屬性,是與 runMode 這個(gè)方法的機(jī)制息息相關(guān)的:
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
這個(gè)方法的意思是將當(dāng)前的線程處于 run 的狀態(tài)在 beforeDate 之前枣接,distantFuture很大颂暇,所以能保持永遠(yuǎn)的處于 run 狀態(tài)的等待狀態(tài),但是有一個(gè)關(guān)鍵的點(diǎn)是:這個(gè)線程每執(zhí)行一次任務(wù)但惶,這個(gè)方法就返回了耳鸯。所以這里需要加一個(gè) stopped 屬性,當(dāng) runMode 返回之后再 runMode膀曾。
其次 while (weakSelf && !weakSelf.isStopped) 能換成 while (!weakSelf.isStopped) 答案是不可以的县爬,當(dāng) weakSelf 為 nil 的時(shí)候 !weakSelf.isStopped 的值為 YES. 所以。添谊。财喳。。碉钠。纲缓。
2.3 waitUntilDone參數(shù)選擇
主要在兩個(gè)地方使用了 performSelector:onThread:withObject:waitUntilDone: 方法卷拘,但是在 waitUntilDone 的參數(shù)卻各不相同,為啥祝高?
首先要明白這個(gè)參數(shù)的意義:代表從當(dāng)前的線程執(zhí)行另一個(gè)線程方法的時(shí)候栗弟,是否要等另一個(gè)線程的方法執(zhí)行結(jié)束之后,再返回工闺。為 YES 的時(shí)候代表必須要等另一個(gè)線程執(zhí)行結(jié)束了乍赫,才會(huì)返回。為 NO 的時(shí)候代表陆蟆,不等另一個(gè)線程的方法執(zhí)行結(jié)束雷厂,立馬就返回。
那這里為啥要使用 YES 呢叠殷?
這是因?yàn)檫@個(gè) __stop 的方法可能是在 HGPermenantThread 的 dealloc 方法中調(diào)用的改鲫。如果使用 NO 的話,那么HGPermenantThread 對(duì)象已經(jīng)沒(méi)有了林束,但是在 __stop 中又訪問(wèn)了 HGPermenantThread 對(duì)象像棘,所以壞內(nèi)存訪問(wèn)錯(cuò)誤了。
0x05 GCD 中的 Runloop 的 autorelease
直接看一段代碼:
NSLog(@"開(kāi)始上班了");
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"我應(yīng)該做點(diǎn)啥~");
});
NSLog(@"下班了");
可以嘗試回答一下這個(gè)打印順序是不是這樣的壶冒?
- 開(kāi)始上班了
- 下班了
- 我應(yīng)該做點(diǎn)啥~