聲明:當時以爲這篇文章寫的比較好,在此作了copy,原文分爲上下篇,在此合爲了一篇,原文連接地址是:原文地址html
《招聘一個靠譜的 iOS》—參考答案(上,下)ios
說明:面試題來源是微博@我就叫Sunny怎麼了的這篇博文:《招聘一個靠譜的 iOS》,其中共55題,除第一題爲糾錯題外,其餘54道均爲簡答題。git
博文中給出了高質量的面試題,可是未給出答案,我嘗試着總結了下答案,分兩篇發:這是上篇 ,下一篇文章將發佈在這裏,會把剩餘問題總結下,而且進行勘誤,歡迎各位指正文中的錯誤。請持續關注微博@iOS程序犭袁。(答案未經出題者校對,若有紕漏,請向微博@iOS程序犭袁指正。)
出題者簡介: 孫源(sunnyxx),目前就任於百度,負責百度知道 iOS 客戶端的開發工做,對技術喜歡刨根問底和總結最佳實踐,熱愛分享和開源,維護一個叫 forkingdog 的開源小組。github
1. 風格糾錯題web
修改方法有不少種,現給出一種作示例:
面試
下面對具體修改的地方,分兩部分作下介紹:硬傷部分和優化部分 。由於硬傷部分沒什麼技術含量,爲了節省你們時間,放在後面講,大神請直接看優化部分。編程
優化部分api
1)enum建議使用 NS_ENUM 和 NS_OPTIONS 宏來定義枚舉類型,參見官方的 Adopting Modern Objective-C 一文:數組
1
2
3
4
5
|
//定義一個枚舉
typedef NS_ENUM(NSInteger, CYLSex) {
CYLSexMan,
CYLSexWoman
};
|
2)age屬性的類型:應避免使用基本類型,建議使Foundation數據類型,對應關係以下:緩存
1
2
3
4
|
int -> NSInteger
unsigned -> NSUInteger
float -> CGFloat
動畫時間 -> NSTimeInterval
|
同時考慮到age的特色,應使用NSUInteger,而非int。 這樣作的是基於64-bit 適配考慮,詳情可參考出題者的博文《64-bit Tips》。
3)若是工程項目很是龐大,須要拆分紅不一樣的模塊,能夠在類、typedef宏命名的時候使用前綴。
4)doLogIn 方法不該寫在該類中:雖然LogIn的命名不太清晰,但筆者猜想是login的意思,而登陸操做屬於業務邏輯,觀察類名UserModel,以及屬性的命 名方式,應該使用的是MVC模式,並不是MVVM,在MVC中業務邏輯不該當寫在Model中。(若是是MVVM,拋開命名規範,UserModel這個類 可能對應的是用戶註冊頁面,若是有特殊的業務需求,好比:login對應的應當是註冊並登陸的一個Button,出現login方法也多是合理的。)
5)doLogIn方法命名不規範:添加了多餘的動詞前綴。 請牢記:
若是方法表示讓對象執行一個動做,使用動詞打頭來命名,注意不要使用do,does這種多餘的關鍵字,動詞自己的暗示就足夠了。
6)- (id)initUserModelWithUserName: (NSString*)name withAge:(int)age;方法中不要用with來鏈接兩個參數:withAge:應當換爲age:,age:已經足以清晰說明參數的做用,也不 建議用andAge::一般狀況下,即便有相似withA:withB:的命名需求,也一般是使用withA:andB:這種命名,用來表示方法執行了兩 個相對獨立的操做(從設計上來講,這時候也能夠拆分紅兩個獨立的方法),它不該該用做闡明有多個參數,好比下面的:
1
2
3
4
5
6
|
//錯誤,不要使用"and"來鏈接參數
- (int)runModalForDirectory:(NSString *)path andFile:(NSString *)name andTypes:(NSArray *)fileTypes;
//錯誤,不要使用"and"來闡明有多個參數
- (instancetype)initWithName:(CGFloat)width andAge:(CGFloat)height;
//正確,使用"and"來表示兩個相對獨立的操做
- (BOOL)openFile:(NSString *)fullPath withApplication:(NSString *)appName andDeactivate:(BOOL)flag;
|
7)因爲字符串值可能會改變,因此要把相關屬性的「內存管理語義」聲明爲copy。(緣由在下文有詳細論述:用@property聲明的NSString(或NSArray,NSDictionary)常用copy關鍵字,爲何?)
8)「性別」(sex)屬性的:該類中只給出了一種「初始化方法」 (initializer)用於設置「姓名」(Name)和「年齡」(Age)的初始值,那如何對「性別」(Sex)初始化?
Objective-C 有 designated 和 secondary 初始化方法的觀念。 designated 初始化方法是提供全部的參數,secondary 初始化方法是一個或多個,而且提供一個或者更多的默認參數來調用 designated 初始化方法的初始化方法。舉例說明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// .m文件
//
@implementation CYLUser
- (instancetype)initWithName:(NSString *)name
age:(int)age
sex:(CYLSex)sex {
if
(self = [
super
init]) {
_name = [name copy];
_age = age;
_sex = sex;
}
return
self;
}
- (instancetype)initWithName:(NSString *)name
age:(int)age {
return
[self initWithName:name age:age sex:nil];
}
@end
|
上面的代碼中initWithName:age:sex: 就是 designated 初始化方法,另外的是 secondary 初始化方法。由於僅僅是調用類實現的 designated 初始化方法。
因 爲出題者沒有給出.m文件,因此有兩種猜想:1:原本打算只設計一個designated 初始化方法,但漏掉了「性別」(sex)屬性。那麼最終的修改代碼就是上文給出的第一種修改方法。2:不打算初始時初始化「性別」(sex)屬性,打算後 期再修改,若是是這種狀況,那麼應該把「性別」(sex)屬性設爲readwrite屬性,最終給出的修改代碼應該是:
.h中暴露 designated 初始化方法,是爲了方便子類化 (想了解更多,請戳--》 《禪與 Objective-C 編程藝術 (Zen and the Art of the Objective-C Craftsmanship 中文翻譯)》。)
9)按照接口設計的慣例,若是設計了「初始化方法」 (initializer),也應當搭配一個快捷構造方法。而快捷構造方法的返回值,建議爲instancetype,爲保持一致性,init方法和快捷構造方法的返回類型最好都用instancetype。
10) 若是基於第一種修改方法:既然該類中已經有一個「初始化方法」 (initializer),用於設置「姓名」(Name)、「年齡」(Age)和「性別」(Sex)的初始值: 那麼在設計對應@property時就應該儘可能使用不可變的對象:其三個屬性都應該設爲「只讀」。用初始化方法設置好屬性值以後,就不能再改變了。在本例 中,仍需聲明屬性的「內存管理語義」。因而能夠把屬性的定義改爲這樣
1
2
3
|
@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, assign, readonly) NSUInter age;
@property (nonatomic, assign, readonly) CYLSex sex;
|
由 因而只讀屬性,因此編譯器不會爲其建立對應的「設置方法」,即使如此,咱們仍是要寫上這些屬性的語義,以此代表初始化方法在設置這些屬性值時所用的方式。 要是不寫明語義的話,該類的調用者就不知道初始化方法裏會拷貝這些屬性,他們有可能會在調用初始化方法以前自行拷貝屬性值。這種操做多餘並且低效。
11)initUserModelWithUserName若是改成initWithName會更加簡潔,並且足夠清晰。
12)UserModel若是改成User會更加簡潔,並且足夠清晰。
13)UserSex若是改成Sex會更加簡潔,並且足夠清晰。
硬傷部分
1)在-和(void)之間應該有一個空格
2)enum中駝峯命名法和下劃線命名法混用錯誤:枚舉類型的命名規則和函數的命名規則相同:命名時使用駝峯命名法,勿使用下劃線命名法。
3)enum左括號前加一個空格,或者將左括號換到下一行
4)enum右括號後加一個空格
5)UserModel :NSObject 應爲UserModel : NSObject,也就是:右側少了一個空格。
6)@interface與@property屬性聲明中間應當間隔一行。
7)兩個方法定義之間不須要換行,有時爲了區分方法的功能也可間隔一行,但示例代碼中間隔了兩行。
8)-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;方法中方法名與參數之間多了空格。並且- 與(id)之間少了空格。
9)-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;方法中方法名與參數之間多了空格:(NSString*)name前多了空格。
10)-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;方法中(NSString*)name,應爲(NSString *)name,少了空格。
11)doLogIn方法命名不清晰:筆者猜想是login的意思,應該是粗心手誤形成的。
12)第二個@property中assign和nonatomic調換位置。
2. 什麼狀況使用 weak 關鍵字,相比 assign 有什麼不一樣?
什麼狀況使用 weak 關鍵字?
1)在ARC中,在有可能出現循環引用的時候,每每要經過讓其中一端使用weak來解決,好比:delegate代理屬性
2)自身已經對它進行一次強引用,沒有必要再強引用一次,此時也會使用weak,自定義IBOutlet控件屬性通常也使用weak;固然,也可使用strong。在下文也有論述:《IBOutlet連出來的視圖屬性爲何能夠被設置成weak?》
不一樣點:
1)weak 此特質代表該屬性定義了一種「非擁有關係」 (nonowning relationship)。爲這種屬性設置新值時,設置方法既不保留新值,也不釋放舊值。此特質同assign相似, 然而在屬性所指的對象遭到摧毀時,屬性值也會清空(nil out)。 而 assign 的「設置方法」只會執行鍼對「純量類型」 (scalar type,例如 CGFloat 或 NSlnteger 等)的簡單賦值操做。
2)assigin 能夠用非OC對象,而weak必須用於OC對象
3. 怎麼用 copy 關鍵字?
用途:
1)NSString、NSArray、NSDictionary 等等常用copy關鍵字,是由於他們有對應的可變類型:NSMutableString、NSMutableArray、NSMutableDictionary;
2)block也常用copy關鍵字,具體緣由見官方文檔:Objects Use Properties to Keep Track of Blocks:
block 使用copy是從MRC遺留下來的「傳統」,在MRC中,方法內部的block是在棧區的,使用copy能夠把它放到堆區.在ARC中寫不寫都行:對於 block使用copy仍是strong效果是同樣的,但寫上copy也無傷大雅,還能時刻提醒咱們:編譯器自動對block進行了copy操做。
下 面作下解釋: copy此特質所表達的所屬關係與strong相似。然而設置方法並不保留新值,而是將其「拷貝」 (copy)。 當屬性類型爲NSString時,常常用此特質來保護其封裝性,由於傳遞給設置方法的新值有可能指向一個NSMutableString類的實例。這個類 是NSString的子類,表示一種可修改其值的字符串,此時如果不拷貝字符串,那麼設置完屬性以後,字符串的值就可能會在對象不知情的狀況下遭人更改。 因此,這時就要拷貝一份「不可變」 (immutable)的字符串,確保對象中的字符串值不會無心間變更。只要實現屬性所用的對象是「可變的」 (mutable),就應該在設置新屬性值時拷貝一份。
用 @property聲明 NSString、NSArray、NSDictionary 常用copy關鍵字,是由於他們有對應的可變類型:NSMutableString、NSMutableArray、 NSMutableDictionary,他們之間可能進行賦值操做,爲確保對象中的字符串值不會無心間變更,應該在設置新屬性值時拷貝一份。
該問題在下文中也有論述:用@property聲明的NSString(或NSArray,NSDictionary)常用copy關鍵字,爲何?若是改用strong關鍵字,可能形成什麼問題?
4. 這個寫法會出什麼問題: @property (copy) NSMutableArray *array;
兩個問題:
一、添加,刪除,修改數組內的元素的時候,程序會由於找不到對應的方法而崩潰.由於copy就是複製一個不可變NSArray的對象;
二、使用了atomic屬性會嚴重影響性能。
第1條的相關緣由在下文中有論述《用@property聲明的NSString(或NSArray,NSDictionary)常用copy關鍵字,爲何?若是改用strong關鍵字,可能形成什麼問題?》 以及上文《怎麼用 copy 關鍵字?》也有論述。
第2條緣由,以下:
該屬性使用了同步鎖,會在建立時生成一些額外的代碼用於幫助編寫多線程程序,這會帶來性能問題,經過聲明nonatomic能夠節省這些雖然很小可是沒必要要額外開銷。
在 默認狀況下,由編譯器所合成的方法會經過鎖定機制確保其原子性(atomicity)。若是屬性具有nonatomic特質,則不使用同步鎖。請注意,盡 管沒有名爲「atomic」的特質(若是某屬性不具有nonatomic特質,那它就是「原子的」(atomic))。
在iOS開發中,你會發現,幾乎全部屬性都聲明爲nonatomic。
一 般狀況下並不要求屬性必須是「原子的」,由於這並不能保證「線程安全」 ( thread safety),若要實現「線程安全」的操做,還需採用更爲深層的鎖定機制才行。例如,一個線程在連續屢次讀取某屬性值的過程當中有別的線程在同時改寫該 值,那麼即使將屬性聲明爲atomic,也仍是會讀到不一樣的屬性值。
所以,開發iOS程序時通常都會使用nonatomic屬性。可是在開發Mac OS X程序時,使用 atomic屬性一般都不會有性能瓶頸。
5. 如何讓本身的類用 copy 修飾符?如何重寫帶 copy 關鍵字的 setter?
若想令本身所寫的對象具備拷貝功能,則需實現NSCopying協議。若是自定義的對象分爲可變版本與不可變版本,那麼就要同時實現NSCopyiog與NSMutableCopying協議。
具體步驟:
1)需聲明該類聽從NSCopying協議
2)實現NSCopying協議。該協議只有一個方法:
1
|
- (id)copyWithZone: (NSZone*) zone
|
注意:一提到讓本身的類用 copy 修飾符,咱們老是想覆寫copy方法,其實真正須要實現的倒是「copyWithZone」方法。
以第一題的代碼爲例:
而後實現協議中規定的方法:
但 在實際的項目中,不可能這麼簡單,遇到更復雜一點,好比類對象中的數據結構可能並未在初始化方法中設置好,須要另行設置。舉個例子,假如CYLUser中 含有一個數組,與其餘CYLUser對象創建或解除朋友關係的那些方法都須要操做這個數組。那麼在這種狀況下,你得把這個包含朋友對象的數組也一併拷貝過 來。下面列出了實現此功能所需的所有代碼:
// .m文件
以上作法能知足基本的需求,可是也有缺陷:若是你所寫的對象須要深拷貝,那麼可考慮新增一個專門執行深拷貝的方法。
【注:深淺拷貝的概念,在下文中有介紹,詳見下文的:用@property聲明的NSString(或NSArray,NSDictionary)常用copy關鍵字,爲何?若是改用strong關鍵字,可能形成什麼問題?】
在例子中,存放朋友對象的set是用「copyWithZooe:」方法來拷貝的,這種淺拷貝方式不會逐個複製set中的元素。若須要深拷貝的話,則可像下面這樣,編寫一個專供深拷貝所用的方法:
1
2
3
4
5
6
7
8
9
|
- (id)deepCopy {
CYLUser *copy = [[[self copy] allocWithZone:zone]
initWithName:_name
age:_age
sex:sex];
copy->_friends = [[NSMutableSet alloc] initWithSet:_friends
copyItems:YES];
return
copy;
}
|
至於如何重寫帶 copy 關鍵字的 setter這個問題,
若是拋開本例來回答的話,以下:
1
2
3
|
- (void)setName:(NSString *)name {
_name = [name copy];
}
|
如 果單單就上文的代碼而言,咱們不須要也不能重寫name的 setter :因爲是name是隻讀屬性,因此編譯器不會爲其建立對應的「設置方法」,用初始化方法設置好屬性值以後,就不能再改變了。( 在本例中,之因此還要聲明屬性的「內存管理語義」--copy,是由於:若是不寫copy,該類的調用者就不知道初始化方法裏會拷貝這些屬性,他們有可能 會在調用初始化方法以前自行拷貝屬性值。這種操做多餘而低效。)。
那如何確保name被copy?在初始化方法(initializer)中作:
1
2
3
4
5
6
7
8
9
10
11
|
- (instancetype)initWithName:(NSString *)name
age:(int)age
sex:(CYLSex)sex {
if
(self = [
super
init]) {
_name = [name copy];
_age = age;
_sex = sex;
_friends = [[NSMutableSet alloc] init];
}
return
self;
}
|
6. @property 的本質是什麼?ivar、getter、setter 是如何生成並添加到這個類中的。
@property 的本質是什麼?
@property = ivar + getter + setter;
下面解釋下:
「屬性」 (property)有兩大概念:ivar(實例變量)、存取方法(access method = getter + setter)。
「屬 性」 (property)做爲 Objective-C 的一項特性,主要的做用就在於封裝對象中的數據。 Objective-C 對象一般會把其所須要的數據保存爲各類實例變量。實例變量通常經過「存取方法」(access method)來訪問。其中,「獲取方法」 (getter)用於讀取變量值,而「設置方法」 (setter)用於寫入變量值。這個概念已經定型,而且經由「屬性」這一特性而成爲Objective-C 2.0的一部分。 而在正規的 Objective-C 編碼風格中,存取方法有着嚴格的命名規範。 正由於有了這種嚴格的命名規範,因此 Objective-C 這門語言才能根據名稱自動建立出存取方法。其實也能夠把屬性當作一種關鍵字,其表示:
編譯器會自動寫出一套存取方法,用以訪問給定類型中具備給定名稱的變量。 因此你也能夠這麼說:
@property = getter + setter;
例以下面這個類:
1
2
3
4
|
@interface Person : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end
|
上述代碼寫出來的類與下面這種寫法等效:
1
2
3
4
5
6
|
@interface Person : NSObject
- (NSString *)firstName;
- (void)setFirstName:(NSString *)firstName;
- (NSString *)lastName;
- (void)setLastName:(NSString *)lastName;
@end
|
ivar、getter、setter 是如何生成並添加到這個類中的?
「自動合成」( autosynthesis)
完 成屬性定義後,編譯器會自動編寫訪問這些屬性所需的方法,此過程叫作「自動合成」( autosynthesis)。須要強調的是,這個過程由編譯 器在編譯期執行,因此編輯器裏看不到這些「合成方法」(synthesized method)的源代碼。除了生成方法代碼 getter、setter 以外,編譯器還要自動向類中添加適當類型的實例變量,而且在屬性名前面加下劃線,以此做爲實例變量的名字。在前例中,會生成兩個實例變量,其名稱分別爲 _firstName與_lastName。也能夠在類的實現代碼裏經過 @synthesize語法來指定實例變量的名字.
1
2
3
4
|
@implementation Person
@synthesize firstName = _myFirstName;
@synthesize lastName = myLastName;
@end
|
我爲了搞清屬性是怎麼實現的,曾經反編譯過相關的代碼,大體生成了五個東西:
1)OBJC_IVAR_$類名$屬性名稱 :該屬性的「偏移量」 (offset),這個偏移量是「硬編碼」 (hardcode),表示該變量距離存放對象的內存區域的起始地址有多遠。
2)setter與getter方法對應的實現函數
3)ivar_list :成員變量列表
4)method_list :方法列表
5)prop_list :屬性列表
也 就是說咱們每次在增長一個屬性,系統都會在ivar_list中添加一個成員變量的描述,在method_list中增長setter與getter方法 的描述,在屬性列表中增長一個屬性的描述,而後計算該屬性在對象中的偏移量,而後給出setter與getter方法對應的實現,在setter方法中從 偏移量的位置開始賦值,在getter方法中從偏移量開始取值,爲了可以讀取正確字節數,系統對象偏移量的指針類型進行了類型強轉.
7. @protocol 和 category 中如何使用 @property
1)在protocol中使用property只會生成setter和getter方法聲明,咱們使用屬性的目的,是但願遵照我協議的對象能實現該屬性
2)category 使用 @property 也是隻會生成setter和getter方法的聲明,若是咱們真的須要給category增長屬性的實現,須要藉助於運行時的兩個函數:
①objc_setAssociatedObject
②objc_getAssociatedObject
8. runtime 如何實現 weak 屬性
要實現weak屬性,首先要搞清楚weak屬性的特色:
weak 此特質代表該屬性定義了一種「非擁有關係」 (nonowning relationship)。爲這種屬性設置新值時,設置方法既不保留新值,也不釋放舊值。此特質同assign相似, 然而在屬性所指的對象遭到摧毀時,屬性值也會清空(nil out)。
那麼runtime如何實現weak變量的自動置nil?
runtime 對註冊的類, 會進行佈局,對於 weak 對象會放入一個 hash 表中。 用 weak 指向的對象內存地址做爲 key,當此對象的引用計數爲0的時候會 dealloc,假如 weak 指向的對象內存地址是a,那麼就會以a爲鍵, 在這個 weak 表中搜索,找到全部以a爲鍵的 weak 對象,從而設置爲 nil。
咱們能夠設計一個函數(僞代碼)來表示上述機制:
objc_storeWeak(&a, b)函數:
objc_storeWeak 函數把第二個參數--賦值對象(b)的內存地址做爲鍵值key,將第一個參數--weak修飾的屬性變量(a)的內存地址(&a)做爲 value,註冊到 weak 表中。若是第二個參數(b)爲0(nil),那麼把變量(a)的內存地址(&a)從weak表中刪除,
你能夠把objc_storeWeak(&a, b)理解爲:objc_storeWeak(value, key),而且當key變nil,將value置nil。
在b非nil時,a和b指向同一個內存地址,在b變nil時,a變nil。此時向a發送消息不會崩潰:在Objective-C中向nil發送消息是安全的。
而若是a是由assign修飾的,則: 在b非nil時,a和b指向同一個內存地址,在b變nil時,a仍是指向該內存地址,變野指針。此時向a發送消息極易崩潰。
下面咱們將基於objc_storeWeak(&a, b)函數,使用僞代碼模擬「runtime如何實現weak屬性」:
1
2
3
4
5
6
7
|
// 使用僞代碼模擬:runtime如何實現weak屬性
id obj1;
objc_initWeak(&obj1, obj);
/*obj引用計數變爲0,變量做用域結束*/
objc_destroyWeak(&obj1);
|
下面對用到的兩個方法objc_initWeak和objc_destroyWeak作下解釋:
整體說來,做用是: 經過objc_initWeak函數初始化「附有weak修飾符的變量(obj1)」,在變量做用域結束時經過objc_destoryWeak函數釋放該變量(obj1)。
下面分別介紹下方法的內部實現:
objc_initWeak函數的實現是這樣的:在將「附有weak修飾符的變量(obj1)」初始化爲0(nil)後,會將「賦值對象」(obj)做爲參數,調用objc_storeWeak函數。
1
2
|
obj1 = 0;
obj_storeWeak(&obj1, obj);
|
也就是說:
weak 修飾的指針默認值是 nil (在Objective-C中向nil發送消息是安全的)
而後obj_destroyWeak函數將0(nil)做爲參數,調用objc_storeWeak函數。
1
|
objc_storeWeak(&obj1, 0);
|
前面的源代碼與下列源代碼相同。
1
2
3
4
5
6
7
8
|
// 使用僞代碼模擬:runtime如何實現weak屬性
id obj1;
obj1 = 0;
objc_storeWeak(&obj1, obj);
/* ... obj的引用計數變爲0,被置nil ... */
objc_storeWeak(&obj1, 0);
|
objc_storeWeak 函數把第二個參數--賦值對象(obj)的內存地址做爲鍵值,將第一個參數--weak修飾的屬性變量(obj1)的內存地址註冊到 weak 表中。若是第二個參數(obj)爲0(nil),那麼把變量(obj1)的地址從weak表中刪除,在後面的相關一題會詳解。
使用僞代碼是爲了方便理解,下面咱們「真槍實彈」地實現下:
如何讓不使用weak修飾的@property,擁有weak的效果。
咱們從setter方法入手:
1
2
3
4
5
6
7
|
- (void)setObject:(NSObject *)object
{
objc_setAssociatedObject(self,
"object"
, object, OBJC_ASSOCIATION_ASSIGN);
[object cyl_runAtDealloc:^{
_object = nil;
}];
}
|
也就是有兩個步驟:
1)在setter方法中作以下設置:
1
|
objc_setAssociatedObject(self,
"object"
, object, OBJC_ASSOCIATION_ASSIGN);
|
2)在屬性所指的對象遭到摧毀時,屬性值也會清空(nil out)。作到這點,一樣要藉助runtime:
1
2
3
4
5
6
7
8
|
//要銷燬的目標對象
id objectToBeDeallocated;
//能夠理解爲一個「事件」:當上面的目標對象銷燬時,同時要發生的「事件」。
id objectWeWantToBeReleasedWhenThatHappens;
objc_setAssociatedObject(objectToBeDeallocted,
someUniqueKey,
objectWeWantToBeReleasedWhenThatHappens,
OBJC_ASSOCIATION_RETAIN);
|
知道了思路,咱們就開始實現cyl_runAtDealloc方法,實現過程分兩部分:
第一部分:建立一個類,能夠理解爲一個「事件」:當目標對象銷燬時,同時要發生的「事件」。藉助block執行「事件」。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
// .h文件
// 這個類,能夠理解爲一個「事件」:當目標對象銷燬時,同時要發生的「事件」。藉助block執行「事件」。
typedef void (^voidBlock)(void);
@interface CYLBlockExecutor : NSObject
- (id)initWithBlock:(voidBlock)block;
@end
// .m文件
// 這個類,能夠理解爲一個「事件」:當目標對象銷燬時,同時要發生的「事件」。藉助block執行「事件」。
#import "CYLBlockExecutor.h"
@interface CYLBlockExecutor() {
voidBlock _block;
}
@implementation CYLBlockExecutor
- (id)initWithBlock:(voidBlock)aBlock
{
self = [
super
init];
if
(self) {
_block = [aBlock copy];
}
return
self;
}
- (void)dealloc
{
_block ? _block() : nil;
}
@end
|
第二部分:核心代碼:利用runtime實現cyl_runAtDealloc方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
// CYLNSObject+RunAtDealloc.h文件
// 利用runtime實現cyl_runAtDealloc方法
#import "CYLBlockExecutor.h"
const void *runAtDeallocBlockKey = &runAtDeallocBlockKey;
@interface NSObject (CYLRunAtDealloc)
- (void)cyl_runAtDealloc:(voidBlock)block;
@end
// CYLNSObject+RunAtDealloc.m文件
// 利用runtime實現cyl_runAtDealloc方法
#import "CYLNSObject+RunAtDealloc.h"
#import "CYLBlockExecutor.h"
@implementation NSObject (CYLRunAtDealloc)
- (void)cyl_runAtDealloc:(voidBlock)block
{
if
(block) {
CYLBlockExecutor *executor = [[CYLBlockExecutor alloc] initWithBlock:block];
objc_setAssociatedObject(self,
runAtDeallocBlockKey,
executor,
OBJC_ASSOCIATION_RETAIN);
}
}
@end
|
使用方法: 導入
1
|
#import "CYLNSObject+RunAtDealloc.h"
|
而後就可使用了:
1
2
3
4
|
NSObject *foo = [[NSObject alloc] init];
[foo cyl_runAtDealloc:^{
NSLog(@
"正在釋放foo!"
);
}];
|
若是對cyl_runAtDealloc的實現原理有興趣,能夠看下這篇博文 Fun With the Objective-C Runtime: Run Code at Deallocation of Any Object
9. @property中有哪些屬性關鍵字?/ @property 後面能夠有哪些修飾符?
屬性能夠擁有的特質分爲四類:
原子性---nonatomic特質
在 默認狀況下,由編譯器合成的方法會經過鎖定機制確保其原子性(atomicity)。若是屬性具有nonatomic特質,則不使用同步鎖。請注意,儘管 沒有名爲「atomic」的特質(若是某屬性不具有nonatomic特質,那它就是「原子的」 ( atomic) ),可是仍然能夠在屬性特質中寫明這一點,編譯器不會報錯。如果本身定義存取方法,那麼就應該聽從與屬性特質相符的原子性。
讀/寫權限---readwrite(讀寫)、readooly (只讀)
內存管理語義---assign、strong、 weak、unsafe_unretained、copy
方法名---getter=、setter=
getter=的樣式:
1
|
@property (nonatomic, getter=isOn) BOOL on;
|
( setter=這種不經常使用,也不推薦使用。故不在這裏給出寫法。)
不經常使用的:nonnull,null_resettable,nullable
10. weak屬性須要在dealloc中置nil麼?
不須要。
在ARC環境不管是強指針仍是弱指針都無需在deallco設置爲nil,ARC會自動幫咱們處理。
即使是編譯器不幫咱們作這些,weak也不須要在dealloc中置nil:
正如上文的:runtime 如何實現 weak 屬性 中提到的:
咱們模擬下weak的setter方法,應該以下:
1
2
3
4
5
6
7
|
- (void)setObject:(NSObject *)object
{
objc_setAssociatedObject(self,
"object"
, object, OBJC_ASSOCIATION_ASSIGN);
[object cyl_runAtDealloc:^{
_object = nil;
}];
}
|
也即:在屬性所指的對象遭到摧毀時,屬性值也會清空(nil out)。
11. @synthesize和@dynamic分別有什麼做用?
1)@property有兩個對應的詞,一個是@synthesize,一個是@dynamic。若是@synthesize和@dynamic都沒寫,那麼默認的就是@syntheszie var = _var;
2)@synthesize的語義是若是你沒有手動實現setter方法和getter方法,那麼編譯器會自動爲你加上這兩個方法。
3)@dynamic 告訴編譯器:屬性的setter與getter方法由用戶本身實現,不自動生成。(固然對於readonly的屬性只需提供getter便可)。假如一個 屬性被聲明爲@dynamic var,而後你沒有提供@setter方法和@getter方法,編譯的時候沒問題,可是當程序運行到instance.var = someVar,因爲缺setter方法會致使程序崩潰;或者當運行到 someVar = var時,因爲缺getter方法一樣會致使崩潰。編譯時沒問題,運行時才執行相應的方法,這就是所謂的動態綁定。
12. ARC下,不顯式指定任何屬性關鍵字時,默認的關鍵字都有哪些?
對應基本數據類型默認關鍵字是
atomic,readwrite,assign
對於普通的OC對象
atomic,readwrite,strong
參考連接:
13. 用@property聲明的NSString(或NSArray,NSDictionary)常用copy關鍵字,爲何?若是改用strong關鍵字,可能形成什麼問題?
1)由於父類指針能夠指向子類對象,使用copy的目的是爲了讓本對象的屬性不受外界影響,使用copy不管給我傳入是一個可變對象仍是不可對象,我自己持有的就是一個不可變的副本.
2)若是咱們使用是strong,那麼這個屬性就有可能指向一個可變對象,若是這個可變對象在外部被修改了,那麼會影響該屬性.
copy 此特質所表達的所屬關係與strong相似。然而設置方法並不保留新值,而是將其「拷貝」 (copy)。 當屬性類型爲NSString時,常常用此特質來保護其封裝性,由於傳遞給設置方法的新值有可能指向一個NSMutableString類的實例。這個類 是NSString的子類,表示一種可修改其值的字符串,此時如果不拷貝字符串,那麼設置完屬性以後,字符串的值就可能會在對象不知情的狀況下遭人更改。 因此,這時就要拷貝一份「不可變」 (immutable)的字符串,確保對象中的字符串值不會無心間變更。只要實現屬性所用的對象是「可變的」 (mutable),就應該在設置新屬性值時拷貝一份。
爲了理解這種作法,首先要知道,對非集合類對象的copy操做:
在非集合類對象中:對immutable對象進行copy操做,是指針複製,mutableCopy操做時內容複製;對mutable對象進行copy和mutableCopy都是內容複製。用代碼簡單表示以下:
[immutableObject copy] // 淺複製
[immutableObject mutableCopy] //深複製
[mutableObject copy] //深複製
[mutableObject mutableCopy] //深複製
好比如下代碼:
1
2
|
NSMutableString *string = [NSMutableString stringWithString:@
"origin"
];
//copy
NSString *stringCopy = [string copy];
|
查看內存,會發現 string、stringCopy 內存地址都不同,說明此時都是作內容拷貝、深拷貝。即便你進行以下操做:
1
|
[string appendString:@
"origion!"
]
|
stringCopy的值也不會所以改變,可是若是不使用copy,stringCopy的值就會被改變。 集合類對象以此類推。 因此,
用 @property聲明 NSString、NSArray、NSDictionary 常用copy關鍵字,是由於他們有對應的可變類型:NSMutableString、NSMutableArray、 NSMutableDictionary,他們之間可能進行賦值操做,爲確保對象中的字符串值不會無心間變更,應該在設置新屬性值時拷貝一份。
參考連接:iOS 集合的深複製與淺複製
14. @synthesize合成實例變量的規則是什麼?假如property名爲foo,存在一個名爲_foo的實例變量,那麼還會自動合成新變量麼?
在回答以前先說明下一個概念:
實例變量 = 成員變量 = ivar
這些說法,筆者下文中,可能都會用到,指的是一個東西。
正如 Apple官方文檔 You Can Customize Synthesized Instance Variable Names 所說:
如 果使用了屬性的話,那麼編譯器就會自動編寫訪問屬性所需的方法,此過程叫作「自動合成」( auto synthesis)。須要強調的是,這個過程由編譯器在編譯期執行,因此編輯器裏看不到這些「合成方法」 (synthesized method)的源代碼。除了生成方法代碼以外,編譯器還要自動向類中添加適當類型的實例變量,而且在屬性名前面加下劃線,以此做爲實例變量的名字。
1
2
3
4
|
@interface CYLPerson : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end
|
在上例中,會生成兩個實例變量,其名稱分別爲 _firstName與_lastName。也能夠在類的實現代碼裏經過@synthesize語法來指定實例變量的名字:
1
2
3
4
|
@implementation CYLPerson
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end
|
上 述語法會將生成的實例變量命名爲_myFirstName與_myLastName,而再也不使用默認的名字。通常狀況下無須修改默認的實例變量名,可是如 果你不喜歡如下劃線來命名實例變量,那麼能夠用這個辦法將其改成本身想要的名字。筆者仍是推薦使用默認的命名方案,由於若是全部人都堅持這套方案,那麼寫 出來的代碼你們都能看得懂。
總結下@synthesize合成實例變量的規則,有如下幾點:
1)若是指定了成員變量的名稱,會生成一個指定的名稱的成員變量,
2)若是這個成員已經存在了就再也不生成了.
3)若是是 @synthesize foo; 還會生成一個名稱爲foo的成員變量,也就是說:若是沒有指定成員變量的名稱會自動生成一個屬性同名的成員變量。
4)若是是 @synthesize foo = _foo; 就不會生成成員變量了.
假如property名爲foo,存在一個名爲_foo的實例變量,那麼還會自動合成新變量麼? 不會。以下圖:
15. 在有了自動合成屬性實例變量以後,@synthesize還有哪些使用場景?
回答這個問題前,咱們要搞清楚一個問題,什麼狀況下不會autosynthesis(自動合成)?
同時重寫了setter和getter時
重寫了只讀屬性的getter時
使用了@dynamic時
在 @protocol 中定義的全部屬性
在 category 中定義的全部屬性
重載的屬性
當你在子類中重載了父類中的屬性,你必須 使用@synthesize來手動合成ivar。
除 了後三條,對其餘幾個咱們能夠總結出一個規律:當你想手動管理@property的全部內容時,你就會嘗試經過實現@property的全部「存取方法」 (the accessor methods)或者使用@dynamic來達到這個目的,這時編譯器就會認爲你打算手動管理@property,因而編譯器就禁用了 autosynthesis(自動合成)。
由於有了 autosynthesis(自動合成),大部分開發者已經習慣不去手動定義ivar,而是依賴於autosynthesis(自動合成),可是一旦你需 要使用ivar,而autosynthesis(自動合成)又失效了,若是不去手動定義ivar,那麼你就得藉助@synthesize來手動合成 ivar。
其實,@synthesize語法還有一個應用場景,可是不太建議你們使用:
能夠在類的實現代碼裏經過@synthesize語法來指定實例變量的名字:
1
2
3
4
|
@implementation CYLPerson
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end
|
上 述語法會將生成的實例變量命名爲_myFirstName與_myLastName,而再也不使用默認的名字。通常狀況下無須修改默認的實例變量名,可是如 果你不喜歡如下劃線來命名實例變量,那麼能夠用這個辦法將其改成本身想要的名字。筆者仍是推薦使用默認的命名案,由於若是全部人都堅持這套方案,那麼寫出 來的代碼你們都能看得懂。
舉例說明:應用場景:
結果編譯器報錯:
當你同時重寫了setter和getter時,系統就不會生成ivar(實例變量/成員變量)。這時候有兩種選擇:
要麼如第14行:手動建立ivar
要麼如第17行:使用@synthesize foo = _foo; ,關聯@property與ivar。
更多信息,請戳- 》 When should I use @synthesize explicitly?
16. objc中向一個nil對象發送消息將會發生什麼?
在Objective-C中向nil發送消息是徹底有效的——只是在運行時不會有任何做用:
若是一個方法返回值是一個對象,那麼發送給nil的消息將返回0(nil)。例如:
1
|
Person * motherInlaw = [[aPerson spouse] mother];
|
若是spouse對象爲nil,那麼發送給nil的消息mother也將返回nil。
1)若是方法返回值爲指針類型,其指針大小爲小於或者等於sizeof(void*),float,double,long double 或者long long的整型標量,發送給nil的消息將返回0。
2)若是方法返回值爲結構體,發送給nil的消息將返回0。結構體中各個字段的值將都是0。
3)若是方法的返回值不是上述提到的幾種狀況,那麼發送給nil的消息的返回值將是未定義的。
具體緣由以下:
objc是動態語言,每一個方法在運行時會被動態轉爲消息發送,即:objc_msgSend(receiver, selector)。
那麼,爲了方便理解這個內容,仍是貼一個objc的源代碼:
1
2
3
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
//isa指針指向Meta Class,由於Objc的類的自己也是一個Object,爲了處理這個關係,runtime就創造了Meta Class,當給類發送[NSObject alloc]這樣消息時,其實是把這個消息發給了Class Object
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
// 父類
const char *name OBJC2_UNAVAILABLE;
// 類名
long version OBJC2_UNAVAILABLE;
// 類的版本信息,默認爲0
long info OBJC2_UNAVAILABLE;
// 類信息,供運行期使用的一些位標識
long instance_size OBJC2_UNAVAILABLE;
// 該類的實例變量大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
// 該類的成員變量鏈表
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
// 方法定義的鏈表
struct objc_cache *cache OBJC2_UNAVAILABLE;
// 方法緩存,對象接到一個消息會根據isa指針查找消息對象,這時會在method Lists中遍歷,若是cache了,經常使用的方法調用時就可以提升調用的效率。
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
// 協議鏈表
#endif
} OBJC2_UNAVAILABLE;
|
objc 在向一個對象發送消息時,runtime庫會根據對象的isa指針找到該對象實際所屬的類,而後在該類中的方法列表以及其父類方法列表中尋找方法運行,然 後在發送消息的時候,objc_msgSend方法不會返回值,所謂的返回內容都是具體調用時執行的。 那麼,回到本題,若是向一個nil對象發送消息,首先在尋找對象的isa指針時就是0地址返回了,因此不會出現任何錯誤。
17. objc中向一個對象發送消息[obj foo]和objc_msgSend()函數之間有什麼關係?
具體緣由同上題:該方法編譯以後就是objc_msgSend()函數調用.若是我沒有記錯的大概是這樣的:
1
|
((void ()(id, SEL))(void )objc_msgSend)((id)obj, sel_registerName(
"foo"
));
|
也就是說:
[obj foo];在objc動態編譯時,會被轉意爲:objc_msgSend(obj, @selector(foo));。
18. 何時會報unrecognized selector的異常?
簡單來講:當該對象上某個方法,而該對象上沒有實現這個方法的時候, 能夠經過「消息轉發」進行解決。
簡單的流程以下,在上一題中也提到過:objc是動態語言,每一個方法在運行時會被動態轉爲消息發送,即:objc_msgSend(receiver, selector)。
objc 在向一個對象發送消息時,runtime庫會根據對象的isa指針找到該對象實際所屬的類,而後在該類中的方法列表以及其父類方法列表中尋找方法運行,如 果,在最頂層的父類中依然找不到相應的方法時,程序在運行時會掛掉並拋出異常unrecognized selector sent to XXX 。可是在這以前,objc的運行時會給出三次拯救程序崩潰的機會:
Method resolution
objc 運行時會調用+resolveInstanceMethod:或者 +resolveClassMethod:,讓你有機會提供一個函數實現。若是你添加了函數並返回 YES,那運行時系統就會從新啓動一次消息發送的過程,若是 resolve 方法返回 NO ,運行時就會移到下一步,消息轉發(Message Forwarding)。
Fast forwarding
如 果目標對象實現了-forwardingTargetForSelector:,Runtime 這時就會調用這個方法,給你把這個消息轉發給其餘對象的機會。 只要這個方法返回的不是nil和self,整個消息發送的過程就會被重啓,固然發送的對象會變成你返回的那個對象。不然,就會繼續Normal Fowarding。 這裏叫Fast,只是爲了區別下一步的轉發機制。由於這一步不會建立任何新的對象,但下一步轉發會建立一個NSInvocation對象,因此相對更快 點。
Normal forwarding
這 一步是Runtime最後一次給你挽救的機會。首先它會發送-methodSignatureForSelector:消息得到函數的參數和返回值類型。 若是-methodSignatureForSelector:返回nil,Runtime則會發出 -doesNotRecognizeSelector:消息,程序這時也就掛掉了。若是返回了一個函數簽名,Runtime就會建立一個 NSInvocation對象併發送-forwardInvocation:消息給目標對象。
19. 一個objc對象如何進行內存佈局?(考慮有父類的狀況)
全部父類的成員變量和本身的成員變量都會存放在該對象所對應的存儲空間中.
每個對象內部都有一個isa指針,指向他的類對象,類對象中存放着本對象的
1)對象方法列表(對象可以接收的消息列表,保存在它所對應的類對象中)
2)成員變量的列表
3)屬性列表
它內部也有一個isa指針指向元對象(meta class),元對象內部存放的是類方法列表,類對象內部還有一個superclass的指針,指向他的父類對象。
1)根對象就是NSobject,它的superclass指針指向nil。
2)類對象既然稱爲對象,那它也是一個實例。類對象中也有一個isa指針指向它的元類(meta class),即類對象是元類的實例。元類內部存放的是類方法列表,根元類的isa指針指向本身,superclass指針指向NSObject類。
如圖:
20. 一個objc對象的isa的指針指向什麼?有什麼做用?
指向他的類對象,從而能夠找到對象上的方法
21. 下面的代碼輸出什麼?
1
2
3
4
5
6
7
8
9
10
11
|
@implementation Son : Father
- (id)init
{
self = [
super
init];
if
(self) {
NSLog(@
"%@"
, NSStringFromClass([self class]));
NSLog(@
"%@"
, NSStringFromClass([
super
class]));
}
return
self;
}
@end
|
答案:
都輸出 Son
1
2
|
NSStringFromClass([self class]) = Son
NSStringFromClass([
super
class]) = Son
|
解惑:
(如下解惑部分摘自微博@Chun_iOS的博文刨根問底Objective-C Runtime(1)- Self & Super)
這個題目主要是考察關於objc中對 self 和 super 的理解。
self 是類的隱藏參數,指向當前調用方法的這個類的實例。而 super 是一個 Magic Keyword, 它本質是一個編譯器標示符,和 self 是指向的同一個消息接受者。
上面的例子無論調用[self class]仍是[super class],接受消息的對象都是當前 Son *xxx 這個對象。而不一樣的是,super是告訴編譯器,調用 class 這個方法時,要去父類的方法,而不是本類裏的。
當使用 self 調用方法時,會從當前類的方法列表中開始找,若是沒有,就從父類中再找;而當使用 super 時,則從父類的方法列表中開始找。而後調用父類的這個方法。
真的是這樣嗎?繼續看:
使用clang重寫命令:
1
|
$ clang -rewrite-objc test.m
|
發現上述代碼被轉化爲:
1
2
|
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_0, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName(
"class"
))));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_1, NSStringFromClass(((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass(
"Son"
)) }, sel_registerName(
"class"
))));
|
從上面的代碼中,咱們能夠發如今調用 [self class] 時,會轉化成 objc_msgSend函數。看下函數定義:
1
|
id objc_msgSend(id self, SEL op, ...)
|
咱們把 self 作爲第一個參數傳遞進去。
而在調用 [super class]時,會轉化成 objc_msgSendSuper函數。看下函數定義:
1
|
id objc_msgSendSuper(struct objc_super *
super
, SEL op, ...)
|
第一個參數是 objc_super 這樣一個結構體,其定義以下:
1
2
3
4
|
struct objc_super {
__unsafe_unretained id receiver;
__unsafe_unretained Class super_class;
};
|
結構體有兩個成員,第一個成員是 receiver, 相似於上面的 objc_msgSend函數第一個參數self 。第二個成員是記錄當前類的父類是什麼。
所 以,當調用 [self class] 時,實際先調用的是 objc_msgSend函數,第一個參數是 Son當前的這個實例,而後在 Son 這個類裏面去找 - (Class)class這個方法,沒有,去父類 Father裏找,也沒有,最後在 NSObject類中發現這個方法。而 - (Class)class的實現就是返回self的類別,故上述輸出結果爲 Son。
objc Runtime開源代碼對- (Class)class方法的實現:
1
2
3
|
- (Class)class {
return
object_getClass(self);
}
|
而 當調用 [super class]時,會轉換成objc_msgSendSuper函數。第一步先構造 objc_super 結構體,結構體第一個成員就是 self 。 第二個成員是 (id)class_getSuperclass(objc_getClass(「Son」)) , 實際該函數輸出結果爲 Father。 第二步是去 Father這個類裏去找 - (Class)class,沒有,而後去NSObject類去找,找到了。最後內部是使用 objc_msgSend(objc_super->receiver, @selector(class))去調用, 此時已經和[self class]調用相同了,故上述輸出結果仍然返回 Son。
22. runtime如何經過selector找到對應的IMP地址?(分別考慮類方法和實例方法)
每個類對象中都一個方法列表,方法列表中記錄着方法的名稱,方法實現,以及參數類型,其實selector本質就是方法名稱,經過這個方法名稱就能夠在方法列表中找到對應的方法實現.
23. 使用runtime Associate方法關聯的對象,須要在主對象dealloc的時候釋放麼?
在ARC下不須要
在MRC中,對於使用retain或copy策略的須要
不管在MRC下仍是ARC下均不須要
2011年版本的Apple API 官方文檔 - Associative References 一節中有一個MRC環境下的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// 在MRC下,使用runtime Associate方法關聯的對象,不須要在主對象dealloc的時候釋放
// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)
// 摘自2011年版本的Apple API 官方文檔 - Associative References
static char overviewKey;
NSArray *array =
[[NSArray alloc] initWithObjects:@
"One"
, @
"Two"
, @
"Three"
, nil];
// For the purposes of illustration, use initWithFormat: to ensure
// the string can be deallocated
NSString *overview =
[[NSString alloc] initWithFormat:@
"%@"
, @
"First three numbers"
];
objc_setAssociatedObject (
array,
&overviewKey,
overview,
OBJC_ASSOCIATION_RETAIN
);
[overview release];
// (1) overview valid
[array release];
// (2) overview invalid
|
文檔指出
At point 1, the string overview is still valid because the OBJC_ASSOCIATION_RETAIN policy specifies that the array retains the associated object. When the array is deallocated, however (at point 2), overview is released and so in this case also deallocated.
咱們能夠看到,在[array release];以後,overview就會被release釋放掉了。
既然會被銷燬,那麼具體在什麼時間點?
根據 WWDC 2011, Session 322 (第36分22秒) 中發佈的內存銷燬時間表,被關聯的對象在生命週期內要比對象自己釋放的晚不少。它們會在被 NSObject -dealloc 調用的 object_dispose() 方法中釋放。
對象的內存銷燬時間表,分四個步驟:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// 對象的內存銷燬時間表
// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)
// 根據 WWDC 2011, Session 322 (36分22秒)中發佈的內存銷燬時間表
1. 調用 -release :引用計數變爲零
* 對象正在被銷燬,生命週期即將結束.
* 不能再有新的 __weak 弱引用, 不然將指向 nil.
* 調用 [self dealloc]
2. 父類 調用 -dealloc
* 繼承關係中最底層的父類 在調用 -dealloc
* 若是是 MRC 代碼 則會手動釋放實例變量們(iVars)
* 繼承關係中每一層的父類 都在調用 -dealloc
3. NSObject 調 -dealloc
* 只作一件事:調用 Objective-C runtime 中的 object_dispose() 方法
4. 調用 object_dispose()
* 爲 C++ 的實例變量們(iVars)調用 destructors
* 爲 ARC 狀態下的 實例變量們(iVars) 調用 -release
* 解除全部使用 runtime Associate方法關聯的對象
* 解除全部 __weak 引用
* 調用 free()
|
對象的內存銷燬時間表:參考連接。
24. objc中的類方法和實例方法有什麼本質區別和聯繫?
類方法:
類方法是屬於類對象的
類方法只能經過類對象調用
類方法中的self是類對象
類方法能夠調用其餘的類方法
類方法中不能訪問成員變量
類方法中不定直接調用對象方法
實例方法:
實例方法是屬於實例對象的實例方法只能經過實例對象調用
實例方法中的self是實例對象
實例方法中能夠訪問成員變量
實例方法中直接調用實例方法
實例方法中也能夠調用類方法(經過類名)
_objc_msgForward
函數是作什麼的,直接調用它將會發生什麼?
_objc_msgForward
是 IMP 類型,用於消息轉發的:當向一個對象發送一條消息,但它並無實現的時候,_objc_msgForward
會嘗試作消息轉發。
咱們能夠這樣建立一個_objc_msgForward
對象:
IMP msgForwardIMP = _objc_msgForward;
在上篇中的《objc中向一個對象發送消息[obj foo]
和objc_msgSend()
函數之間有什麼關係?》曾提到objc_msgSend
在「消息傳遞」中的做用。在「消息傳遞」過程當中,objc_msgSend
的動做比較清晰:首先在 Class 中的緩存查找 IMP (沒緩存則初始化緩存),若是沒找到,則向父類的 Class 查找。若是一直查找到根類仍舊沒有實現,則用_objc_msgForward
函數指針代替 IMP 。最後,執行這個 IMP 。
Objective-C運行時是開源的,因此咱們能夠看到它的實現。打開 Apple Open Source 裏Mac代碼裏的obj包 下載一個最新版本,找到 objc-runtime-new.mm
,進入以後搜索_objc_msgForward
。
裏面有對_objc_msgForward
的功能解釋:
/***********************************************************************
* lookUpImpOrForward. * The standard IMP lookup. * initialize==NO tries to avoid +initialize (but sometimes fails) * cache==NO skips optimistic unlocked lookup (but uses cache elsewhere) * Most callers should use initialize==YES and cache==YES. * inst is an instance of cls or a subclass thereof, or nil if none is known. * If cls is an un-initialized metaclass then a non-nil inst is faster. * May return _objc_msgForward_impcache. IMPs destined for external use * must be converted to _objc_msgForward or _objc_msgForward_stret. * If you don't want forwarding at all, use lookUpImpOrNil() instead. **********************************************************************/
對 objc-runtime-new.mm
文件裏與_objc_msgForward
有關的三個函數使用僞代碼展現下:
// objc-runtime-new.mm 文件裏與 _objc_msgForward 有關的三個函數使用僞代碼展現
// Created by https://github.com/ChenYilong // Copyright (c) 微博@iOS程序犭袁(http://weibo.com/luohanchenyilong/). All rights reserved. // 同時,這也是 obj_msgSend 的實現過程 id objc_msgSend(id self, SEL op, ...) { if (!self) return nil; IMP imp = class_getMethodImplementation(self->isa, SEL op); imp(self, op, ...); //調用這個函數,僞代碼... } //查找IMP IMP class_getMethodImplementation(Class cls, SEL sel) { if (!cls || !sel) return nil; IMP imp = lookUpImpOrNil(cls, sel); if (!imp) return _objc_msgForward; //_objc_msgForward 用於消息轉發 return imp; } IMP lookUpImpOrNil(Class cls, SEL sel) { if (!cls->initialize()) { _class_initialize(cls); } Class curClass = cls; IMP imp = nil; do { //先查緩存,緩存沒有時重建,仍舊沒有則向父類查詢 if (!curClass) break; if (!curClass->cache) fill_cache(cls, curClass); imp = cache_getImp(curClass, sel); if (imp) break; } while (curClass = curClass->superclass); return imp; }
雖然Apple沒有公開_objc_msgForward
的實現源碼,可是咱們仍是能得出結論:
_objc_msgForward
是一個函數指針(和 IMP 的類型同樣),是用於消息轉發的:當向一個對象發送一條消息,但它並無實現的時候,_objc_msgForward
會嘗試作消息轉發。在上篇中的《objc中向一個對象發送消息
[obj foo]
和objc_msgSend()
函數之間有什麼關係?》曾提到objc_msgSend
在「消息傳遞」中的做用。在「消息傳遞」過程當中,objc_msgSend
的動做比較清晰:首先在 Class 中的緩存查找 IMP (沒緩存則初始化緩存),若是沒找到,則向父類的 Class 查找。若是一直查找到根類仍舊沒有實現,則用_objc_msgForward
函數指針代替 IMP 。最後,執行這個 IMP 。
爲了展現消息轉發的具體動做,這裏嘗試向一個對象發送一條錯誤的消息,並查看一下_objc_msgForward
是如何進行轉發的。
首先開啓調試模式、打印出全部運行時發送的消息: 能夠在代碼裏執行下面的方法:
(void)instrumentObjcMessageSends(YES);
或者斷點暫停程序運行,並在 gdb 中輸入下面的命令:
call (void)instrumentObjcMessageSends(YES)
以第二種爲例,操做以下所示:
以後,運行時發送的全部消息都會打印到/tmp/msgSend-xxxx
文件裏了。
終端中輸入命令前往:
open /private/tmp
可能看到有多條,找到最新生成的,雙擊打開
在模擬器上執行執行如下語句(這一套調試方案僅適用於模擬器,真機不可用,關於該調試方案的拓展連接: Can the messages sent to an object in Objective-C be monitored or printed out? ),向一個對象發送一條錯誤的消息:
//
// main.m // CYLObjcMsgForwardTest // // Created by http://weibo.com/luohanchenyilong/. // Copyright (c) 2015年 微博@iOS程序犭袁. All rights reserved. // #import <UIKit/UIKit.h> #import "AppDelegate.h" #import "CYLTest.h" int main(int argc, char * argv[]) { @autoreleasepool { CYLTest *test = [[CYLTest alloc] init]; [test performSelector:(@selector(iOS程序犭袁))]; return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }
你能夠在/tmp/msgSend-xxxx
(我這一次是/tmp/msgSend-9805
)文件裏,看到打印出來:
+ CYLTest NSObject initialize
+ CYLTest NSObject alloc
- CYLTest NSObject init
- CYLTest NSObject performSelector:
+ CYLTest NSObject resolveInstanceMethod:
+ CYLTest NSObject resolveInstanceMethod:
- CYLTest NSObject forwardingTargetForSelector:
- CYLTest NSObject forwardingTargetForSelector:
- CYLTest NSObject methodSignatureForSelector:
- CYLTest NSObject methodSignatureForSelector:
- CYLTest NSObject class
- CYLTest NSObject doesNotRecognizeSelector:
- CYLTest NSObject doesNotRecognizeSelector:
- CYLTest NSObject class
結合《NSObject官方文檔》,排除掉 NSObject 作的事,剩下的就是_objc_msgForward
消息轉發作的幾件事:
調用resolveInstanceMethod:
方法 (或 resolveClassMethod:
)。容許用戶在此時爲該 Class 動態添加實現。若是有實現了,則調用並返回YES,那麼從新開始objc_msgSend
流程。這一次對象會響應這個選擇器,通常是由於它已經調用過class_addMethod
。若是仍沒實現,繼續下面的動做。
調用forwardingTargetForSelector:
方法,嘗試找到一個能響應該消息的對象。若是獲取到,則直接把消息轉發給它,返回非 nil 對象。不然返回 nil ,繼續下面的動做。注意,這裏不要返回 self ,不然會造成死循環。
調用methodSignatureForSelector:
方法,嘗試得到一個方法簽名。若是獲取不到,則直接調用doesNotRecognizeSelector
拋出異常。若是能獲取,則返回非nil:建立一個 NSlnvocation 並傳給forwardInvocation:
。
調用forwardInvocation:
方法,將第3步獲取到的方法簽名包裝成 Invocation 傳入,如何處理就在這裏面了,並返回非ni。
調用doesNotRecognizeSelector:
,默認的實現是拋出異常。若是第3步沒能得到一個方法簽名,執行該步驟。
上面前4個方法均是模板方法,開發者能夠override,由 runtime 來調用。最多見的實現消息轉發:就是重寫方法3和4,吞掉一個消息或者代理給其餘對象都是沒問題的
也就是說_objc_msgForward
在進行消息轉發的過程當中會涉及如下這幾個方法:
resolveInstanceMethod:
方法 (或 resolveClassMethod:
)。
forwardingTargetForSelector:
方法
methodSignatureForSelector:
方法
forwardInvocation:
方法
doesNotRecognizeSelector:
方法
爲了能更清晰地理解這些方法的做用,git倉庫裏也給出了一個Demo,名稱叫「 _objc_msgForward_demo
」,可運行起來看看。
下面回答下第二個問題「直接_objc_msgForward
調用它將會發生什麼?」
直接調用_objc_msgForward
是很是危險的事,若是用很差會直接致使程序Crash,可是若是用得好,能作不少很是酷的事。
就好像跑酷,幹得好,叫「耍酷」,幹很差就叫「做死」。
正如前文所說:
_objc_msgForward
是 IMP 類型,用於消息轉發的:當向一個對象發送一條消息,但它並無實現的時候,_objc_msgForward
會嘗試作消息轉發。
如何調用_objc_msgForward
? _objc_msgForward
隸屬 C 語言,有三個參數 :
-- | _objc_msgForward 參數 |
類型 |
---|---|---|
1. | 所屬對象 | id類型 |
2. | 方法名 | SEL類型 |
3. | 可變參數 | 可變參數類型 |
首先了解下如何調用 IMP 類型的方法,IMP類型是以下格式:
爲了直觀,咱們能夠經過以下方式定義一個 IMP類型 :
typedef void (*voidIMP)(id, SEL, ...)
一旦調用_objc_msgForward
,將跳過查找 IMP 的過程,直接觸發「消息轉發」,
若是調用了_objc_msgForward
,即便這個對象確實已經實現了這個方法,你也會告訴objc_msgSend
:
「我沒有在這個對象裏找到這個方法的實現」
想象下objc_msgSend
會怎麼作?一般狀況下,下面這張圖就是你正常走objc_msgSend
過程,和直接調用_objc_msgForward
的先後差異:
有哪些場景須要直接調用_objc_msgForward
?最多見的場景是:你想獲取某方法所對應的NSInvocation
對象。舉例說明:
JSPatch (Github 連接)就是直接調用_objc_msgForward
來實現其核心功能的:
JSPatch 以小巧的體積作到了讓JS調用/替換任意OC方法,讓iOS APP具有熱更新的能力。
做者的博文《JSPatch實現原理詳解》詳細記錄了實現原理,有興趣能夠看下。
runtime 對註冊的類, 會進行佈局,對於 weak 對象會放入一個 hash 表中。 用 weak 指向的對象內存地址做爲 key,當此對象的引用計數爲0的時候會 dealloc,假如 weak 指向的對象內存地址是a,那麼就會以a爲鍵, 在這個 weak 表中搜索,找到全部以a爲鍵的 weak 對象,從而設置爲 nil。
在上篇中的《runtime 如何實現 weak 屬性》有論述。(注:在上篇的《使用runtime Associate方法關聯的對象,須要在主對象dealloc的時候釋放麼?》裏給出的「對象的內存銷燬時間表」也提到__weak
引用的解除時間。)
咱們能夠設計一個函數(僞代碼)來表示上述機制:
objc_storeWeak(&a, b)
函數:
objc_storeWeak
函數把第二個參數--賦值對象(b)的內存地址做爲鍵值key,將第一個參數--weak 修飾的屬性變量(a)的內存地址(&a)做爲value,註冊到 weak 表中。若是第二個參數(b)爲0(nil),那麼把變量(a)的內存地址(&a)從weak表中刪除,
你能夠把objc_storeWeak(&a, b)
理解爲:objc_storeWeak(value, key)
,而且當key變nil,將value置nil。
在b非nil時,a和b指向同一個內存地址,在b變nil時,a變nil。此時向a發送消息不會崩潰:在Objective-C中向nil發送消息是安全的。
而若是a是由assign修飾的,則: 在b非nil時,a和b指向同一個內存地址,在b變nil時,a仍是指向該內存地址,變野指針。此時向a發送消息極易崩潰。
下面咱們將基於objc_storeWeak(&a, b)
函數,使用僞代碼模擬「runtime如何實現weak屬性」:
// 使用僞代碼模擬:runtime如何實現weak屬性
// http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong id obj1; objc_initWeak(&obj1, obj); /*obj引用計數變爲0,變量做用域結束*/ objc_destroyWeak(&obj1);
下面對用到的兩個方法objc_initWeak
和objc_destroyWeak
作下解釋:
整體說來,做用是: 經過objc_initWeak
函數初始化「附有weak修飾符的變量(obj1)」,在變量做用域結束時經過objc_destoryWeak
函數釋放該變量(obj1)。
下面分別介紹下方法的內部實現:
objc_initWeak
函數的實現是這樣的:在將「附有weak修飾符的變量(obj1)」初始化爲0(nil)後,會將「賦值對象」(obj)做爲參數,調用objc_storeWeak
函數。
obj1 = 0;
obj_storeWeak(&obj1, obj);
也就是說:
weak 修飾的指針默認值是 nil (在Objective-C中向nil發送消息是安全的)
而後obj_destroyWeak
函數將0(nil)做爲參數,調用objc_storeWeak
函數。
objc_storeWeak(&obj1, 0);
前面的源代碼與下列源代碼相同。
// 使用僞代碼模擬:runtime如何實現weak屬性
// http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong id obj1; obj1 = 0; objc_storeWeak(&obj1, obj); /* ... obj的引用計數變爲0,被置nil ... */ objc_storeWeak(&obj1, 0);
objc_storeWeak
函數把第二個參數--賦值對象(obj)的內存地址做爲鍵值,將第一個參數--weak修飾的屬性變量(obj1)的內存地址註冊到 weak 表中。若是第二個參數(obj)爲0(nil),那麼把變量(obj1)的地址從weak表中刪除。
解釋下:
由於編譯後的類已經註冊在 runtime 中,類結構體中的 objc_ivar_list
實例變量的鏈表 和 instance_size
實例變量的內存大小已經肯定,同時runtime 會調用 class_setIvarLayout
或 class_setWeakIvarLayout
來處理 strong weak 引用。因此不能向存在的類中添加實例變量;
運行時建立的類是能夠添加實例變量,調用 class_addIvar
函數。可是得在調用 objc_allocateClassPair
以後,objc_registerClassPair
以前,緣由同上。
總的說來,Run loop,正如其名,loop表示某種循環,和run放在一塊兒就表示一直在運行着的循環。實際上,run loop和線程是緊密相連的,能夠這樣說run loop是爲了線程而生,沒有線程,它就沒有存在的必要。Run loops是線程的基礎架構部分, Cocoa 和 CoreFundation 都提供了 run loop 對象方便配置和管理線程的 run loop (如下都以 Cocoa 爲例)。每一個線程,包括程序的主線程( main thread )都有與之相應的 run loop 對象。
runloop 和線程的關係:
主線程的run loop默認是啓動的。
iOS的應用程序裏面,程序啓動後會有一個以下的main()函數
int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }
重點是UIApplicationMain()函數,這個方法會爲main thread設置一個NSRunLoop對象,這就解釋了:爲何咱們的應用能夠在無人操做的時候休息,須要讓它幹活的時候又能立馬響應。
對其它線程來講,run loop默認是沒有啓動的,若是你須要更多的線程交互則能夠手動配置和啓動,若是線程只是去執行一個長時間的已肯定的任務則不須要。
在任何一個 Cocoa 程序的線程中,均可以經過如下代碼來獲取到當前線程的 run loop 。
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
參考連接:《Objective-C之run loop詳解》。
model 主要是用來指定事件在運行循環中的優先級的,分爲:
蘋果公開提供的 Mode 有兩個:
RunLoop只能運行在一種mode下,若是要換mode,當前的loop也須要停下重啓成新的。利用這個機制,ScrollView滾動過程當中 NSDefaultRunLoopMode(kCFRunLoopDefaultMode)的mode會切換到 UITrackingRunLoopMode來保證ScrollView的流暢滑動:只能在NSDefaultRunLoopMode模式下處理的事件會 影響scrllView的滑動。
若是咱們把一個NSTimer對象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主運行循環中的時候, ScrollView滾動過程當中會由於mode的切換,而致使NSTimer將再也不被調度。
同時由於mode仍是可定製的,因此:
Timer計時會被scrollView的滑動影響的問題能夠經過將timer添加到NSRunLoopCommonModes(kCFRunLoopCommonModes)來解決。代碼以下:
//
// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁) // https://github.com/ChenYilong //將timer添加到NSDefaultRunLoopMode中 [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTick:) userInfo:nil repeats:YES]; //而後再添加到NSRunLoopCommonModes裏 NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTick:) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
通常來說,一個線程一次只能執行一個任務,執行完成後線程就會退出。若是咱們須要一個機制,讓線程能隨時處理事件但並不退出,一般的代碼邏輯 是這樣的:
function loop() { initialize(); do { var message = get_next_message(); process_message(message); } while (message != quit); }
或使用僞代碼來展現下:
// // http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁) // https://github.com/ChenYilong int main(int argc, char * argv[]) { //程序一直運行狀態 while (AppIsRunning) { //睡眠狀態,等待喚醒事件 id whoWakesMe = SleepForWakingUp(); //獲得喚醒事件 id event = GetEvent(whoWakesMe); //開始處理事件 HandleEvent(event); } return 0; }
參考連接:
經過 retainCount 的機制來決定對象是否須要釋放。 每次 runloop 的時候,都會檢查對象的 retainCount,若是retainCount 爲 0,說明該對象沒有地方須要繼續使用了,能夠釋放掉了。
編譯時根據代碼上下文,插入 retain/release
ARC相對於MRC,不是在編譯時添加retain/release/autorelease這麼簡單。應該是編譯期和運行期兩部分共同幫助開發者管理內存。
在編譯期,ARC用的是更底層的C接口實現的retain/release/autorelease,這樣作性能更好,也是爲何不能在ARC環境 下手動retain/release/autorelease,同時對同一上下文的同一對象的成對retain/release操做進行優化(即忽略掉不 必要的操做);ARC也包含運行期組件,這個地方作的優化比較複雜,但也不能被忽略。【TODO:後續更新會詳細描述下】
分兩種狀況:手動干預釋放時機、系統自動去釋放。
系統自動去釋放--不手動指定autoreleasepool
Autorelease對象出了做用域以後,會被添加到最近一次建立的自動釋放池中,並會在當前的 runloop 迭代結束時釋放。
釋放的時機總結起來,能夠用下圖來表示:
下面對這張圖進行詳細的解釋:
從程序啓動到加載完成是一個完整的運行循環,而後會停下來,等待用戶交互,用戶的每一次交互都會啓動一次運行循環,來處理用戶全部的點擊事件、觸摸事件。
咱們都是知道: 全部 autorelease 的對象,在出了做用域以後,會被自動添加到最近建立的自動釋放池中。
可是若是每次都放進應用程序的 main.m
中的 autoreleasepool 中,早晚有被撐滿的一刻。這個過程當中一定有一個釋放的動做。什麼時候?
在一次完整的運行循環結束以前,會被銷燬。
那什麼時間會建立自動釋放池?運行循環檢測到事件並啓動後,就會建立自動釋放池。
子線程的 runloop 默認是不工做,沒法主動建立,必須手動建立。
自定義的 NSOperation 和 NSThread 須要手動建立自動釋放池。好比: 自定義的 NSOperation 類中的 main 方法裏就必須添加自動釋放池。不然出了做用域後,自動釋放對象會由於沒有自動釋放池去處理它,而形成內存泄露。
但對於 blockOperation 和 invocationOperation 這種默認的Operation ,系統已經幫咱們封裝好了,不須要手動建立自動釋放池。
@autoreleasepool 當自動釋放池被銷燬或者耗盡時,會向自動釋放池中的全部對象發送 release 消息,釋放自動釋放池中的全部對象。
若是在一個vc的viewDidLoad中建立一個 Autorelease對象,那麼該對象會在 viewDidAppear 方法執行前就被銷燬了。
參考連接:《黑幕背後的Autorelease》
訪問了野指針,好比對一個已經釋放的對象執行了release、訪問已經釋放對象的成員變量或者發消息。 死循環
autoreleasepool 以一個隊列數組的形式實現,主要經過下列三個函數完成.
objc_autoreleasepoolPush
objc_autoreleasepoolPop
objc_autorelease
看函數名就能夠知道,對 autorelease 分別執行 push,和 pop 操做。銷燬對象時執行release操做。
舉例說明:咱們都知道用類方法建立的對象都是 Autorelease 的,那麼一旦 Person 出了做用域,當在 Person 的 dealloc 方法中打上斷點,咱們就能夠看到這樣的調用堆棧信息:
一個對象中強引用了block,在block中又使用了該對象,就會發射循環引用。 解決方法是將該對象使用__weak或者__block修飾符修飾以後再在block中使用。
默認狀況下,在block中訪問的外部變量是複製過去的,即:寫操做不對原變量生效。可是你能夠加上__block
來讓其寫操做生效,示例代碼以下:
__block int a = 0; void (^foo)(void) = ^{ a = 1; } f00(); //這裏,a的值被修改成1
參考連接:微博@唐巧_boy的著做《iOS開發進階》中的第11.2.3章節
系統的某些block api中,UIView的block版本寫動畫時不須要考慮,但也有一些api 須要考慮:
所謂「引用循環」是指雙向的強引用,因此那些「單向的強引用」(block 強引用 self )沒有問題,好比這些:
[UIView animateWithDuration:duration animations:^{ [self.superview layoutIfNeeded]; }];
[[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.someProperty = xyz; }];
[[NSNotificationCenter defaultCenter] addObserverForName:@"someNotification" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * notification) { self.someProperty = xyz; }];
這些狀況不須要考慮「引用循環」。
但若是你使用一些參數中可能含有 ivar 的系統 api ,如 GCD 、NSNotificationCenter就要當心一點:好比GCD 內部若是引用了 self,並且 GCD 的其餘參數是 ivar,則要考慮到循環引用:
__weak __typeof__(self) weakSelf = self;
dispatch_group_async(_operationsGroup, _operationsQueue, ^ { __typeof__(self) strongSelf = weakSelf; [strongSelf doSomething]; [strongSelf doSomethingElse]; } );
相似的:
__weak __typeof__(self) weakSelf = self;
_observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"testKey" object:nil queue:nil usingBlock:^(NSNotification *note) { __typeof__(self) strongSelf = weakSelf; [strongSelf dismissModalViewControllerAnimated:YES]; }];
self --> _observer --> block --> self 顯然這也是一個循環引用。
dispatch_queue_t
)分哪兩種類型?使用Dispatch Group追加block到Global Group Queue,這些block若是所有執行完畢,就會執行Main Dispatch Queue中的結束處理的block。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, queue, ^{ /*加載圖片1 */ }); dispatch_group_async(group, queue, ^{ /*加載圖片2 */ }); dispatch_group_async(group, queue, ^{ /*加載圖片3 */ }); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // 合併圖片 });
dispatch_barrier_async
的做用是什麼?在並行隊列中,爲了保持某些任務的順序,須要等待一些任務完成後才能繼續進行,使用 barrier 來等待以前任務完成,避免數據競爭等問題。 dispatch_barrier_async
函數會等待追加到Concurrent Dispatch Queue並行隊列中的操做所有執行完以後,而後再執行 dispatch_barrier_async
函數追加的處理,等 dispatch_barrier_async
追加的處理執行結束以後,Concurrent Dispatch Queue才恢復以前的動做繼續執行。
打個比方:好比大家公司週末跟團旅遊,高速休息站上,司機說:你們都去上廁所,速戰速決,上完廁所就上高速。超大的公共廁所,你們同時去,程序猿很快就結束了,但程序媛就可能會慢一些,即便你第一個回來,司機也不會出發,司機要等待全部人都回來後,才能出發。 dispatch_barrier_async
函數追加的內容就如同 「上完廁所就上高速」這個動做。
(注意:使用 dispatch_barrier_async
,該函數只能搭配自定義並行隊列 dispatch_queue_t
使用。不能使用: dispatch_get_global_queue
,不然 dispatch_barrier_async
的做用會和 dispatch_async
的做用如出一轍。 )
dispatch_get_current_queue
?dispatch_get_current_queue
容易形成死鎖
- (void)viewDidLoad { [super viewDidLoad]; NSLog(@"1"); dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"2"); }); NSLog(@"3"); }
只輸出:1 。發生主線程鎖死。
// 添加鍵值觀察
/* 1 觀察者,負責處理監聽事件的對象 2 觀察的屬性 3 觀察的選項 4 上下文 */ [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"Person Name"];
observer中須要實現一下方法:
// 全部的 kvo 監聽到事件,都會調用此方法
/* 1. 觀察的屬性 2. 觀察的對象 3. change 屬性變化字典(新/舊) 4. 上下文,與監聽的時候傳遞的一致 */ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
所謂的「手動觸發」是區別於「自動觸發」:
自動觸發是指相似這種場景:在註冊 KVO 以前設置一個初始值,註冊以後,設置一個不同的值,就能夠觸發了。
想知道如何手動觸發,必須知道自動觸發 KVO 的原理:
鍵值觀察通知依賴於 NSObject 的兩個方法: willChangeValueForKey:
和 didChangevlueForKey:
。在一個被觀察屬性發生改變以前, willChangeValueForKey:
必定會被調用,這就 會記錄舊的值。而當改變發生後, didChangeValueForKey:
會被調用,繼而 observeValueForKey:ofObject:change:context:
也會被調用。若是能夠手動實現這些調用,就能夠實現「手動觸發」了。
那麼「手動觸發」的使用場景是什麼?通常咱們只在但願能控制「回調的調用時機」時纔會這麼作。
具體作法以下:
若是這個 value
是 表示時間的 self.now
,那麼代碼以下:最後兩行代碼缺一不可。
// .m文件
// Created by https://github.com/ChenYilong // 微博@iOS程序犭袁(http://weibo.com/luohanchenyilong/). // 手動觸發 value 的KVO,最後兩行代碼缺一不可。 //@property (nonatomic, strong) NSDate *now; - (void)viewDidLoad { [super viewDidLoad]; [self willChangeValueForKey:@"now"]; // 「手動觸發self.now的KVO」,必寫。 [self didChangeValueForKey:@"now"]; // 「手動觸發self.now的KVO」,必寫。 }
可是平時咱們通常不會這麼幹,咱們都是等系統去「自動觸發」。「自動觸發」的實現原理:
好比調用
setNow:
時,系統還會以某種方式在中間插入wilChangeValueForKey:
、didChangeValueForKey:
和observeValueForKeyPath:ofObject:change:context:
的調用。
你們可能覺得這是由於 setNow:
是合成方法,有時候咱們也能看到人們這麼寫代碼:
- (void)setNow:(NSDate *)aDate { [self willChangeValueForKey:@"now"]; // 沒有必要 _now = aDate; [self didChangeValueForKey:@"now"];// 沒有必要 }
這是徹底沒有必要的代碼,不要這麼作,這樣的話,KVO代碼會被調用兩次。KVO在調用存取方法以前老是調用 willChangeValueForKey:
,以後老是調用 didChangeValueForkey:
。怎麼作到的呢?答案是經過 isa 混寫(isa-swizzling)。下文《apple用什麼方式實現對一個對象的KVO?》會有詳述。
參考連接: Manual Change Notification---Apple 官方文檔
NSString *_foo
,調用setValue:forKey:時,能夠以foo仍是 _foo
做爲key?均可以。
KVO支持實例變量
請參考:《如何本身動手實現 KVO》
Apple 的文檔對 KVO 實現的描述:
Automatic key-value observing is implemented using a technique called isa-swizzling... When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class ...
從Apple 的文檔能夠看出:Apple 並不但願過多暴露 KVO 的實現細節。不過,要是藉助 runtime 提供的方法去深刻挖掘,全部被掩蓋的細節都會原形畢露:
當你觀察一個對象時,一個新的類會被動態建立。這個類繼承自該對象的本來的類,並重寫了被觀察屬性的 setter 方法。重寫的 setter 方法會負責在調用原 setter 方法以前和以後,通知全部觀察對象:值的更改。最後經過
isa 混寫(isa-swizzling)
把這個對象的 isa 指針 ( isa 指針告訴 Runtime 系統這個對象的類是什麼 ) 指向這個新建立的子類,對象就神奇的變成了新建立的子類的實例。我畫了一張示意圖,以下所示:
KVO 確實有點黑魔法:
Apple 使用了
isa 混寫(isa-swizzling)
來實現 KVO 。
下面作下詳細解釋:
鍵值觀察通知依賴於 NSObject 的兩個方法: willChangeValueForKey:
和 didChangevlueForKey:
。在一個被觀察屬性發生改變以前, willChangeValueForKey:
必定會被調用,這就會記錄舊的值。而當改變發生後, didChangeValueForKey:
會被調用,繼而 observeValueForKey:ofObject:change:context:
也會被調用。能夠手動實現這些調用,但不多有人這麼作。通常咱們只在但願能控制回調的調用時機時纔會這麼作。大部分狀況下,改變通知會自動調用。
好比調用 setNow:
時,系統還會以某種方式在中間插入 wilChangeValueForKey:
、 didChangeValueForKey:
和 observeValueForKeyPath:ofObject:change:context:
的調用。你們可能覺得這是由於 setNow:
是合成方法,有時候咱們也能看到人們這麼寫代碼:
- (void)setNow:(NSDate *)aDate { [self willChangeValueForKey:@"now"]; // 沒有必要 _now = aDate; [self didChangeValueForKey:@"now"];// 沒有必要 }
這是徹底沒有必要的代碼,不要這麼作,這樣的話,KVO代碼會被調用兩次。KVO在調用存取方法以前老是調用 willChangeValueForKey:
,以後老是調用 didChangeValueForkey:
。怎麼作到的呢?答案是經過 isa 混寫(isa-swizzling)。第一次對一個對象調用 addObserver:forKeyPath:options:context:
時,框架會建立這個類的新的 KVO 子類,並將被觀察對象轉換爲新子類的對象。在這個 KVO 特殊子類中, Cocoa 建立觀察屬性的 setter ,大體工做原理以下:
- (void)setNow:(NSDate *)aDate { [self willChangeValueForKey:@"now"]; [super setValue:aDate forKey:@"now"]; [self didChangeValueForKey:@"now"]; }
這種繼承和方法注入是在運行時而不是編譯時實現的。這就是正確命名如此重要的緣由。只有在使用KVC命名約定時,KVO才能作到這一點。
KVO 在實現中經過 isa 混寫(isa-swizzling)
把這個對象的 isa 指針 ( isa 指針告訴 Runtime 系統這個對象的類是什麼 ) 指向這個新建立的子類,對象就神奇的變成了新建立的子類的實例。這在Apple 的文檔能夠獲得印證:
Automatic key-value observing is implemented using a technique called isa-swizzling... When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class ...
然而 KVO 在實現中使用了 isa 混寫( isa-swizzling)
,這個的確不是很容易發現:Apple 還重寫、覆蓋了 -class
方法並返回原來的類。 企圖欺騙咱們:這個類沒有變,就是本來那個類。。。
可是,假設「被監聽的對象」的類對象是 MYClass
,有時候咱們能看到對 NSKVONotifying_MYClass
的引用而不是對 MYClass
的引用。藉此咱們得以知道 Apple 使用了 isa 混寫(isa-swizzling)
。具體探究過程可參考 這篇博文 。
參考連接: Should IBOutlets be strong or weak under ARC?
文章告訴咱們:
由於既然有外鏈那麼視圖在xib或者storyboard中確定存在,視圖已經對它有一個強引用了。
不過這個回答漏了個重要知識,使用storyboard(xib不行)建立的vc,會有一個叫 _topLevelObjectsToKeepAliveFromStoryboard的私有數組強引用全部top level的對象,因此這時即使outlet聲明成weak也不要緊
它可以經過KVC的方式配置一些你在interface builder 中不能配置的屬性。當你但願在IB中做盡量多得事情,這個特性可以幫助你編寫更加輕量級的viewcontroller
設置全局斷點快速定位問題代碼所在行
更多 lldb(gdb) 調試命令可查看