1. 程式人生 > IOS開發 >Runtime原始碼解讀5(屬性)

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。屬性的特性指定了屬性作為訪問器的行為特徵。聲明瞭屬性只是意味著聲明瞭訪問器,此時的訪問器是沒有gettersetter的實現的,想要訪問器關聯特定的成員變數在程式碼上有兩種方式:1、使用@synthesize修飾符合成屬性;2、實現屬性的 getter 和 setter。但是兩者的本質是一樣的,就是按屬性的特性實現屬性的 getter 和 setter。

注意:@dynamic修飾屬性,表示不合成屬性的 getter 和 setter。此時要麼在當前類或子類的實現中實現getter/setter、要麼在子類實現中用@synthesize

合成屬性。

二、資料結構

類的class_rw_t中包含屬性列表property_array_t類的properties成員,property_array_tlist_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)&prop;
            }
        }
    }
    
    return nil;
}
複製程式碼

四、訪問屬性的實現原理

存取屬性值的程式碼集中在objc-accessors.mm原始檔中。

4.1 獲取物件屬性值

呼叫objc_getProperty_gc(...)獲取物件的屬性值,實際上只是按一定的方式訪問了屬性對應的成員變數空間。若屬性為atomic則會在獲取屬性值的程式碼兩頭新增spinlock_t的加鎖解鎖程式碼,這也是atomicnonatomic的區別所在。

注意:實際上,第三節 介紹的動態新增屬性對應用開發者並沒有什麼用處(對 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個位元組儲存的就是testObjarr屬性所指向的實際的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並不是OSSpinLockOSSpinLock已知存在效能問題,已經被棄用。

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 方法SELIMP和 setter 方法SELIMP,並將其新增到類的方法列表中,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 方法實現,開發者可以;

  • 下一篇文章介紹分類。