《招聘一個靠譜的 iOS》—參考答案(上)
說明:面試題來源是微博@我就叫Sunny怎麼了的這篇博文:《招聘一個靠譜的 iOS》,其中共55題,除第一題為糾錯題外,其他54道均為簡答題。
博文中給出了高質量的面試題,但是未給出答案,我嘗試著總結了下答案,分兩篇發:這是上篇 ,下一篇文章將發布在這裡,會把剩余問題總結下,並且進行勘誤,歡迎各位指正文中的錯誤。請持續關注微博@iOS程序犭袁。(答案未經出題者校對,如有纰漏,請向微博@iOS程序犭袁指正。)
出題者簡介: 孫源(sunnyxx),目前就職於百度,負責百度知道 iOS 客戶端的開發工作,對技術喜歡刨根問底和總結最佳實踐,熱愛分享和開源,維護一個叫 forkingdog 的開源小組。
1. 風格糾錯題
修改方法有很多種,現給出一種做示例:
下面對具體修改的地方,分兩部分做下介紹:硬傷部分和優化部分 。因為硬傷部分沒什麼技術含量,為了節省大家時間,放在後面講,大神請直接看優化部分。
優化部分
1)enum建議使用 NS_ENUM 和 NS_OPTIONS 宏來定義枚舉類型,參見官方的 Adopting Modern Objective-C 一文:
//定義一個枚舉 typedef NS_ENUM(NSInteger, CYLSex) { CYLSexMan, CYLSexWoman };
2)age屬性的類型:應避免使用基本類型,建議使Foundation數據類型,對應關系如下:
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:這種命名,用來表示方法執行了兩個相對獨立的操作(從設計上來說,這時候也可以拆分成兩個獨立的方法),它不應該用作闡明有多個參數,比如下面的:
//錯誤,不要使用"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 初始化方法的初始化方法。舉例說明:
// .m文件 // http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong // @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時就應該盡量使用不可變的對象:其三個屬性都應該設為“只讀”。用初始化方法設置好屬性值之後,就不能再改變了。在本例中,仍需聲明屬性的“內存管理語義”。於是可以把屬性的定義改成這樣
@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;
兩個問題:
1、添加,刪除,修改數組內的元素的時候,程序會因為找不到對應的方法而崩潰.因為copy就是復制一個不可變NSArray的對象;
2、使用了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協議。該協議只有一個方法:
- (id)copyWithZone: (NSZone*) zone
注意:一提到讓自己的類用 copy 修飾符,我們總是想覆寫copy方法,其實真正需要實現的卻是“copyWithZone”方法。
以第一題的代碼為例:
然後實現協議中規定的方法:
但在實際的項目中,不可能這麼簡單,遇到更復雜一點,比如類對象中的數據結構可能並未在初始化方法中設置好,需要另行設置。舉個例子,假如CYLUser中含有一個數組,與其他CYLUser對象建立或解除朋友關系的那些方法都需要操作這個數組。那麼在這種情況下,你得把這個包含朋友對象的數組也一並拷貝過來。下面列出了實現此功能所需的全部代碼:
// .m文件
以上做法能滿足基本的需求,但是也有缺陷:如果你所寫的對象需要深拷貝,那麼可考慮新增一個專門執行深拷貝的方法。
【注:深淺拷貝的概念,在下文中有介紹,詳見下文的:用@property聲明的NSString(或NSArray,NSDictionary)經常使用copy關鍵字,為什麼?如果改用strong關鍵字,可能造成什麼問題?】
在例子中,存放朋友對象的set是用“copyWithZooe:”方法來拷貝的,這種淺拷貝方式不會逐個復制set中的元素。若需要深拷貝的話,則可像下面這樣,編寫一個專供深拷貝所用的方法:
- (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這個問題,
如果拋開本例來回答的話,如下:
- (void)setName:(NSString *)name { _name = [name copy]; }
如果單單就上文的代碼而言,我們不需要也不能重寫name的 setter :由於是name是只讀屬性,所以編譯器不會為其創建對應的“設置方法”,用初始化方法設置好屬性值之後,就不能再改變了。( 在本例中,之所以還要聲明屬性的“內存管理語義”--copy,是因為:如果不寫copy,該類的調用者就不知道初始化方法裡會拷貝這些屬性,他們有可能會在調用初始化方法之前自行拷貝屬性值。這種操作多余而低效。)。
那如何確保name被copy?在初始化方法(initializer)中做:
- (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;
例如下面這個類:
@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
我為了搞清屬性是怎麼實現的,曾經反編譯過相關的代碼,大致生成了五個東西:
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屬性”:
// 使用偽代碼模擬: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 { objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN); [object cyl_runAtDealloc:^{ _object = nil; }]; }
也就是有兩個步驟:
1)在setter方法中做如下設置:
objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
2)在屬性所指的對象遭到摧毀時,屬性值也會清空(nil out)。做到這點,同樣要借助runtime:
//要銷毀的目標對象 id objectToBeDeallocated; //可以理解為一個“事件”:當上面的目標對象銷毀時,同時要發生的“事件”。 id objectWeWantToBeReleasedWhenThatHappens; objc_setAssociatedObject(objectToBeDeallocted, someUniqueKey, objectWeWantToBeReleasedWhenThatHappens, OBJC_ASSOCIATION_RETAIN);
知道了思路,我們就開始實現cyl_runAtDealloc方法,實現過程分兩部分:
第一部分:創建一個類,可以理解為一個“事件”:當目標對象銷毀時,同時要發生的“事件”。借助block執行“事件”。
// .h文件 // http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong // 這個類,可以理解為一個“事件”:當目標對象銷毀時,同時要發生的“事件”。借助block執行“事件”。 typedef void (^voidBlock)(void); @interface CYLBlockExecutor : NSObject - (id)initWithBlock:(voidBlock)block; @end // .m文件 // http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong // 這個類,可以理解為一個“事件”:當目標對象銷毀時,同時要發生的“事件”。借助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方法
// CYLNSObject+RunAtDealloc.h文件 // http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong // 利用runtime實現cyl_runAtDealloc方法 #import "CYLBlockExecutor.h" 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方法 #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
使用方法: 導入
#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(讀寫)、readooly (只讀)
內存管理語義---assign、strong、 weak、unsafe_unretained、copy
方法名---getter=
getter=
@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方法,應該如下:
- (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
參考鏈接:
Objective-C ARC: strong vs retain and weak vs assign
Variable property attributes or Modifiers in iOS
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] //深復制
比如以下代碼:
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,他們之間可能進行賦值操作,為確保對象中的字符串值不會無意間變動,應該在設置新屬性值時拷貝一份。
參考鏈接:iOS 集合的深復制與淺復制
14. @synthesize合成實例變量的規則是什麼?假如property名為foo,存在一個名為_foo的實例變量,那麼還會自動合成新變量麼?
在回答之前先說明下一個概念:
實例變量 = 成員變量 = ivar
這些說法,筆者下文中,可能都會用到,指的是一個東西。
正如 Apple官方文檔 You Can Customize Synthesized Instance Variable Names 所說:
如果使用了屬性的話,那麼編譯器就會自動編寫訪問屬性所需的方法,此過程叫做“自動合成”( 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合成實例變量的規則,有以下幾點:
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語法來指定實例變量的名字:
@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)。例如:
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的源代碼:
// 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()函數調用.如果我沒有記錯的大概是這樣的:
((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. 下面的代碼輸出什麼?
@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
解惑:
(以下解惑部分摘自微博@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重寫命令:
$ 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 { 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環境下的例子:
// 在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秒)中發布的內存銷毀時間表 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是實例對象
實例方法中可以訪問成員變量
實例方法中直接調用實例方法
實例方法中也可以調用類方法(通過類名)
未完待續~~~~
下一篇文章將發布在 這裡