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

JABread發表於2018-08-03

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

Objective-C 基礎

接下來跟大家分享一下 Objective-C 和 iOS 開發的基礎內容,而且主要會圍繞一句普通的程式碼進行展開:

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

其實這部分內容大都是我自己對這行程式碼冒出的一些的問題和想法進行的解釋,而且準備得有些倉促,所以難免會有些不全面和錯漏的地方,請多多見諒~

基本含義

我們先來看看這句程式碼的基本含義,嘗試從 NSObject 這個角度去解讀

這行程式碼中寫有兩個 NSObject ,但他們表示的意思是不一樣的。

等號左邊表示:建立了一個 NSObject 型別的指標 obj 。(開闢一個 NSObject 型別大小的記憶體空間,並用指標變數 obj 指向它) 等號右邊表示:呼叫 NSObject 物件的類方法 alloc 進行記憶體空間的分配,呼叫例項方法 init 進行構造工作,如成員變數的初始化等。 等號右邊的 NSObject 物件初始化完成之後將記憶體地址賦值給左邊的 obj 。

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 去做一些訊息事件的傳遞,而現在我們也可以利用響應者鏈的關係來進行訊息事件的傳遞。

相關文章