由 NSObject *obj = [[NSObject alloc] init] 引起的一二事兒

本文是爲了準備在實習公司新人串講中部份內容的草稿,主要介紹一些 Objective-C 和 iOS 基礎的東西,純屬拋磚引玉~html

Objective-C 基礎

接下來跟你們分享一下 Objective-C 和 iOS 開發的基礎內容,並且主要會圍繞一句普通的代碼進行展開:編程

NSObject *obj = [[NSObject alloc] init];
複製代碼

其實這部份內容大都是我本身對這行代碼冒出的一些的問題和想法進行的解釋,並且準備得有些倉促,因此不免會有些不全面和錯漏的地方,請多多見諒~swift

基本含義

咱們先來看看這句代碼的基本含義,嘗試從 NSObject 這個角度去解讀promise

這行代碼中寫有兩個 NSObject ,但他們表示的意思是不同的。安全

等號左邊表示:建立了一個 NSObject 類型的指針 obj 。(開闢一個 NSObject 類型大小的內存空間,並用指針變量 obj 指向它) 等號右邊表示:調用 NSObject 對象的類方法 alloc 進行內存空間的分配,調用實例方法 init 進行構造工做,如成員變量的初始化等。 等號右邊的 NSObject 對象初始化完成以後將內存地址賦值給左邊的 obj 。bash

new 方法和 alloc/init

感受使用 Java 的人常常會說 new 一個對象,雖然 Objective-C 也給咱們提供了這個方法,但咱們卻不多直接使用 new ,而是使用 alloc init ,爲何?微信

使用 new 和 使用 alloc init 均可以建立一個對象,並且在 new 的內部其實也是調用 alloc 和 init 方法,只是 alloc 會在分配內存時使用到 zone ,其實整體來看沒啥區別。網絡

NSZone 是 Apple 用來處理內存碎片化的優化方式,處理對象的初始化及釋放等問題,以提升性能。但聽說效果並很差。閉包

使用 new 的好處是什麼?併發

  • 簡單,比使用 alloc init 少碼一個單詞和一對中括號

使用 alloc init 的好處是什麼?

  • 顯式調用,少了 new 內部實現的轉換,可能速度更快(速度慢)
  • 支持自定義構造方法。通常在咱們的自定義類的構造方法中都會使用到如 initWithXxx

爲啥 Objective-C 的構造方法都是 initWithXxx ,而 swift 可使用 init

  • Objective-C 不支持函數重載,因此使用 initWithXxx 替代
  • swift 支持函數重載

只是聲明一個變量 NSObject *obj; ?swift 呢 ?

在 Objective-C 中容許只聲明一個變量並被使用,編譯器不會報錯。若是聲明的是一個 Objective-C 對象,輸出的值是 null ,若是是基本類型,輸出的值是 0 ,若是是結構體如 CGRect ,會用 0 填充。

可是在 swift 中,狀況就不同了,聲明一個變量如 let a : Int 並被使用時,編譯器會對這種行爲報以錯誤提示,Variable 'a' used before being initialized ,表示變量 a 在使用前未被初始化。

爲何?

其實在 Objective-C 中聲明一個變量時,它是會有一個默認值的,但在 swift 中則不會提供默認值,由於 swift 做爲一種強類型語言,它老是強制類型定義,而且要求變量的使用要嚴格符合定義,全部變量都必須先定義,且初始化後才能被使用。

這裏有一個例外 -- 可選類型,如 let b : Int? ,可選屬性不須要設置初始值,默認的初始值都是 nil ,無論是基礎類型仍是對象類型的可選類型屬性的初始值都是 nil 。並且可選類型是在設置數值的時候才分配空間,是一種 lazy-evaluation 即延遲計算的行爲。

Objective-C 和 swift 構造方法順序的區別?

  • 在 swift 的構造方法中,先正確初始化本類的成員變量,再調用父類的構造方法
  • 在 Objective-C 的構造方法中,先調用父類的構造方法,再正確初始化本類的成員變量

構造方法 init 中使用 if(self = [super init])寫法

日常在構造方法裏作一些初始化工做時都會寫上這樣的代碼, self = [super init] 這裏先調用父類的構造方法也符合上述的構造順序問題,但疑惑的是,爲何 [super init] 要賦值給 self ?爲何須要使用 if 做校驗?

Objective-C 方法的調用,會轉換成消息發送的代碼,如 id objc_msgSend(id self, SEL op, ...);

MyClass *myObject = [[MyClass alloc] initWithString:@"someString"];
複製代碼

上述代碼會被編譯器轉換成:

class myClass = objc_getClass("MyClass");
SEL allocSelector = @selector(alloc);
MyClass *myObject1 = objc_msgSend(myClass, allocSelector);

SEL initSelector = @selector(initWithString:);
MyClass *myObject2 = objc_msgSend(myObject1, initSelector, @"someString");
複製代碼

能夠看到,當調用 objc_msgSend(myObject1, initSelector, @"someString") 時 self 已經有值了,它的值是 myObject1 。

回到 [super init] 這句代碼,要注意,它不是被編譯器轉換成 objc_msgSend(super, @selector(init)) ,而是會被轉換成 objc_msgSendSuper(self, @selector(init))

是的,self 在初始化方法開始執行時已經有值了。

這裏的 super 是一個編譯器指令,和 self 指向同一個消息接受者,即當前調用方法的實例。他們兩個的不一樣點在於:super 會告訴編譯器,執行 [super xxx] 時轉換成 objc_msgSendSuper ,即要去父類的方法列表找,而不是本類。

那麼爲何要將 [super init] 方法的返回值賦值給 self 呢?

來看一段常見的構造方法代碼片斷:

- (id)initWithString:(NSString *)aString
{
    self = [super init];
    if (self)
    {
        instanceString = [aString retain];
    }
    return self;
}
複製代碼

經典解釋:執行 [super init] 會產生如下三種結果中的一種:

  1. 返回方法的隱含參數 self ,且初始化完成繼承的實例變量
  2. 返回一個不一樣的對象,且初始化完成繼承的實例變量
  3. 返回 nil ,初始化失敗

第一種結果,賦值操做對 self 沒有影響,後面的實例變量賦值在了原始對象上。 第三種結果,初始化失敗,self 被賦值爲 nil ,返回。

至於第二種結果,若是返回的對象不同,那麼就須要將 instanceString = [aString retain] (被轉換成 self->instanceString = [aString retain])方法實現裏的 self 指向新的值。

那麼問題來了,[super init] 會返回不一樣的對象?

是的!在如下狀況會返回不一樣的對象(所謂不一樣對象,是內存地址的不一樣):

  • 單例對象
  • 一些特別的對象(如 [NSNumber numberWithInteger:0] 老是返回全局的 "zero" 對象)
  • 類簇
  • 根據傳入初始化方法的參數從新分配原(或兼容)類。這種狀況下若是繼續初始化返回的已經改變的對象是一種錯誤的行爲,由於這時返回的對象已經被徹底初始化了,而且跟自己的類再也不相關。

如今,根據返回的對象是否不一樣,執行 [super init] 產生的結果擴展爲如下四種:

  1. 返回方法的隱含參數 self ,且初始化完成繼承的實例變量
  2. 返回一個相同的對象,但需要進一步的初始化工做
  3. 返回一個不一樣的已經初始化完成的對象
  4. 返回 nil ,初始化失敗

能夠看到,case 2 和 3 實際上是互斥的,咱們通常沒法使用一種途徑來知足全部的這四種 case 。

常見的可以知足 case 1,2 和 4 的作法是: self = [super init]; 即上面的作法。

這裏展現一種可以知足 case 1,3,和 4 的途徑,即日常會被問到可否用一個變量替代 self 的作法是:

- (id)initWithString:(NSString *)aString
{
    id result = [super init];
    if (self == result)
    {
        instanceString = [aString retain];
    }
    return result;
}
複製代碼

因此類簇,單例和特殊的對象都是 case 3 ,NSManagedObject 是 case 2 。

能夠看到 case 3 很是常見,可是在構造方法中知足 case 1,2 和 4 變成了一種 standard (雖然在某些隱藏條件下是錯誤的作法)。

解釋參考

Objective-C 中的基礎類型和對象類型

在這行代碼 NSObject *obj = [[NSObject alloc] init]; 等號左邊的 NSObject 表示的是對象類型,那麼在 Objective-C 中常見的基礎類型和對象類型有哪些

基礎類型:

  • 整型:int(32位)、Integer(根據計算機位數調整)
  • 浮點型:float(4 字節)、double(8 字節)、CGFloat(根據計算機位數調整)
  • 字符型:1 字節,Objective-C 字符變量不支持中文字符,字符須要使用 ` ` 包起來,char 類型也能夠看做整型值來使用,它是一個 8 位無符號整數
  • 布爾型:YES、NO
  • 枚舉型

對象類型:

  • NSObject
  • NSString 及可變版本 NSMutableString
  • NSArray 及可變版本 NSMutableArray
  • NSDictionary 及可變版本 NSMutableDictionary

這裏有一個注意點是,有可變與不可變類型的對象,爲了安全起見,用 copy 修飾不可變版本,用 strong 修飾可變版本,這樣作的緣由是,若是有一個不可變的字符串 str 且用 strong 修飾,這時被賦值了一個可變字符串 mStr ,這樣可能會發生這樣的狀況:一個原本預想中不可變的字符串 str 會因 mStr 的改變而改變。因此這裏要仔細考量一下使用 copy 仍是 strong 去修飾。

Objective-C 世界中的 「非 0 即真」 ?

在 Objective-C 中,BOOl 的定義是這樣的:

typedef signed char BOOL;
#define YES (BOOL)1
#define NO (BOOL)0
複製代碼

其餘相關的布爾型以下:

bool :

C99標準定義了一個新的關鍵字_Bool,提供了布爾類型
#define bool _Bool
#define true 1 
#define false 0
複製代碼

Boolean:

typedef unsigned char Boolean;
enum DYLD_BOOL { FALSE, TRUE };
複製代碼

Objective-C 對象存儲在堆區

上面談到了 NSObject 做爲類型展開的一些內容,如今咱們來看看 NSObject 做爲對象來延伸出 Objective-C 對象存儲位置相關的內容。

先來看看內存的五大區:

  • 靜態區(BSS 段)
  • 常量區(數據段)
  • 代碼段

仍是回到最開始的那行代碼來進行解釋:

NSObject *obj = [[NSObject alloc] init];
複製代碼

咱們知道,在 Objective-C 中,對象一般是指一塊有特定佈局的連續內存區域。這行代碼建立了一個 NSObject 類型的指針 obj 和一個 NSObject 類型的對象,obj 指針存儲在棧上,而其指向的對象則存儲在堆上。

在棧上就不能建立對象嗎?

* 不能直接建立,但可經過在結構體中的 isa 來間接建立對象。

  • 其實 block 也能夠存儲在棧上

那麼這裏又帶來了幾個問題,isa 和 block ,這在後面會單獨聊。

棧對象的優缺點

那麼爲何 Objective-C 會選擇使用堆來存儲對象而不是棧,來看看棧對象的優缺點。

優勢:

  • 建立速度和運行時速度快:相對於堆對象建立時間快幾十倍;編譯期能肯定大部份內存佈局,於是在運行時分配空間幾乎不耗時
  • 生命週期固定:對象出棧就會被釋放,不會存在內存泄漏

缺點:

  • 生命週期固定,可能會出現這種狀況:一個棧對象被建立以後被傳遞到別的方法,當棧對象的建立方法返回時,棧對象會被一塊兒 pop 出棧而釋放,致使無法在別處被繼續持有,此時 retain 會失效,所以,棧對象會給對象的內存管理形成至關大的麻煩。
  • 空間:棧跟線程具備綁定關係,而棧的可用空間很是有限的。所以對象若是都在棧上建立不太現實,而堆只要物理內存不警告便可使用。
  • 512 KB (secondary threads)
  • 8 MB (OS X main thread)
  • 1 MB (iOS main thread)

綜上,Objective-C 選擇使用堆存儲對象。

NSString 的存儲位置

關於 NSString 的存儲位置很是複雜,能夠分配在棧區、堆區、常量區,粗略的理解以下:

  • 當建立的 NSString 類型底層是 NSTaggedPointerString 時,其本質再也不是一個對象,而是真正的值,存儲在棧區
  • 當建立的 NSString 類型底層是 __NSCFConstantString 時,其 retainCount 極大,意味着不會被釋放,存儲在常量區(通常經過字面量進行建立)

block 的存儲位置

block 能夠存儲在棧上,也能夠存儲在堆上。一般咱們會使用 copy 將一個棧上的 block 複製到堆上。

順便談談關於 block 的其餘內容:

block 的意思是擁有自動變量的匿名函數。

  • 在 Objective-C 中稱爲 block
  • 在 swift 中稱爲閉包
  • 在 Java 中稱爲 lambda(也稱閉包)

這裏要注意的是,因爲棧對象的有效區域僅限於其所在的塊 {} ,即其捕獲自動變量的範圍也僅限於所在塊。

還有一個修改自動變量時的注意點是:

  • 靜態全局變量,全局變量因爲做用域的緣由,能夠直接在 block 裏被修改
  • 靜態變量因爲傳遞給 block 的是內存地址值,因此也能在 block 裏被修改
  • 默認狀況下是不容許修改捕獲到的自動變量值,但咱們能夠經過使用 __block(storage-class-specifier,存儲域說明符) 修飾變量來使該變量也能在 block 裏被修改

Mansory 的 block 爲何不循環引用

到這兒由 block 聯想到了咱們項目中使用到的自動佈局框架 Mansory ,好比項目中的一個代碼片斷:

[self.carousel mas_remakeConstraints:^(MASConstraintMaker *make) {
            make.top.mas_equalTo(self.headerView);
            make.left.mas_equalTo(self.headerView).offset(20);
            make.right.mas_equalTo(self.headerView).offset(-20);
            make.height.mas_equalTo(CGFLOAT_MIN);
        }];
複製代碼

一般在使用 block 時都會避免在 block 內部使用 self ,以避免產生循環引用,形成內存泄漏,因此一般會在 block 外部對 self 進行一次弱引用,再在內部進行一次強引用,用這種組合作法來避免產生循環引用現象,這裏的循環引用現象多是:self -> block -> self 。

而後咱們經過觀察其源碼實現來進一步瞭解:

- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    constraintMaker.removeExisting = YES;
    block(constraintMaker);
    return [constraintMaker install];
}
複製代碼

結合前面介紹的關於 block 存儲位置的內容,咱們能夠知道,雖然 block 內部引用了 self ,但因爲這是一個局部的 block ,存儲在棧上而不是堆上,於是在出了 block 所在做用域後會被 pop 出棧而自動銷燬,因此不存在引用環。

聊聊 block 和 Delegate

講完 block 的存儲位置,天然會想到它的一些使用場景,特別是選在擇使用 block 仍是代理的一些爭執吧。

其實我以爲如何進行選擇更多的是依據我的的一些編碼風格和習慣,還有就是要符合原有項目的須要,說到底使用 block 和代理都沒問題。但因爲一些函數式框架的出現,好比 RAC 、RxSwift 、 promisekit ,裏面鏈式調用 + 閉包的操做實在是很方便,並且也更加符合低耦合高內聚的編程理念,因此可能選擇使用 block 又多了一個理由。

下面再聊一下 block 和 delegate 的一些本質區別,這部份內容主要是引述微信技術羣裏面的一位大佬的解釋:

代理的 debug 追蹤性確實會比 block 好,可是若是跟 block 在可讀性方面比較的話其實算是弱項。

代理和 block 實際上都是函數 imp 的調用,但區別是,代理就等價於 weak 持有一個代理對象,你不寫 protocol 不寫 delegate ,一股腦把全部方法全寫在 header 裏,而後把代理對象自己直接傳過去給另外一個對象,在另外一個對象中 weak 持有這個代理對象,這種寫法和代理是沒有區別的。

而 block 是一種還原上下文環境,甚至自動包裹一些自由變量的閉包概念,換句話說,block 的回調代碼,和寫 block 的代碼,是能夠同處於一個函數內,在一個可讀代碼上下文內,即 block 在代碼上是一個連續的過程。

代理方法實際上傳值傳的是一整個對象,你把 a.delegate = self 實際上是把 self 傳給了 a 持有,跟通常的屬性賦值無異,若是再次傳遞,徹底能夠繼續傳遞 self 給別人。

block 繼續傳遞,其實是把 imp 和上下文環境的自動變量打包進行傳遞,這個過程當中不必定會傳遞一個對象。從這個角度看 block 的控制力度更強一些。

這裏會涉及到一個安全性方面的考慮,你把 self 傳給了一個不知名的三方庫,他雖然只是 id 看起來只能調用 protocol 裏限定的方法,但其實 OC 這個約束只是騙騙編譯器的。若是你把一個 self 傳給了一個三方,設定爲代理,若是三方有其餘意圖,他其實能夠直接控制你的 self 對象的任意值或者方法。但 block ,你傳過去的 block ,他只能操做 block 自己包裹的上下文環境。

ARC 下編譯器自動補 __strong 修飾

扯得有點遠了,咱回頭看回最開始的那行代碼 NSObject *obj = [[NSObject alloc] init]; ,在 ARC 下會變成 __strong NSObject *obj = [[NSObject alloc] init]; ,這涉及到 iOS 開發的內存管理相關內容。

在早期 macOS 開發中使用 GC 進行內存管理但如今都跟 iOS 開發同樣已統一使用引用計數進行內存管理:當對象的引用計數爲 0 時會被銷燬,當對象被引用時其引用計數會 +1 ,當對象的引用被銷燬時引用計數 -1 。

  • ARC(automatic reference counting),自動引用計數
  • MRC(manual reference counting),手動引用計數

__strong 是一個變量修飾符,但這裏不打算列舉其餘的變量修飾符,而會在下一條聊聊關於內存管理相關的屬性修飾符。

屬性修飾符

內存管理相關的變量修飾符都有相對應的屬性修飾符,通常的寫法是在屬性修飾符前添加兩個下劃線。

這裏列舉一下內管管理語義的屬性修飾符:

  • assign
  • strong
  • weak
  • unsafe_unretained
  • copy

設置上述屬性修飾符會在屬性自動生成 setter 方法的時候爲咱們添加內存管理語義,明確內存管理全部權,若是咱們自定義 setter 訪問器,則需手動指定。

  • assign:在 setter 方法中只是進行簡單的賦值操做。適用於一些標量類型和 id 類型,如 CGFloat、NSInteger 。
  • strong:會指定全部權關係。在 setter 方法中,當一個新值要被設置時,首先 retain 新值,而後 release 舊值,最後再進行賦值。
  • weak:會指定無全部權關係。在 setter 方法中,當一個新值要被設置時,既不會 retain 新值,也不會 release 舊值,只會進行簡單的賦值操做。在屬性所指向的對象銷燬時,屬性值會清空。
  • unsafe_unretained:跟 assign 的語義相同,但適用於 OC 對象。而且與 weak 不一樣的是,在屬性所指向的對象銷燬時,屬性值不會被清空。
  • copy:與 strong 相似,但在 setter 方法中不會 retain 新值,而是將其拷貝。

標量類型缺省是 assign ,對象類型缺省是 strong 。

再聊聊屬性 @property

咱們知道,屬性 = 實例變量(ivar) + setter + getter ,他的具體過程是這樣的:

完成屬性定義後,編譯器會自動編寫訪問這些屬性所需的訪問器,此過程稱爲「自動合成」(autosynthesize)。須要強調的是,這個過程由編譯器在編譯期執行,因此在編譯器中看不到自動生成的源代碼。除了生成訪問器 getter 、setter 以外,編譯器還要自動向類中添加適當類型的實例變量,實例變量名稱是在屬性名前加下劃線,咱們也能夠在類的實現代碼裏經過 @synthsize 語法來指定實例變量的名字,如:

@implementation Person
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end
複製代碼

這裏有一個注意的地方,@synthesize firstName; 像這樣不指定實例變量的名字,那麼生成的實例變量名會跟屬性名一致,而不會再加下劃線。

還有一個要注意的關鍵字:@dynamic ,他不會在編譯階段自動生成 getter 和 setter 方法,並且使用點語法或者賦值操做在編譯階段仍可以經過,可是該屬性的訪問器必須在運行時由用戶本身實現,不然會 crash 。

isa

在前面咱們有說到 Objective-C 對象一般是一塊有特定佈局的連續內存區域,因此接下來牽扯的內容可能會扯得比較遠。

在計算機網絡中有一堆協議,遵照同一個協議,那麼他們之間即可以知曉對方的身份,接着愉快的進行通訊。

那麼 Objective-C 做爲一門面向對象語言,他是怎樣判斷一個東西是否是對象,又是如何進行對象間的通訊?

Objective-C 中的對象是一個指向 ClassObject 地址的變量: id obj = &ClassObject

這個地址其實就是在最高位的 isa 指針。

而對象的實例變量則是:

void *ivar = isa + offset(N)

因此 isa 就至關於對象之間的一個協議。

Objective-C 面向對象的一個 bug :好比一個 Person 實例,她調用一個 talk 方法,按理說,這個 talk 方法應該直接在該實例裏面調用對應的實現,可是實際卻不是這樣,她會經過實例本身的 isa 指針找到對應的類,而後在類或及其類的繼承結構一直往上尋找 talk 方法,該方法被找到後就會調用,這樣看來就並非最初的那個實例進行調用。

atomic 不安全

屬性還有一類原子性修飾符,atomic 和 nonatomic ,原子性和非原子性,缺省是原子性的,但在 iOS 開發中,幾乎全部屬性都會主動聲明爲 nonatomic 。

緣由有兩個:

  1. atomic 有性能消耗
  2. atomic 沒法保證更大粒度時的安全問題

第一點,是由於使用 atomic 修飾的屬性由編譯器所合成的方法會經過鎖機制(底層使用自旋鎖)來確保原子性。 第二點,是由於即使原子操做阻止了屬性被多個線程同時進行訪問,但這並不表明咱們最終使用它們的代碼時時線程安全的,好比並發訪問粒度更大的實例中的屬性,舉個例子:

// Person.h
@property(atomic, copy) NSString *firstName;
@property(atomic, copy) NSString *lastName;
- (void)updateWithFirstName:(NSString *)firstName lastName:(NSString *)lastName delay:(double)t on:(dispatch_queue_t) q;

// Person.m
- (void)updateWithFirstName:(NSString *)firstName lastName:(NSString *)lastName delay:(double)t on:(dispatch_queue_t)q{
    if (firstName != nil) {
        self.firstName = firstName;
    }

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(t * NSEC_PER_SEC)), q, ^{
        if (lastName != nil) {
            self.lastName = lastName;
        }
    });

}
    
// ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];

    Person *p = [[Person alloc] init];
    p.firstName = @"Holy";
    p.lastName = @"H";

    dispatch_queue_t queueA = dispatch_queue_create("queueA", 0);
    dispatch_queue_t queueB = dispatch_queue_create("queueB", 0);

    dispatch_async(queueA, ^{
        [p updateWithFirstName:@"John" lastName:@"J" delay:0.0 on:queueA];
    });

    dispatch_async(queueB, ^{
        [p updateWithFirstName:@"Ben" lastName:@"B" delay:0.0 on:queueB];
    });

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"%@ %@", p.firstName, p.lastName);
    });

}
複製代碼

如今運行結果可能會出現

Ben J
複製代碼

這顯然不是咱們最初想要獲得的。

到目前爲止,Apple 公司的開發者們並無爲 swift 的屬性提供標記 atomic/nonatomic 的方法,也沒有下面提到的 @synchronized 塊那樣去作互斥操做,而咱們能夠經過使用 @synchronized 底層使用到的 objc_sync_enter(obj)objc_sync_exit(obj) 去實現,但由於 objc_sync_xxx 是至關底層的方案,通常不推薦直接使用,而應選擇其餘高階的方案。

併發

那麼如何解決上述示例的問題?咱們能夠經過在修改時添加 @synchroized(obj) {} 塊,將原子操做的粒度擴大到 obj 對象的修改域。

還有其餘一些常見的同步機制如:NSLock、pthread、OSSpinLock、信號量等。

iOS 基礎

事件產生、傳遞和響應鏈

最後簡單介紹一下 iOS 開發中事件的產生、傳遞和響應鏈。

事件產生:

系統註冊了一個 Source 1(基於 mach port)用來接收系統事件,其回調函數爲 _IOHIDEventSystemClientQueueCallback() 。當一個硬件事件(好比觸摸/鎖屏/搖晃等)發生後,首先由 IOKit.framework 生成一個 IOHIDEvent 事件並由 SpringBoard 接收,SpringBoard 只接收按鍵(鎖屏/靜音等)、觸摸、加速、傳感器等幾種 event ,隨後用 mach port 轉發給須要的 APP 進程。隨後蘋果註冊的哪一個 Source 1 就會觸發回調,並調用 _UIApplicationHandleEventQueue() 進行應用內部的分發。

_UIApplicationHandleEventQueue() 會以先進先出的順序把 IOHIDEvent 處理幷包裝成 UIEvent 進行處理分發,其中包括識別 UIGesture /處理屏幕旋轉/發送給 UIWindow 等。一般事件好比 UIButton 點擊、touchesBegan/Moved/End/Cancel 等都是在這個回調中完成的。

觸摸事件傳遞,大體是從父控件傳遞到子控件:

UIApplication -> UIWindow -> UIView (or Gesture recognizer 這時會被當前 vc 截斷) -> 尋找處理事件最合適的 view

那麼如何尋找處理事件最合適的 view ?步驟:

  1. 首先判斷主窗口可否接收事件
  2. 觸摸點是否在本身身上
  3. 若在,從後往前遍歷本身的子控件,重複前兩個步驟(可否接收事件?是否在控件上?)
  4. 直到找不到合適的子控件,那麼本身就成爲最合適的 view ,以後調用具體的如 touches 方法處理

底層實現主要涉及兩個方法:func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?func point(inside point: CGPoint, with event: UIEvent?) -> Bool

hitTest 方法會根據視圖層級結構往上調用 pointInside 方法,肯定可否接收事件。若是 pointInside 返回 true ,則繼續調用子視圖層級結構,直到在最遠的視圖找到點擊的 point 。若是一個視圖沒有找到該 point ,則不會繼續它往上的視圖層級結構。

咱們能夠經過調用這個方法來截獲和轉發事件。

事件響應,大體是從子控件傳遞到父控件:

過程:

  1. 若是當前 view 是控制器的 view ,那麼控制器就是上一個響應者,事件就傳遞給控制器;若是當前 view 不是控制器的 view ,那麼父視圖就是當前 view 的上一個響應者,事件就傳遞給它的父視圖
  2. 在視圖層次結構的最頂級視圖,若是也不能處理收到的事件或消息,則其將事件或消息傳遞給 window 對象進行處理
  3. 若是 window 對象也不處理,則其將事件或消息傳遞給 UIApplication 對象
  4. 若是 UIApplication 也不能處理該事件或消息,則將其丟棄

在上述過程當中,若是某個控件實現了 touchesXxx 方法,則這個事件將由該控件接管,若是調用 super 的 touchesXxx ,就會將事件順着響應者鏈繼續往上傳遞,接着會調用上一個響應者的 touchesXxx 方法。

通常咱們會選擇使用 block 或者 delegate 或者 notification center 去作一些消息事件的傳遞,而如今咱們也能夠利用響應者鏈的關係來進行消息事件的傳遞。

相關文章
相關標籤/搜索