本文是爲了準備在實習公司新人串講中部份內容的草稿,主要介紹一些 Objective-C 和 iOS 基礎的東西,純屬拋磚引玉~html
接下來跟你們分享一下 Objective-C 和 iOS 開發的基礎內容,並且主要會圍繞一句普通的代碼進行展開:編程
NSObject *obj = [[NSObject alloc] init];
複製代碼
其實這部份內容大都是我本身對這行代碼冒出的一些的問題和想法進行的解釋,並且準備得有些倉促,因此不免會有些不全面和錯漏的地方,請多多見諒~swift
咱們先來看看這句代碼的基本含義,嘗試從 NSObject 這個角度去解讀promise
這行代碼中寫有兩個 NSObject ,但他們表示的意思是不同的。安全
等號左邊表示:建立了一個 NSObject 類型的指針 obj 。(開闢一個 NSObject 類型大小的內存空間,並用指針變量 obj 指向它) 等號右邊表示:調用 NSObject 對象的類方法 alloc 進行內存空間的分配,調用實例方法 init 進行構造工做,如成員變量的初始化等。 等號右邊的 NSObject 對象初始化完成以後將內存地址賦值給左邊的 obj 。bash
感受使用 Java 的人常常會說 new 一個對象,雖然 Objective-C 也給咱們提供了這個方法,但咱們卻不多直接使用 new ,而是使用 alloc init ,爲何?微信
使用 new 和 使用 alloc init 均可以建立一個對象,並且在 new 的內部其實也是調用 alloc 和 init 方法,只是 alloc 會在分配內存時使用到 zone ,其實整體來看沒啥區別。網絡
NSZone 是 Apple 用來處理內存碎片化的優化方式,處理對象的初始化及釋放等問題,以提升性能。但聽說效果並很差。閉包
使用 new 的好處是什麼?併發
使用 alloc init 的好處是什麼?
initWithXxx
initWithXxx
,而 swift 可使用 init
?initWithXxx
替代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 即延遲計算的行爲。
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]
會產生如下三種結果中的一種:
第一種結果,賦值操做對 self 沒有影響,後面的實例變量賦值在了原始對象上。 第三種結果,初始化失敗,self 被賦值爲 nil ,返回。
至於第二種結果,若是返回的對象不同,那麼就須要將 instanceString = [aString retain]
(被轉換成 self->instanceString = [aString retain]
)方法實現裏的 self 指向新的值。
那麼問題來了,[super init]
會返回不一樣的對象?
是的!在如下狀況會返回不一樣的對象(所謂不一樣對象,是內存地址的不一樣):
[NSNumber numberWithInteger:0]
老是返回全局的 "zero" 對象)如今,根據返回的對象是否不一樣,執行 [super init]
產生的結果擴展爲如下四種:
能夠看到,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 (雖然在某些隱藏條件下是錯誤的作法)。
在這行代碼 NSObject *obj = [[NSObject alloc] init];
等號左邊的 NSObject 表示的是對象類型,那麼在 Objective-C 中常見的基礎類型和對象類型有哪些
基礎類型:
對象類型:
這裏有一個注意點是,有可變與不可變類型的對象,爲了安全起見,用 copy 修飾不可變版本,用 strong 修飾可變版本,這樣作的緣由是,若是有一個不可變的字符串 str 且用 strong 修飾,這時被賦值了一個可變字符串 mStr ,這樣可能會發生這樣的狀況:一個原本預想中不可變的字符串 str 會因 mStr 的改變而改變。因此這裏要仔細考量一下使用 copy 仍是 strong 去修飾。
在 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 };
複製代碼
上面談到了 NSObject 做爲類型展開的一些內容,如今咱們來看看 NSObject 做爲對象來延伸出 Objective-C 對象存儲位置相關的內容。
先來看看內存的五大區:
仍是回到最開始的那行代碼來進行解釋:
NSObject *obj = [[NSObject alloc] init];
複製代碼
咱們知道,在 Objective-C 中,對象一般是指一塊有特定佈局的連續內存區域。這行代碼建立了一個 NSObject 類型的指針 obj 和一個 NSObject 類型的對象,obj 指針存儲在棧上,而其指向的對象則存儲在堆上。
在棧上就不能建立對象嗎?
* 不能直接建立,但可經過在結構體中的 isa 來間接建立對象。
那麼這裏又帶來了幾個問題,isa 和 block ,這在後面會單獨聊。
那麼爲何 Objective-C 會選擇使用堆來存儲對象而不是棧,來看看棧對象的優缺點。
優勢:
缺點:
- 512 KB (secondary threads)
- 8 MB (OS X main thread)
- 1 MB (iOS main thread)
綜上,Objective-C 選擇使用堆存儲對象。
關於 NSString 的存儲位置很是複雜,能夠分配在棧區、堆區、常量區,粗略的理解以下:
block 能夠存儲在棧上,也能夠存儲在堆上。一般咱們會使用 copy
將一個棧上的 block 複製到堆上。
順便談談關於 block 的其餘內容:
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 的存儲位置,天然會想到它的一些使用場景,特別是選在擇使用 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 自己包裹的上下文環境。
扯得有點遠了,咱回頭看回最開始的那行代碼 NSObject *obj = [[NSObject alloc] init];
,在 ARC 下會變成 __strong NSObject *obj = [[NSObject alloc] init];
,這涉及到 iOS 開發的內存管理相關內容。
在早期 macOS 開發中使用 GC 進行內存管理但如今都跟 iOS 開發同樣已統一使用引用計數進行內存管理:當對象的引用計數爲 0 時會被銷燬,當對象被引用時其引用計數會 +1 ,當對象的引用被銷燬時引用計數 -1 。
__strong 是一個變量修飾符,但這裏不打算列舉其餘的變量修飾符,而會在下一條聊聊關於內存管理相關的屬性修飾符。
內存管理相關的變量修飾符都有相對應的屬性修飾符,通常的寫法是在屬性修飾符前添加兩個下劃線。
這裏列舉一下內管管理語義的屬性修飾符:
設置上述屬性修飾符會在屬性自動生成 setter 方法的時候爲咱們添加內存管理語義,明確內存管理全部權,若是咱們自定義 setter 訪問器,則需手動指定。
標量類型缺省是 assign ,對象類型缺省是 strong 。
咱們知道,屬性 = 實例變量(ivar) + setter + getter ,他的具體過程是這樣的:
完成屬性定義後,編譯器會自動編寫訪問這些屬性所需的訪問器,此過程稱爲「自動合成」(autosynthesize)。須要強調的是,這個過程由編譯器在編譯期執行,因此在編譯器中看不到自動生成的源代碼。除了生成訪問器 getter 、setter 以外,編譯器還要自動向類中添加適當類型的實例變量,實例變量名稱是在屬性名前加下劃線,咱們也能夠在類的實現代碼裏經過 @synthsize 語法來指定實例變量的名字,如:
@implementation Person
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end
複製代碼
這裏有一個注意的地方,@synthesize firstName;
像這樣不指定實例變量的名字,那麼生成的實例變量名會跟屬性名一致,而不會再加下劃線。
還有一個要注意的關鍵字:@dynamic ,他不會在編譯階段自動生成 getter 和 setter 方法,並且使用點語法或者賦值操做在編譯階段仍可以經過,可是該屬性的訪問器必須在運行時由用戶本身實現,不然會 crash 。
在前面咱們有說到 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 和 nonatomic ,原子性和非原子性,缺省是原子性的,但在 iOS 開發中,幾乎全部屬性都會主動聲明爲 nonatomic 。
緣由有兩個:
第一點,是由於使用 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 開發中事件的產生、傳遞和響應鏈。
事件產生:
系統註冊了一個 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 ?步驟:
底層實現主要涉及兩個方法:func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
和 func point(inside point: CGPoint, with event: UIEvent?) -> Bool
hitTest 方法會根據視圖層級結構往上調用 pointInside 方法,肯定可否接收事件。若是 pointInside 返回 true ,則繼續調用子視圖層級結構,直到在最遠的視圖找到點擊的 point 。若是一個視圖沒有找到該 point ,則不會繼續它往上的視圖層級結構。
咱們能夠經過調用這個方法來截獲和轉發事件。
事件響應,大體是從子控件傳遞到父控件:
過程:
在上述過程當中,若是某個控件實現了 touchesXxx 方法,則這個事件將由該控件接管,若是調用 super 的 touchesXxx ,就會將事件順着響應者鏈繼續往上傳遞,接着會調用上一個響應者的 touchesXxx 方法。
通常咱們會選擇使用 block 或者 delegate 或者 notification center 去作一些消息事件的傳遞,而如今咱們也能夠利用響應者鏈的關係來進行消息事件的傳遞。