探究Objective-C屬性關(guān)鍵字

來自我的個人博客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_listobjc_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做了以下事情:

  1. 創(chuàng)建該屬性,設(shè)置其objc_ivar,通過偏移量和內(nèi)存占用就可以方便獲取。
  2. 生成其getter和setter。詳情請查閱objc中方法的實(shí)現(xiàn)(SEL,IMP)。
  3. 將屬性的ivar添加到類的ivar_list中,作為類的成員變量存在。
  4. 將getter和setter加入類的method_list中。之后可以通過直接調(diào)用或者點(diǎn)語法來使用。
  5. 將屬性的描述添加到類的屬性描述列表中。

屬性的獲取

為了記錄屬性,有以下幾個變量:
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_unretainedretain對應(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方法

相關(guān)資料

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

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