來自我的個人博客Minecode.link
在使用Objective-C時,頻繁用到屬性關(guān)鍵字。我們應(yīng)該理解每種屬性的意義,并了解一些偏底層的實(shí)現(xiàn),故在此對OC的屬性關(guān)鍵字做個淺析。
基礎(chǔ)概念:ivar、getter、setter
在C語言中,我們通常是直接操作成員變量。而在Objective-C中,使用了“屬性”這一概念來封裝對象中的數(shù)據(jù),OC對象會把需要的數(shù)據(jù)保存為各種實(shí)例變量,同時通過“存取方法”(Access Method)來進(jìn)行訪問,也就是常說的getter和setter。
所以,ivar是對象的各種實(shí)例變量,getter用于獲取變量的值,setter用于寫入變量的值。
我們來看一個標(biāo)準(zhǔn)的ivar+getter+setter
樣板代碼:
@interface Person: NSObject
{
// ivar聲明
@private
NSString *_myName;
}
// getter方法
- (NSString *)myName {
return _myName;
}
// setter方法
- (void)setMyName:(NSString *)newName {
_myName = newName;
}
可以看到,這樣的組合方式造成了代碼的臃腫,大大降低了開發(fā)效率和可讀性,實(shí)際開發(fā)中使用ivar+getter+setter
的情況并不常見,這就要引入@synthesize
和@property
這個關(guān)鍵字。
@synthesize
這個屬性已經(jīng)很少見到了,它是屬于MRC和32bit時代的產(chǎn)物。@synthesize
屬性用來合成一個屬性,變量名如果沒有顯式聲明則默認(rèn)添加一個下劃線的前綴(_變量名
)。當(dāng)然也可以手動聲明變量名并建立與@property
的關(guān)系。
為了加深理解,我們看一下以下代碼,它的邏輯為:手動聲明ivar,使用property聲明存取方法,使用@synthesize建立ivar和property的關(guān)系
。
@interface SubClass ()
{
// 聲明ivar
NSString *_myName;
}
// 聲明屬性(并合成getter+setter)
@property (nonatomic, copy) NSString* myName;
@end
@implementation SubClass
// 建立myName屬性與_myName成員變量的關(guān)系
@synthesize myName = _myName;
@end
可以看出@synthesize和@property各自負(fù)責(zé)的工作,雖然這些工作已經(jīng)由編譯器幫我們做了,但是理解這一概念還是很重要的。
現(xiàn)在我們知道了省略@synthesize聲明實(shí)際上是因?yàn)長LVM的Clang為在ARC模式下會自動生成@synthesize聲明,但是這僅限于64位OC運(yùn)行時中,當(dāng)使用32位系統(tǒng)時,我們必須要手動聲明,否則會報錯。我們可以設(shè)置NS_BUILD_32_LIKE_64
宏來解決這個問題。
@dynamic
相對于@synthesize,@dynamic告訴編譯器該屬性的getter和setter由程序員自行實(shí)現(xiàn),編譯器不再自動生成。在運(yùn)行時執(zhí)行過程中如果找不到對應(yīng)存取方法,則會報錯。這便是Runtime中的動態(tài)綁定。
同時,使用了@dynamic修飾則必須動態(tài)生成方法實(shí)現(xiàn),沒有@dynamic myName = _myName;
的語法,也就是說我們沒有辦法靜態(tài)的建立getter/setter并訪問下劃線前綴的ivar。對應(yīng)的解決方法是消息轉(zhuǎn)發(fā)和動態(tài)方法解析,本文不過多討論。
@property
本質(zhì)上來說,@property
實(shí)際上是告知編譯器為你的ivar生成getter和setter,并不生成ivar,要理解這一點(diǎn)。但是由于@synthesize
無須再手動聲明,所以我們使用@property
后實(shí)際上是聲明了ivar+getter+setter
的標(biāo)準(zhǔn)模板。
Runtime下的定義
我們首先反編譯為cpp代碼,有關(guān)反編譯的內(nèi)容請見Objective-C開發(fā)中Clang的使用。
可以發(fā)現(xiàn)property在OC運(yùn)行時中是objc_property_t
類型的,定義如下:
typedef struct objc_property *objc_property_t;
struct property_t {
const char *name;
const char *attributes;
};
property結(jié)構(gòu)體有name和attributes兩個成員變量,而attributes則是property的屬性定義,我們看一下它的定義:
/// Defines a property attribute
typedef struct {
const char *name; /**< The name of the attribute */
const char *value; /**< The value of the attribute (usually empty) */
} objc_property_attribute_t;
我們可以通過以下方法獲取對應(yīng)變量:
// 獲取所有屬性列表
class_copyPropertyList
// 獲取屬性名
property_getName
// 獲取屬性描述字符串
property_getAttributes
// 獲取所有屬性列表
property_copyAttributeList
可以看到,每一個attribute對應(yīng)一種屬性修飾符,property所定義的屬性就包含其中。對應(yīng)關(guān)系如下
屬性修飾符類型 | name | value |
---|---|---|
屬性類型 | T | 屬性類型名 |
內(nèi)存管理 | C(copy) &(strong/retain) W(weak) R(readonly) | 空 |
自定義getter/setter | G(getter) S(setter) | 方法名 |
原子/非原子類型 | N(nonatomic) 空(atomic) | 空 |
ivar名稱 | V | 變量名稱 |
比如我們分別定義一個對象類型、標(biāo)量、以及id類型的屬性來看一下
屬性定義 | attributes描述 |
---|---|
@property char charDefault; | Tc,V_charDefault |
@property (nonatomic, copy) NSString *myString; | T@"NSString",C,N,V_myString |
@property(nonatomic, readonly, retain) id idVar; | T@,R,&,V_idVar |
注意:注意描述符中的
V_ivar名稱
,此描述符是基于64bit系統(tǒng)的,因?yàn)闀詣雍铣蒳var,如果是32bit系統(tǒng)則不會有下劃線,前文已做解釋。
Runtime下的實(shí)現(xiàn)
了解了屬性在運(yùn)行時系統(tǒng)下的定義,我們現(xiàn)在探究一下其的實(shí)現(xiàn)。
運(yùn)行時中有ivar、method、class、object等概念,其中@property就涉及到了ivar和method(get方法和set方法),具體如何實(shí)現(xiàn)呢,我們通過反編譯來一探究竟。
在OC中,所有對象都可以認(rèn)為是id類型,id類型定義為下:
typedef struct objc_object {
Class isa;
} *id;
而id類型就是指向Class類型的指針,那么Class又是什么呢?
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
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;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
};
現(xiàn)在我們大致了解了OC中對象的實(shí)現(xiàn)原理。OC中所有對象都可以認(rèn)為是id類型,而id又是指向Class的指針,Class類型實(shí)際是objc_class結(jié)構(gòu)體,其定義了OC對象的基本信息。
更多Runtime的內(nèi)容在此不再贅述,我們來看一下屬性涉及到的類型:objc_ivar_list
和objc_method_list
:
struct objc_ivar {
char *ivar_name;
char *ivar_type;
int ivar_offset;
int space;
};
struct objc_ivar_list {
int ivar_count;
int space;
struct objc_ivar ivar_list[1];
}
在此我們看到了ivar的真面目,它包含了名稱、類型、基地址偏移、內(nèi)存空間。
同樣,objc_method_list
定義如下:
struct objc_method_list {
struct objc_method_list *obsolete;
int method_count;
#ifdef __LP64__
int space;
#endif
/* variable length structure */
struct objc_method method_list[1];
};
struct objc_method {
SEL method_name;
char *method_types; /* a string representing argument/return types */
IMP method_imp;
};
所以,當(dāng)在類中創(chuàng)建一個屬性時。Runtime做了以下事情:
- 創(chuàng)建該屬性,設(shè)置其objc_ivar,通過偏移量和內(nèi)存占用就可以方便獲取。
- 生成其getter和setter。詳情請查閱objc中方法的實(shí)現(xiàn)(SEL,IMP)。
- 將屬性的ivar添加到類的ivar_list中,作為類的成員變量存在。
- 將getter和setter加入類的method_list中。之后可以通過直接調(diào)用或者點(diǎn)語法來使用。
- 將屬性的描述添加到類的屬性描述列表中。
屬性的獲取
為了記錄屬性,有以下幾個變量:
ivar_list
: 記錄成員變量的描述
method_list
: 記錄該變量getter和setter的描述
prop_list
: 記錄屬性的描述
OBJC_IVAR_$類名_$屬性名稱
: 記錄屬性相對對象地址的偏移地址(重要)
其中,記錄變量的偏移地址很重要。我們來看一下實(shí)現(xiàn):
// 生成一個SubClass類型,包含一個屬性
@interface SubClass ()
@property (nonatomic, strong) NSMutableArray* array;
@end
// 在該類的實(shí)現(xiàn)中創(chuàng)建一個方法
@implementation SubClass
- (void)testArrayMethod {
self.array = [NSMutableArray array];
}
@end
反編譯代碼,我們查看一下它是如何賦值的:
// 屬性的定義
extern "C" unsigned long OBJC_IVAR_$_SubClass$_array;
struct SubClass_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSMutableArray *_array;
};
// 賦值(已經(jīng)去掉了復(fù)雜的類型轉(zhuǎn)換代碼)
static void _I_SubClass_testArrayMethod(SubClass * self, SEL _cmd) {
(objc_msgSend)(self, sel_registerName("setArray:"), (objc_getClass("NSMutableArray"), sel_registerName("array")));
}
// 屬性的setter方法
static void _I_SubClass_setArray_(SubClass * self, SEL _cmd, NSMutableArray *array) {
*(self + OBJC_IVAR_$_SubClass$_array) = array;
}
我們可以看到,屬性的偏移地址命名為OBJC_IVAR_$類名_$屬性名稱
,點(diǎn)語法本質(zhì)上是調(diào)用了setter,而setter中確定屬性對應(yīng)ivar的內(nèi)存地址則是通過 對象地址+偏移量 來尋址,即*(self + OBJC_IVAR_$_SubClass$_array)
。
@Property的屬性修飾符
談完@property的底層實(shí)現(xiàn),再看一下屬性修飾符。此處僅討論@property的屬性修飾符,對于ARC的所有權(quán)修飾符(__strong,__weak,__unsafe_unretained,__autorealesing
)會專門寫一篇文章討論。
屬性符作用及區(qū)別
屬性 | 內(nèi)容 |
---|---|
readwrite | 屬性可讀可寫,生成getter+setter,默認(rèn)屬性 |
readonly | 屬性只讀,只生成getter |
nonatomic | 非原子屬性,提高性能但線程不安全 |
atomic | 原子屬性,線程安全但可能降低性能 |
MRC模式下 | |
assign | 直接賦值,不增加引用計數(shù) |
retain | 持有對象,引用計數(shù)+1 |
copy | 生成并持有一個新對象,并深拷貝對象的值 |
ARC模式下 | |
strong | 強(qiáng)引用,持有對象,引用計數(shù)+1,相當(dāng)于MRC的retain |
weak | 弱引用,不持有對象,不增加引用計數(shù),相當(dāng)于MRC的assign,但在對象銷毀后會置為nil |
copy | 深拷貝,同MRC的copy |
unsafe_unretained | 無須內(nèi)存管理的對象,相當(dāng)于MRC的assign,對象銷毀后不會置nil,可能造成野指針。(iOS 4之后基本廢棄,使用assign替代) |
同時,根據(jù)LLVM文檔所述,ARC模式下依舊可以使用MRC修飾符,編譯器會自動轉(zhuǎn)換。assign
對應(yīng)unsafe_unretained
,retain
對應(yīng)strong
。
原子屬性atomic
原子屬性(atomic)通過加鎖來實(shí)現(xiàn)訪問/賦值的線程安全,但atomic只是保證了getter和setter的線程安全,并沒有保證整個對象是線程安全的。比如線程A在讀數(shù)據(jù),而線程BCD在寫數(shù)據(jù),雖然BCD并不能同時寫,但A讀到的數(shù)據(jù)卻是BCD某個時間寫入的,無法保證線程安全。同樣的,對于objectAtIndex:等非getter/setter方法,則不是線程安全的。
weak的使用場景及與assign的區(qū)別
首先,weak與assign都表示了一種“非持有關(guān)系”(nonowning relationship),也成弱引用,在使用時不會增加被引用變量的引用計數(shù)。而weak在引用的對象被銷毀后會被指向nil,保證了安全,相反assign不會被置nil,成為野指針。
其次,對于標(biāo)量(基礎(chǔ)數(shù)據(jù)類型:int,double,以及OC中使用宏定義的數(shù)據(jù)類型:CGFloat,NSInteger),只能使用assign。weak只能用于對象,assign可用于對象和標(biāo)量。
copy的使用場景及注意事項(xiàng)
使用copy修飾的對象在賦值的時候創(chuàng)建對象的副本,也成深拷貝。實(shí)際則是調(diào)用了copy方法。支持copy方法要遵守NSCopying協(xié)議,實(shí)現(xiàn)copyWithZone:方法來生成并持有對象的副本。同時,還有mutableCopy用于實(shí)現(xiàn)對于可變對象的深拷貝,如NSMutableArray。
當(dāng)我們想復(fù)制字符串的值而非直接引用該字符串時,我們就應(yīng)該深拷貝一份,否則會出現(xiàn)修改原對象值的情況。NSArray、NSDictionary,以及我們自己的類同理。
但是,對于@property的copy修飾符,只是調(diào)用了copy方法,所以只能生成不可變對象。對于如下代碼:
@property (nonatomic, copy) NSMutableArray *mutableArray;
/* ... */
NSMutableArray *anotherMutableArray = [NSMutableArray arrayWithObjects:@1,@2,nil];
self.mutableArray = anotherMutableArray;
[self.mutableArray removeObjectAtIndex:0];
會發(fā)生崩潰。原因在于copy生成了不可變對象,導(dǎo)致removeObjectAtIndex:方法報錯。
所以,對于可變對象,不要使用copy屬性修飾符,而是調(diào)用mutableCopy方法。