《招聘一個靠譜的 iOS》—參考答案(上)
說明:面試題來源是微博@我就叫Sunny怎么了的這篇博文:《招聘一個靠譜的 iOS》屋厘,其中共55題蛆挫,除第一題為糾錯題外,其他54道均為簡答題。
出題者簡介: 孫源(sunnyxx)踱讨,目前就職于百度,負(fù)責(zé)百度知道 iOS 客戶端的開發(fā)工作砍的,對技術(shù)喜歡刨根問底和總結(jié)最佳實(shí)踐痹筛,熱愛分享和開源,維護(hù)一個叫 forkingdog 的開源小組廓鞠。
答案為微博@iOS程序犭袁整理帚稠,未經(jīng)出題者校對,如有紕漏床佳,請向微博@iOS程序犭袁指正滋早。
索引
@synthesize合成實(shí)例變量的規(guī)則是什么狰腌?假如property名為foo,存在一個名為_foo的實(shí)例變量牧氮,那么還會自動合成新變量么琼腔?
objc中向一個對象發(fā)送消息[obj foo]和objc_msgSend()函數(shù)之間有什么關(guān)系光坝?
@implementation Son : Father
- (id)init
{
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
- 22--55題鸳惯,請看下篇。
1. 風(fēng)格糾錯題
修改完的代碼:
修改方法有很多種叠萍,現(xiàn)給出一種做示例:
// .h文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 修改完的代碼芝发,這是第一種修改方法,后面會給出第二種修改方法
typedef NS_ENUM(NSInteger, CYLSex) {
CYLSexMan,
CYLSexWoman
};
@interface CYLUser : NSObject<NSCopying>
@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
下面對具體修改的地方苛谷,分兩部分做下介紹:硬傷部分和優(yōu)化部分
辅鲸。因?yàn)?strong>硬傷部分沒什么技術(shù)含量,為了節(jié)省大家時間抄腔,放在后面講瓢湃,大神請直接看優(yōu)化部分。
優(yōu)化部分
-
enum 建議使用
NS_ENUM
和NS_OPTIONS
宏來定義枚舉類型赫蛇,參見官方的 Adopting Modern Objective-C 一文:
//定義一個枚舉
typedef NS_ENUM(NSInteger, CYLSex) {
CYLSexMan,
CYLSexWoman
};
(僅僅讓性別包含男和女可能并不嚴(yán)謹(jǐn)绵患,最嚴(yán)謹(jǐn)?shù)淖龇梢詤⒖?[這里](https://github.com/ChenYilong/iOSInterviewQuestions/issues/9) 。)
2. age 屬性的類型:應(yīng)避免使用基本類型悟耘,建議使用 Foundation 數(shù)據(jù)類型落蝙,對應(yīng)關(guān)系如下:
```Objective-C
int -> NSInteger
unsigned -> NSUInteger
float -> CGFloat
動畫時間 -> NSTimeInterval
同時考慮到 age 的特點(diǎn),應(yīng)使用 NSUInteger 暂幼,而非 int 筏勒。
這樣做的是基于64-bit 適配考慮,詳情可參考出題者的博文《64-bit Tips》旺嬉。
- 如果工程項(xiàng)目非常龐大管行,需要拆分成不同的模塊,可以在類邪媳、typedef宏命名的時候使用前綴捐顷。
- doLogIn方法不應(yīng)寫在該類中: <p><del>雖然
LogIn
的命名不太清晰,但筆者猜測是login的意思雨效, (勘誤:Login是名詞迅涮,LogIn 是動詞,都表示登陸的意思徽龟。見: Log in vs. login )</del></p>登錄操作屬于業(yè)務(wù)邏輯叮姑,觀察類名 UserModel ,以及屬性的命名方式据悔,該類應(yīng)該是一個 Model 而不是一個“ MVVM 模式下的 ViewModel ”:
無論是 MVC 模式還是 MVVM 模式传透,業(yè)務(wù)邏輯都不應(yīng)當(dāng)寫在 Model 里:MVC 應(yīng)在 C耘沼,MVVM 應(yīng)在 VM。
(如果拋開命名規(guī)范旷祸,假設(shè)該類真的是 MVVM 模式里的 ViewModel 耕拷,那么 UserModel 這個類可能對應(yīng)的是用戶注冊頁面,如果有特殊的業(yè)務(wù)需求托享,比如: -logIn
對應(yīng)的應(yīng)當(dāng)是注冊并登錄的一個 Button ,出現(xiàn) -logIn
方法也可能是合理的浸赫。)
- doLogIn 方法命名不規(guī)范:添加了多余的動詞前綴闰围。
請牢記:
如果方法表示讓對象執(zhí)行一個動作,使用動詞打頭來命名既峡,注意不要使用
do
羡榴,does
這種多余的關(guān)鍵字,動詞本身的暗示就足夠了运敢。
應(yīng)為 -logIn
(注意: Login
是名詞, LogIn
是動詞,都表示登陸表伦。 見 Log in vs. login )
-
-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;
方法中不要用with
來連接兩個參數(shù):withAge:
應(yīng)當(dāng)換為age:
体啰,age:
已經(jīng)足以清晰說明參數(shù)的作用,也不建議用andAge:
:通常情況下卦方,即使有類似withA:withB:
的命名需求羊瘩,也通常是使用withA:andB:
這種命名,用來表示方法執(zhí)行了兩個相對獨(dú)立的操作(從設(shè)計(jì)上來說盼砍,這時候也可以拆分成兩個獨(dú)立的方法)尘吗,它不應(yīng)該用作闡明有多個參數(shù),比如下面的:
//錯誤浇坐,不要使用"and"來連接參數(shù)
- (int)runModalForDirectory:(NSString *)path andFile:(NSString *)name andTypes:(NSArray *)fileTypes;
//錯誤睬捶,不要使用"and"來闡明有多個參數(shù)
- (instancetype)initWithName:(CGFloat)width andAge:(CGFloat)height;
//正確,使用"and"來表示兩個相對獨(dú)立的操作
- (BOOL)openFile:(NSString *)fullPath withApplication:(NSString *)appName andDeactivate:(BOOL)flag;
- 由于字符串值可能會改變近刘,所以要把相關(guān)屬性的“內(nèi)存管理語義”聲明為 copy 擒贸。(原因在下文有詳細(xì)論述:用@property聲明的NSString(或NSArray,NSDictionary)經(jīng)常使用copy關(guān)鍵字跌宛,為什么酗宋?)
- “性別”(sex)屬性的:該類中只給出了一種“初始化方法” (initializer)用于設(shè)置“姓名”(Name)和“年齡”(Age)的初始值,那如何對“性別”(Sex)初始化疆拘?
Objective-C 有 designated 和 secondary 初始化方法的觀念蜕猫。 designated 初始化方法是提供所有的參數(shù),secondary 初始化方法是一個或多個哎迄,并且提供一個或者更多的默認(rèn)參數(shù)來調(diào)用 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 初始化方法。因?yàn)閮H僅是調(diào)用類實(shí)現(xiàn)的 designated 初始化方法翔烁。
因?yàn)槌鲱}者沒有給出 .m
文件渺氧,所以有兩種猜測:1:本來打算只設(shè)計(jì)一個 designated 初始化方法,但漏掉了“性別”(sex)屬性蹬屹。那么最終的修改代碼就是上文給出的第一種修改方法侣背。2:不打算初始時初始化“性別”(sex)屬性,打算后期再修改慨默,如果是這種情況贩耐,那么應(yīng)該把“性別”(sex)屬性設(shè)為 readwrite 屬性,最終給出的修改代碼應(yīng)該是:
// .h文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 第二種修改方法(基于第一種修改方法的基礎(chǔ)上)
typedef NS_ENUM(NSInteger, CYLSex) {
CYLSexMan,
CYLSexWoman
};
@interface CYLUser : NSObject<NSCopying>
@property (nonatomic, readonly, copy) NSString *name;
@property (nonatomic, readonly, assign) NSUInteger age;
@property (nonatomic, readwrite, assign) CYLSex sex;
- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age;
+ (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
@end
.h
中暴露 designated 初始化方法厦取,是為了方便子類化 (想了解更多潮太,請戳--》 《禪與 Objective-C 編程藝術(shù) (Zen and the Art of the Objective-C Craftsmanship 中文翻譯)》。)
- 按照接口設(shè)計(jì)的慣例虾攻,如果設(shè)計(jì)了“初始化方法” (initializer)铡买,也應(yīng)當(dāng)搭配一個快捷構(gòu)造方法。而快捷構(gòu)造方法的返回值霎箍,建議為 instancetype奇钞,為保持一致性,init 方法和快捷構(gòu)造方法的返回類型最好都用 instancetype朋沮。
- 如果基于第一種修改方法:既然該類中已經(jīng)有一個“初始化方法” (initializer)蛇券,用于設(shè)置“姓名”(Name)、“年齡”(Age)和“性別”(Sex)的初始值:
那么在設(shè)計(jì)對應(yīng)@property
時就應(yīng)該盡量使用不可變的對象:其三個屬性都應(yīng)該設(shè)為“只讀”樊拓。用初始化方法設(shè)置好屬性值之后纠亚,就不能再改變了。在本例中筋夏,仍需聲明屬性的“內(nèi)存管理語義”蒂胞。于是可以把屬性的定義改成這樣
@property (nonatomic, readonly, copy) NSString *name;
@property (nonatomic, readonly, assign) NSUInteger age;
@property (nonatomic, readonly, assign) CYLSex sex;
由于是只讀屬性,所以編譯器不會為其創(chuàng)建對應(yīng)的“設(shè)置方法”条篷,即便如此骗随,我們還是要寫上這些屬性的語義,以此表明初始化方法在設(shè)置這些屬性值時所用的方式赴叹。要是不寫明語義的話鸿染,該類的調(diào)用者就不知道初始化方法里會拷貝這些屬性,他們有可能會在調(diào)用初始化方法之前自行拷貝屬性值乞巧。這種操作多余而且低效涨椒。
-
initUserModelWithUserName
如果改為initWithName
會更加簡潔,而且足夠清晰。 -
UserModel
如果改為User
會更加簡潔蚕冬,而且足夠清晰免猾。 -
UserSex
如果改為Sex
會更加簡潔,而且足夠清晰囤热。 - 第二個
@property
中 assign 和 nonatomic 調(diào)換位置猎提。
推薦按照下面的格式來定義屬性
@property (nonatomic, readwrite, copy) NSString *name;
屬性的參數(shù)應(yīng)該按照下面的順序排列: 原子性,讀寫 和 內(nèi)存管理旁蔼。 這樣做你的屬性更容易修改正確锨苏,并且更好閱讀。這在《禪與Objective-C編程藝術(shù) >》里有介紹棺聊。而且習(xí)慣上修改某個屬性的修飾符時蚓炬,一般從屬性名從右向左搜索需要修動的修飾符。最可能從最右邊開始修改這些屬性的修飾符躺屁,根據(jù)經(jīng)驗(yàn)這些修飾符被修改的可能性從高到底應(yīng)為:內(nèi)存管理 > 讀寫權(quán)限 >原子操作。
硬傷部分
- 在-和(void)之間應(yīng)該有一個空格
- enum 中駝峰命名法和下劃線命名法混用錯誤:枚舉類型的命名規(guī)則和函數(shù)的命名規(guī)則相同:命名時使用駝峰命名法经宏,勿使用下劃線命名法犀暑。
- enum 左括號前加一個空格,或者將左括號換到下一行
- enum 右括號后加一個空格
-
UserModel :NSObject
應(yīng)為UserModel : NSObject
烁兰,也就是:
右側(cè)少了一個空格耐亏。 -
@interface
與@property
屬性聲明中間應(yīng)當(dāng)間隔一行。 - 兩個方法定義之間不需要換行沪斟,有時為了區(qū)分方法的功能也可間隔一行广辰,但示例代碼中間隔了兩行。
-
-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;
方法中方法名與參數(shù)之間多了空格主之。而且-
與(id)
之間少了空格择吊。
`-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;`方法中方法名與參數(shù)之間多了空格:`(NSString*)name` 前多了空格。
`-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;` 方法中 `(NSString*)name`,應(yīng)為 `(NSString *)name`槽奕,少了空格几睛。
- <p><del>doLogIn方法中的
LogIn
命名不清晰:筆者猜測是login的意思,應(yīng)該是粗心手誤造成的粤攒。
(勘誤:Login
是名詞所森,LogIn
是動詞,都表示登陸的意思夯接。見: Log in vs. login )</del></p>
2. 什么情況使用 weak 關(guān)鍵字焕济,相比 assign 有什么不同?
什么情況使用 weak 關(guān)鍵字盔几?
在 ARC 中,在有可能出現(xiàn)循環(huán)引用的時候,往往要通過讓其中一端使用 weak 來解決,比如: delegate 代理屬性
自身已經(jīng)對它進(jìn)行一次強(qiáng)引用,沒有必要再強(qiáng)引用一次,此時也會使用 weak,自定義 IBOutlet 控件屬性一般也使用 weak晴弃;當(dāng)然,也可以使用strong。在下文也有論述:《IBOutlet連出來的視圖屬性為什么可以被設(shè)置成weak?》
不同點(diǎn):
weak
此特質(zhì)表明該屬性定義了一種“非擁有關(guān)系” (nonowning relationship)肝匆。為這種屬性設(shè)置新值時粒蜈,設(shè)置方法既不保留新值,也不釋放舊值旗国。此特質(zhì)同assign類似枯怖,
然而在屬性所指的對象遭到摧毀時,屬性值也會清空(nil out)能曾。
而assign
的“設(shè)置方法”只會執(zhí)行針對“純量類型” (scalar type度硝,例如 CGFloat 或
NSlnteger 等)的簡單賦值操作。assigin 可以用非 OC 對象,而 weak 必須用于 OC 對象
3. 怎么用 copy 關(guān)鍵字寿冕?
用途:
- NSString蕊程、NSArray、NSDictionary 等等經(jīng)常使用copy關(guān)鍵字驼唱,是因?yàn)樗麄冇袑?yīng)的可變類型:NSMutableString藻茂、NSMutableArray、NSMutableDictionary玫恳;
- 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 也無傷大雅京办,還能時刻提醒我們:編譯器自動對 block 進(jìn)行了 copy 操作掀序。如果不寫 copy ,該類的調(diào)用者有可能會忘記或者根本不知道“編譯器會自動對 block 進(jìn)行了 copy 操作”惭婿,他們有可能會在調(diào)用之前自行拷貝屬性值不恭。這種操作多余而低效。
下面做下解釋:
copy 此特質(zhì)所表達(dá)的所屬關(guān)系與 strong 類似财饥。然而設(shè)置方法并不保留新值换吧,而是將其“拷貝” (copy)。
當(dāng)屬性類型為 NSString 時佑力,經(jīng)常用此特質(zhì)來保護(hù)其封裝性式散,因?yàn)閭鬟f給設(shè)置方法的新值有可能指向一個 NSMutableString 類的實(shí)例。這個類是 NSString 的子類打颤,表示一種可修改其值的字符串暴拄,此時若是不拷貝字符串,那么設(shè)置完屬性之后编饺,字符串的值就可能會在對象不知情的情況下遭人更改乖篷。所以,這時就要拷貝一份“不可變” (immutable)的字符串透且,確保對象中的字符串值不會無意間變動撕蔼。只要實(shí)現(xiàn)屬性所用的對象是“可變的” (mutable)豁鲤,就應(yīng)該在設(shè)置新屬性值時拷貝一份。
用
@property
聲明 NSString鲸沮、NSArray琳骡、NSDictionary 經(jīng)常使用 copy 關(guān)鍵字,是因?yàn)樗麄冇袑?yīng)的可變類型:NSMutableString讼溺、NSMutableArray楣号、NSMutableDictionary,他們之間可能進(jìn)行賦值操作怒坯,為確保對象中的字符串值不會無意間變動炫狱,應(yīng)該在設(shè)置新屬性值時拷貝一份。
該問題在下文中也有論述:用@property聲明的NSString(或NSArray剔猿,NSDictionary)經(jīng)常使用copy關(guān)鍵字视译,為什么?如果改用strong關(guān)鍵字归敬,可能造成什么問題酷含?
4. 這個寫法會出什么問題: @property (copy) NSMutableArray *array;
兩個問題:1、添加,刪除,修改數(shù)組內(nèi)的元素的時候,程序會因?yàn)檎也坏綄?yīng)的方法而崩潰.因?yàn)?copy 就是復(fù)制一個不可變 NSArray 的對象汪茧;2第美、使用了 atomic 屬性會嚴(yán)重影響性能 ;
第1條的相關(guān)原因在下文中有論述《用@property聲明的NSString(或NSArray陆爽,NSDictionary)經(jīng)常使用 copy 關(guān)鍵字,為什么扳缕?如果改用strong關(guān)鍵字慌闭,可能造成什么問題?》 以及上文《怎么用 copy 關(guān)鍵字躯舔?》也有論述驴剔。
比如下面的代碼就會發(fā)生崩潰
// .h文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 下面的代碼就會發(fā)生崩潰
@property (nonatomic, copy) NSMutableArray *mutableArray;
// .m文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 下面的代碼就會發(fā)生崩潰
NSMutableArray *array = [NSMutableArray arrayWithObjects:@1,@2,nil];
self.mutableArray = array;
[self.mutableArray removeObjectAtIndex:0];
接下來就會奔潰:
-[__NSArrayI removeObjectAtIndex:]: unrecognized selector sent to instance 0x7fcd1bc30460
第2條原因,如下:
該屬性使用了同步鎖粥庄,會在創(chuàng)建時生成一些額外的代碼用于幫助編寫多線程程序丧失,這會帶來性能問題,通過聲明 nonatomic 可以節(jié)省這些雖然很小但是不必要額外開銷惜互。
在默認(rèn)情況下布讹,由編譯器所合成的方法會通過鎖定機(jī)制確保其原子性(atomicity)。如果屬性具備 nonatomic 特質(zhì)训堆,則不使用同步鎖描验。請注意,盡管沒有名為“atomic”的特質(zhì)(如果某屬性不具備 nonatomic 特質(zhì)坑鱼,那它就是“原子的”(atomic))膘流。
在iOS開發(fā)中,你會發(fā)現(xiàn),幾乎所有屬性都聲明為 nonatomic呼股。
一般情況下并不要求屬性必須是“原子的”耕魄,因?yàn)檫@并不能保證“線程安全” ( thread safety),若要實(shí)現(xiàn)“線程安全”的操作彭谁,還需采用更為深層的鎖定機(jī)制才行吸奴。例如,一個線程在連續(xù)多次讀取某屬性值的過程中有別的線程在同時改寫該值马靠,那么即便將屬性聲明為 atomic奄抽,也還是會讀到不同的屬性值。
因此甩鳄,開發(fā)iOS程序時一般都會使用 nonatomic 屬性逞度。但是在開發(fā) Mac OS X 程序時,使用
atomic 屬性通常都不會有性能瓶頸妙啃。
5. 如何讓自己的類用 copy 修飾符档泽?如何重寫帶 copy 關(guān)鍵字的 setter?
若想令自己所寫的對象具有拷貝功能揖赴,則需實(shí)現(xiàn) NSCopying 協(xié)議馆匿。如果自定義的對象分為可變版本與不可變版本,那么就要同時實(shí)現(xiàn)
NSCopying
與NSMutableCopying
協(xié)議燥滑。
具體步驟:
- 需聲明該類遵從 NSCopying 協(xié)議
- 實(shí)現(xiàn) NSCopying 協(xié)議渐北。該協(xié)議只有一個方法:
- (id)copyWithZone:(NSZone *)zone;
注意:一提到讓自己的類用 copy 修飾符,我們總是想覆寫copy方法铭拧,其實(shí)真正需要實(shí)現(xiàn)的卻是 “copyWithZone” 方法赃蛛。
以第一題的代碼為例:
// .h文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 修改完的代碼
typedef NS_ENUM(NSInteger, CYLSex) {
CYLSexMan,
CYLSexWoman
};
@interface CYLUser : NSObject<NSCopying>
@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
然后實(shí)現(xiàn)協(xié)議中規(guī)定的方法:
- (id)copyWithZone:(NSZone *)zone {
CYLUser *copy = [[[self class] allocWithZone:zone]
initWithName:_name
age:_age
sex:_sex];
return copy;
}
但在實(shí)際的項(xiàng)目中,不可能這么簡單搀菩,遇到更復(fù)雜一點(diǎn)呕臂,比如類對象中的數(shù)據(jù)結(jié)構(gòu)可能并未在初始化方法中設(shè)置好,需要另行設(shè)置肪跋。舉個例子歧蒋,假如 CYLUser 中含有一個數(shù)組,與其他 CYLUser 對象建立或解除朋友關(guān)系的那些方法都需要操作這個數(shù)組州既。那么在這種情況下谜洽,你得把這個包含朋友對象的數(shù)組也一并拷貝過來。下面列出了實(shí)現(xiàn)此功能所需的全部代碼:
// .h文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 以第一題《風(fēng)格糾錯題》里的代碼為例
typedef NS_ENUM(NSInteger, CYLSex) {
CYLSexMan,
CYLSexWoman
};
@interface CYLUser : NSObject<NSCopying>
@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;
- (void)addFriend:(CYLUser *)user;
- (void)removeFriend:(CYLUser *)user;
@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:person];
}
- (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
以上做法能滿足基本的需求吴叶,但是也有缺陷:
如果你所寫的對象需要深拷貝褥琐,那么可考慮新增一個專門執(zhí)行深拷貝的方法称龙。
【注:深淺拷貝的概念疹娶,在下文中有介紹,詳見下文的:用@property聲明的 NSString(或NSArray鞋仍,NSDictionary)經(jīng)常使用 copy 關(guān)鍵字,為什么磕洪?如果改用 strong 關(guān)鍵字吭练,可能造成什么問題?】
在例子中析显,存放朋友對象的 set 是用 “copyWithZone:” 方法來拷貝的鲫咽,這種淺拷貝方式不會逐個復(fù)制 set 中的元素。若需要深拷貝的話谷异,則可像下面這樣分尸,編寫一個專供深拷貝所用的方法:
- (id)deepCopy {
CYLUser *copy = [[[self class] allocWithZone:zone]
initWithName:_name
age:_age
sex:_sex];
copy->_friends = [[NSMutableSet alloc] initWithSet:_friends
copyItems:YES];
return copy;
}
至于如何重寫帶 copy 關(guān)鍵字的 setter這個問題,
如果拋開本例來回答的話歹嘹,如下:
- (void)setName:(NSString *)name {
//[_name release];
_name = [name copy];
}
不過也有爭議箩绍,有人說“蘋果如果像下面這樣干,是不是效率會高一些尺上?”
- (void)setName:(NSString *)name {
if (_name != name) {
//[_name release];//MRC
_name = [name copy];
}
}
這樣真得高效嗎材蛛?不見得!這種寫法“看上去很美怎抛、很合理”卑吭,但在實(shí)際開發(fā)中,它更像下圖里的做法:
克強(qiáng)總理這樣評價(jià)你的代碼風(fēng)格:
我和總理的意見基本一致:
老百姓 copy 一下马绝,咋就這么難豆赏?
你可能會說:
之所以在這里做if判斷
這個操作:是因?yàn)橐粋€ if 可能避免一個耗時的copy,還是很劃算的富稻。
(在剛剛講的:《如何讓自己的類用 copy 修飾符河绽?》里的那種復(fù)雜的copy,我們可以稱之為 “耗時的copy”唉窃,但是對 NSString 的 copy 還稱不上。)
但是你有沒有考慮過代價(jià):
你每次調(diào)用
setX:
都會做 if 判斷纹笼,這會讓setX:
變慢纹份,如果你在setX:
寫了一串復(fù)雜的if+elseif+elseif+...
判斷,將會更慢廷痘。
要回答“哪個效率會高一些蔓涧?”這個問題,不能脫離實(shí)際開發(fā)笋额,就算 copy 操作十分耗時元暴,if 判斷也不見得一定會更快,除非你把一個“ @property他當(dāng)前的值 ”賦給了他自己兄猩,代碼看起來就像:
[a setX:x1];
[a setX:x1]; //你確定你要這么干茉盏?與其在setter中判斷鉴未,為什么不把代碼寫好?
或者
[a setX:[a x]]; //隊(duì)友咆哮道:你在干嘛鸠姨?M选!
不要在 setter 里進(jìn)行像
if(_obj != newObj)
這樣的判斷讶迁。(該觀點(diǎn)參考鏈接: How To Write Cocoa Object Setters: Principle 3: Only Optimize After You Measure
)
什么情況會在 copy setter 里做 if 判斷连茧?
例如,車速可能就有最高速的限制巍糯,車速也不可能出現(xiàn)負(fù)值啸驯,如果車子的最高速為300,則 setter 的方法就要改寫成這樣:
-(void)setSpeed:(int)_speed{
if(_speed < 0) speed = 0;
if(_speed > 300) speed = 300;
_speed = speed;
}
回到這個題目祟峦,如果單單就上文的代碼而言罚斗,我們不需要也不能重寫 name 的 setter :由于是 name 是只讀屬性,所以編譯器不會為其創(chuàng)建對應(yīng)的“設(shè)置方法”搀愧,用初始化方法設(shè)置好屬性值之后惰聂,就不能再改變了。( 在本例中咱筛,之所以還要聲明屬性的“內(nèi)存管理語義”--copy搓幌,是因?yàn)椋喝绻粚?copy,該類的調(diào)用者就不知道初始化方法里會拷貝這些屬性迅箩,他們有可能會在調(diào)用初始化方法之前自行拷貝屬性值溉愁。這種操作多余而低效)。
那如何確保 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;
}
6. @property 的本質(zhì)是什么拐揭?ivar、getter奕塑、setter 是如何生成并添加到這個類中的
@property 的本質(zhì)是什么堂污?
@property = ivar + getter + setter;
下面解釋下:
“屬性” (property)有兩大概念:ivar(實(shí)例變量)、存取方法(access method = getter + setter)龄砰。
“屬性” (property)作為 Objective-C 的一項(xiàng)特性盟猖,主要的作用就在于封裝對象中的數(shù)據(jù)。 Objective-C 對象通常會把其所需要的數(shù)據(jù)保存為各種實(shí)例變量换棚。實(shí)例變量一般通過“存取方法”(access method)來訪問式镐。其中,“獲取方法” (getter)用于讀取變量值固蚤,而“設(shè)置方法” (setter)用于寫入變量值娘汞。這個概念已經(jīng)定型,并且經(jīng)由“屬性”這一特性而成為 Objective-C 2.0
的一部分夕玩。
而在正規(guī)的 Objective-C 編碼風(fēng)格中你弦,存取方法有著嚴(yán)格的命名規(guī)范惊豺。
正因?yàn)橛辛诉@種嚴(yán)格的命名規(guī)范,所以 Objective-C 這門語言才能根據(jù)名稱自動創(chuàng)建出存取方法鳖目。其實(shí)也可以把屬性當(dāng)做一種關(guān)鍵字扮叨,其表示:
編譯器會自動寫出一套存取方法,用以訪問給定類型中具有給定名稱的變量领迈。
所以你也可以這么說:
@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)。需要強(qiáng)調(diào)的是尘喝,這個過程由編譯
器在編譯期執(zhí)行磁浇,所以編輯器里看不到這些“合成方法”(synthesized method)的源代碼。除了生成方法代碼 getter朽褪、setter 之外置吓,編譯器還要自動向類中添加適當(dāng)類型的實(shí)例變量,并且在屬性名前面加下劃線缔赠,以此作為實(shí)例變量的名字衍锚。在前例中,會生成兩個實(shí)例變量嗤堰,其名稱分別為
_firstName
與 _lastName
戴质。也可以在類的實(shí)現(xiàn)代碼里通過
@synthesize
語法來指定實(shí)例變量的名字.
@implementation Person
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end
我為了搞清屬性是怎么實(shí)現(xiàn)的,曾經(jīng)反編譯過相關(guān)的代碼,他大致生成了五個東西
-
OBJC_IVAR_$類名$屬性名稱
:該屬性的“偏移量” (offset),這個偏移量是“硬編碼” (hardcode)踢匣,表示該變量距離存放對象的內(nèi)存區(qū)域的起始地址有多遠(yuǎn)告匠。 - setter 與 getter 方法對應(yīng)的實(shí)現(xiàn)函數(shù)
-
ivar_list
:成員變量列表 -
method_list
:方法列表 -
prop_list
:屬性列表
也就是說我們每次在增加一個屬性,系統(tǒng)都會在 ivar_list
中添加一個成員變量的描述,在 method_list
中增加 setter 與 getter 方法的描述,在屬性列表中增加一個屬性的描述,然后計(jì)算該屬性在對象中的偏移量,然后給出 setter 與 getter 方法對應(yīng)的實(shí)現(xiàn),在 setter 方法中從偏移量的位置開始賦值,在 getter 方法中從偏移量開始取值,為了能夠讀取正確字節(jié)數(shù),系統(tǒng)對象偏移量的指針類型進(jìn)行了類型強(qiáng)轉(zhuǎn).
7. @protocol 和 category 中如何使用 @property
在 protocol 中使用 property 只會生成 setter 和 getter 方法聲明,我們使用屬性的目的,是希望遵守我協(xié)議的對象能實(shí)現(xiàn)該屬性
category 使用 @property 也是只會生成 setter 和 getter 方法的聲明,如果我們真的需要給 category 增加屬性的實(shí)現(xiàn),需要借助于運(yùn)行時的兩個函數(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è)置方法既不保留新值,也不釋放舊值输莺。此特質(zhì)同 assign 類似戚哎, 然而在屬性所指的對象遭到摧毀時,屬性值也會清空(nil out)模闲。
那么 runtime 如何實(shí)現(xiàn) weak 變量的自動置nil?
runtime 對注冊的類崭捍, 會進(jìn)行布局尸折,對于 weak 對象會放入一個 hash 表中。 用 weak 指向的對象內(nèi)存地址作為 key殷蛇,當(dāng)此對象的引用計(jì)數(shù)為0的時候會 dealloc实夹,假如 weak 指向的對象內(nèi)存地址是a橄浓,那么就會以a為鍵, 在這個 weak 表中搜索亮航,找到所有以a為鍵的 weak 對象荸实,從而設(shè)置為 nil。
(注:在下文的《使用runtime Associate方法關(guān)聯(lián)的對象缴淋,需要在主對象dealloc的時候釋放么准给?》里給出的“對象的內(nèi)存銷毀時間表”也提到__weak
引用的解除時間。)
我們可以設(shè)計(jì)一個函數(shù)(偽代碼)來表示上述機(jī)制:
objc_storeWeak(&a, b)
函數(shù):
objc_storeWeak
函數(shù)把第二個參數(shù)--賦值對象(b)的內(nèi)存地址作為鍵值key重抖,將第一個參數(shù)--weak修飾的屬性變量(a)的內(nèi)存地址(&a)作為value露氮,注冊到 weak 表中。如果第二個參數(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時,a和b指向同一個內(nèi)存地址畜埋,在b變nil時莫绣,a變nil。此時向a發(fā)送消息不會崩潰:在Objective-C中向nil發(fā)送消息是安全的由捎。
而如果a是由 assign 修飾的兔综,則:
在 b 非 nil 時,a 和 b 指向同一個內(nèi)存地址狞玛,在 b 變 nil 時软驰,a 還是指向該內(nèi)存地址,變野指針心肪。此時向 a 發(fā)送消息極易崩潰锭亏。
下面我們將基于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);
下面對用到的兩個方法objc_initWeak
和objc_destroyWeak
做下解釋:
總體說來慧瘤,作用是:
通過objc_initWeak
函數(shù)初始化“附有weak修飾符的變量(obj1)”,在變量作用域結(jié)束時通過objc_destoryWeak
函數(shù)釋放該變量(obj1)固该。
下面分別介紹下方法的內(nèi)部實(shí)現(xiàn):
objc_initWeak
函數(shù)的實(shí)現(xiàn)是這樣的:在將“附有weak修飾符的變量(obj1)”初始化為0(nil)后锅减,會將“賦值對象”(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ù)把第二個參數(shù)--賦值對象(obj)的內(nèi)存地址作為鍵值,將第一個參數(shù)--weak修飾的屬性變量(obj1)的內(nèi)存地址注冊到 weak 表中剿骨。如果第二個參數(shù)(obj)為0(nil)代芜,那么把變量(obj1)的地址從 weak 表中刪除,在后面的相關(guān)一題會詳解浓利。
使用偽代碼是為了方便理解挤庇,下面我們“真槍實(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;
}];
}
也就是有兩個步驟:
- 在setter方法中做如下設(shè)置:
objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
- 在屬性所指的對象遭到摧毀時罚随,屬性值也會清空(nil out)。做到這點(diǎn)羽资,同樣要借助 runtime:
//要銷毀的目標(biāo)對象
id objectToBeDeallocated;
//可以理解為一個“事件”:當(dāng)上面的目標(biāo)對象銷毀時淘菩,同時要發(fā)生的“事件”。
id objectWeWantToBeReleasedWhenThatHappens;
objc_setAssociatedObject(objectToBeDeallocted,
someUniqueKey,
objectWeWantToBeReleasedWhenThatHappens,
OBJC_ASSOCIATION_RETAIN);
知道了思路屠升,我們就開始實(shí)現(xiàn) cyl_runAtDealloc
方法潮改,實(shí)現(xiàn)過程分兩部分:
第一部分:創(chuàng)建一個類,可以理解為一個“事件”:當(dāng)目標(biāo)對象銷毀時腹暖,同時要發(fā)生的“事件”汇在。借助 block 執(zhí)行“事件”。
// .h文件
// .h文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 這個類脏答,可以理解為一個“事件”:當(dāng)目標(biāo)對象銷毀時糕殉,同時要發(fā)生的“事件”。借助block執(zhí)行“事件”殖告。
typedef void (^voidBlock)(void);
@interface CYLBlockExecutor : NSObject
- (id)initWithBlock:(voidBlock)block;
@end
// .m文件
// .m文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 這個類阿蝶,可以理解為一個“事件”:當(dāng)目標(biāo)對象銷毀時,同時要發(fā)生的“事件”黄绩。借助block執(zhí)行“事件”羡洁。
#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實(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)情況下,由編譯器合成的方法會通過鎖定機(jī)制確保其原子性(atomicity)粤蝎。如果屬性具備 nonatomic 特質(zhì)真仲,則不使用同步鎖。請注意初澎,盡管沒有名為“atomic”的特質(zhì)(如果某屬性不具備 nonatomic 特質(zhì)秸应,那它就是“原子的” ( atomic) ),但是仍然可以在屬性特質(zhì)中寫明這一點(diǎn),編譯器不會報(bào)錯灸眼。若是自己定義存取方法,那么就應(yīng)該遵從與屬性特質(zhì)相符的原子性墓懂。
讀/寫權(quán)限---
readwrite(讀寫)
焰宣、readonly (只讀)
內(nèi)存管理語義---
assign
、strong
捕仔、weak
匕积、unsafe_unretained
、copy
方法名---
getter=<name>
榜跌、setter=<name>
getter=<name>
的樣式:
@property (nonatomic, getter=isOn) BOOL on;
( setter=<name>
這種不常用闪唆,也不推薦使用。故不在這里給出寫法钓葫。)
- 不常用的:
nonnull
,null_resettable
,nullable
10. weak屬性需要在dealloc中置nil么悄蕾?
不需要。
在ARC環(huán)境無論是強(qiáng)指針還是弱指針都無需在 dealloc 設(shè)置為 nil 础浮, ARC 會自動幫我們處理
即便是編譯器不幫我們做這些帆调,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;
}];
}
也即:
在屬性所指的對象遭到摧毀時豆同,屬性值也會清空(nil out)番刊。
11. @synthesize和@dynamic分別有什么作用?
- @property有兩個對應(yīng)的詞影锈,一個是 @synthesize芹务,一個是 @dynamic。如果 @synthesize和 @dynamic都沒寫鸭廷,那么默認(rèn)的就是
@syntheszie var = _var;
- @synthesize 的語義是如果你沒有手動實(shí)現(xiàn) setter 方法和 getter 方法枣抱,那么編譯器會自動為你加上這兩個方法。
- @dynamic 告訴編譯器:屬性的 setter 與 getter 方法由用戶自己實(shí)現(xiàn)靴姿,不自動生成沃但。(當(dāng)然對于 readonly 的屬性只需提供 getter 即可)。假如一個屬性被聲明為 @dynamic var佛吓,然后你沒有提供 @setter方法和 @getter 方法宵晚,編譯的時候沒問題,但是當(dāng)程序運(yùn)行到
instance.var = someVar
维雇,由于缺 setter 方法會導(dǎo)致程序崩潰淤刃;或者當(dāng)運(yùn)行到someVar = var
時,由于缺 getter 方法同樣會導(dǎo)致崩潰吱型。編譯時沒問題逸贾,運(yùn)行時才執(zhí)行相應(yīng)的方法,這就是所謂的動態(tài)綁定。
12. ARC下铝侵,不顯式指定任何屬性關(guān)鍵字時灼伤,默認(rèn)的關(guān)鍵字都有哪些?
- 對應(yīng)基本數(shù)據(jù)類型默認(rèn)關(guān)鍵字是
atomic,readwrite,assign
- 對于普通的 Objective-C 對象
atomic,readwrite,strong
參考鏈接: