iOS 屬性 @property 詳細(xì)探究

探究系列已發(fā)布文章列表,有興趣的同學(xué)可以翻閱一下:

第一篇 | iOS 屬性 @property 詳細(xì)探究

第二篇 | iOS 深入理解 Block 使用及原理

第三篇 | iOS 類別 Category 和擴(kuò)展 Extension 及關(guān)聯(lián)對(duì)象詳解

第四篇 | iOS 常用鎖 NSLock ,@synchronized 等的底層實(shí)現(xiàn)詳解

第五篇 | iOS 全面理解 Nullability

------- 正文開(kāi)始 -------

引言

@property 應(yīng)該是面試過(guò)程中被問(wèn)到最多的一個(gè)技術(shù)點(diǎn),既能考察一個(gè)人的基礎(chǔ),又能挖掘一個(gè)人對(duì)技術(shù)細(xì)節(jié)的掌握度,本文著重全面,細(xì)致的介紹一下 @property 都有哪些技術(shù)點(diǎn)值得我們關(guān)注。

  • 代碼規(guī)范

聲明 @property 時(shí),注意關(guān)鍵詞及字符間的空格。

@property (nonatomic, copy) NSString *name;

  • 本質(zhì)

@property 的本質(zhì)其實(shí)是:ivar (實(shí)例變量) + getter + setter ;

  • 常用關(guān)鍵詞

// 存取器方法
1. getter=getterName
2. setter=setterName

// 讀寫(xiě)權(quán)限
1. readonly
2. readwrite

// 內(nèi)存管理
1. strong
2. assign
3. copy
4. weak
5. retain
6. unsafe_unretained

// 原子性
1. nonatomic
2. atomic

接下來(lái)逐個(gè)介紹一下,每個(gè)關(guān)鍵詞的作用:

存取器方法

  1. getter=getterName
  2. setter=setterName:

指定獲取屬性對(duì)象的名字為 getterName,如果你沒(méi)有使用 getter 指定getterName ,系統(tǒng)默認(rèn)直接使用 propertyName 訪問(wèn)即可。通常來(lái)說(shuō),只有所指屬性需要我們指定 isPropertyName 對(duì)應(yīng)的 Bool 值時(shí),才使用指定 getterName ,一般直接用 PropertyName 即可。setter=setterName: 則是用來(lái)指定設(shè)置屬性所使用的的 setter 方法,即設(shè)置屬性值時(shí)使用 setterName: 方法,此處 setterName 是一個(gè)方法名,因此要以":"結(jié)尾,具體示例如下:

// 指定getter訪問(wèn)名為`isHappy`
@property (nonatomic, assign, getter=isHappy) BOOL happy;

// 使用系統(tǒng)默認(rèn)getter/setter方法
@property (nonatomic, assign) NSInteger age;

// 指定setter方法名為`setNickName:`
@property (nonatomic, copy, setter=setNickName:) NSString *name;

讀寫(xiě)權(quán)限

  1. readwrite
  2. readonly
  • readwrite:表示自動(dòng)生成對(duì)應(yīng)的 gettersetter 方法,即可讀可寫(xiě)權(quán)限, readwrite是編譯器的默認(rèn)選項(xiàng)。
  • readonly:表示只生成 getter ,不需要生成 setter ,即只可讀,不可以修改。

內(nèi)存管理

  1. strong // 強(qiáng)引用,引用計(jì)數(shù)+1
  2. assign // assign是指針賦值,不對(duì)引用計(jì)數(shù)操作,對(duì)象銷毀后不會(huì)自動(dòng)置為nil
  3. copy // copy出一個(gè)新對(duì)象,引用計(jì)數(shù)為1
  4. weak // 弱引用,不對(duì)引用計(jì)數(shù)操作,對(duì)象銷毀時(shí)自動(dòng)置為nil
  5. retain // 強(qiáng)引用,對(duì)象引用計(jì)數(shù)+1
  6. unsafe_unretained // 弱引用,不對(duì)引用計(jì)數(shù)操作,對(duì)象銷毀時(shí)不會(huì)自動(dòng)置為nil
  • strong

表示強(qiáng)引用關(guān)系,即修飾對(duì)象的引用計(jì)數(shù)會(huì)+1,通常用來(lái)修飾對(duì)象類型,可變集合及可變字符串類型。當(dāng)對(duì)象引用計(jì)數(shù)為0,即不被任何對(duì)象持有,且此對(duì)象不再顯示在列表中時(shí),對(duì)象就會(huì)從內(nèi)存中釋放。

  • assign

對(duì)象不進(jìn)行 retain 操作,即不改變對(duì)象引用計(jì)數(shù)。通常用來(lái)修飾基本數(shù)據(jù)類型( NSInteger, CGFloat, Bool, NSTimeInterval 等),內(nèi)存在棧上由系統(tǒng)自動(dòng)回收。

assign 也可以用來(lái)修飾 NSObject 類型對(duì)象,因?yàn)?assign 不會(huì)改變修飾對(duì)象的引用計(jì)數(shù),所以當(dāng)修飾對(duì)象的引用計(jì)數(shù)為0,對(duì)象銷毀的時(shí)候,對(duì)象指針不會(huì)被自動(dòng)清空。而此時(shí)對(duì)象指針指向的地址已被銷毀,這時(shí)再訪問(wèn)該屬性會(huì)產(chǎn)生野指針錯(cuò)誤:EXC_BAD_ACCESS,因此 assign 通常用來(lái)修飾基本數(shù)據(jù)類型。

  • copy

當(dāng)調(diào)用修飾對(duì)象的 setter 方法時(shí),會(huì)建立一個(gè)引用計(jì)數(shù)為 1 的新對(duì)象,即對(duì)象會(huì)在內(nèi)存里拷貝一份副本,兩個(gè)指針指向不同的內(nèi)存地址。一般用于修飾字符串( NSString )和集合類( NSArray , NSDictionary )的不可變變量,Block 也是用 copy 修飾。

針對(duì) copy ,這里又牽涉到了深 copy 和淺 copy 的問(wèn)題,這里做一下簡(jiǎn)單介紹,后續(xù)會(huì)有文章專門(mén)探討這個(gè)問(wèn)題:

淺 copy :是對(duì)指針的 copy ,指針指向的內(nèi)容是同一個(gè)地址,對(duì)象的引用計(jì)數(shù)+1;

深 copy :是對(duì)內(nèi)容的 copy ,會(huì)開(kāi)辟新的內(nèi)存空間,將內(nèi)容重新 copy 一份;

  • 非集合對(duì)象的 copy 與 mutableCopy

不可變對(duì)象:copy 操作為淺 copy,mutableCopy 操作為深 copy。

可變對(duì)象:copy 操作為深 copy,mutableCopy 操作也為深 copy。

  • 集合類對(duì)象的 copy 與 mutableCopy

不可變對(duì)象:copy 操作為深 copy ,mutableCopy 操作為深copy。

可變對(duì)象:copy 操作為深 copy ,可變對(duì)象的 mutableCopy 也為深 copy 。

注意:當(dāng)使用 copy 修飾的屬性賦值時(shí),copy 出來(lái)的是一份不可變對(duì)象。因此當(dāng)對(duì)象是一個(gè)可變對(duì)象時(shí),切記不要使用 copy 進(jìn)行修飾。如果這時(shí)使用 copy 修飾,當(dāng)使用 copy 出來(lái)的對(duì)象調(diào)用可變對(duì)象所特有的方法時(shí),會(huì)因?yàn)檎也坏綄?duì)應(yīng)的方法而 Crash。

  • weak

表示弱引用關(guān)系,修飾對(duì)象的引用計(jì)數(shù)不會(huì)增加,當(dāng)修飾對(duì)象被銷毀的時(shí)候,對(duì)象指針會(huì)自動(dòng)置為 nil,防止出現(xiàn)野指針。weak 也用來(lái)修飾 delegate ,避免循環(huán)引用。另外 weak 只能用來(lái)修飾對(duì)象類型,且是在 ARC 下新引入的修飾詞,MRC 下相當(dāng)于使用 assign 。

weak的底層實(shí)現(xiàn)原理

weak 的底層實(shí)現(xiàn)是基于 Runtime 底層維護(hù)的 SideTables 的 hash 數(shù)組,里面存儲(chǔ)的是一個(gè) SideTable 的數(shù)據(jù)結(jié)構(gòu):

struct SideTable {
    spinlock_t slock; // 確保原子性操作的鎖,雖然名字還叫 spinlock_t ,其實(shí)本質(zhì)已經(jīng)是 mutex_t,具體可見(jiàn) objc-os 源碼( `using spinlock_t = mutex_tt<LOCKDEBUG>;` )

    RefcountMap refcnts; // 用來(lái)存儲(chǔ)對(duì)象引用計(jì)數(shù)的 hash 表
    
    weak_table_t weak_table // 存儲(chǔ)對(duì)象 weak 引用指針的 hash 表
    //...
};
  • weak 功能實(shí)現(xiàn)核心的數(shù)據(jù)結(jié)構(gòu) weak_table_t:
/**
 * The global weak references table. Stores object ids as keys,
 * and weak_entry_t structs as their values.
 */
struct weak_table_t {
    weak_entry_t *weak_entries; // 存儲(chǔ) weak 對(duì)象信息的 hash 數(shù)組
    size_t    num_entries; // 數(shù)組中元素的個(gè)數(shù)
    uintptr_t mask;        // 計(jì)數(shù)輔助量
    uintptr_t max_hash_displacement; // hash 元素最大偏移值
};
  • weak_entry_t也是一個(gè)hash結(jié)構(gòu):
#define WEAK_INLINE_COUNT 4
#define REFERRERS_OUT_OF_LINE 2

struct weak_entry_t {
    DisguisedPtr<objc_object> referent;
    union {
        // 動(dòng)態(tài)數(shù)組
        struct {
            weak_referrer_t *referrers; // 弱引用該對(duì)象的對(duì)象指針的hash數(shù)組
            uintptr_t        out_of_line_ness : 2;
            uintptr_t        num_refs : PTR_MINUS_2;
            uintptr_t        mask;
            uintptr_t        max_hash_displacement;
        };
        // 定長(zhǎng)數(shù)組,最大值為4,蘋(píng)果考慮到一半弱引用的指針個(gè)數(shù)不會(huì)超過(guò)這個(gè)數(shù),因此為了提升運(yùn)行效率,一次分配一整塊的連續(xù)內(nèi)存空間
        struct {
            // out_of_line_ness field is low bits of inline_referrers[1]
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    };

    // 判斷當(dāng)前是動(dòng)態(tài)數(shù)組,還是定長(zhǎng)數(shù)組
    bool out_of_line() {
        return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
    }

    weak_entry_t& operator=(const weak_entry_t& other) {
        memcpy(this, &other, sizeof(other));
        return *this;
    }

    weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
        : referent(newReferent)
    {
        inline_referrers[0] = newReferrer;
        for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
            inline_referrers[i] = nil;
        }
    }
};

這里重點(diǎn)說(shuō)一下 weak_entry_t 定長(zhǎng)數(shù)組動(dòng)態(tài)數(shù)組的切換,首先會(huì)將原來(lái)定長(zhǎng)數(shù)組中的內(nèi)容轉(zhuǎn)移到動(dòng)態(tài)數(shù)組中,然后再在動(dòng)態(tài)數(shù)組中插入新的元素。

而對(duì)于動(dòng)態(tài)數(shù)組中元素個(gè)數(shù)大于或等于總空間的 3/4 時(shí),會(huì)對(duì)動(dòng)態(tài)數(shù)組進(jìn)行總空間 * 2 的擴(kuò)容

    if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) {
        return grow_refs_and_insert(entry, new_referrer);
    }

每次動(dòng)態(tài)數(shù)組擴(kuò)容,都會(huì)將原先數(shù)組中的內(nèi)容重新插入到新的數(shù)組中。

當(dāng)對(duì)象的引用計(jì)數(shù)為 0 時(shí),底層會(huì)調(diào)用 _objc_rootDealloc 方法對(duì)對(duì)象進(jìn)行釋放,而在 _objc_rootDealloc 方法里面會(huì)調(diào)用 rootDealloc 方法,如果對(duì)象有被 weak 引用,則會(huì)進(jìn)入 object_dispose , 之后會(huì)在 objc_destructInstance 函數(shù)里面調(diào)用 obj->clearDeallocating(); 根據(jù)對(duì)象地址獲取所有 weak 指針地址數(shù)組,遍歷數(shù)組找到對(duì)應(yīng)的值,將其值為 nil ,然后將 entryweak 表中移除,最后從引用計(jì)數(shù)表中刪除以廢棄對(duì)象的地址為鍵值的記錄。

備注: 此處省略了 weak 底層實(shí)現(xiàn)的很多細(xì)節(jié),具體詳細(xì)實(shí)現(xiàn),后續(xù)會(huì)單獨(dú)發(fā)文介紹。

  • retain 是在 MRC 下常用的修飾詞:

ARC 下已不再使用 retain ,而是使用 strong 代替。retainstrong 類似,用來(lái)修飾對(duì)象類型,強(qiáng)引用對(duì)象,其修飾對(duì)象的引用計(jì)數(shù)會(huì) +1,不會(huì)對(duì)對(duì)象分配新的內(nèi)存空間。

  • unsafe_unretained 同 weak 類似:

unsafe_unretained 不會(huì)對(duì)對(duì)象的引用計(jì)數(shù) +1,只能用來(lái)修飾對(duì)象類型,修飾的對(duì)象在被銷毀時(shí),其指針不會(huì)自動(dòng)清空,指向的仍然是已銷毀的對(duì)象,這時(shí)再調(diào)用該指針會(huì)產(chǎn)生野指針 :EXC_BAD_ACCESS 錯(cuò)誤。


原子性

atomic 原子性:系統(tǒng)會(huì)自動(dòng)給生成的 getter/setter 方法進(jìn)行加鎖操作;

nonatomic 非原子性:系統(tǒng)不會(huì)給自動(dòng)生成的 getter/setter 方法進(jìn)行加鎖操作;

設(shè)置屬性函數(shù) reallySetProperty(...) 的原子性非原子性實(shí)現(xiàn)如下:

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

獲取屬性函數(shù) objc_getProperty(...) 的內(nèi)部實(shí)現(xiàn)如下:

    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);

由此可見(jiàn),對(duì)屬性對(duì)象的加鎖操作僅限于對(duì)象的 getter/setter 操作,如果是 getter/setter 以外的操作,該加鎖并沒(méi)有意義。因此 atomic 的原子性,僅能保障對(duì)象的 getter/setter 的線程安全,并不能保障多線程下對(duì)對(duì)象的其他操作安全。如一個(gè)線程在 getter/setter 操作,另一個(gè)線程進(jìn)行 release 操作,可能會(huì)導(dǎo)致 crash。此種場(chǎng)景的線程安全,還需要由開(kāi)發(fā)者自己進(jìn)行處理。


拓展知識(shí)

  • Category 中添加屬性 @property

Category 中添加 @property,只會(huì)生成 setter/getter 方法的聲明,并不會(huì)有具體的代碼實(shí)現(xiàn)。這是因?yàn)?Category 在運(yùn)行期對(duì)象的內(nèi)存布局已經(jīng)確定,此時(shí)如果添加實(shí)例變量就會(huì)破壞對(duì)象的內(nèi)存布局,這將會(huì)是災(zāi)難性的。因此 Category 無(wú)法添加實(shí)例變量。

那如何給 Category 實(shí)現(xiàn)類似實(shí)例變量功能呢?簡(jiǎn)單列舉兩種方式,此處暫時(shí)不做具體詳解,后續(xù)會(huì)有文章單獨(dú)介紹:

  1. 使用臨時(shí)全局變量代替成員變量,并在屬性的 setter/getter 函數(shù)中進(jìn)行存取值操作;
  2. 通過(guò) Runtime 添加關(guān)聯(lián)對(duì)象實(shí)現(xiàn)成員變量,并在屬性的 setter/getter 函數(shù)中進(jìn)行存取值操作,其關(guān)鍵調(diào)用有兩個(gè):
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy) // 設(shè)置關(guān)聯(lián)對(duì)象值調(diào)用

objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key) // 獲取關(guān)聯(lián)對(duì)象值調(diào)用
  • Protocol 中添加屬性 @property

protocol 中添加屬性 @property,其實(shí)就是聲明該屬性的 setter/getter 方法,在實(shí)現(xiàn)該 protocol 時(shí),并沒(méi)有生成對(duì)應(yīng)的成員變量,此時(shí)有動(dòng)態(tài)實(shí)現(xiàn)和自動(dòng)實(shí)現(xiàn)兩種方式:

  1. 動(dòng)態(tài)實(shí)現(xiàn),需要在實(shí)現(xiàn)類中添加 protocol 中聲明屬性對(duì)應(yīng)的 setter/getter 方法,并聲明一個(gè)私有成員變量用來(lái)進(jìn)行存取操作。備注: 實(shí)現(xiàn)類如果沒(méi)有實(shí)現(xiàn)對(duì)應(yīng)的 setter/getter 方法,在調(diào)用 protocol 屬性的 setter/getter 方法時(shí)會(huì)因?yàn)榉椒ㄕ也坏蕉?Crash
  2. 自動(dòng)實(shí)現(xiàn),可以通過(guò) @synthesize propertyName; 告訴編譯器自動(dòng)添加對(duì)應(yīng)的 setter/getter 方法,并生成對(duì)應(yīng)的成員變量;
  • @synthesize 作用

@synthesize 的作用是告訴編譯器,自動(dòng)創(chuàng)建屬性的 setter/getter 方法,同時(shí)生成成員變量,并且可以給屬性指定別名。如:

@synthesize name = nickName;

  • @dynamic 作用

@dynamic 作用是告訴編譯器,無(wú)需自動(dòng)創(chuàng)建屬性的 setter/getter 方法,這個(gè)時(shí)候就需要我們手動(dòng)實(shí)現(xiàn)相應(yīng)的 setter/getter,否則,在使用到相應(yīng)屬性的 setter/getter 方法時(shí),會(huì)因找不到方法而 Crash。

  • null 相關(guān)的一些關(guān)鍵詞
  1. nullable : 可以為空
  2. nonnull : 不可以為空
  3. null_unspecified : 未知類型
  4. null_resettable : get不能為空,set可以為空
  5. __nullable : 可以為 Null 或 nil
  6. __nonnull : 不可以為空
  • Swift下的 unowned 與 weak 區(qū)別:
  1. Swift下 weak 的使用同 Objective-C 下 weak 的使用相同。
  2. unowned 無(wú)主引用標(biāo)記對(duì)象,即使它的原引用已經(jīng)被釋放,它仍然會(huì)保持對(duì)已釋放對(duì)象的引用,它不是 Optional ,也不會(huì)被置為 nil 。當(dāng)我們?cè)噲D訪問(wèn)這樣的 unowned 引用時(shí),程序會(huì)發(fā)生錯(cuò)誤。而 weak 在引用的對(duì)象被釋放后,標(biāo)記為 weak 的對(duì)象將會(huì)自動(dòng)置為 nil 。

Define a capture in a closure as an unowned reference when the closure and the instance it captures will always refer to each other, and will always be deallocated at the same time.

Conversely, define a capture as a weak reference when the captured reference may become nil at some point in the future. Weak references are always of an optional type, and automatically become nil when the instance they reference is deallocated. This enables you to check for their existence within the closure’s body.

NOTE:

If the captured reference will never become nil, it should always be captured as an unowned reference, rather than a weak reference.

根據(jù)蘋(píng)果官方文檔的建議,如果捕獲的引用永遠(yuǎn)不會(huì)變?yōu)?nil ,我們應(yīng)該使用 unowned,否則應(yīng)該使用 weak 。


總結(jié)

@property 延展相關(guān)的技術(shù)點(diǎn)有很多,如:copy 相關(guān)的 NSCopying 協(xié)議,weak 底層詳細(xì)的實(shí)現(xiàn)原理,如何保障對(duì)象的多線程安全。還有很多技術(shù)點(diǎn)跟 Runtime 、Runloop 有關(guān),后續(xù)文章會(huì)陸續(xù)介紹。

知識(shí)點(diǎn)完整說(shuō)下來(lái)就是一整套系統(tǒng)的協(xié)同運(yùn)轉(zhuǎn),各個(gè)環(huán)節(jié)緊密相扣,最終才成為我們現(xiàn)在看到的樣子。本文及以后的文章都會(huì)盡可能的收縮一下單篇文章探討的范圍,以期能夠讓話題更加緊密。


參考資料:

The Objective-C Programming Language


關(guān)于技術(shù)組

iOS 技術(shù)組主要用來(lái)學(xué)習(xí)、分享日常開(kāi)發(fā)中使用到的技術(shù),一起保持學(xué)習(xí),保持進(jìn)步。文章倉(cāng)庫(kù)在這里:https://github.com/minhechen/iOSTechTeam
微信公眾號(hào):iOS技術(shù)組,歡迎聯(lián)系交流,感謝閱讀。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容