Runtime原始碼解讀5(屬性)
2019-10-15
屬性(property)是為類的成員變數提供公開的訪問器。屬性與方法有非常緊密的聯絡,可讀寫的屬性有 getter 和 setter 兩個方法與之對應。
一、屬性概述
屬性(property)大多數情況是作為成員變數的訪問器(accessor)使用,為外部訪問成員變數提供介面。使用@property
宣告屬性時需要指定屬性的特性(attribute),包括:
- 讀寫特性(
readwrite
/readonly
); - 原子性(
atomic
/nonatomic
); - 記憶體管理特性(
assign
/strong
/weak
/copy
); - 是否可空(
nullable
/nonnull
);
注意:上面括號中的第一個值是屬性的預設特性,不過是否可空有其特殊性,可以通過
NS_ASSUME_NONNULL_BEGIN
/NS_ASSUME_NONNULL_END
巨集包圍屬性的宣告語句,將屬性的預設可空特性置為nonnull
。
除了上述特性還可以顯式指定getter、setter。屬性的特性指定了屬性作為訪問器的行為特徵。聲明瞭屬性只是意味著聲明瞭訪問器,此時的訪問器是沒有getter
和setter
的實現的,想要訪問器關聯特定的成員變數在程式碼上有兩種方式:1、使用@synthesize
修飾符合成屬性;2、實現屬性的 getter 和 setter。但是兩者的本質是一樣的,就是按屬性的特性實現屬性的 getter 和 setter。
注意:
@dynamic
修飾屬性,表示不合成屬性的 getter 和 setter。此時要麼在當前類或子類的實現中實現getter/setter、要麼在子類實現中用@synthesize
合成屬性。
二、資料結構
類的class_rw_t
中包含屬性列表property_array_t
類的properties
成員,property_array_t
為list_array_tt<property_t,property_list_t>
,因此類中的屬性列表儲存property_list_t
的陣列,同樣是個二維陣列,property_list_t
繼承自entsize_list_tt
順序表容器,元素型別是property_t
。相關資料結構如下,大部分在介紹成員變數和方法列表時有提及,因此不再贅述。從屬性相關的資料結構可知,類中儲存的屬性資訊只有屬性名、特性資訊。
屬性在類中的儲存與方法列表的儲存也非常相似。class_ro_t
property_list_t
型別的basePropertyList
僅儲存類定義時定義的基本屬性,這些屬性是編譯時決議的;class_rw_t
中的property_array_t
型別的properties
儲存類的完整屬性列表,包括類的基本屬性,以及執行時決議的 類的分類中定義的屬性以及執行時動態新增的屬性。
class property_array_t :
public list_array_tt<property_t,property_list_t>
{
typedef list_array_tt<property_t,property_list_t> Super;
public:
property_array_t duplicate() {
return Super::duplicate<property_array_t>();
}
};
struct property_list_t : entsize_list_tt<property_t,property_list_t,0> {
};
struct property_t {
const char *name;
const char *attributes;
};
複製程式碼
三、新增屬性的實現原理
新增屬性呼叫class_addProperty(...)
函式,注意到在原始碼中並沒有與方法列表相關的操作,推測屬性關聯方法列表的操作隱藏在@synthesize
、@dynamic
以及屬性的attributes
解析的實現中沒有公開。屬性新增與方法新增也基本一樣,是新增到class_rw_t
的完整屬性列表properties
的外層一位陣列容器的開頭,因此也滿足優先順序關係:執行時動態新增的屬性 > 類的分類定義的屬性 > 類定義時定義的基本屬性
。
// 新增屬性
BOOL
class_addProperty(Class cls,const char *name,const objc_property_attribute_t *attrs,unsigned int n)
{
return _class_addProperty(cls,name,attrs,n,NO);
}
// 新增屬性、替換屬性的實現邏輯
static bool
_class_addProperty(Class cls,unsigned int count,bool replace)
{
if (!cls) return NO;
if (!name) return NO;
// 根據名稱獲取類的屬性
property_t *prop = class_getProperty(cls,name);
if (prop && !replace) {
// 已存在且不是指定替換屬性
return NO;
}
else if (prop) {
// 替換屬性
rwlock_writer_t lock(runtimeLock);
try_free(prop->attributes);
prop->attributes = copyPropertyAttributeString(attrs,count);
return YES;
}
else {
rwlock_writer_t lock(runtimeLock);
assert(cls->isRealized());
property_list_t *proplist = (property_list_t *)
malloc(sizeof(*proplist));
proplist->count = 1;
proplist->entsizeAndFlags = sizeof(proplist->first);
proplist->first.name = strdup(name);
proplist->first.attributes = copyPropertyAttributeString(attrs,count);
cls->data()->properties.attachLists(&proplist,1);
return YES;
}
}
objc_property_t class_getProperty(Class cls,const char *name)
{
if (!cls || !name) return nil;
mutex_locker_t lock(runtimeLock);
checkIsKnownClass(cls);
assert(cls->isRealized());
for ( ; cls; cls = cls->superclass) {
for (auto& prop : cls->data()->properties) {
if (0 == strcmp(name,prop.name)) {
return (objc_property_t)∝
}
}
}
return nil;
}
複製程式碼
四、訪問屬性的實現原理
存取屬性值的程式碼集中在objc-accessors.mm
原始檔中。
4.1 獲取物件屬性值
呼叫objc_getProperty_gc(...)
獲取物件的屬性值,實際上只是按一定的方式訪問了屬性對應的成員變數空間。若屬性為atomic
則會在獲取屬性值的程式碼兩頭新增spinlock_t
的加鎖解鎖程式碼,這也是atomic
和nonatomic
的區別所在。
注意:實際上,第三節 介紹的動態新增屬性對應用開發者並沒有什麼用處(對 runtime 本身當然是有用的),原因是:1、沒有指定屬性關聯成員變數的 runtime API;2、可以通過定義函式關聯物件模擬屬性,此時動態新增的屬性就成了雞肋,可有可無。
#if SUPPORT_GC
id objc_getProperty_gc(id self,SEL _cmd,ptrdiff_t offset,BOOL atomic) {
return *(id*) ((char*)self + offset);
}
#else
id
objc_getProperty(id self,BOOL atomic)
{
return objc_getProperty_non_gc(self,_cmd,offset,atomic);
}
#endif
id objc_getProperty_non_gc(id self,BOOL atomic) {
if (offset == 0) {
return object_getClass(self);
}
// Retain release world
id *slot = (id*) ((char*)self + offset);
if (!atomic) return *slot;
// Atomic retain release world
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
// for performance,we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value);
}
複製程式碼
獲取屬性值對copy
型別沒有做相關處理,也就是說**copy
屬性的getter返回的也是屬性指向物件本身,copy屬性的getter並不包含拷貝操作**。用以下程式碼可以驗證,在標記處打斷點執行。檢視testObj
的記憶體,第8-16個位元組儲存的就是testObj
的arr
屬性所指向的實際的NSArray
地址。會發現打印出來的testObj.arr
地址和testObj
的記憶體的第8-16個位元組內容是一致的。
@interface TestPropCopy: NSObject
@property(copy,nonatomic) NSArray* arr; // 不要用NSString型別,除錯時不好看地址
@end
@implementation TestPropCopy
+(void)testPropCopy{
NSArray* arr = [NSArray arrayWithObject:@"app"];
TestPropCopy* testObj = [[self alloc] init];
testObj.arr = arr;
NSLog(@"testObj:%@",testObj);
NSLog(@"arr:%@",arr);
NSLog(@"testObj.arr:%@",testObj.arr);
// 此處打斷點
}
@end
複製程式碼
注意:
spinlock_t
的本質是os_lock_handoff_s
鎖,關於這個鎖網上找不到什麼資料,推測是個互斥鎖。注意spinlock_t
並不是OSSpinLock
。OSSpinLock
已知存在效能問題,已經被棄用。
4.2 修改物件屬性值
呼叫objc_setProperty(...)
設定物件的屬性值,同樣是是按一定的方式訪問屬性對應的成員變數空間。同樣,若屬性為atomic
則會在設定屬性值的程式碼兩頭新增spinlock_t
的加鎖解鎖程式碼。若屬性為copy
時,則將傳入 setter 的引數指向的物件 拷貝到對應的成員變數空間。
#if SUPPORT_GC
void objc_setProperty_gc(id self,id newValue,BOOL atomic,signed char shouldCopy) {
if (shouldCopy) {
newValue = (shouldCopy == MUTABLE_COPY ? [newValue mutableCopyWithZone:nil] : [newValue copyWithZone:nil]);
}
objc_assign_ivar(newValue,self,offset);
}
#else
void
objc_setProperty(id self,signed char shouldCopy)
{
objc_setProperty_non_gc(self,newValue,atomic,shouldCopy);
}
#endif
void objc_setProperty_non_gc(id self,signed char shouldCopy)
{
bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
bool mutableCopy = (shouldCopy == MUTABLE_COPY);
reallySetProperty(self,copy,mutableCopy);
}
// 設定屬性值的總入口
static inline void reallySetProperty(id self,bool atomic,bool copy,bool mutableCopy)
{
if (offset == 0) {
object_setClass(self,newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
// 若屬性為copy,則將newVal引數指向的物件拷貝到對應成員變數空間
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
// 是否原子性判斷
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
複製程式碼
注意:存取屬性值的實現是直接呼叫屬性的getter、setter的對應的方法的
SEL
觸發的,屬性與方法的關聯細節則沒有公佈原始碼。
4.3 探討屬性關聯成員變數的實現
Objective-C 程式碼中,屬性關聯成員變數是通過@synthesize
實現的,runtime 並沒有公開這塊程式碼。本節探討@synthesize
的實現原理。
類定義屬性且不手動定義屬性的 getter 和 setter 方法時,類的方法列表會新增對應的prop
方法及setProp
方法。且object_getProperty(...)
的引數列表包含self
和_cmd
,和訊息的格式很類似。因此 推測 宣告屬性@synthesize
時,runtime 會根據屬性的attributes
生成屬性的 getter 方法SEL
、IMP
和 setter 方法SEL
、IMP
,並將其新增到類的方法列表中,getter 和 setter 的IMP
虛擬碼如下,其中#
號包圍的時編譯時可確定的引數;
id propGetter(id self,SEL _cmd) {
char* ivarName = synthizeName ? : ( '_' + #propertyName#)
Class selfClass = object_getClass(self)
Ivar ivar = getIvar(Class cls,ivarName)
uint_32 ivarOffset = ivar_getOffset(ivar)
objc_getProperty(self,#propertyNameAttr#,
ivar,#propertyAtomicAttr#)
}
void propSetter(id self,id newVal) {
char* ivarName = #synthesizeName# ? : ( '_' + #propertyName#)
Class selfClass = object_getClass(self)
Ivar ivar = getIvar(Class cls,ivarName)
uint_32 ivarOffset = ivar_getOffset(ivar)
objc_setProperty(self,
newVal
ivar,#propertyAtomicAttr#,
#propertyShouldCopyAttr#)
}
複製程式碼
但是上面的處理是有明顯的效能缺陷的,每次訪問成員變數是都要呼叫getIvar(...)
,而getIvar(...)
是遍歷類的整個成員變數列表,根據成員變數名查詢成員變數,實際實現顯然不應如此。因此上述程式碼只是模擬了屬性的實現流程,而具體實現細節將在後續將在獨立文章中介紹。
五、總結
-
Runtime 提供的關於屬性的動態特性對應用開發的意義不大,runtime 屬性關聯成員變數是隱藏在
@synthesize
的實現程式碼中,而且成員變數不能動態新增,因此即使提供也是意義不大; -
動態新增屬性時,是不包含新增屬性 getter 和 setter 方法的操作的,因此必須手動實現其getter 和 setter 方法,為分類定義屬性也是不包含 getter 和 setter 方法實現,開發者可以;
-
下一篇文章介紹分類。