iOS-面試題2-Runtime、Runloop

目錄:

  1. isa存儲(chǔ)信息分析
  2. Class的內(nèi)部結(jié)構(gòu)、method_t、cache
  3. objc_msgSend底層調(diào)用流程
  4. super
  5. Runtime-API
  6. Runloop

一. Runtime

1. isa存儲(chǔ)信息分析

  1. 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ǔ)更多的信息。

  2. 為什么使用共用體?

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á)到了代碼可讀性的目的。

博客地址:Runtime1-isa存儲(chǔ)信息分析

2. Class的內(nèi)部結(jié)構(gòu)、method_t、cache

  1. 關(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ù)的地址

  1. 查找方法的過程

這里用對(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)用流程

  1. 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。

消息發(fā)送.png
動(dòng)態(tài)方法解析.png
消息轉(zhuǎn)發(fā).png
  1. 說一下消息轉(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。

  2. @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

  1. [super message]的底層實(shí)現(xiàn)
    ① 消息接收者仍然是子類對(duì)象
    ② 從父類開始查找方法的實(shí)現(xiàn)

  2. 如何降低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)或者第三方框架的方法。

  1. 如何攔截所有按鈕的點(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)擊事件。

  1. 為什么上面要加個(gè)dispatch_once?
    按理說load方法只會(huì)調(diào)用一次,萬(wàn)一別人主動(dòng)調(diào)用了load方法那不就調(diào)用兩次了嗎,這樣方法就交換兩次了和沒交換一樣,所以加個(gè)dispatch_once。

  2. 交換方法實(shí)現(xiàn)的原理是什么?
    method_exchangeImplementations方法是傳入兩個(gè)Method,以前我們講過Method的內(nèi)部結(jié)構(gòu),其實(shí)交換方法實(shí)現(xiàn)就是把Method里面的IMP交換了。

  3. 對(duì)于交換方法,如果這個(gè)方法有緩存,怎么辦?
    其實(shí),調(diào)用method_exchangeImplementations函數(shù)會(huì)清空緩存,這樣就保證了交換方法之后調(diào)用方法不會(huì)出錯(cuò)。

  4. 如何預(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];
}
  1. 如何預(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];
}
  1. 什么是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)用。

  2. 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

  1. 什么是Runloop?
    顧名思義,Runloop就是運(yùn)行循環(huán),就是在程序運(yùn)行過程中循環(huán)做一些事情。

  2. RunLoop的基本作用
    ① 保持程序的持續(xù)運(yùn)行
    ② 處理App中的各種事件(比如觸摸事件、定時(shí)器事件等)
    ③ 節(jié)省CPU資源,提高程序性能:該做事時(shí)做事,該休息時(shí)休息

  3. 兩套R(shí)unLoop API
    ① NSRunLoop和CFRunLoopRef都代表著RunLoop對(duì)象
    ② NSRunLoop是基于CFRunLoopRef的一層OC包裝
    ③ CFRunLoopRef是開源的:Core Foundation源碼

  4. 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。

  5. 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類型的東西)。

如下圖所示:

結(jié)構(gòu).png

① 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等等。

  1. 為什么多種模式要分開呢?
    比如scrollView滾動(dòng)的時(shí)候讓它切換到滾動(dòng)模式,那么在滾動(dòng)模式下,scrollView就專心處理滾動(dòng)相關(guān)的就可以了,以前模式下的事情就不處理了。如果不滾動(dòng),在正常模式下,就專心處理正常模式下的事情就好了,這樣可以做到流暢不卡頓。

  2. 常見的兩種Mode
    ① kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默認(rèn)Mode,通常主線程是在這個(gè)Mode下運(yùn)行。
    ② UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動(dòng),保證界面滑動(dòng)時(shí)不受其他 Mode 影響。

  3. 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ì)象。

  1. 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)
};
  1. 如何監(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中。

  2. RunLoop運(yùn)行流程圖

RunLoop的運(yùn)行流程.png
  1. 下面代碼是誰(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。

  1. RunLoop線程休眠的實(shí)現(xiàn)原理?
    線程休眠就是因?yàn)橛脩魬B(tài)和內(nèi)核態(tài)的切換。

  2. RunLoop在實(shí)際開發(fā)中的應(yīng)用有哪些?
    ① 解決NSTimer在滑動(dòng)時(shí)停止工作的問題
    ② 控制線程生命周期(線程?;睿?br> ③ 監(jiān)控應(yīng)用卡頓
    ④ 性能優(yōu)化

  3. NSRunLoopCommonModes是什么?
    NSRunLoopCommonModes并不是一個(gè)真的模式,它只是一個(gè)標(biāo)記,定時(shí)器能在_commonModes數(shù)組中存放的模式下工作。

  4. timer 與 runloop 的關(guān)系?
    ① RunLoop對(duì)象里面有個(gè)_modes數(shù)組,里面放一堆模式,模式里面會(huì)放timer,如果timer被標(biāo)記為commonModes,那么timer就能在_commonModes數(shù)組中存放的模式下工作,能在commonModes“模式”下工作的東西都會(huì)被添加到_commonModeItems數(shù)組里中。
    ② 如果線程休眠了,timer也可以喚醒休眠的RunLoop。

  5. runloop 是怎么響應(yīng)用戶操作的,具體流程是什么樣的?
    當(dāng)用戶有個(gè)點(diǎn)擊事件,這個(gè)系統(tǒng)事件會(huì)先被Source1捕捉,Source1捕捉之后會(huì)包裝成事件隊(duì)列(EventQuene),再放到Source0里面進(jìn)行處理,然后RunLoop循環(huán)再處理Source0里面的事件。

博客地址:
Runloop
線程?;?/a>

友情鏈接更多精彩內(nèi)容