整理的別人寫的總結,覺得這鞋不光光是面試的總結,也應該成為平時工作關注的點。get新技能吧 !
風格糾錯題enter image description here 修改完的代碼:
修改方法有很多種,現給出一種做示例:
typedef NS_ENUM(NSInteger, CYLSex) {
CYLSexMan,
CYLSexWoman
};<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPkBpbnRlcmZhY2UgQ1lMVXNlciA6IE5TT2JqZWN0PC9wPg0KPHA+QHByb3BlcnR5IChub25hdG9taWMsIHJlYWRvbmx5LCBjb3B5KSBOU1N0cmluZyAqbmFtZTs8YnIgLz4NCkBwcm9wZXJ0eSAobm9uYXRvbWljLCByZWFkb25seSwgYXNzaWduKSBOU1VJbnRlZ2VyIGFnZTs8YnIgLz4NCkBwcm9wZXJ0eSAobm9uYXRvbWljLCByZWFkb25seSwgYXNzaWduKSBDWUxTZXggc2V4OzwvcD4NCihpbnN0YW5jZXR5cGUpaW5pdFdpdGhOYW1lOihOU1N0cmluZyAqKW5hbWUgYWdlOihOU1VJbnRlZ2VyKWFnZSBzZXg6KENZTFNleClzZXg7IChpbnN0YW5jZXR5cGUpdXNlcldpdGhOYW1lOihOU1N0cmluZyAqKW5hbWUgYWdlOihOU1VJbnRlZ2VyKWFnZSBzZXg6KENZTFNleClzZXg7DQo8cD5AZW5kPC9wPg0KPHA+z8LD5rbUvt/M5dDeuMS1xLXYt72jrLfWwb2yv7fW1/bPwr3pydyjutOyycuyv7fWus3Txbuvsr+31iCho9LyzqrTssnLsr+31sO7yrLDtLy8yvW6rMG/o6zOqsHLvdrKobTzvNLKsbzko6y3xdTauvPD5r2yo6y088nxx+vWsb3Tv7TTxbuvsr+31qGjPGJyIC8+DQrTxbuvsr+31jwvcD4NCjxwcmUgY2xhc3M9"brush:java;">
enum 建議使用 NS_ENUM 和 NS_OPTIONS 宏來定義枚舉類型,參見官方的 Adopting Modern Objective-C 一文:
//定義一個枚舉
typedef NS_ENUM(NSInteger, CYLSex) {
CYLSexMan,
CYLSexWoman
};
(僅僅讓性別包含男和女可能並不嚴謹,最嚴謹的做法可以參考 這裡 。)
age 屬性的類型:應避免使用基本類型,建議使用 Foundation 數據類型,對應關系如下:
int -> NSInteger
unsigned -> NSUInteger
float -> CGFloat
動畫時間 -> NSTimeInterval
同時考慮到 age 的特點,應使用 NSUInteger ,而非 int 。 這樣做的是基於64-bit 適配考慮,詳情可參考出題者的博文《64-bit Tips》。
如果工程項目非常龐大,需要拆分成不同的模塊,可以在類、typedef宏命名的時候使用前綴。
doLogIn方法不應寫在該類中:
雖然LogIn的命名不太清晰,但筆者猜測是login的意思, (勘誤:Login是名詞,LogIn 是動詞,都表示登陸的意思。見: Log in vs. login )
登錄操作屬於業務邏輯,觀察類名 UserModel ,以及屬性的命名方式,該類應該是一個 Model 而不是一個“ MVVM 模式下的 ViewModel ”:
無論是 MVC 模式還是 MVVM 模式,業務邏輯都不應當寫在 Model 裡:MVC 應在 C,MVVM 應在 VM。
(如果拋開命名規范,假設該類真的是 MVVM 模式裡的 ViewModel ,那麼 UserModel 這個類可能對應的是用戶注冊頁面,如果有特殊的業務需求,比如: -logIn 對應的應當是注冊並登錄的一個 Button ,出現 -logIn 方法也可能是合理的。)
doLogIn 方法命名不規范:添加了多余的動詞前綴。 請牢記:
如果方法表示讓對象執行一個動作,使用動詞打頭來命名,注意不要使用 do,does 這種多余的關鍵字,動詞本身的暗示就足夠了。
應為 -logIn (注意: Login 是名詞, LogIn 是動詞,都表示登陸。 見 Log in vs. login )
-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;方法中不要用 with 來連接兩個參數: withAge: 應當換為age:,age: 已經足以清晰說明參數的作用,也不建議用 andAge: :通常情況下,即使有類似 withA:withB: 的命名需求,也通常是使用withA:andB: 這種命名,用來表示方法執行了兩個相對獨立的操作(從設計上來說,這時候也可以拆分成兩個獨立的方法),它不應該用作闡明有多個參數,比如下面的:
//錯誤,不要使用"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;
由於字符串值可能會改變,所以要把相關屬性的“內存管理語義”聲明為 copy 。(原因在下文有詳細論述:用@property聲明的NSString(或NSArray,NSDictionary)經常使用copy關鍵字,為什麼?)
“性別”(sex)屬性的:該類中只給出了一種“初始化方法” (initializer)用於設置“姓名”(Name)和“年齡”(Age)的初始值,那如何對“性別”(Sex)初始化?
Objective-C 有 designated 和 secondary 初始化方法的觀念。 designated 初始化方法是提供所有的參數,secondary 初始化方法是一個或多個,並且提供一個或者更多的默認參數來調用 designated 初始化方法的初始化方法。舉例說明:
// .m文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
//
@implementation CYLUser
- (instancetype)initWithName:(NSString *)name
age:(NSUInteger)age
sex:(CYLSex)sex {
if(self = [super init]) {
_name = [name copy];
_age = age;
_sex = sex;
}
return self;
}
- (instancetype)initWithName:(NSString *)name
age:(NSUInteger)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文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 第二種修改方法(基於第一種修改方法的基礎上)
typedef NS_ENUM(NSInteger, CYLSex) {
CYLSexMan,
CYLSexWoman
};
@interface CYLUser : NSObject
硬傷部分
在-和(void)之間應該有一個空格 enum 中駝峰命名法和下劃線命名法混用錯誤:枚舉類型的命名規則和函數的命名規則相同:命名時使用駝峰命名法,勿使用下劃線命名法。 enum 左括號前加一個空格,或者將左括號換到下一行 enum 右括號後加一個空格 UserModel :NSObject 應為UserModel : NSObject,也就是:右側少了一個空格。 @interface 與 @property 屬性聲明中間應當間隔一行。 兩個方法定義之間不需要換行,有時為了區分方法的功能也可間隔一行,但示例代碼中間隔了兩行。 -(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;方法中方法名與參數之間多了空格。而且 - 與 (id) 之間少了空格。 -(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;方法中方法名與參數之間多了空格:(NSString*)name 前多了空格。 -(id)initUserModelWithUserName: (NSString*)name withAge:(int)age; 方法中 (NSString*)name,應為 (NSString *)name,少了空格。 doLogIn方法中的 LogIn 命名不清晰:筆者猜測是login的意思,應該是粗心手誤造成的。 (勘誤: Login 是名詞, LogIn 是動詞,都表示登陸的意思。見: Log in vs. login )什麼情況使用 weak 關鍵字,相比 assign 有什麼不同?
什麼情況使用 weak 關鍵字?
在 ARC 中,在有可能出現循環引用的時候,往往要通過讓其中一端使用 weak 來解決,比如: delegate 代理屬性 自身已經對它進行一次強引用,沒有必要再強引用一次,此時也會使用 weak,自定義 IBOutlet 控件屬性一般也使用 weak;當然,也可以使用strong。在下文也有論述:《IBOutlet連出來的視圖屬性為什麼可以被設置成weak?》
不同點:
weak 此特質表明該屬性定義了一種“非擁有關系” (nonowning relationship)。為這種屬性設置新值時,設置方法既不保留新值,也不釋放舊值。此特質同assign類似, 然而在屬性所指的對象遭到摧毀時,屬性值也會清空(nil out)。 而 assign 的“設置方法”只會執行針對“純量類型” (scalar type,例如 CGFloat 或 NSlnteger 等)的簡單賦值操作。 assigin 可以用非 OC 對象,而 weak 必須用於 OC 對象怎麼用 copy 關鍵字?
用途:
NSString、NSArray、NSDictionary 等等經常使用copy關鍵字,是因為他們有對應的可變類型:NSMutableString、NSMutableArray、NSMutableDictionary; block 也經常使用 copy 關鍵字,具體原因見官方文檔:Objects Use Properties to Keep Track of Blocks: block 使用 copy 是從 MRC 遺留下來的“傳統”,在 MRC 中,方法內部的 block 是在棧區的,使用 copy 可以把它放到堆區.在 ARC 中寫不寫都行:對於 block 使用 copy 還是 strong 效果是一樣的,但寫上 copy 也無傷大雅,還能時刻提醒我們:編譯器自動對 block 進行了 copy 操作。如果不寫 copy ,該類的調用者有可能會忘記或者根本不知道“編譯器會自動對 block 進行了 copy 操作”,他們有可能會在調用之前自行拷貝屬性值。這種操作多余而低效。你也許會感覺我這種做法有些怪異,不需要寫依然寫。如果你這樣想,其實是你“日用而不知”,你平時開發中是經常在用我說的這種做法的,比如下面的屬性不寫copy也行,但是你會選擇寫還是不寫呢? @property (nonatomic, copy) NSString *userId; - (instancetype)initWithUserId:(NSString *)userId { self = [super init]; if (!self) { return nil; } _userId = [userId copy]; return self; }
下面做下解釋: 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;
兩個問題:1、添加,刪除,修改數組內的元素的時候,程序會因為找不到對應的方法而崩潰.因為 copy 就是復制一個不可變 NSArray 的對象;2、使用了 atomic 屬性會嚴重影響性能 ;
第1條的相關原因在下文中有論述《用@property聲明的NSString(或NSArray,NSDictionary)經常使用 copy 關鍵字,為什麼?如果改用strong關鍵字,可能造成什麼問題?》 以及上文《怎麼用 copy 關鍵字?》也有論述。
比如下面的代碼就會發生崩潰
// .h文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 下面的代碼就會發生崩潰
@property (nonatomic, copy) NSMutableArray *mutableArray;
// .m文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 下面的代碼就會發生崩潰
NSMutableArray *array = [NSMutableArray arrayWithObjects:@1,@2,nil];
self.mutableArray = array;
[self.mutableArray removeObjectAtIndex:0];
接下來就會奔潰:
-[__NSArrayI removeObjectAtIndex:]: unrecognized selector sent to instance 0x7fcd1bc30460
第2條原因,如下:
該屬性使用了同步鎖,會在創建時生成一些額外的代碼用於幫助編寫多線程程序,這會帶來性能問題,通過聲明 nonatomic 可以節省這些雖然很小但是不必要額外開銷。
在默認情況下,由編譯器所合成的方法會通過鎖定機制確保其原子性(atomicity)。如果屬性具備 nonatomic 特質,則不使用同步鎖。請注意,盡管沒有名為“atomic”的特質(如果某屬性不具備 nonatomic 特質,那它就是“原子的”(atomic))。
在iOS開發中,你會發現,幾乎所有屬性都聲明為 nonatomic。
一般情況下並不要求屬性必須是“原子的”,因為這並不能保證“線程安全” ( thread safety),若要實現“線程安全”的操作,還需采用更為深層的鎖定機制才行。例如,一個線程在連續多次讀取某屬性值的過程中有別的線程在同時改寫該值,那麼即便將屬性聲明為 atomic,也還是會讀到不同的屬性值。
因此,開發iOS程序時一般都會使用 nonatomic 屬性。但是在開發 Mac OS X 程序時,使用 atomic 屬性通常都不會有性能瓶頸。
5. 如何讓自己的類用 copy 修飾符?如何重寫帶 copy 關鍵字的 setter?
若想令自己所寫的對象具有拷貝功能,則需實現 NSCopying 協議。如果自定義的對象分為可變版本與不可變版本,那麼就要同時實現 NSCopying 與 NSMutableCopying 協議。
具體步驟:
需聲明該類遵從 NSCopying 協議 實現 NSCopying 協議。該協議只有一個方法: - (id)copyWithZone:(NSZone *)zone; 注意:一提到讓自己的類用 copy 修飾符,我們總是想覆寫copy方法,其實真正需要實現的卻是 “copyWithZone” 方法。
以第一題的代碼為例:
// .h文件 // http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong // 修改完的代碼 typedef NS_ENUM(NSInteger, CYLSex) { CYLSexMan, CYLSexWoman }; @interface CYLUser : NSObject@property (nonatomic, readonly, copy) NSString *name; @property (nonatomic, readonly, assign) NSUInteger age; @property (nonatomic, readonly, assign) CYLSex sex; - (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex; + (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex; @end
然後實現協議中規定的方法:
(id)copyWithZone:(NSZone *)zone {但在實際的項目中,不可能這麼簡單,遇到更復雜一點,比如類對象中的數據結構可能並未在初始化方法中設置好,需要另行設置。舉個例子,假如 CYLUser 中含有一個數組,與其他 CYLUser 對象建立或解除朋友關系的那些方法都需要操作這個數組。那麼在這種情況下,你得把這個包含朋友對象的數組也一並拷貝過來。下面列出了實現此功能所需的全部代碼:
// .h文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 以第一題《風格糾錯題》裡的代碼為例
typedef NS_ENUM(NSInteger, CYLSex) {
CYLSexMan,
CYLSexWoman
};
@interface CYLUser : NSObject
@property (nonatomic, readonly, copy) NSString *name;
@property (nonatomic, readonly, assign) NSUInteger age;
@property (nonatomic, readonly, assign) CYLSex sex;
@end
// .m文件
// .m文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
//
@implementation CYLUser {
NSMutableSet *_friends;
}
(void)setName:(NSString *)name {
_name = [name copy];
}
(instancetype)initWithName:(NSString *)name
age:(NSUInteger)age
sex:(CYLSex)sex {
if(self = [super init]) {
_name = [name copy];
_age = age;
_sex = sex;
_friends = [[NSMutableSet alloc] init];
}
return self;
}
(void)addFriend:(CYLUser *)user {
[_friends addObject:user];
}
(void)removeFriend:(CYLUser *)user {
[_friends removeObject:user];
}
(id)copyWithZone:(NSZone *)zone {
CYLUser *copy = [[[self class] allocWithZone:zone]
initWithName:_name
age:_age
sex:_sex];
copy->_friends = [_friends mutableCopy];
return copy;
}
(id)deepCopy {
CYLUser *copy = [[[self class] allocWithZone:zone]
initWithName:_name
age:_age
sex:_sex];
copy->_friends = [[NSMutableSet alloc] initWithSet:_friends
copyItems:YES];
return copy;
}
@end
以上做法能滿足基本的需求,但是也有缺陷:
如果你所寫的對象需要深拷貝,那麼可考慮新增一個專門執行深拷貝的方法。
【注:深淺拷貝的概念,在下文中有介紹,詳見下文的:用@property聲明的 NSString(或NSArray,NSDictionary)經常使用 copy 關鍵字,為什麼?如果改用 strong 關鍵字,可能造成什麼問題?】
在例子中,存放朋友對象的 set 是用 “copyWithZone:” 方法來拷貝的,這種淺拷貝方式不會逐個復制 set 中的元素。若需要深拷貝的話,則可像下面這樣,編寫一個專供深拷貝所用的方法:
(id)deepCopy {至於如何重寫帶 copy 關鍵字的 setter這個問題,
如果拋開本例來回答的話,如下:
(void)setName:(NSString *)name {不過也有爭議,有人說“蘋果如果像下面這樣干,是不是效率會高一些?”
(void)setName:(NSString *)name {這樣真得高效嗎?不見得!這種寫法“看上去很美、很合理”,但在實際開發中,它更像下圖裡的做法:
enter image description here
克強總理這樣評價你的代碼風格:
enter image description here
我和總理的意見基本一致:
老百姓 copy 一下,咋就這麼難?
你可能會說:
之所以在這裡做if判斷 這個操作:是因為一個 if 可能避免一個耗時的copy,還是很劃算的。 (在剛剛講的:《如何讓自己的類用 copy 修飾符?》裡的那種復雜的copy,我們可以稱之為 “耗時的copy”,但是對 NSString 的 copy 還稱不上。)
但是你有沒有考慮過代價:
你每次調用 setX: 都會做 if 判斷,這會讓 setX: 變慢,如果你在 setX:寫了一串復雜的 if+elseif+elseif+... 判斷,將會更慢。
要回答“哪個效率會高一些?”這個問題,不能脫離實際開發,就算 copy 操作十分耗時,if 判斷也不見得一定會更快,除非你把一個“ @property他當前的值 ”賦給了他自己,代碼看起來就像:
[a setX:x1];
[a setX:x1]; //你確定你要這麼干?與其在setter中判斷,為什麼不把代碼寫好?
或者
[a setX:[a x]]; //隊友咆哮道:你在干嘛?!!
不要在 setter 裡進行像 if(_obj != newObj) 這樣的判斷。(該觀點參考鏈接: How To Write Cocoa Object Setters: Principle 3: Only Optimize After You Measure )
什麼情況會在 copy setter 裡做 if 判斷? 例如,車速可能就有最高速的限制,車速也不可能出現負值,如果車子的最高速為300,則 setter 的方法就要改寫成這樣:
-(void)setSpeed:(int)_speed{
if(_speed < 0) speed = 0;
if(_speed > 300) speed = 300;
_speed = speed;
}
回到這個題目,如果單單就上文的代碼而言,我們不需要也不能重寫 name 的 setter :由於是 name 是只讀屬性,所以編譯器不會為其創建對應的“設置方法”,用初始化方法設置好屬性值之後,就不能再改變了。( 在本例中,之所以還要聲明屬性的“內存管理語義”–copy,是因為:如果不寫 copy,該類的調用者就不知道初始化方法裡會拷貝這些屬性,他們有可能會在調用初始化方法之前自行拷貝屬性值。這種操作多余而低效)。
那如何確保 name 被 copy?在初始化方法(initializer)中做:
- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex { if(self = [super init]) { _name = [name copy]; _age = age; _sex = sex; _friends = [[NSMutableSet alloc] init]; } return self; }@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;
例如下面這個類:
@interface Person : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end
上述代碼寫出來的類與下面這種寫法等效:
@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 語法來指定實例變量的名字.
@implementation Person
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end
我為了搞清屬性是怎麼實現的,曾經反編譯過相關的代碼,他大致生成了五個東西
OBJC_IVAR_$類名$屬性名稱 :該屬性的“偏移量” (offset),這個偏移量是“硬編碼” (hardcode),表示該變量距離存放對象的內存區域的起始地址有多遠。 setter 與 getter 方法對應的實現函數 ivar_list :成員變量列表 method_list :方法列表 prop_list :屬性列表
也就是說我們每次在增加一個屬性,系統都會在 ivar_list 中添加一個成員變量的描述,在 method_list 中增加 setter 與 getter 方法的描述,在屬性列表中增加一個屬性的描述,然後計算該屬性在對象中的偏移量,然後給出 setter 與 getter 方法對應的實現,在 setter 方法中從偏移量的位置開始賦值,在 getter 方法中從偏移量開始取值,為了能夠讀取正確字節數,系統對象偏移量的指針類型進行了類型強轉.
7. @protocol 和 category 中如何使用 @property
在 protocol 中使用 property 只會生成 setter 和 getter 方法聲明,我們使用屬性的目的,是希望遵守我協議的對象能實現該屬性 category 使用 @property 也是只會生成 setter 和 getter 方法的聲明,如果我們真的需要給 category 增加屬性的實現,需要借助於運行時的兩個函數: objc_setAssociatedObject objc_getAssociatedObjectruntime 如何實現 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。
(注:在下文的《使用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 表中刪除,在後面的相關一題會詳解。
使用偽代碼是為了方便理解,下面我們“真槍實彈”地實現下:
如何讓不使用weak修飾的@property,擁有weak的效果。
我們從setter方法入手:
(void)setObject:(NSObject *)object也就是有兩個步驟:
在setter方法中做如下設置: objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN); 在屬性所指的對象遭到摧毀時,屬性值也會清空(nil out)。做到這點,同樣要借助 runtime: //要銷毀的目標對象 id objectToBeDeallocated; //可以理解為一個“事件”:當上面的目標對象銷毀時,同時要發生的“事件”。 id objectWeWantToBeReleasedWhenThatHappens; objc_setAssociatedObject(objectToBeDeallocted, someUniqueKey, objectWeWantToBeReleasedWhenThatHappens, OBJC_ASSOCIATION_RETAIN);
知道了思路,我們就開始實現 cyl_runAtDealloc 方法,實現過程分兩部分:
第一部分:創建一個類,可以理解為一個“事件”:當目標對象銷毀時,同時要發生的“事件”。借助 block 執行“事件”。
// .h文件
// .h文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 這個類,可以理解為一個“事件”:當目標對象銷毀時,同時要發生的“事件”。借助block執行“事件”。
typedef void (^voidBlock)(void);
@interface CYLBlockExecutor : NSObject
(id)initWithBlock:(voidBlock)block;@end
// .m文件
// .m文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 這個類,可以理解為一個“事件”:當目標對象銷毀時,同時要發生的“事件”。借助block執行“事件”。
@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方法
// CYLNSObject+RunAtDealloc.h文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 利用runtime實現cyl_runAtDealloc方法
const void *runAtDeallocBlockKey = &runAtDeallocBlockKey;
@interface NSObject (CYLRunAtDealloc)
(void)cyl_runAtDealloc:(voidBlock)block;@end
// CYLNSObject+RunAtDealloc.m文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 利用runtime實現cyl_runAtDealloc方法
@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
使用方法: 導入
#import "CYLNSObject+RunAtDealloc.h"
然後就可以使用了:
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(讀寫)、readonly (只讀) 內存管理語義---assign、strong、 weak、unsafe_unretained、copy 方法名---getter=、setter= getter= 的樣式: @property (nonatomic, getter=isOn) BOOL on; ( `setter=`這種不常用,也不推薦使用。故不在這裡給出寫法。) setter= 一般用在特殊的情境下,比如:
在數據反序列化、轉模型的過程中,服務器返回的字段如果以 init 開頭,所以你需要定義一個 init 開頭的屬性,但默認生成的 setter 與 getter 方法也會以 init 開頭,而編譯器會把所有以 init 開頭的方法當成初始化方法,而初始化方法只能返回 self 類型,因此編譯器會報錯。
這時你就可以使用下面的方式來避免編譯器報錯:
@property(nonatomic, strong, getter=p_initBy, setter=setP_initBy:)NSString *initBy;
另外也可以用關鍵字進行特殊說明,來避免編譯器報錯:
@property(nonatomic, readwrite, copy, null_resettable) NSString *initBy;
- (NSString *)initBy attribute((objc_method_family(none)));
不常用的:nonnull,null_resettable,nullableweak屬性需要在dealloc中置nil麼?
不需要。
在ARC環境無論是強指針還是弱指針都無需在 dealloc 設置為 nil , ARC 會自動幫我們處理
即便是編譯器不幫我們做這些,weak也不需要在 dealloc 中置nil:
正如上文的:runtime 如何實現 weak 屬性 中提到的:
我們模擬下 weak 的 setter 方法,應該如下:
(void)setObject:(NSObject *)object也即:
在屬性所指的對象遭到摧毀時,屬性值也會清空(nil out)。
@synthesize和@dynamic分別有什麼作用?
@property有兩個對應的詞,一個是 @synthesize,一個是 @dynamic。如果 @synthesize和 @dynamic都沒寫,那麼默認的就是@syntheszie var = _var;
@synthesize 的語義是如果你沒有手動實現 setter 方法和 getter 方法,那麼編譯器會自動為你加上這兩個方法。
@dynamic 告訴編譯器:屬性的 setter 與 getter 方法由用戶自己實現,不自動生成。(當然對於 readonly 的屬性只需提供 getter 即可)。假如一個屬性被聲明為 @dynamic var,然後你沒有提供 @setter方法和 @getter 方法,編譯的時候沒問題,但是當程序運行到 instance.var = someVar,由於缺 setter 方法會導致程序崩潰;或者當運行到 someVar = var 時,由於缺 getter 方法同樣會導致崩潰。編譯時沒問題,運行時才執行相應的方法,這就是所謂的動態綁定。
ARC下,不顯式指定任何屬性關鍵字時,默認的關鍵字都有哪些?
對應基本數據類型默認關鍵字是
atomic,readwrite,assign
對於普通的 Objective-C 對象
atomic,readwrite,strong
參考鏈接:
Objective-C ARC: strong vs retain and weak vs assign Variable property attributes or Modifiers in iOS
用@property聲明的NSString(或NSArray,NSDictionary)經常使用copy關鍵字,為什麼?如果改用strong關鍵字,可能造成什麼問題?
因為父類指針可以指向子類對象,使用 copy 的目的是為了讓本對象的屬性不受外界影響,使用 copy 無論給我傳入是一個可變對象還是不可對象,我本身持有的就是一個不可變的副本.
如果我們使用是 strong ,那麼這個屬性就有可能指向一個可變對象,如果這個可變對象在外部被修改了,那麼會影響該屬性.
copy 此特質所表達的所屬關系與 strong 類似。然而設置方法並不保留新值,而是將其“拷貝” (copy)。 當屬性類型為 NSString 時,經常用此特質來保護其封裝性,因為傳遞給設置方法的新值有可能指向一個 NSMutableString 類的實例。這個類是 NSString 的子類,表示一種可修改其值的字符串,此時若是不拷貝字符串,那麼設置完屬性之後,字符串的值就可能會在對象不知情的情況下遭人更改。所以,這時就要拷貝一份“不可變” (immutable)的字符串,確保對象中的字符串值不會無意間變動。只要實現屬性所用的對象是“可變的” (mutable),就應該在設置新屬性值時拷貝一份。
舉例說明:
定義一個以 strong 修飾的 array:
@property (nonatomic ,readwrite, strong) NSArray *array;
然後進行下面的操作:
NSMutableArray *mutableArray = [[NSMutableArray alloc] init]; NSArray *array = @[ @1, @2, @3, @4 ]; self.array = mutableArray; [mutableArray removeAllObjects];; NSLog(@"%@",self.array); [mutableArray addObjectsFromArray:array]; self.array = [mutableArray copy]; [mutableArray removeAllObjects];; NSLog(@"%@",self.array);
打印結果如下所示:
2015-09-27 19:10:32.523 CYLArrayCopyDmo[10681:713670] (
)
2015-09-27 19:10:32.524 CYLArrayCopyDmo[10681:713670] (
1,
2,
3,
4
)
(詳見倉庫內附錄的 Demo。)
為了理解這種做法,首先要知道,兩種情況:
對非集合類對象的 copy 與 mutableCopy 操作; 對集合類對象的 copy 與 mutableCopy 操作。對非集合類對象的copy操作:
在非集合類對象中:對 immutable 對象進行 copy 操作,是指針復制,mutableCopy 操作時內容復制;對 mutable 對象進行 copy 和 mutableCopy 都是內容復制。用代碼簡單表示如下:
[immutableObject copy] // 淺復制 [immutableObject mutableCopy] //深復制 [mutableObject copy] //深復制 [mutableObject mutableCopy] //深復制
比如以下代碼:
NSMutableString *string = [NSMutableString stringWithString:@”origin”];//copy
NSString *stringCopy = [string copy];
查看內存,會發現 string、stringCopy 內存地址都不一樣,說明此時都是做內容拷貝、深拷貝。即使你進行如下操作:
[string appendString:@”origion!”]
stringCopy 的值也不會因此改變,但是如果不使用 copy,stringCopy 的值就會被改變。 集合類對象以此類推。 所以,
用 @property 聲明 NSString、NSArray、NSDictionary 經常使用 copy 關鍵字,是因為他們有對應的可變類型:NSMutableString、NSMutableArray、NSMutableDictionary,他們之間可能進行賦值操作,為確保對象中的字符串值不會無意間變動,應該在設置新屬性值時拷貝一份。
2、集合類對象的copy與mutableCopy
集合類對象是指 NSArray、NSDictionary、NSSet … 之類的對象。下面先看集合類immutable對象使用 copy 和 mutableCopy 的一個例子:
NSArray *array = @[@[@”a”, @”b”], @[@”c”, @”d”]];
NSArray *copyArray = [array copy];
NSMutableArray *mCopyArray = [array mutableCopy];
查看內容,可以看到 copyArray 和 array 的地址是一樣的,而 mCopyArray 和 array 的地址是不同的。說明 copy 操作進行了指針拷貝,mutableCopy 進行了內容拷貝。但需要強調的是:此處的內容拷貝,僅僅是拷貝 array 這個對象,array 集合內部的元素仍然是指針拷貝。這和上面的非集合 immutable 對象的拷貝還是挺相似的,那麼mutable對象的拷貝會不會類似呢?我們繼續往下,看 mutable 對象拷貝的例子:
NSMutableArray *array = [NSMutableArray arrayWithObjects:[NSMutableString stringWithString:@”a”],@”b”,@”c”,nil];
NSArray *copyArray = [array copy];
NSMutableArray *mCopyArray = [array mutableCopy];
查看內存,如我們所料,copyArray、mCopyArray和 array 的內存地址都不一樣,說明 copyArray、mCopyArray 都對 array 進行了內容拷貝。同樣,我們可以得出結論:
在集合類對象中,對 immutable 對象進行 copy,是指針復制, mutableCopy 是內容復制;對 mutable 對象進行 copy 和 mutableCopy 都是內容復制。但是:集合對象的內容復制僅限於對象本身,對象元素仍然是指針復制。用代碼簡單表示如下:
[immutableObject copy] // 淺復制
[immutableObject mutableCopy] //單層深復制
[mutableObject copy] //單層深復制
[mutableObject mutableCopy] //單層深復制
這個代碼結論和非集合類的非常相似。
參考鏈接:iOS 集合的深復制與淺復制
14. @synthesize合成實例變量的規則是什麼?假如property名為foo,存在一個名為_foo的實例變量,那麼還會自動合成新變量麼?
在回答之前先說明下一個概念:
實例變量 = 成員變量 = ivar
這些說法,筆者下文中,可能都會用到,指的是一個東西。
正如 Apple官方文檔 You Can Customize Synthesized Instance Variable Names 所說: enter image description here
如果使用了屬性的話,那麼編譯器就會自動編寫訪問屬性所需的方法,此過程叫做“自動合成”( auto synthesis)。需要強調的是,這個過程由編譯器在編譯期執行,所以編輯器裡看不到這些“合成方法” (synthesized method)的源代碼。除了生成方法代碼之外,編譯器還要自動向類中添加適當類型的實例變量,並且在屬性名前面加下劃線,以此作為實例變量的名字。
@interface CYLPerson : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end
在上例中,會生成兩個實例變量,其名稱分別為 _firstName 與 _lastName。也可以在類的實現代碼裡通過 @synthesize 語法來指定實例變量的名字:
@implementation CYLPerson
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end
上述語法會將生成的實例變量命名為 _myFirstName 與 _myLastName ,而不再使用默認的名字。一般情況下無須修改默認的實例變量名,但是如果你不喜歡以下劃線來命名實例變量,那麼可以用這個辦法將其改為自己想要的名字。筆者還是推薦使用默認的命名方案,因為如果所有人都堅持這套方案,那麼寫出來的代碼大家都能看得懂。
總結下 @synthesize 合成實例變量的規則,有以下幾點:
如果指定了成員變量的名稱,會生成一個指定的名稱的成員變量, 如果這個成員已經存在了就不再生成了. 如果是 @synthesize foo; 還會生成一個名稱為foo的成員變量,也就是說: 如果沒有指定成員變量的名稱會自動生成一個屬性同名的成員變量, 如果是 @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 語法來指定實例變量的名字:
@implementation CYLPerson
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end
上述語法會將生成的實例變量命名為 _myFirstName 與 _myLastName,而不再使用默認的名字。一般情況下無須修改默認的實例變量名,但是如果你不喜歡以下劃線來命名實例變量,那麼可以用這個辦法將其改為自己想要的名字。筆者還是推薦使用默認的命名方案,因為如果所有人都堅持這套方案,那麼寫出來的代碼大家都能看得懂。
舉例說明:應用場景:
//
// .m文件
// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)
// https://github.com/ChenYilong
// 打開第14行和第17行中任意一行,就可編譯成功
@import Foundation;
@interface CYLObject : NSObject
@property (nonatomic, copy) NSString *title;
@end
@implementation CYLObject {
// NSString *_title;
}
//@synthesize title = _title;
(instancetype)init
{
self = [super init];
if (self) {
_title = @”微博@iOS程序犭袁”;
}
return self;
}
(NSString *)title {
return _title;
}
(void)setTitle:(NSString *)title {
_title = [title copy];
}
@end
結果編譯器報錯:
當你同時重寫了 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)。例如: Person * motherInlaw = [[aPerson spouse] mother]; 如果 spouse 對象為 nil,那麼發送給 nil 的消息 mother 也將返回 nil。 如果方法返回值為指針類型,其指針大小為小於或者等於sizeof(void*),float,double,long double 或者 long long 的整型標量,發送給 nil 的消息將返回0。 如果方法返回值為結構體,發送給 nil 的消息將返回0。結構體中各個字段的值將都是0。 如果方法的返回值不是上述提到的幾種情況,那麼發送給 nil 的消息的返回值將是未定義的。
具體原因如下:
objc是動態語言,每個方法在運行時會被動態轉為消息發送,即:objc_msgSend(receiver, selector)。
那麼,為了方便理解這個內容,還是貼一個objc的源代碼:
// runtime.h(類在runtime中的定義)
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
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()函數調用.
我們用 clang 分析下,clang 提供一個命令,可以將Objective-C的源碼改寫成C++語言,借此可以研究下[obj foo]和objc_msgSend()函數之間有什麼關系。
以下面的代碼為例,由於 clang 後的代碼達到了10萬多行,為了便於區分,添加了一個叫 iOSinit 方法,
//
// main.m
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// Copyright (c) 2015年 微博@iOS程序犭袁. All rights reserved.
//
int main(int argc, char * argv[]) {
@autoreleasepool {
CYLTest *test = [[CYLTest alloc] init];
[test performSelector:(@selector(iOSinit))];
return 0;
}
}
在終端中輸入
clang -rewrite-objc main.m
就可以生成一個main.cpp的文件,在最低端(10萬4千行左右)
我們可以看到大概是這樣的:
((void ()(id, SEL))(void )objc_msgSend)((id)obj, sel_registerName(“foo”));
也就是說:
[obj foo];在objc動態編譯時,會被轉意為:objc_msgSend(obj, @selector(foo));。什麼時候會報unrecognized selector的異常?
簡單來說:
當調用該對象上某個方法,而該對象上沒有實現這個方法的時候, 可以通過“消息轉發”進行解決。
簡單的流程如下,在上一題中也提到過:
objc是動態語言,每個方法在運行時會被動態轉為消息發送,即:objc_msgSend(receiver, selector)。
objc在向一個對象發送消息時,runtime庫會根據對象的isa指針找到該對象實際所屬的類,然後在該類中的方法列表以及其父類方法列表中尋找方法運行,如果,在最頂層的父類中依然找不到相應的方法時,程序在運行時會掛掉並拋出異常unrecognized selector sent to XXX 。但是在這之前,objc的運行時會給出三次拯救程序崩潰的機會:
Method resolution objc運行時會調用+resolveInstanceMethod:或者 +resolveClassMethod:,讓你有機會提供一個函數實現。如果你添加了函數,那運行時系統就會重新啟動一次消息發送的過程,否則 ,運行時就會移到下一步,消息轉發(Message Forwarding)。 Fast forwarding 如果目標對象實現了-forwardingTargetForSelector:,Runtime 這時就會調用這個方法,給你把這個消息轉發給其他對象的機會。 只要這個方法返回的不是nil和self,整個消息發送的過程就會被重啟,當然發送的對象會變成你返回的那個對象。否則,就會繼續Normal Fowarding。 這裡叫Fast,只是為了區別下一步的轉發機制。因為這一步不會創建任何新的對象,但下一步轉發會創建一個NSInvocation對象,所以相對更快點。 Normal forwarding 這一步是Runtime最後一次給你挽救的機會。首先它會發送-methodSignatureForSelector:消息獲得函數的參數和返回值類型。如果-methodSignatureForSelector:返回nil,Runtime則會發出-doesNotRecognizeSelector:消息,程序這時也就掛掉了。如果返回了一個函數簽名,Runtime就會創建一個NSInvocation對象並發送-forwardInvocation:消息給目標對象。
為了能更清晰地理解這些方法的作用,git倉庫裡也給出了一個Demo,名稱叫“ _objc_msgForward_demo ”,可運行起來看看。
19. 一個objc對象如何進行內存布局?(考慮有父類的情況)
所有父類的成員變量和自己的成員變量都會存放在該對象所對應的存儲空間中. 每一個對象內部都有一個isa指針,指向他的類對象,類對象中存放著本對象的 對象方法列表(對象能夠接收的消息列表,保存在它所對應的類對象中) 成員變量的列表, 屬性列表, 它內部也有一個isa指針指向元對象(meta class),元對象內部存放的是類方法列表,類對象內部還有一個superclass的指針,指向他的父類對象。
每個 Objective-C 對象都有相同的結構,如下圖所示:
翻譯過來就是
Objective-C 對象的結構圖
ISA指針
根類的實例變量
倒數第二層父類的實例變量
…
父類的實例變量
類的實例變量
根對象就是NSObject,它的superclass指針指向nil 類對象既然稱為對象,那它也是一個實例。類對象中也有一個isa指針指向它的元類(meta class),即類對象是元類的實例。元類內部存放的是類方法列表,根元類的isa指針指向自己,superclass指針指向NSObject類。
如圖:
20. 一個objc對象的isa的指針指向什麼?有什麼作用?
指向他的類對象,從而可以找到對象上的方法
21. 下面的代碼輸出什麼?
@implementation Son : Father - (id)init { self = [super init]; if (self) { NSLog(@"%@", NSStringFromClass([self class])); NSLog(@"%@", NSStringFromClass([super class])); } return self; } @end
答案:
都輸出 Son
NSStringFromClass([self class]) = Son
NSStringFromClass([super class]) = Son
這個題目主要是考察關於 Objective-C 中對 self 和 super 的理解。
我們都知道:self 是類的隱藏參數,指向當前調用方法的這個類的實例。那 super 呢?
很多人會想當然的認為“ super 和 self 類似,應該是指向父類的指針吧!”。這是很普遍的一個誤區。其實 super 是一個 Magic Keyword, 它本質是一個編譯器標示符,和 self 是指向的同一個消息接受者!他們兩個的不同點在於:super 會告訴編譯器,調用 class 這個方法時,要去父類的方法,而不是本類裡的。
上面的例子不管調用[self class]還是[super class],接受消息的對象都是當前 Son *xxx 這個對象。
當使用 self 調用方法時,會從當前類的方法列表中開始找,如果沒有,就從父類中再找;而當使用 super 時,則從父類的方法列表中開始找。然後調用父類的這個方法。
這也就是為什麼說“不推薦在 init 方法中使用點語法”,如果想訪問實例變量 iVar 應該使用下劃線( _iVar ),而非點語法( self.iVar )。
點語法( self.iVar )的壞處就是子類有可能覆寫 setter 。假設 Person 有一個子類叫 ChenPerson,這個子類專門表示那些姓“陳”的人。該子類可能會覆寫 lastName 屬性所對應的設置方法:
//
// ChenPerson.m
//
//
// Created by https://github.com/ChenYilong on 15/8/30.
// Copyright (c) 2015年 http://weibo.com/luohanchenyilong/ 微博@iOS程序犭袁. All rights reserved.
//
@implementation ChenPerson
@synthesize lastName = _lastName;
(instancetype)init
{
self = [super init];
if (self) {
NSLog(@”??類名與方法名:%s(在第%d行),描述:%@”, PRETTY_FUNCTION, LINE, NSStringFromClass([self class]));
NSLog(@”??類名與方法名:%s(在第%d行),描述:%@”, PRETTY_FUNCTION, LINE, NSStringFromClass([super class]));
}
return self;
}
(void)setLastName:(NSString*)lastName
{
//設置方法一:如果setter采用是這種方式,就可能引起崩潰
// if (![lastName isEqualToString:@”陳”])
// {
// [NSException raise:NSInvalidArgumentException format:@”姓不是陳”];
// }
// _lastName = lastName;
//設置方法二:如果setter采用是這種方式,就可能引起崩潰
_lastName = @”陳”;
NSLog(@”??類名與方法名:%s(在第%d行),描述:%@”, PRETTY_FUNCTION, LINE, @”會調用這個方法,想一下為什麼?”);
}
@end
在基類 Person 的默認初始化方法中,可能會將姓氏設為空字符串。此時若使用點語法( self.lastName )也即 setter 設置方法,那麼調用將會是子類的設置方法,如果在剛剛的 setter 代碼中采用設置方法一,那麼就會拋出異常,
為了方便采用打印的方式展示,究竟發生了什麼,我們使用設置方法二。
如果基類的代碼是這樣的:
//
// Person.m
// nil對象調用點語法
//
// Created by https://github.com/ChenYilong on 15/8/29.
// Copyright (c) 2015年 http://weibo.com/luohanchenyilong/ 微博@iOS程序犭袁. All rights reserved.
//
@implementation Person
(instancetype)init
{
self = [super init];
if (self) {
self.lastName = @”“;
//NSLog(@”??類名與方法名:%s(在第%d行),描述:%@”, PRETTY_FUNCTION, LINE, NSStringFromClass([self class]));
//NSLog(@”??類名與方法名:%s(在第%d行),描述:%@”, PRETTY_FUNCTION, LINE, self.lastName);
}
return self;
}
(void)setLastName:(NSString*)lastName
{
NSLog(@”??類名與方法名:%s(在第%d行),描述:%@”, PRETTY_FUNCTION, LINE, @”根本不會調用這個方法”);
_lastName = @”炎黃”;
}
@end
那麼打印結果將會是這樣的:
??類名與方法名:-[ChenPerson setLastName:](在第36行),描述:會調用這個方法,想一下為什麼?
??類名與方法名:-[ChenPerson init](在第19行),描述:ChenPerson
??類名與方法名:-[ChenPerson init](在第20行),描述:ChenPerson
我在倉庫裡也給出了一個相應的 Demo(名字叫:Demo_21題_下面的代碼輸出什麼)。有興趣可以跑起來看一下,主要看下他是怎麼打印的,思考下為什麼這麼打印。
接下來讓我們利用 runtime 的相關知識來驗證一下 super 關鍵字的本質,使用clang重寫命令:
$ clang -rewrite-objc test.m
將這道題目中給出的代碼被轉化為:
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函數。看下函數定義:
id objc_msgSend(id self, SEL op, ...)
我們把 self 做為第一個參數傳遞進去。
而在調用 [super class]時,會轉化成 objc_msgSendSuper函數。看下函數定義:
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
第一個參數是 objc_super 這樣一個結構體,其定義如下:
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方法的實現:
(Class)class {而當調用 [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。
參考鏈接:微博@Chun_iOS的博文刨根問底Objective-C Runtime(1)- Self & Super
22. runtime如何通過selector找到對應的IMP地址?(分別考慮類方法和實例方法)
每一個類對象中都一個方法列表,方法列表中記錄著方法的名稱,方法實現,以及參數類型,其實selector本質就是方法名稱,通過這個方法名稱就可以在方法列表中找到對應的方法實現.
23. 使用runtime Associate方法關聯的對象,需要在主對象dealloc的時候釋放麼?
在ARC下不需要。 在MRC中,對於使用retain或copy策略的需要 。 在MRC下也不需要 無論在MRC下還是ARC下均不需要。
2011年版本的Apple API 官方文檔 - Associative References 一節中有一個MRC環境下的例子:
// 在MRC下,使用runtime Associate方法關聯的對象,不需要在主對象dealloc的時候釋放
// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)
// https://github.com/ChenYilong
// 摘自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() 方法中釋放。
對象的內存銷毀時間表,分四個步驟:
// 對象的內存銷毀時間表
// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)
// https://github.com/ChenYilong
// 根據 WWDC 2011, Session 322 (36分22秒)中發布的內存銷毀時間表
對象的內存銷毀時間表:參考鏈接。
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.
//
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實現原理詳解》詳細記錄了實現原理,有興趣可以看下。
同時 RAC(ReactiveCocoa) 源碼中也用到了該方法。
26. runtime如何實現weak變量的自動置nil?
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表中刪除。
27. 能否向編譯後得到的類中增加實例變量?能否向運行時創建的類中添加實例變量?為什麼?
不能向編譯後得到的類中增加實例變量; 能向運行時創建的類中添加實例變量;
解釋下:
因為編譯後的類已經注冊在 runtime 中,類結構體中的 objc_ivar_list 實例變量的鏈表 和 instance_size 實例變量的內存大小已經確定,同時runtime 會調用 class_setIvarLayout 或 class_setWeakIvarLayout 來處理 strong weak 引用。所以不能向存在的類中添加實例變量; 運行時創建的類是可以添加實例變量,調用 class_addIvar 函數。但是得在調用 objc_allocateClassPair 之後,objc_registerClassPair 之前,原因同上。runloop和線程有什麼關系?
總的說來,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詳解》。
29. runloop的mode作用是什麼?
model 主要是用來指定事件在運行循環中的優先級的,分為:
NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默認,空閒狀態 UITrackingRunLoopMode:ScrollView滑動時 UIInitializationRunLoopMode:啟動時 NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合
蘋果公開提供的 Mode 有兩個:
NSDefaultRunLoopMode(kCFRunLoopDefaultMode) NSRunLoopCommonModes(kCFRunLoopCommonModes)以+ scheduledTimerWithTimeInterval…的方式觸發的timer,在滑動頁面上的列表時,timer會暫定回調,為什麼?如何解決?
RunLoop只能運行在一種mode下,如果要換mode,當前的loop也需要停下重啟成新的。利用這個機制,ScrollView滾動過程中NSDefaultRunLoopMode(kCFRunLoopDefaultMode)的mode會切換到UITrackingRunLoopMode來保證ScrollView的流暢滑動:只能在NSDefaultRunLoopMode模式下處理的事件會影響ScrollView的滑動。
如果我們把一個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];
猜想runloop內部是如何實現的?
一般來講,一個線程一次只能執行一個任務,執行完成後線程就會退出。如果我們需要一個機制,讓線程能隨時處理事件但並不退出,通常的代碼邏輯 是這樣的:
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;
}
參考鏈接:
《深入理解RunLoop》 摘自博文CFRunLoop,原作者是微博@我就叫Sunny怎麼了objc使用什麼機制管理對象內存?
通過 retainCount 的機制來決定對象是否需要釋放。 每次 runloop 的時候,都會檢查對象的 retainCount,如果retainCount 為 0,說明該對象沒有地方需要繼續使用了,可以釋放掉了。
33. ARC通過什麼方式幫助開發者管理內存?
編譯時根據代碼上下文,插入 retain/release
ARC相對於MRC,不是在編譯時添加retain/release/autorelease這麼簡單。應該是編譯期和運行期兩部分共同幫助開發者管理內存。
在編譯期,ARC用的是更底層的C接口實現的retain/release/autorelease,這樣做性能更好,也是為什麼不能在ARC環境下手動retain/release/autorelease,同時對同一上下文的同一對象的成對retain/release操作進行優化(即忽略掉不必要的操作);ARC也包含運行期組件,這個地方做的優化比較復雜,但也不能被忽略。【TODO:後續更新會詳細描述下】
34. 不手動指定autoreleasepool的前提下,一個autorealese對象在什麼時刻釋放?(比如在一個vc的viewDidLoad中創建)
分兩種情況:手動干預釋放時機、系統自動去釋放。
手動干預釋放時機--指定autoreleasepool 就是所謂的:當前作用域大括號結束時釋放。 系統自動去釋放--不手動指定autoreleasepool Autorelease對象出了作用域之後,會被添加到最近一次創建的自動釋放池中,並會在當前的 runloop 迭代結束時釋放。
釋放的時機總結起來,可以用下圖來表示:
autoreleasepool與 runloop 的關系圖
下面對這張圖進行詳細的解釋:
從程序啟動到加載完成是一個完整的運行循環,然後會停下來,等待用戶交互,用戶的每一次交互都會啟動一次運行循環,來處理用戶所有的點擊事件、觸摸事件。
我們都知道: 所有 autorelease 的對象,在出了作用域之後,會被自動添加到最近創建的自動釋放池中。
但是如果每次都放進應用程序的 main.m 中的 autoreleasepool 中,遲早有被撐滿的一刻。這個過程中必定有一個釋放的動作。何時?
在一次完整的運行循環結束之前,會被銷毀。
那什麼時間會創建自動釋放池?運行循環檢測到事件並啟動後,就會創建自動釋放池。
子線程的 runloop 默認是不工作,無法主動創建,必須手動創建。
自定義的 NSOperation 和 NSThread 需要手動創建自動釋放池。比如: 自定義的 NSOperation 類中的 main 方法裡就必須添加自動釋放池。否則出了作用域後,自動釋放對象會因為沒有自動釋放池去處理它,而造成內存洩露。
但對於 blockOperation 和 invocationOperation 這種默認的Operation ,系統已經幫我們封裝好了,不需要手動創建自動釋放池。
@autoreleasepool 當自動釋放池被銷毀或者耗盡時,會向自動釋放池中的所有對象發送 release 消息,釋放自動釋放池中的所有對象。
如果在一個vc的viewDidLoad中創建一個 Autorelease對象,那麼該對象會在 viewDidAppear 方法執行前就被銷毀了。
參考鏈接:《黑幕背後的Autorelease》
35. BAD_ACCESS在什麼情況下出現?
訪問了野指針,比如對一個已經釋放的對象執行了release、訪問已經釋放對象的成員變量或者發消息。 死循環
36. 蘋果是如何實現autoreleasepool的?
autoreleasepool 以一個隊列數組的形式實現,主要通過下列三個函數完成.
objc_autoreleasepoolPush objc_autoreleasepoolPop objc_autorelease
看函數名就可以知道,對 autorelease 分別執行 push,和 pop 操作。銷毀對象時執行release操作。
舉例說明:我們都知道用類方法創建的對象都是 Autorelease 的,那麼一旦 Person 出了作用域,當在 Person 的 dealloc 方法中打上斷點,我們就可以看到這樣的調用堆棧信息:
37. 使用block時什麼情況會發生引用循環,如何解決?
一個對象中強引用了block,在block中又強引用了該對象,就會發射循環引用。
解決方法是將該對象使用__weak或者__block修飾符修飾之後再在block中使用。
id weak weakSelf = self; 或者 weak __typeof(&*self)weakSelf = self該方法可以設置宏 id __block weakSelf = self;
或者將其中一方強制制空 xxx = nil。
檢測代碼中是否存在循環引用問題,可使用 Facebook 開源的一個檢測工具 FBRetainCycleDetector 。
38. 在block內如何修改block外部變量?
默認情況下,在block中訪問的外部變量是復制過去的,即:寫操作不對原變量生效。但是你可以加上 __block 來讓其寫操作生效,示例代碼如下:
__block int a = 0; void (^foo)(void) = ^{ a = 1; }; foo(); //這裡,a的值被修改為1
這是 微博@唐巧_boy的《iOS開發進階》中的第11.2.3章節中的描述。你同樣可以在面試中這樣回答,但你並沒有答到“點子上”。真正的原因,並沒有書這本書裡寫的這麼“神奇”,而且這種說法也有點牽強。面試官肯定會追問“為什麼寫操作就生效了?”真正的原因是這樣的:
我們都知道:Block不允許修改外部變量的值,這裡所說的外部變量的值,指的是棧中指針的內存地址。__block 所起到的作用就是只要觀察到該變量被 block 所持有,就將“外部變量”在棧中的內存地址放到了堆中。進而在block內部也可以修改外部變量的值。
Block不允許修改外部變量的值。Apple這樣設計,應該是考慮到了block的特殊性,block也屬於“函數”的范疇,變量進入block,實際就是已經改變了作用域。在幾個作用域之間進行切換時,如果不加上這樣的限制,變量的可維護性將大大降低。又比如我想在block內聲明了一個與外部同名的變量,此時是允許呢還是不允許呢?只有加上了這樣的限制,這樣的情景才能實現。於是棧區變成了紅燈區,堆區變成了綠燈區。
我們可以打印下內存地址來進行驗證:
__block int a = 0; NSLog(@"定義前:%p", &a); //棧區 void (^foo)(void) = ^{ a = 1; NSLog(@"block內部:%p", &a); //堆區 }; NSLog(@"定義後:%p", &a); //堆區 foo();
2016-05-17 02:03:33.559 LeanCloudChatKit-iOS[1505:713679] 定義前:0x16fda86f8
2016-05-17 02:03:33.559 LeanCloudChatKit-iOS[1505:713679] 定義後:0x155b22fc8
2016-05-17 02:03:33.559 LeanCloudChatKit-iOS[1505:713679] block內部: 0x155b22fc8
“定義後”和“block內部”兩者的內存地址是一樣的,我們都知道 block 內部的變量會被 copy 到堆區,“block內部”打印的是堆地址,因而也就可以知道,“定義後”打印的也是堆的地址。
那麼如何證明“block內部”打印的是堆地址?
把三個16進制的內存地址轉成10進制就是:
定義後前:6171559672 block內部:5732708296 定義後後:5732708296
中間相差438851376個字節,也就是 418.5M 的空間,因為堆地址要小於棧地址,又因為iOS中一個進程的棧區內存只有1M,Mac也只有8M,顯然a已經是在堆區了。
這也證實了:a 在定義前是棧區,但只要進入了 block 區域,就變成了堆區。這才是 __block 關鍵字的真正作用。
__block 關鍵字修飾後,int類型也從4字節變成了32字節,這是 Foundation 框架 malloc 出來的。這也同樣能證實上面的結論。(PS:居然比 NSObject alloc 出來的 16 字節要多一倍)。
理解到這是因為堆棧地址的變更,而非所謂的“寫操作生效”,這一點至關重要,要不然你如何解釋下面這個現象:
以下代碼編譯可以通過,並且在block中成功將a的從Tom修改為Jerry。
NSMutableString *a = [NSMutableString stringWithString:@"Tom"]; NSLog(@"\n 定以前:------------------------------------\n\ a指向的堆中地址:%p;a在棧中的指針地址:%p", a, &a); //a在棧區 void (^foo)(void) = ^{ a.string = @"Jerry"; NSLog(@"\n block內部:------------------------------------\n\ a指向的堆中地址:%p;a在棧中的指針地址:%p", a, &a); //a在棧區 a = [NSMutableString stringWithString:@"William"]; }; foo(); NSLog(@"\n 定以後:------------------------------------\n\ a指向的堆中地址:%p;a在棧中的指針地址:%p", a, &a); //a在棧區
這裡的a已經由基本數據類型,變成了對象類型。block會對對象類型的指針進行copy,copy到堆中,但並不會改變該指針所指向的堆中的地址,所以在上面的示例代碼中,block體內修改的實際是a指向的堆中的內容。
但如果我們嘗試像上面圖片中的65行那樣做,結果會編譯不通過,那是因為此時你在修改的就不是堆中的內容,而是棧中的內容。
上文已經說過:Block不允許修改外部變量的值,這裡所說的外部變量的值,指的是棧中指針的內存地址。棧區是紅燈區,堆區才是綠燈區。
39. 使用系統的某些block api(如UIView的block版本寫動畫時),是否也考慮引用循環問題?
系統的某些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 顯然這也是一個循環引用。
檢測代碼中是否存在循環引用問題,可使用 Facebook 開源的一個檢測工具 FBRetainCycleDetector 。
40. GCD的隊列(dispatch_queue_t)分哪兩種類型?
串行隊列Serial Dispatch Queue 並行隊列Concurrent Dispatch Queue如何用GCD同步若干個異步調用?(如根據若干個url異步加載多張圖片,然後在都下載完成後合成一張整圖)
使用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(), ^{
// 合並圖片
});
在並行隊列中,為了保持某些任務的順序,需要等待一些任務完成後才能繼續進行,使用 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 的作用一模一樣。 )
43. 蘋果為什麼要廢棄dispatch_get_current_queue?
dispatch_get_current_queue容易造成死鎖
44. 以下代碼運行結果如何?
只輸出:1 。發生主線程鎖死。
45. addObserver:forKeyPath:options:context:各個參數的作用分別是什麼,observer中需要實現哪個方法才能獲得KVO回調?
// 添加鍵值觀察
/*
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: 一定會被調用,這就 會記錄舊的值。而當改變發生後, observeValueForKey:ofObject:change:context: 會被調用,繼而 didChangeValueForKey: 也會被調用。如果可以手動實現這些調用,就可以實現“手動觸發”了。
那麼“手動觸發”的使用場景是什麼?一般我們只在希望能控制“回調的調用時機”時才會這麼做。
具體做法如下:
如果這個 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];
_now = [NSDate date];
[self addObserver:self forKeyPath:@”now” options:NSKeyValueObservingOptionNew context:nil];
NSLog(@”1”);
[self willChangeValueForKey:@”now”]; // “手動觸發self.now的KVO”,必寫。
NSLog(@”2”);
[self didChangeValueForKey:@”now”]; // “手動觸發self.now的KVO”,必寫。
NSLog(@”4”);
}
但是平時我們一般不會這麼干,我們都是等系統去“自動觸發”。“自動觸發”的實現原理:
比如調用 setNow: 時,系統還會以某種方式在中間插入 wilChangeValueForKey: 、 didChangeValueForKey: 和 observeValueForKeyPath:ofObject:change:context: 的調用。
大家可能以為這是因為 setNow: 是合成方法,有時候我們也能看到有人這麼寫代碼:
(void)setNow:(NSDate *)aDate {這完全沒有必要,不要這麼做,這樣的話,KVO代碼會被調用兩次。KVO在調用存取方法之前總是調用 willChangeValueForKey: ,之後總是調用 didChangeValueForkey: 。怎麼做到的呢?答案是通過 isa 混寫(isa-swizzling)。下文《apple用什麼方式實現對一個對象的KVO?》會有詳述。
參考鏈接: Manual Change Notification—Apple 官方文檔
47. 若一個類有實例變量 NSString *_foo ,調用setValue:forKey:時,可以以foo還是 _foo 作為key?
都可以。
48. KVC的keyPath中的集合運算符如何使用?
必須用在集合對象上或普通對象的集合屬性上 簡單集合運算符有@avg, @count , @max , @min ,@sum, 格式 @"@sum.age"或 @"集合屬性[email protected]"KVC和KVO的keyPath一定是屬性麼?
KVO支持實例變量
50. 如何關閉默認的KVO的默認實現,並進入自定義的KVO實現?
請參考:
《如何自己動手實現 KVO》 KVO for manually implemented propertiesapple用什麼方式實現對一個對象的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: 一定會被調用,這就會記錄舊的值。而當改變發生後, observeValueForKey:ofObject:change:context: 會被調用,繼而 didChangeValueForKey: 也會被調用。可以手動實現這些調用,但很少有人這麼做。一般我們只在希望能控制回調的調用時機時才會這麼做。大部分情況下,改變通知會自動調用。
比如調用 setNow: 時,系統還會以某種方式在中間插入 wilChangeValueForKey: 、 didChangeValueForKey: 和 observeValueForKeyPath:ofObject:change:context: 的調用。大家可能以為這是因為 setNow: 是合成方法,有時候我們也能看到有人這麼寫代碼:
(void)setNow:(NSDate *)aDate {這完全沒有必要,不要這麼做,這樣的話,KVO代碼會被調用兩次。KVO在調用存取方法之前總是調用 willChangeValueForKey: ,之後總是調用 didChangeValueForkey: 。怎麼做到的呢?答案是通過 isa 混寫(isa-swizzling)。第一次對一個對象調用 addObserver:forKeyPath:options:context: 時,框架會創建這個類的新的 KVO 子類,並將被觀察對象轉換為新子類的對象。在這個 KVO 特殊子類中, Cocoa 創建觀察屬性的 setter ,大致工作原理如下:
(void)setNow:(NSDate *)aDate {這種繼承和方法注入是在運行時而不是編譯時實現的。這就是正確命名如此重要的原因。只有在使用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)。具體探究過程可參考 這篇博文 。
那麼 wilChangeValueForKey: 、 didChangeValueForKey: 和 observeValueForKeyPath:ofObject:change:context: 這三個方法的執行順序是怎樣的呢?
wilChangeValueForKey: 、 didChangeValueForKey: 很好理解,observeValueForKeyPath:ofObject:change:context: 的執行時機是什麼時候呢?
先看一個例子:
代碼已放在倉庫裡。
(void)viewDidLoad {
[super viewDidLoad];
[self addObserver:self forKeyPath:@”now” options:NSKeyValueObservingOptionNew context:nil];
NSLog(@”1”);
[self willChangeValueForKey:@”now”]; // “手動觸發self.now的KVO”,必寫。
NSLog(@”2”);
[self didChangeValueForKey:@”now”]; // “手動觸發self.now的KVO”,必寫。
NSLog(@”4”);
}
(void)observeValueForKeyPath:(NSString )keyPath ofObject:(id)object change:(NSDictionary