《招聘一個(gè)靠譜的iOS》面試題參考答案(上)

出題者簡介: 孫源(sunnyxx),目前就職于百度,負(fù)責(zé)百度知道 iOS 客戶端的開發(fā)工作,對技術(shù)喜歡刨根問底和總結(jié)最佳實(shí)踐,熱愛分享和開源,維護(hù)一個(gè)叫 forkingdog 的開源小組。

1. 風(fēng)格糾錯(cuò)題

修改方法有很多種,現(xiàn)給出一種做示例:

下面對具體修改的地方,分兩部分做下介紹:硬傷部分和優(yōu)化部分。因?yàn)橛矀糠譀]什么技術(shù)含量,為了節(jié)省大家時(shí)間,放在后面講,大神請直接看優(yōu)化部分。

優(yōu)化部分

1)enum建議使用 NS_ENUM 和 NS_OPTIONS 宏來定義枚舉類型,參見官方的Adopting Modern Objective-C一文:

5//定義一個(gè)枚舉

typedef?NS_ENUM(NSInteger,?CYLSex)?{

CYLSexMan,

CYLSexWoman

};

2)age屬性的類型:應(yīng)避免使用基本類型,建議使Foundation數(shù)據(jù)類型,對應(yīng)關(guān)系如下:

4int?->?NSInteger

unsigned?->?NSUInteger

float?->?CGFloat

動(dòng)畫時(shí)間?->?NSTimeInterval

同時(shí)考慮到age的特點(diǎn),應(yīng)使用NSUInteger,而非int。 這樣做的是基于64-bit 適配考慮,詳情可參考出題者的博文《64-bit Tips》。

3)如果工程項(xiàng)目非常龐大,需要拆分成不同的模塊,可以在類、typedef宏命名的時(shí)候使用前綴。

4)doLogIn

方法不應(yīng)寫在該類中:雖然LogIn的命名不太清晰,但筆者猜測是login的意思,而登錄操作屬于業(yè)務(wù)邏輯,觀察類名UserModel,以及屬性的命名方式,應(yīng)該使用的是MVC模式,并非MVVM,在MVC中業(yè)務(wù)邏輯不應(yīng)當(dāng)寫在Model中。(如果是MVVM,拋開命名規(guī)范,UserModel這個(gè)類可能對應(yīng)的是用戶注冊頁面,如果有特殊的業(yè)務(wù)需求,比如:login對應(yīng)的應(yīng)當(dāng)是注冊并登錄的一個(gè)Button,出現(xiàn)login方法也可能是合理的。)

5)doLogIn方法命名不規(guī)范:添加了多余的動(dòng)詞前綴。 請牢記:如果方法表示讓對象執(zhí)行一個(gè)動(dòng)作,使用動(dòng)詞打頭來命名,注意不要使用do,does這種多余的關(guān)鍵字,動(dòng)詞本身的暗示就足夠了。

6)-(id)initUserModelWithUserName: (NSString*)name?withAge:(int)age;方法中不要用with來連接兩個(gè)參數(shù):withAge:應(yīng)當(dāng)換為age:,age:已經(jīng)足以清晰說明參數(shù)的作用,也不建議用andAge::通常情況下,即使有類似withA:withB:的命名需求,也通常是使用withA:andB:這種命名,用來表示方法執(zhí)行了兩個(gè)相對獨(dú)立的操作(從設(shè)計(jì)上來說,這時(shí)候也可以拆分成兩個(gè)獨(dú)立的方法),它不應(yīng)該用作闡明有多個(gè)參數(shù),比如下面的:

6//錯(cuò)誤,不要使用"and"來連接參數(shù)

-?(int)runModalForDirectory:(NSString?*)path?andFile:(NSString?*)name?andTypes:(NSArray?*)fileTypes;

//錯(cuò)誤,不要使用"and"來闡明有多個(gè)參數(shù)

-?(instancetype)initWithName:(CGFloat)width?andAge:(CGFloat)height;

//正確,使用"and"來表示兩個(gè)相對獨(dú)立的操作

-?(BOOL)openFile:(NSString?*)fullPath?withApplication:(NSString?*)appName?andDeactivate:(BOOL)flag;

7)由于字符串值可能會(huì)改變,所以要把相關(guān)屬性的“內(nèi)存管理語義”聲明為copy。(原因在下文有詳細(xì)論述:用@property聲明的NSString(或NSArray,NSDictionary)經(jīng)常使用copy關(guān)鍵字,為什么?)

8)“性別”(sex)屬性的:該類中只給出了一種“初始化方法” (initializer)用于設(shè)置“姓名”(Name)和“年齡”(Age)的初始值,那如何對“性別”(Sex)初始化?

Objective-C

有 designated 和 secondary 初始化方法的觀念。 designated 初始化方法是提供所有的參數(shù),secondary初始化方法是一個(gè)或多個(gè),并且提供一個(gè)或者更多的默認(rèn)參數(shù)來調(diào)用 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;

? ?}

? ? ? returnself;

}

-?(instancetype)initWithName:(NSString?*)name?age:(int)age?{

? ? ? return [self?initWithName:name?age:age?sex:nil];

}

@end

上面的代碼中initWithName:age:sex: 就是 designated 初始化方法,另外的是 secondary 初始化方法。因?yàn)閮H僅是調(diào)用類實(shí)現(xiàn)的 designated 初始化方法。

因?yàn)槌鲱}者沒有給出.m文件,所以有兩種猜測:1:本來打算只設(shè)計(jì)一個(gè)designated 初始化方法,但漏掉了“性別”(sex)屬性。那么最終的修改代碼就是上文給出的第一種修改方法。2:不打算初始時(shí)初始化“性別”(sex)屬性,打算后期再修改,如果是這種情況,那么應(yīng)該把“性別”(sex)屬性設(shè)為readwrite屬性,最終給出的修改代碼應(yīng)該是:

.h中暴露 designated 初始化方法,是為了方便子類化 (想了解更多,請戳--》 《禪與 Objective-C 編程藝術(shù) (Zen and the Art of the Objective-C Craftsmanship 中文翻譯)》。)

9)按照接口設(shè)計(jì)的慣例,如果設(shè)計(jì)了“初始化方法” (initializer),也應(yīng)當(dāng)搭配一個(gè)快捷構(gòu)造方法。而快捷構(gòu)造方法的返回值,建議為instancetype,為保持一致性,init方法和快捷構(gòu)造方法的返回類型最好都用instancetype。

10)如果基于第一種修改方法:既然該類中已經(jīng)有一個(gè)“初始化方法”

(initializer),用于設(shè)置“姓名”(Name)、“年齡”(Age)和“性別”(Sex)的初始值:

那么在設(shè)計(jì)對應(yīng)@property時(shí)就應(yīng)該盡量使用不可變的對象:其三個(gè)屬性都應(yīng)該設(shè)為“只讀”。用初始化方法設(shè)置好屬性值之后,就不能再改變了。在本例

中,仍需聲明屬性的“內(nèi)存管理語義”。于是可以把屬性的定義改成這樣

@property?(nonatomic,?copy,?readonly)?NSString?*name;

@property?(nonatomic,?assign,?readonly)?NSUInter?age;

@property?(nonatomic,?assign,?readonly)?CYLSex?sex;

由于是只讀屬性,所以編譯器不會(huì)為其創(chuàng)建對應(yīng)的“設(shè)置方法”,即便如此,我們還是要寫上這些屬性的語義,以此表明初始化方法在設(shè)置這些屬性值時(shí)所用的方式。

要是不寫明語義的話,該類的調(diào)用者就不知道初始化方法里會(huì)拷貝這些屬性,他們有可能會(huì)在調(diào)用初始化方法之前自行拷貝屬性值。這種操作多余而且低效。

11)initUserModelWithUserName如果改為initWithName會(huì)更加簡潔,而且足夠清晰。

12)UserModel如果改為User會(huì)更加簡潔,而且足夠清晰。

13)UserSex如果改為Sex會(huì)更加簡潔,而且足夠清晰。

硬傷部分

1)在-和(void)之間應(yīng)該有一個(gè)空格

2)enum中駝峰命名法和下劃線命名法混用錯(cuò)誤:枚舉類型的命名規(guī)則和函數(shù)的命名規(guī)則相同:命名時(shí)使用駝峰命名法,勿使用下劃線命名法。

3)enum左括號前加一個(gè)空格,或者將左括號換到下一行

4)enum右括號后加一個(gè)空格

5)UserModel :NSObject 應(yīng)為UserModel : NSObject,也就是:右側(cè)少了一個(gè)空格。

6)@interface與@property屬性聲明中間應(yīng)當(dāng)間隔一行。

7)兩個(gè)方法定義之間不需要換行,有時(shí)為了區(qū)分方法的功能也可間隔一行,但示例代碼中間隔了兩行。

8)-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;方法中方法名與參數(shù)之間多了空格。而且- 與(id)之間少了空格。

9)-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;方法中方法名與參數(shù)之間多了空格:(NSString*)name前多了空格。

10)-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;方法中(NSString*)name,應(yīng)為(NSString *)name,少了空格。

11)doLogIn方法命名不清晰:筆者猜測是login的意思,應(yīng)該是粗心手誤造成的。

12)第二個(gè)@property中assign和nonatomic調(diào)換位置。

2. 什么情況使用 weak 關(guān)鍵字,相比 assign 有什么不同?

什么情況使用 weak 關(guān)鍵字?

1)在ARC中,在有可能出現(xiàn)循環(huán)引用的時(shí)候,往往要通過讓其中一端使用weak來解決,比如:delegate代理屬性

2)自身已經(jīng)對它進(jìn)行一次強(qiáng)引用,沒有必要再強(qiáng)引用一次,此時(shí)也會(huì)使用weak,自定義IBOutlet控件屬性一般也使用weak;當(dāng)然,也可以使用strong。在下文也有論述:《IBOutlet連出來的視圖屬性為什么可以被設(shè)置成weak?》

不同點(diǎn):1)weak?此特質(zhì)表明該屬性定義了一種“非擁有關(guān)系” (nonowning?relationship)。為這種屬性設(shè)置新值時(shí),設(shè)置方法既不保留新值,也不釋放舊值。此特質(zhì)同assign類似,然而在屬性所指的對象遭到摧毀時(shí),屬性值也會(huì)清空(nil out)。 而 assign 的“設(shè)置方法”只會(huì)執(zhí)行針對“純量類型” (scalar?type,例如 CGFloat 或 NSlnteger 等)的簡單賦值操作。

2)assigin?可以用非OC對象,而weak必須用于OC對象

3. 怎么用 copy 關(guān)鍵字?

用途:

1)NSString、NSArray、NSDictionary 等等經(jīng)常使用copy關(guān)鍵字,是因?yàn)樗麄冇袑?yīng)的可變類型:NSMutableString、NSMutableArray、NSMutableDictionary;

2)block也經(jīng)常使用copy關(guān)鍵字,具體原因見官方文檔:Objects Use Properties to Keep Track of Blocks

block

使用copy是從MRC遺留下來的“傳統(tǒng)”,在MRC中,方法內(nèi)部的block是在棧區(qū)的,使用copy可以把它放到堆區(qū).在ARC中寫不寫都行:對于

block使用copy還是strong效果是一樣的,但寫上copy也無傷大雅,還能時(shí)刻提醒我們:編譯器自動(dòng)對block進(jìn)行了copy操作。

下面做下解釋: copy此特質(zhì)所表達(dá)的所屬關(guān)系與strong類似。然而設(shè)置(set)方法并不保留新值,而是將其“拷貝” (copy)。

當(dāng)屬性類型為NSString時(shí),經(jīng)常用此特質(zhì)來保護(hù)其封裝性,因?yàn)閭鬟f給設(shè)置方法的新值有可能指向一個(gè)NSMutableString類的實(shí)例。這個(gè)類是NSString的子類,表示一種可修改其值的字符串,此時(shí)若是不拷貝字符串,那么設(shè)置完屬性之后,字符串的值就可能會(huì)在對象不知情的情況下遭人更改。

所以,這時(shí)就要拷貝一份“不可變” (immutable)的字符串,確保對象中的字符串值不會(huì)無意間變動(dòng)。只要實(shí)現(xiàn)屬性所用的對象是“可變的”(mutable),就應(yīng)該在設(shè)置新屬性值時(shí)拷貝一份。

用@property聲明 NSString、NSArray、NSDictionary經(jīng)常使用copy關(guān)鍵字,是因?yàn)樗麄冇袑?yīng)的可變類型:NSMutableString、NSMutableArray、NSMutableDictionary,他們之間可能進(jìn)行賦值操作,為確保對象中的字符串值不會(huì)無意間變動(dòng),應(yīng)該在設(shè)置(set)新屬性值時(shí)拷貝一份。

該問題在下文中也有論述:用@property聲明的NSString(或NSArray,NSDictionary)經(jīng)常使用copy關(guān)鍵字,為什么?如果改用strong關(guān)鍵字,可能造成什么問題?

4. 這個(gè)寫法會(huì)出什么問題: @property (copy) NSMutableArray *array;

兩個(gè)問題:

1、添加,刪除,修改數(shù)組內(nèi)的元素的時(shí)候,程序會(huì)因?yàn)檎也坏綄?yīng)的方法而崩潰.因?yàn)閏opy就是復(fù)制一個(gè)不可變NSArray的對象;

NSMutableArray copy操作之后,變成一個(gè)新的NSArray對象,就無法添加、刪除、修改操作了。

2、使用了atomic屬性會(huì)嚴(yán)重影響性能。(默認(rèn)是atomic)

第1條的相關(guān)原因在下文中有論述《用@property聲明的NSString(或NSArray,NSDictionary)經(jīng)常使用copy關(guān)鍵字,為什么?如果改用strong關(guān)鍵字,可能造成什么問題?》 以及上文《怎么用 copy 關(guān)鍵字?》也有論述。

第2條原因,如下:

該屬性使用了同步鎖,會(huì)在創(chuàng)建時(shí)生成一些額外的代碼用于幫助編寫多線程程序,這會(huì)帶來性能問題,通過聲明nonatomic可以節(jié)省這些雖然很小但是不必要額外開銷。

在默認(rèn)情況下,由編譯器所合成的方法會(huì)通過鎖定機(jī)制確保其原子性(atomicity)。如果屬性具備nonatomic特質(zhì),則不使用同步鎖。請注意,盡管沒有名為“atomic”的特質(zhì)(如果某屬性不具備nonatomic特質(zhì),那它就是“原子的”(atomic))。

在iOS開發(fā)中,你會(huì)發(fā)現(xiàn),幾乎所有屬性都聲明為nonatomic。

一般情況下并不要求屬性必須是“原子的”,因?yàn)檫@并不能保證“線程安全” ( thread

safety),若要實(shí)現(xiàn)“線程安全”的操作,還需采用更為深層的鎖定機(jī)制才行。例如,一個(gè)線程在連續(xù)多次讀取某屬性值的過程中有別的線程在同時(shí)改寫該值,那么即便將屬性聲明為atomic,也還是會(huì)讀到不同的屬性值。

因此,開發(fā)iOS程序時(shí)一般都會(huì)使用nonatomic屬性。但是在開發(fā)Mac OS X程序時(shí),使用 atomic屬性通常都不會(huì)有性能瓶頸。

5. 如何讓自己的類用 copy 修飾符?如何重寫帶 copy 關(guān)鍵字的 setter?

若想令自己所寫的對象具有拷貝功能,則需實(shí)現(xiàn)NSCopying協(xié)議。如果自定義的對象分為可變版本與不可變版本,那么就要同時(shí)實(shí)現(xiàn)NSCopyiog與NSMutableCopying協(xié)議。

具體步驟:

1)需聲明該類遵從NSCopying協(xié)議

2)實(shí)現(xiàn)NSCopying協(xié)議。該協(xié)議只有一個(gè)方法:

-?(id)copyWithZone:?(NSZone*)?zone

注意:一提到讓自己的類用 copy 修飾符,我們總是想覆寫copy方法,其實(shí)真正需要實(shí)現(xiàn)的卻是“copyWithZone”方法。

以第一題的代碼為例:

然后實(shí)現(xiàn)協(xié)議中規(guī)定的方法:

但在實(shí)際的項(xiàng)目中,不可能這么簡單,遇到更復(fù)雜一點(diǎn),比如類對象中的數(shù)據(jù)結(jié)構(gòu)可能并未在初始化方法中設(shè)置好,需要另行設(shè)置。舉個(gè)例子,假如CYLUser中含有一個(gè)數(shù)組,與其他CYLUser對象建立或解除朋友關(guān)系的那些方法都需要操作這個(gè)數(shù)組。那么在這種情況下,你得把這個(gè)包含朋友對象的數(shù)組也一并拷貝過來。下面列出了實(shí)現(xiàn)此功能所需的全部代碼:

// .m文件

以上做法能滿足基本的需求,但是也有缺陷:如果你所寫的對象需要深拷貝,那么可考慮新增一個(gè)專門執(zhí)行深拷貝的方法。

【注:深淺拷貝的概念,在下文中有介紹,詳見下文的:用@property聲明的NSString(或NSArray,NSDictionary)經(jīng)常使用copy關(guān)鍵字,為什么?如果改用strong關(guān)鍵字,可能造成什么問題?】

在例子中,存放朋友對象的set是用“copyWithZooe:”方法來拷貝的,這種淺拷貝方式不會(huì)逐個(gè)復(fù)制set中的元素。若需要深拷貝的話,則可像下面這樣,編寫一個(gè)專供深拷貝所用的方法:

-?(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 關(guān)鍵字的 setter這個(gè)問題,

如果拋開本例來回答的話,如下:

-?(void)setName:(NSString?*)name?{

? ? ? ?_name?=?[name?copy];

}

如果單單就上文的代碼而言,我們不需要也不能重寫name的 setter:由于是name是只讀屬性,所以編譯器不會(huì)為其創(chuàng)建對應(yīng)的“設(shè)置方法”,用初始化方法設(shè)置好屬性值之后,就不能再改變了。(在本例中,之所以還要聲明屬性的“內(nèi)存管理語義”--copy,是因?yàn)椋喝绻粚慶opy,該類的調(diào)用者就不知道初始化方法里會(huì)拷貝這些屬性,他們有可能會(huì)在調(diào)用初始化方法之前自行拷貝屬性值。這種操作多余而低效。)。

那如何確保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 的本質(zhì)是什么?ivar、getter、setter 是如何生成并添加到這個(gè)類中的。

@property 的本質(zhì)是什么?

@property = ivar(成員變量) + getter + setter;

下面解釋下:

“屬性” (property)有兩大概念:ivar(實(shí)例變量)、存取方法(access method = getter + setter)。

“屬性” (property)作為 Objective-C 的一項(xiàng)特性,主要的作用就在于封裝對象中的數(shù)據(jù)。 Objective-C對象通常會(huì)把其所需要的數(shù)據(jù)保存為各種實(shí)例變量實(shí)例變量一般通過“存取方法”(access method)來訪問。其中,“獲取方法”(getter)用于讀取變量值,而“設(shè)置方法”(setter)用于寫入變量值。這個(gè)概念已經(jīng)定型,并且經(jīng)由“屬性”這一特性而成為Objective-C 2.0的一部分。 而在正規(guī)的Objective-C 編碼風(fēng)格中,存取方法有著嚴(yán)格的命名規(guī)范。 正因?yàn)橛辛诉@種嚴(yán)格的命名規(guī)范,所以 Objective-C這門語言才能根據(jù)名稱自動(dòng)創(chuàng)建出存取方法。其實(shí)也可以把屬性當(dāng)做一種關(guān)鍵字,其表示:編譯器會(huì)自動(dòng)寫出一套存取方法,用以訪問給定類型中具有給定名稱的變量。 所以你也可以這么說:

@property = getter + setter;

例如下面這個(gè)類:

@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 是如何生成并添加到這個(gè)類中的?

“自動(dòng)合成”( autosynthesis)

完成屬性定義后,編譯器會(huì)自動(dòng)編寫訪問這些屬性所需的方法,此過程叫做“自動(dòng)合成”( autosynthesis)。需要強(qiáng)調(diào)的是,這個(gè)過程由編譯器在編譯期執(zhí)行,所以編輯器里看不到這些“合成方法”(synthesized method)的源代碼。除了生成方法代碼 getter、setter之外,編譯器還要自動(dòng)向類中添加適當(dāng)類型的實(shí)例變量,并且在屬性名前面加下劃線,以此作為實(shí)例變量的名字。在前例中,會(huì)生成兩個(gè)實(shí)例變量,其名稱分別為

_firstName與_lastName。也可以在類的實(shí)現(xiàn)代碼里通過 @synthesize語法來指定實(shí)例變量的名字.

@implementation?Person

@synthesize?firstName?=?_myFirstName;

@synthesize?lastName?= _myLastName;

@end

我為了搞清屬性是怎么實(shí)現(xiàn)的,曾經(jīng)反編譯過相關(guān)的代碼,大致生成了五個(gè)東西:

1)OBJC_IVAR_$類名$屬性名稱 :該屬性的“偏移量” (offset),這個(gè)偏移量是“硬編碼” (hardcode),表示該變量距離存放對象的內(nèi)存區(qū)域的起始地址有多遠(yuǎn)。

2)setter與getter方法對應(yīng)的實(shí)現(xiàn)函數(shù)

3)ivar_list :成員變量列表

4)method_list :方法列表

5)prop_list :屬性列表

也就是說我們每次在增加一個(gè)屬性,系統(tǒng)都會(huì)在ivar_list中添加一個(gè)成員變量的描述,在method_list中增加setter與getter方法的描述,在屬性列表中增加一個(gè)屬性的描述,然后計(jì)算該屬性在對象中的偏移量,然后給出setter與getter方法對應(yīng)的實(shí)現(xiàn),在setter方法中從偏移量的位置開始賦值,在getter方法中從偏移量開始取值,為了能夠讀取正確字節(jié)數(shù),系統(tǒng)對象偏移量的指針類型進(jìn)行了類型強(qiáng)轉(zhuǎn).

7. @protocol 和 category 中如何使用 @property

1)在protocol中使用property只會(huì)生成setter和getter方法聲明,我們使用屬性的目的,是希望遵守我協(xié)議的對象能實(shí)現(xiàn)該屬性

2)category 使用 @property 也是只會(huì)生成setter和getter方法的聲明,如果我們真的需要給category增加屬性的實(shí)現(xiàn),需要借助于運(yùn)行時(shí)的兩個(gè)函數(shù):

①objc_setAssociatedObject

②objc_getAssociatedObject

8. runtime 如何實(shí)現(xiàn) weak 屬性

要實(shí)現(xiàn)weak屬性,首先要搞清楚weak屬性的特點(diǎn):

weak 此特質(zhì)表明該屬性定義了一種“非擁有關(guān)系” (nonowning relationship)。為這種屬性設(shè)置新值時(shí),設(shè)置方法既不保留新值,也不釋放舊值。此特質(zhì)同assign類似, 然而在屬性所指的對象遭到摧毀時(shí),屬性值也會(huì)清空(nil out)。

那么runtime如何實(shí)現(xiàn)weak變量的自動(dòng)置nil?

runtime對注冊的類, 會(huì)進(jìn)行布局,對于 weak 對象會(huì)放入一個(gè) hash 表中。 用 weak 指向的對象內(nèi)存地址作為key,當(dāng)此對象的引用計(jì)數(shù)為0的時(shí)候會(huì) dealloc,假如 weak 指向的對象內(nèi)存地址是a,那么就會(huì)以a為鍵, 在這個(gè) weak表中搜索,找到所有以a為鍵的 weak 對象,從而設(shè)置為 nil。

我們可以設(shè)計(jì)一個(gè)函數(shù)(偽代碼)來表示上述機(jī)制:

objc_storeWeak(&a, b)函數(shù):

objc_storeWeak

函數(shù)把第二個(gè)參數(shù)--賦值對象(b)的內(nèi)存地址作為鍵值key,將第一個(gè)參數(shù)--weak修飾的屬性變量(a)的內(nèi)存地址(&a)作為value,注冊到 weak 表中。如果第二個(gè)參數(shù)(b)為0(nil),那么把變量(a)的內(nèi)存地址(&a)從weak表中刪除,你可以把objc_storeWeak(&a, b)理解為:objc_storeWeak(value, key),并且當(dāng)key變nil,將value置nil。

在b非nil時(shí),a和b指向同一個(gè)內(nèi)存地址,在b變nil時(shí),a變nil。此時(shí)向a發(fā)送消息不會(huì)崩潰:在Objective-C中向nil發(fā)送消息是安全的。

如果a是由assign修飾的,則在b非nil時(shí),a和b指向同一個(gè)內(nèi)存地址,在b變nil時(shí),a還是指向該內(nèi)存地址,變野指針。此時(shí)向a發(fā)送消息極易崩潰。 ??

?assign修飾,不會(huì)設(shè)置為nil,會(huì)變成野指針。

下面我們將基于objc_storeWeak(&a, b)函數(shù),使用偽代碼模擬“runtime如何實(shí)現(xiàn)weak屬性”:

//?使用偽代碼模擬:runtime如何實(shí)現(xiàn)weak屬性

//http://weibo.com/luohanchenyilong/

//https://github.com/ChenYilong

id?obj1;

objc_initWeak(&obj1,?obj);

/*obj引用計(jì)數(shù)變?yōu)?,變量作用域結(jié)束*/

objc_destroyWeak(&obj1);

下面對用到的兩個(gè)方法objc_initWeak和objc_destroyWeak做下解釋:

總體說來,作用是: 通過objc_initWeak函數(shù)初始化“附有weak修飾符的變量(obj1)”,在變量作用域結(jié)束時(shí)通過objc_destoryWeak函數(shù)釋放該變量(obj1)。

下面分別介紹下方法的內(nèi)部實(shí)現(xiàn):

objc_initWeak函數(shù)的實(shí)現(xiàn)是這樣的:在將“附有weak修飾符的變量(obj1)”初始化為0(nil)后,會(huì)將“賦值對象”(obj)作為參數(shù),調(diào)用objc_storeWeak函數(shù)。

obj1?=?0;

obj_storeWeak(&obj1,?obj);

也就是說:

weak 修飾的指針默認(rèn)值是 nil (在Objective-C中向nil發(fā)送消息是安全的)

然后obj_destroyWeak函數(shù)將0(nil)作為參數(shù),調(diào)用objc_storeWeak函數(shù)。

objc_storeWeak(&obj1,?0);

前面的源代碼與下列源代碼相同。

//?使用偽代碼模擬:runtime如何實(shí)現(xiàn)weak屬性

//http://weibo.com/luohanchenyilong/

//https://github.com/ChenYilong

id?obj1;

obj1?=?0;

objc_storeWeak(&obj1,?obj);

/*?...?obj的引用計(jì)數(shù)變?yōu)?,被置nil?...?*/

objc_storeWeak(&obj1,?0);

objc_storeWeak函數(shù)把第二個(gè)參數(shù)--賦值對象(obj)的內(nèi)存地址作為鍵值,將第一個(gè)參數(shù)--weak修飾的屬性變量(obj1)的內(nèi)存地址注冊到 weak表中。如果第二個(gè)參數(shù)(obj)為0(nil),那么把變量(obj1)的地址從weak表中刪除,在后面的相關(guān)一題會(huì)詳解。

使用偽代碼是為了方便理解,下面我們“真槍實(shí)彈”地實(shí)現(xiàn)下:

如何讓不使用weak修飾的@property,擁有weak的效果。

我們從setter方法入手:

-?(void)setObject:(NSObject?*)object

{

objc_setAssociatedObject(self,"object",?object,?OBJC_ASSOCIATION_ASSIGN);

[object?cyl_runAtDealloc:^{

_object?=?nil;

}];

}

也就是有兩個(gè)步驟:

1)在setter方法中做如下設(shè)置:

objc_setAssociatedObject(self,"object",?object,?OBJC_ASSOCIATION_ASSIGN);

2)在屬性所指的對象遭到摧毀時(shí),屬性值也會(huì)清空(nil out)。做到這點(diǎn),同樣要借助runtime:

//要銷毀的目標(biāo)對象

id?objectToBeDeallocated;

//可以理解為一個(gè)“事件”:當(dāng)上面的目標(biāo)對象銷毀時(shí),同時(shí)要發(fā)生的“事件”。

id?objectWeWantToBeReleasedWhenThatHappens;

objc_setAssociatedObject(objectToBeDeallocted,

someUniqueKey,

objectWeWantToBeReleasedWhenThatHappens,

OBJC_ASSOCIATION_RETAIN);

知道了思路,我們就開始實(shí)現(xiàn)cyl_runAtDealloc方法,實(shí)現(xiàn)過程分兩部分:

第一部分:創(chuàng)建一個(gè)類,可以理解為一個(gè)“事件”:當(dāng)目標(biāo)對象銷毀時(shí),同時(shí)要發(fā)生的“事件”。借助block執(zhí)行“事件”。

//?.h文件

//http://weibo.com/luohanchenyilong/

//https://github.com/ChenYilong

//?這個(gè)類,可以理解為一個(gè)“事件”:當(dāng)目標(biāo)對象銷毀時(shí),同時(shí)要發(fā)生的“事件”。借助block執(zhí)行“事件”。

typedef?void?(^void Block)(void);

@interface?CYLBlockExecutor?:?NSObject

-?(id)initWithBlock:(voidBlock)block;

@end

//?.m文件

//http://weibo.com/luohanchenyilong/

//https://github.com/ChenYilong

//?這個(gè)類,可以理解為一個(gè)“事件”:當(dāng)目標(biāo)對象銷毀時(shí),同時(shí)要發(fā)生的“事件”。借助block執(zhí)行“事件”。

#import?"CYLBlockExecutor.h"

@interface?CYLBlockExecutor()?{

?void Block?_block;

}

@implementation?CYLBlockExecutor

-?(id)initWithBlock:(voidBlock)aBlock

{

self?=?[super init];

if(self)?{

_block?=?[aBlock?copy];

}

returnself;

}

-?(void)dealloc

{

_block???_block()?:?nil;

}

@end

第二部分:核心代碼:利用runtime實(shí)現(xiàn)cyl_runAtDealloc方法

//?CYLNSObject+RunAtDealloc.h文件

//http://weibo.com/luohanchenyilong/

//https://github.com/ChenYilong

//?利用runtime實(shí)現(xiàn)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實(shí)現(xiàn)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

使用方法: 導(dǎo)入#import?"CYLNSObject+RunAtDealloc.h"

然后就可以使用了:

NSObject?*foo?=?[[NSObject?alloc]?init];

[foo?cyl_runAtDealloc:^{

NSLog(@"正在釋放foo!");

}];

如果對cyl_runAtDealloc的實(shí)現(xiàn)原理有興趣,可以看下這篇博文Fun With the Objective-C Runtime: Run Code at Deallocation of Any Object

9. @property中有哪些屬性關(guān)鍵字?/ @property 后面可以有哪些修飾符?

屬性可以擁有的特質(zhì)分為四類:

原子性---nonatomic特質(zhì)

在默認(rèn)情況下,由編譯器合成的方法會(huì)通過鎖定機(jī)制確保其原子性(atomicity)。如果屬性具備nonatomic特質(zhì),則不使用同步鎖。請注意,盡管沒有名為“atomic”的特質(zhì)(如果某屬性不具備nonatomic特質(zhì),那它就是“原子的” ( atomic),但是仍然可以在屬性特質(zhì)中寫明這一點(diǎn),編譯器不會(huì)報(bào)錯(cuò)。若是自己定義存取方法,那么就應(yīng)該遵從與屬性特質(zhì)相符的原子性。

讀/寫權(quán)限---readwrite(讀寫)、readooly (只讀)

內(nèi)存管理語義---assign、strong、 weak、unsafe_unretained、copy

方法名---getter=、setter=

getter=的樣式:

@property?(nonatomic,?getter=isOn)?BOOL?on;

( setter=這種不常用,也不推薦使用。故不在這里給出寫法。)

不常用的:nonnull,null_resettable,nullable

10. weak屬性需要在dealloc中置nil么?

不需要。

在ARC環(huán)境無論是強(qiáng)指針還是弱指針都無需在deallco設(shè)置為nil,ARC會(huì)自動(dòng)幫我們處理。

即便是編譯器不幫我們做這些,weak也不需要在dealloc中置nil:

正如上文的:runtime 如何實(shí)現(xiàn) weak 屬性 中提到的:

我們模擬下weak的setter方法,應(yīng)該如下:

-?(void)setObject:(NSObject?*)object

{

objc_setAssociatedObject(self,"object",?object,?OBJC_ASSOCIATION_ASSIGN);

[object?cyl_runAtDealloc:^{

_object?=?nil;

}];

}

也即:在屬性所指的對象遭到摧毀時(shí),屬性值也會(huì)清空(nil out)。

11. @synthesize和@dynamic分別有什么作用?

1)@property有兩個(gè)對應(yīng)的詞,一個(gè)是@synthesize,一個(gè)是@dynamic。如果@synthesize和@dynamic都沒寫,那么默認(rèn)的就是@syntheszie var = _var;

2)@synthesize的語義是如果你沒有手動(dòng)實(shí)現(xiàn)setter方法和getter方法,那么編譯器會(huì)自動(dòng)為你加上這兩個(gè)方法。

3)@dynamic告訴編譯器:屬性的setter與getter方法由用戶自己實(shí)現(xiàn),不自動(dòng)生成。(當(dāng)然對于readonly的屬性只需提供getter即可)。假如一個(gè)屬性被聲明為@dynamic

var,然后你沒有提供@setter方法和@getter方法,編譯的時(shí)候沒問題,但是當(dāng)程序運(yùn)行到instance.var =?someVar,由于缺setter方法會(huì)導(dǎo)致程序崩潰;或者當(dāng)運(yùn)行到 someVar =?var時(shí),由于缺getter方法同樣會(huì)導(dǎo)致崩潰。編譯時(shí)沒問題,運(yùn)行時(shí)才執(zhí)行相應(yīng)的方法,這就是所謂的動(dòng)態(tài)綁定。

12. ARC下,不顯式指定任何屬性關(guān)鍵字時(shí),默認(rèn)的關(guān)鍵字都有哪些?

對應(yīng)基本數(shù)據(jù)類型默認(rèn)關(guān)鍵字是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)經(jīng)常使用copy關(guān)鍵字,為什么?如果改用strong關(guān)鍵字,可能造成什么問題?

1)因?yàn)楦割愔羔樋梢灾赶蜃宇悓ο?使用copy的目的是為了讓本對象的屬性不受外界影響,使用copy無論給我傳入是一個(gè)可變對象還是不可對象,我本身持有的就是一個(gè)不可變的副本.

2)如果我們使用是strong,那么這個(gè)屬性就有可能指向一個(gè)可變對象,如果這個(gè)可變對象在外部被修改了,那么會(huì)影響該屬性.

copy此特質(zhì)所表達(dá)的所屬關(guān)系與strong類似。然而設(shè)置方法并不保留新值,而是將其“拷貝” (copy)。

當(dāng)屬性類型為NSString時(shí),經(jīng)常用此特質(zhì)來保護(hù)其封裝性,因?yàn)閭鬟f給設(shè)置方法的新值有可能指向一個(gè)NSMutableString類的實(shí)例。這個(gè)類是NSString的子類,表示一種可修改其值的字符串,此時(shí)若是不拷貝字符串,那么設(shè)置完屬性之后,字符串的值就可能會(huì)在對象不知情的情況下遭人更改。

所以,這時(shí)就要拷貝一份“不可變” (immutable)的字符串,確保對象中的字符串值不會(huì)無意間變動(dòng)。只要實(shí)現(xiàn)屬性所用的對象是“可變的”(mutable),就應(yīng)該在設(shè)置新屬性值時(shí)拷貝一份。

為了理解這種做法,首先要知道,對非集合類對象的copy操作:

在非集合類對象中:對immutable對象進(jìn)行copy操作,是指針復(fù)制,mutableCopy操作時(shí)內(nèi)容復(fù)制;對mutable對象進(jìn)行copy和mutableCopy都是內(nèi)容復(fù)制。用代碼簡單表示如下:(內(nèi)容復(fù)制,指針復(fù)制)

[immutableObject copy] // 淺復(fù)制

[immutableObject mutableCopy] //深復(fù)制

[mutableObject copy] //深復(fù)制

[mutableObject mutableCopy] //深復(fù)制

比如以下代碼:

NSMutableString?*string?=?[NSMutableString?stringWithString:@"origin"];//copy

NSString?*stringCopy?=?[string?copy];

查看內(nèi)存,會(huì)發(fā)現(xiàn) string、stringCopy 內(nèi)存地址都不一樣,說明此時(shí)都是做內(nèi)容拷貝、深拷貝。即使你進(jìn)行如下操作:

[string?appendString:@"origion!"]

stringCopy的值也不會(huì)因此改變,但是如果不使用copy,stringCopy的值就會(huì)被改變。 集合類對象以此類推。 所以,

用@property聲明 NSString、NSArray、NSDictionary經(jīng)常使用copy關(guān)鍵字,是因?yàn)樗麄冇袑?yīng)的可變類型:NSMutableString、NSMutableArray、NSMutableDictionary,他們之間可能進(jìn)行賦值操作,為確保對象中的字符串值不會(huì)無意間變動(dòng),應(yīng)該在設(shè)置新屬性值時(shí)拷貝一份。 ?(父子屬性繼承)

參考鏈接:iOS 集合的深復(fù)制與淺復(fù)制

14. @synthesize合成實(shí)例變量的規(guī)則是什么?假如property名為foo,存在一個(gè)名為_foo的實(shí)例變量,那么還會(huì)自動(dòng)合成新變量么?

在回答之前先說明下一個(gè)概念:

實(shí)例變量 = 成員變量 = ivar

這些說法,筆者下文中,可能都會(huì)用到,指的是一個(gè)東西。

正如Apple官方文檔 You Can Customize Synthesized Instance Variable Names所說:

如果使用了屬性的話,那么編譯器就會(huì)自動(dòng)編寫訪問屬性所需的方法,此過程叫做“自動(dòng)合成”( autosynthesis)。需要強(qiáng)調(diào)的是,這個(gè)過程由編譯器在編譯期執(zhí)行,所以編輯器里看不到這些“合成方法” (synthesized?method)的源代碼。除了生成方法代碼之外,編譯器還要自動(dòng)向類中添加適當(dāng)類型的實(shí)例變量,并且在屬性名前面加下劃線,以此作為實(shí)例變量的名字。

@interface?CYLPerson?:?NSObject

@property?NSString?*firstName;

@property?NSString?*lastName;

@end

在上例中,會(huì)生成兩個(gè)實(shí)例變量,其名稱分別為 _firstName與_lastName。也可以在類的實(shí)現(xiàn)代碼里通過@synthesize語法來指定實(shí)例變量的名字:

@implementation?CYLPerson

@synthesize?firstName?=?_myFirstName;

@synthesize?lastName?=?_myLastName;

@end

上述語法會(huì)將生成的實(shí)例變量命名為_myFirstName與_myLastName,而不再使用默認(rèn)的名字。一般情況下無須修改默認(rèn)的實(shí)例變量名,但是如果你不喜歡以下劃線來命名實(shí)例變量,那么可以用這個(gè)辦法將其改為自己想要的名字。筆者還是推薦使用默認(rèn)的命名方案,因?yàn)槿绻腥硕紙?jiān)持這套方案,那么寫出來的代碼大家都能看得懂。

總結(jié)下@synthesize合成實(shí)例變量的規(guī)則,有以下幾點(diǎn):

1)如果指定了成員變量的名稱,會(huì)生成一個(gè)指定的名稱的成員變量,

2)如果這個(gè)成員已經(jīng)存在了就不再生成了.

3)如果是 @synthesize foo; 還會(huì)生成一個(gè)名稱為foo的成員變量,也就是說:如果沒有指定成員變量的名稱會(huì)自動(dòng)生成一個(gè)屬性同名的成員變量。

4)如果是 @synthesize foo = _foo; 就不會(huì)生成成員變量了.

假如property名為foo,存在一個(gè)名為_foo的實(shí)例變量,那么還會(huì)自動(dòng)合成新變量么? 不會(huì)。如下圖:

15. 在有了自動(dòng)合成屬性實(shí)例變量之后,@synthesize還有哪些使用場景?

回答這個(gè)問題前,我們要搞清楚一個(gè)問題,什么情況下不會(huì)autosynthesis(自動(dòng)合成)?

同時(shí)重寫了setter和getter時(shí)

重寫了只讀屬性的getter時(shí)

使用了@dynamic時(shí)

在 @protocol 中定義的所有屬性

在 category 中定義的所有屬性

重載的屬性

當(dāng)你在子類中重載了父類中的屬性,你必須 使用@synthesize來手動(dòng)合成ivar。

除了后三條,對其他幾個(gè)我們可以總結(jié)出一個(gè)規(guī)律:當(dāng)你想手動(dòng)管理@property的所有內(nèi)容時(shí),你就會(huì)嘗試通過實(shí)現(xiàn)@property的所有“存取方法”(the accessor?methods)或者使用@dynamic來達(dá)到這個(gè)目的,這時(shí)編譯器就會(huì)認(rèn)為你打算手動(dòng)管理@property,于是編譯器就禁用了autosynthesis(自動(dòng)合成)。因?yàn)橛辛薬utosynthesis(自動(dòng)合成),大部分開發(fā)者已經(jīng)習(xí)慣不去手動(dòng)定義ivar,而是依賴于autosynthesis(自動(dòng)合成),但是一旦你需要使用ivar,而autosynthesis(自動(dòng)合成)又失效了,如果不去手動(dòng)定義ivar,那么你就得借助@synthesize來手動(dòng)合成ivar。

其實(shí),@synthesize語法還有一個(gè)應(yīng)用場景,但是不太建議大家使用:

可以在類的實(shí)現(xiàn)代碼里通過@synthesize語法來指定實(shí)例變量的名字:

@implementation?CYLPerson

@synthesize?firstName?=?_myFirstName;

@synthesize?lastName?=?_myLastName;

@end

上述語法會(huì)將生成的實(shí)例變量命名為_myFirstName與_myLastName,而不再使用默認(rèn)的名字。一般情況下無須修改默認(rèn)的實(shí)例變量名,但是如果你不喜歡以下劃線來命名實(shí)例變量,那么可以用這個(gè)辦法將其改為自己想要的名字。筆者還是推薦使用默認(rèn)的命名案,因?yàn)槿绻腥硕紙?jiān)持這套方案,那么寫出來的代碼大家都能看得懂。

舉例說明:應(yīng)用場景:

結(jié)果編譯器報(bào)錯(cuò):

當(dāng)你同時(shí)重寫了setter和getter時(shí),系統(tǒng)就不會(huì)生成ivar(實(shí)例變量/成員變量)。這時(shí)候有兩種選擇:

要么如第14行:手動(dòng)創(chuàng)建ivar

要么如第17行:使用@synthesize foo = _foo; ,關(guān)聯(lián)@property與ivar。

更多信息,請戳- 》When should I use @synthesize explicitly?

16. objc中向一個(gè)nil對象發(fā)送消息將會(huì)發(fā)生什么?

在Objective-C中向nil發(fā)送消息是完全有效的——只是在運(yùn)行時(shí)不會(huì)有任何作用:

如果一個(gè)方法返回值是一個(gè)對象,那么發(fā)送給nil的消息將返回0(nil)。例如:

Person?*?motherInlaw?=?[[aPerson?spouse]?mother];

如果spouse對象為nil,那么發(fā)送給nil的消息mother也將返回nil。

1)如果方法返回值為指針類型,其指針大小為小于或者等于sizeof(void*),float,double,long double 或者long long的整型標(biāo)量,發(fā)送給nil的消息將返回0。

2)如果方法返回值為結(jié)構(gòu)體,發(fā)送給nil的消息將返回0。結(jié)構(gòu)體中各個(gè)字段的值將都是0。

3)如果方法的返回值不是上述提到的幾種情況,那么發(fā)送給nil的消息的返回值將是未定義的。

具體原因如下:

objc是動(dòng)態(tài)語言,每個(gè)方法在運(yùn)行時(shí)會(huì)被動(dòng)態(tài)轉(zhuǎn)為消息發(fā)送,即:objc_msgSend(receiver, selector)。

那么,為了方便理解這個(gè)內(nèi)容,還是貼一個(gè)objc的源代碼:

//?runtime.h(類在runtime中的定義)

//http://weibo.com/luohanchenyilong/

//https://github.com/ChenYilong

struct?objc_class?{

Class?isa?OBJC_ISA_AVAILABILITY;//isa指針指向Meta?Class,因?yàn)镺bjc的類的本身也是一個(gè)Object,為了處理這個(gè)關(guān)系,runtime就創(chuàng)造了Meta?Class,當(dāng)給類發(fā)送[NSObject?alloc]這樣消息時(shí),實(shí)際上是把這個(gè)消息發(fā)給了Class?Object

#if?!__OBJC2__

Class?super_class?OBJC2_UNAVAILABLE;//?父類

const?char?*name?OBJC2_UNAVAILABLE;//?類名

long?version?OBJC2_UNAVAILABLE;//?類的版本信息,默認(rèn)為0

long?info?OBJC2_UNAVAILABLE;//?類信息,供運(yùn)行期使用的一些位標(biāo)識

long?instance_size?OBJC2_UNAVAILABLE;//?該類的實(shí)例變量大小

struct?objc_ivar_list?*ivars?OBJC2_UNAVAILABLE;//?該類的成員變量鏈表

struct?objc_method_list?**methodLists?OBJC2_UNAVAILABLE;//?方法定義的鏈表

struct?objc_cache?*cache?OBJC2_UNAVAILABLE;//?方法緩存,對象接到一個(gè)消息會(huì)根據(jù)isa指針查找消息對象,這時(shí)會(huì)在method?Lists中遍歷,如果cache了,常用的方法調(diào)用時(shí)就能夠提高調(diào)用的效率。

struct?objc_protocol_list?*protocols?OBJC2_UNAVAILABLE;//?協(xié)議鏈表

#endif

}?OBJC2_UNAVAILABLE;

objc

在向一個(gè)對象發(fā)送消息時(shí),runtime庫會(huì)根據(jù)對象的isa指針找到該對象實(shí)際所屬的類,然后在該類中的方法列表以及其父類方法列表中尋找方法運(yùn)行,然后在發(fā)送消息的時(shí)候,objc_msgSend方法不會(huì)返回值,所謂的返回內(nèi)容都是具體調(diào)用時(shí)執(zhí)行的。

那么,回到本題,如果向一個(gè)nil對象發(fā)送消息,首先在尋找對象的isa指針時(shí)就是0地址返回了,所以不會(huì)出現(xiàn)任何錯(cuò)誤。

17. objc中向一個(gè)對象發(fā)送消息[obj foo]和objc_msgSend()函數(shù)之間有什么關(guān)系?

具體原因同上題:該方法編譯之后就是objc_msgSend()函數(shù)調(diào)用.如果我沒有記錯(cuò)的大概是這樣的:

((void?()(id,?SEL))(void?)objc_msgSend)((id)obj,?sel_registerName("foo"));

也就是說:[obj foo];在objc動(dòng)態(tài)編譯時(shí),會(huì)被轉(zhuǎn)意為:objc_msgSend(obj, @selector(foo));。

18. 什么時(shí)候會(huì)報(bào)unrecognized selector的異常?

簡單來說:當(dāng)該對象調(diào)用某個(gè)方法,而該對象上沒有實(shí)現(xiàn)這個(gè)方法的時(shí)候, 可以通過“消息轉(zhuǎn)發(fā)”進(jìn)行解決。

簡單的流程如下,在上一題中也提到過:objc是動(dòng)態(tài)語言,每個(gè)方法在運(yùn)行時(shí)會(huì)被動(dòng)態(tài)轉(zhuǎn)為消息發(fā)送,即:objc_msgSend(receiver, selector)。

objc在向一個(gè)對象發(fā)送消息時(shí),runtime庫會(huì)根據(jù)對象的isa指針找到該對象實(shí)際所屬的類,然后在該類中的方法列表以及其父類方法列表中尋找方法運(yùn)行,如果,在最頂層的父類中依然找不到相應(yīng)的方法時(shí),程序在運(yùn)行時(shí)會(huì)掛掉并拋出異常unrecognized selector sent to XXX。但是在這之前,objc的運(yùn)行時(shí)會(huì)給出三次拯救程序崩潰的機(jī)會(huì):

Method resolution

objc運(yùn)行時(shí)會(huì)調(diào)用+resolveInstanceMethod:或者

+resolveClassMethod:,讓你有機(jī)會(huì)提供一個(gè)函數(shù)實(shí)現(xiàn)。如果你添加了函數(shù)并返回

YES,那運(yùn)行時(shí)系統(tǒng)就會(huì)重新啟動(dòng)一次消息發(fā)送的過程,如果 resolve 方法返回 NO ,運(yùn)行時(shí)就會(huì)移到下一步,消息轉(zhuǎn)發(fā)(Message

Forwarding)。

Fast forwarding

如果目標(biāo)對象實(shí)現(xiàn)了-forwardingTargetForSelector:,Runtime這時(shí)就會(huì)調(diào)用這個(gè)方法,給你把這個(gè)消息轉(zhuǎn)發(fā)給其他對象的機(jī)會(huì)。只要這個(gè)方法返回的不是nil和self,整個(gè)消息發(fā)送的過程就會(huì)被重啟,當(dāng)然發(fā)送的對象會(huì)變成你返回的那個(gè)對象。否則,就會(huì)繼續(xù)Normal

Fowarding。

這里叫Fast,只是為了區(qū)別下一步的轉(zhuǎn)發(fā)機(jī)制。因?yàn)檫@一步不會(huì)創(chuàng)建任何新的對象,但下一步轉(zhuǎn)發(fā)會(huì)創(chuàng)建一個(gè)NSInvocation對象,所以相對更快點(diǎn)。

Normal forwarding

這一步是Runtime最后一次給你挽救的機(jī)會(huì)。首先它會(huì)發(fā)送-methodSignatureForSelector:消息獲得函數(shù)的參數(shù)和返回值類型。

如果-methodSignatureForSelector:返回nil,Runtime則會(huì)發(fā)出

-doesNotRecognizeSelector:消息,程序這時(shí)也就掛掉了。如果返回了一個(gè)函數(shù)簽名,Runtime就會(huì)創(chuàng)建一個(gè)NSInvocation對象并發(fā)送-forwardInvocation:消息給目標(biāo)對象。

19. 一個(gè)objc對象如何進(jìn)行內(nèi)存布局?(考慮有父類的情況)

所有父類的成員變量和自己的成員變量都會(huì)存放在該對象所對應(yīng)的存儲(chǔ)空間中.

每一個(gè)對象內(nèi)部都有一個(gè)isa指針,指向他的類對象,類對象中存放著本對象的

1)對象方法列表(對象能夠接收的消息列表,保存在它所對應(yīng)的類對象中)

2)成員變量的列表

3)屬性列表

它內(nèi)部也有一個(gè)isa指針指向元對象(meta class),元對象內(nèi)部存放的是類方法列表,類對象內(nèi)部還有一個(gè)superclass的指針,指向他的父類對象。

1)根對象就是NSobject,它的superclass指針指向nil。

2)類對象既然稱為對象,那它也是一個(gè)實(shí)例。類對象中也有一個(gè)isa指針指向它的元類(meta class),即類對象是元類的實(shí)例。元類內(nèi)部存放的是類方法列表,根元類的isa指針指向自己,superclass指針指向NSObject類。

如圖:

20. 一個(gè)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

這個(gè)題目主要是考察關(guān)于objc中對 self 和 super 的理解。

self 是類的隱藏參數(shù),指向當(dāng)前調(diào)用方法的這個(gè)類的實(shí)例。而 super 是一個(gè) Magic Keyword, 它本質(zhì)是一個(gè)編譯器標(biāo)示符和 self 是指向的同一個(gè)消息接受者。

上面的例子不管調(diào)用[self class]還是[super class],接受消息的對象都是當(dāng)前 Son *xxx 這個(gè)對象。而不同的是,super是告訴編譯器,調(diào)用 class 這個(gè)方法時(shí),要去父類的方法,而不是本類里的。

當(dāng)使用 self 調(diào)用方法時(shí),會(huì)從當(dāng)前類的方法列表中開始找,如果沒有,就從父類中再找;而當(dāng)使用 super 時(shí),則從父類的方法列表中開始找。然后調(diào)用父類的這個(gè)方法。

真的是這樣嗎?繼續(xù)看:

使用clang重寫命令:

1

$?clang?-rewrite-objc?test.m

發(fā)現(xiàn)上述代碼被轉(zhuǎn)化為:

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"))));

從上面的代碼中,我們可以發(fā)現(xiàn)在調(diào)用 [self class] 時(shí),會(huì)轉(zhuǎn)化成 objc_msgSend函數(shù)。看下函數(shù)定義:

id?objc_msgSend(id?self,?SEL?op,?...)

我們把 self 做為第一個(gè)參數(shù)傳遞進(jìn)去。

而在調(diào)用 [super class]時(shí),會(huì)轉(zhuǎn)化成 objc_msgSendSuper函數(shù)。看下函數(shù)定義:

id?objc_msgSendSuper(struct?objc_super?*super,?SEL?op,?...)

第一個(gè)參數(shù)是 objc_super 這樣一個(gè)結(jié)構(gòu)體,其定義如下:

struct?objc_super?{

__unsafe_unretained?id?receiver;

__unsafe_unretained?Class?super_class;

};

結(jié)構(gòu)體有兩個(gè)成員,第一個(gè)成員是 receiver, 類似于上面的 objc_msgSend函數(shù)第一個(gè)參數(shù)self 。第二個(gè)成員是記錄當(dāng)前類的父類是什么。

所以,當(dāng)調(diào)用 [self class] 時(shí),實(shí)際先調(diào)用的是 objc_msgSend函數(shù),第一個(gè)參數(shù)是 Son當(dāng)前的這個(gè)實(shí)例,然后在 Son這個(gè)類里面去找 - (Class)class這個(gè)方法,沒有,去父類 Father里找,也沒有,最后在 NSObject類中發(fā)現(xiàn)這個(gè)方法。而 -(Class)class的實(shí)現(xiàn)就是返回self的類別,故上述輸出結(jié)果為 Son。

objc Runtime開源代碼對- (Class)class方法的實(shí)現(xiàn):

-?(Class)class?{

return object_getClass(self);

}

而當(dāng)調(diào)用 [super class]時(shí),會(huì)轉(zhuǎn)換成objc_msgSendSuper函數(shù)。第一步先構(gòu)造 objc_super結(jié)構(gòu)體,結(jié)構(gòu)體第一個(gè)成員就是 self 。 第二個(gè)成員是

(id)class_getSuperclass(objc_getClass(“Son”)) , 實(shí)際該函數(shù)輸出結(jié)果為 Father。 第二步是去Father這個(gè)類里去找 - (Class)class,沒有,然后去NSObject類去找,找到了。最后內(nèi)部是使用objc_msgSend(objc_super->receiver, @selector(class))去調(diào)用, 此時(shí)已經(jīng)和[self?class]調(diào)用相同了,故上述輸出結(jié)果仍然返回 Son。

22. runtime如何通過selector找到對應(yīng)的IMP地址?(分別考慮類方法和實(shí)例方法)

每一個(gè)類對象中都一個(gè)方法列表,方法列表中記錄著方法的名稱,方法實(shí)現(xiàn),以及參數(shù)類型,其實(shí)selector本質(zhì)就是方法名稱,通過這個(gè)方法名稱就可以在方法列表中找到對應(yīng)的方法實(shí)現(xiàn).

方法列表------方法名稱------方法實(shí)現(xiàn)。

23. 使用runtime Associate方法關(guān)聯(lián)的對象,需要在主對象dealloc的時(shí)候釋放么?

在ARC下不需要

在MRC中,對于使用retain或copy策略的需要

無論在MRC下還是ARC下均不需要

2011年版本的Apple API 官方文檔 - Associative References一節(jié)中有一個(gè)MRC環(huán)境下的例子:

//?在MRC下,使用runtime?Associate方法關(guān)聯(lián)的對象,不需要在主對象dealloc的時(shí)候釋放

//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就會(huì)被release釋放掉了。

既然會(huì)被銷毀,那么具體在什么時(shí)間點(diǎn)?

根據(jù)WWDC 2011, Session 322 (第36分22秒)中發(fā)布的內(nèi)存銷毀時(shí)間表,被關(guān)聯(lián)的對象在生命周期內(nèi)要比對象本身釋放的晚很多。它們會(huì)在被 NSObject -dealloc 調(diào)用的 object_dispose() 方法中釋放。

對象的內(nèi)存銷毀時(shí)間表,分四個(gè)步驟:

//?對象的內(nèi)存銷毀時(shí)間表

//http://weibo.com/luohanchenyilong/(微博@iOS程序犭袁)

//https://github.com/ChenYilong

//?根據(jù)?WWDC?2011,?Session?322?(36分22秒)中發(fā)布的內(nèi)存銷毀時(shí)間表

1.?調(diào)用?-release?:引用計(jì)數(shù)變?yōu)榱?/p>

*?對象正在被銷毀,生命周期即將結(jié)束.

*?不能再有新的?__weak?弱引用,?否則將指向?nil.

*?調(diào)用?[self?dealloc]

2.?父類?調(diào)用?-dealloc

*?繼承關(guān)系中最底層的父類?在調(diào)用?-dealloc

*?如果是?MRC?代碼?則會(huì)手動(dòng)釋放實(shí)例變量們(iVars)

*?繼承關(guān)系中每一層的父類?都在調(diào)用?-dealloc

3.?NSObject?調(diào)?-dealloc

*?只做一件事:調(diào)用?Objective-C?runtime?中的?object_dispose()?方法

4.?調(diào)用?object_dispose()

*?為?C++?的實(shí)例變量們(iVars)調(diào)用?destructors

*?為?ARC?狀態(tài)下的?實(shí)例變量們(iVars)?調(diào)用?-release

*?解除所有使用?runtime?Associate方法關(guān)聯(lián)的對象

*?解除所有?__weak?引用

*?調(diào)用?free()

對象的內(nèi)存銷毀時(shí)間表:參考鏈接

24. objc中的類方法和實(shí)例方法有什么本質(zhì)區(qū)別和聯(lián)系?

類方法:

類方法是屬于類對象的

類方法只能通過類對象調(diào)用

類方法中的self是類對象

類方法可以調(diào)用其他的類方法

類方法中不能訪問成員變量

類方法中不定直接調(diào)用對象方法

實(shí)例方法:

實(shí)例方法是屬于實(shí)例對象的

實(shí)例方法只能通過實(shí)例對象調(diào)用

實(shí)例方法中的self是實(shí)例對象

實(shí)例方法中可以訪問成員變量

實(shí)例方法中直接調(diào)用實(shí)例方法

實(shí)例方法中也可以調(diào)用類方法(通過類名)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容