Runtime源代碼解讀5(屬性)

2019-10-15數組

屬性(property)是爲類的成員變量提供公開的訪問器。屬性與方法有很是緊密的聯繫,可讀寫的屬性有 getter 和 setter 兩個方法與之對應。bash

1、屬性概述

屬性(property)大多數狀況是做爲成員變量的訪問器(accessor)使用,爲外部訪問成員變量提供接口。使用@property聲明屬性時須要指定屬性的特性(attribute),包括:數據結構

  • 讀寫特性(readwrite/readonly);
  • 原子性(atomic/nonatomic);
  • 內存管理特性(assign/strong/weak/copy);
  • 是否可空(nullable/nonnull);

注意:上面括號中的第一個值是屬性的默認特性,不過是否可空有其特殊性,能夠經過NS_ASSUME_NONNULL_BEGIN/NS_ASSUME_NONNULL_END宏包圍屬性的聲明語句,將屬性的默承認空特性置爲nonnullapp

除了上述特性還能夠顯式指定getter、setter。屬性的特性指定了屬性做爲訪問器的行爲特徵。聲明瞭屬性只是意味着聲明瞭訪問器,此時的訪問器是沒有gettersetter的實現的,想要訪問器關聯特定的成員變量在代碼上有兩種方式:一、使用@synthesize修飾符合成屬性;二、實現屬性的 getter 和 setter。可是二者的本質是同樣的,就是按屬性的特性實現屬性的 getter 和 setter。函數

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

2、數據結構

類的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。相關數據結構以下,大部分在介紹成員變量和方法列表時有說起,所以再也不贅述。從屬性相關的數據結構可知,類中保存的屬性信息只有屬性名、特性信息。ui

屬性在類中的保存與方法列表的保存也很是類似。class_ro_t中的property_list_t類型的basePropertyList僅保存類定義時定義的基本屬性,這些屬性是編譯時決議的;class_rw_t中的property_array_t類型的properties保存類的完整屬性列表,包括類的基本屬性,以及運行時決議的 類的分類中定義的屬性以及運行時動態添加的屬性。atom

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;
};
複製代碼

3、添加屬性的實現原理

添加屬性調用class_addProperty(...)函數,注意到在源代碼中並無與方法列表相關的操做,推測屬性關聯方法列表的操做隱藏在@synthesize@dynamic以及屬性的attributes解析的實現中沒有公開。屬性添加與方法添加也基本同樣,是添加到class_rw_t的完整屬性列表properties的外層一位數組容器的開頭,所以也知足優先級關係:運行時動態添加的屬性 > 類的分類定義的屬性 > 類定義時定義的基本屬性spa

// 添加屬性
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, const char *name, 
                   const objc_property_attribute_t *attrs, 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;
}
複製代碼

4、訪問屬性的實現原理

存取屬性值的代碼集中在objc-accessors.mm源文件中。調試

4.1 獲取對象屬性值

調用objc_getProperty_gc(...)獲取對象的屬性值,實際上只是按必定的方式訪問了屬性對應的成員變量空間。若屬性爲atomic則會在獲取屬性值的代碼兩頭添加spinlock_t的加鎖解鎖代碼,這也是atomicnonatomic的區別所在。

注意:實際上,第三節 介紹的動態添加屬性對應用開發者並無什麼用處(對 runtime 自己固然是有用的),緣由是:一、沒有指定屬性關聯成員變量的 runtime API;二、能夠經過定義函數關聯對象模擬屬性,此時動態添加的屬性就成了雞肋,無關緊要。

#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, SEL _cmd, ptrdiff_t offset, BOOL atomic) 
{
    return objc_getProperty_non_gc(self, _cmd, offset, atomic);
}
#endif

id objc_getProperty_non_gc(id self, SEL _cmd, ptrdiff_t offset, 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, SEL _cmd, ptrdiff_t offset, 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, SEL _cmd, ptrdiff_t offset, id newValue, 
                 BOOL atomic, signed char shouldCopy) 
{
    objc_setProperty_non_gc(self, _cmd, offset, newValue, atomic, shouldCopy);
}
#endif

void objc_setProperty_non_gc(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 
{
    bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
    bool mutableCopy = (shouldCopy == MUTABLE_COPY);
    reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}

// 設置屬性值的總入口
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, 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, _cmd, 
                     #propertyNameAttr#, 
                     ivar,
                     #propertyAtomicAttr#)
} 

void propSetter(id self, SEL _cmd, 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, _cmd, 
                     #propertyNameAttr#, 
                     newVal
                     ivar,
                     #propertyAtomicAttr#,
                     #propertyShouldCopyAttr#)
}
複製代碼

可是上面的處理是有明顯的性能缺陷的,每次訪問成員變量是都要調用getIvar(...),而getIvar(...)是遍歷類的整個成員變量列表,根據成員變量名查找成員變量,實際實現顯然不該如此。所以上述代碼只是模擬了屬性的實現流程,而具體實現細節將在後續將在獨立文章中介紹。

5、總結

  • Runtime 提供的關於屬性的動態特性對應用開發的意義不大,runtime 屬性關聯成員變量是隱藏在@synthesize的實現代碼中,並且成員變量不能動態添加,所以即便提供也是意義不大;

  • 動態添加屬性時,是不包含添加屬性 getter 和 setter 方法的操做的,所以必須手動實現其getter 和 setter 方法,爲分類定義屬性也是不包含 getter 和 setter 方法實現,開發者能夠;

  • 下一篇文章介紹分類。

相關文章
相關標籤/搜索