如何通過分類來給對象添加屬性

前言

我們都知道 category 可以為某個類來添加方法,但是是否可以添加屬性呢?如何添加?

category 分析

category 可以為我們重寫類的方法,但是我們?nèi)绻ㄟ^他來添加屬性,會發(fā)生什么

首先,我們創(chuàng)建了一個 Person 類。當(dāng)前此類只有一個屬性 name,而當(dāng)我們聲明一個屬性的時候,實際上是生成了一個成員變量和它的 set get 方法,這樣我們在使用的時候才能正常的拿到對應(yīng)的值。

image.png

那么當(dāng)我們在 category 里聲明這個屬性的時候,能否同樣給我們生成這些內(nèi)容呢?

我們創(chuàng)建一個 Person 的 category 并為它添加一個屬性 age , 然后我們來為這個 age 賦值。

image.png
image.png

可以發(fā)現(xiàn)我們的程序崩潰了,崩潰堆棧信息如下:

2021-09-14 16:40:31.808475+0800 CategroyTest[40231:1531935] Hello, World!
2021-09-14 16:40:31.811430+0800 CategroyTest[40231:1531935] -[Person setAge:]: unrecognized selector sent to instance 0x10063cc60
2021-09-14 16:40:31.814593+0800 CategroyTest[40231:1531935] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person setAge:]: unrecognized selector sent to instance 0x10063cc60'
*** First throw call stack:
(
    0   CoreFoundation                      0x00007fff2066a83b __exceptionPreprocess + 242
    1   libobjc.A.dylib                     0x00007fff203a2d92 objc_exception_throw + 48
    2   CoreFoundation                      0x00007fff206ed34d -[NSObject(NSObject) __retain_OA] + 0
    3   CoreFoundation                      0x00007fff205d28cb ___forwarding___ + 1448
    4   CoreFoundation                      0x00007fff205d2298 _CF_forwarding_prep_0 + 120
    5   CategroyTest                        0x0000000100003e7c main + 92
    6   libdyld.dylib                       0x00007fff20512f3d start + 1
)
libc++abi: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person setAge:]: unrecognized selector sent to instance 0x10063cc60'
terminating with uncaught exception of type NSException

使用 .age 時崩潰如下

2021-09-14 16:44:33.699452+0800 CategroyTest[40267:1535013] Hello, World!
2021-09-14 16:44:33.699984+0800 CategroyTest[40267:1535013] -[Person age]: unrecognized selector sent to instance 0x10482c1c0
2021-09-14 16:44:33.700674+0800 CategroyTest[40267:1535013] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person age]: unrecognized selector sent to instance 0x10482c1c0'
*** First throw call stack:
(
    0   CoreFoundation                      0x00007fff2066a83b __exceptionPreprocess + 242
    1   libobjc.A.dylib                     0x00007fff203a2d92 objc_exception_throw + 48
    2   CoreFoundation                      0x00007fff206ed34d -[NSObject(NSObject) __retain_OA] + 0
    3   CoreFoundation                      0x00007fff205d28cb ___forwarding___ + 1448
    4   CoreFoundation                      0x00007fff205d2298 _CF_forwarding_prep_0 + 120
    5   CategroyTest                        0x0000000100003e77 main + 87
    6   libdyld.dylib                       0x00007fff20512f3d start + 1
)
libc++abi: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person age]: unrecognized selector sent to instance 0x10482c1c0'
terminating with uncaught exception of type NSException

通過堆棧信息我們可以發(fā)現(xiàn),我們雖然可以使用這個屬性,但實際上這個屬性并沒有生成對應(yīng)的 set get 方法, 所以會產(chǎn)生崩潰。

如何正確的給類添加屬性

這個時候我們就需要使用到 runtime api 為我們提供的一套機制

/** 
 * Sets an associated value for a given object using a given key and association policy.
 * 
 * @param object The source object for the association.
 * @param key The key for the association.
 * @param value The value to associate with the key key for object. Pass nil to clear an existing association.
 * @param policy The policy for the association. For possible values, see “Associative Object Behaviors.”
 * 
 * @see objc_setAssociatedObject
 * @see objc_removeAssociatedObjects
 */
OBJC_EXPORT void
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

/** 
 * Returns the value associated with a given object for a given key.
 * 
 * @param object The source object for the association.
 * @param key The key for the association.
 * 
 * @return The value associated with the key \e key for \e object.
 * 
 * @see objc_setAssociatedObject
 */
OBJC_EXPORT id _Nullable
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

/** 
 * Removes all associations for a given object.
 * 
 * @param object An object that maintains associated objects.
 * 
 * @note The main purpose of this function is to make it easy to return an object 
 *  to a "pristine state”. You should not use this function for general removal of
 *  associations from objects, since it also removes associations that other clients
 *  may have added to the object. Typically you should use \c objc_setAssociatedObject 
 *  with a nil value to clear an association.
 * 
 * @see objc_setAssociatedObject
 * @see objc_getAssociatedObject
 */
OBJC_EXPORT void
objc_removeAssociatedObjects(id _Nonnull object)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

objc_setAssociatedObject

objc_setAssociatedObject(<#id  _Nonnull object#>, <#const void * _Nonnull key#>, <#id  _Nullable value#>, <#objc_AssociationPolicy policy#>)

這個方法是添加關(guān)聯(lián)對象。

參數(shù)一:指我們?yōu)檎l來添加關(guān)聯(lián)對象,因為我們是在 category 中寫的,是為 person 添加對象,所以此處直接寫 self 即可。
參數(shù)二:指的是我們存儲值得時候?qū)?yīng)的 Key 是什么,當(dāng)我們需要添加多個屬性時,需要用 key 來做區(qū)分,所以我們要保證 key 值唯一,這樣添加多個屬性才能區(qū)分我們?nèi)〉玫哪膫€屬性的值。我們可以通過定義變量取其地址來定義

static const void* kAge = &kAge;
或者
static const char kAge;  // 使用的時候直接使用 &kAge, char 相比地址更節(jié)省內(nèi)存。

參數(shù)三:指的是我們需要關(guān)聯(lián)的值是哪個?當(dāng)前我們?yōu)?age 添加,所以此處寫 age 即可,此處需要使用對象,所以我們使用 @(age);
參數(shù)四:指的是關(guān)聯(lián)策略,其實就是我們對象寫屬性時的內(nèi)存管理策略,此處我們用 OBJC_ASSOCIATION_ASSIGN

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};

完整代碼如下:

Person+Test.h


#import "Person.h"

NS_ASSUME_NONNULL_BEGIN

@interface Person (Test)

@property (nonatomic,assign) int age;

@end

NS_ASSUME_NONNULL_END

Person+Test.m

#import "Person+Test.h"
#import <objc/runtime.h>

const void* kAge = &kAge;
@implementation Person (Test)

- (void)setAge:(int)age{
    objc_setAssociatedObject(self, kAge, @(age), OBJC_ASSOCIATION_ASSIGN);
}

- (int)age{
    return [objc_getAssociatedObject(self, kAge) intValue];
}
@end

這個時候可以看出我們已經(jīng)可以正常運行了

image.png

原理解讀

Q:通過 category 存儲的內(nèi)容是否會存儲到原有對象的數(shù)據(jù)結(jié)構(gòu)中?如果存儲的?
A:不會影響原來結(jié)構(gòu)

通過查看 runtime 源碼可以發(fā)現(xiàn),重要的類和數(shù)據(jù)結(jié)構(gòu)有下面幾個

AssociationsManager
AssocaitionsHashMap
AssociationsMap
ObjectAssociation

通過 AssociationsManager 來管理 AssocaitionsHashMap,
而 AssocaitionsHashMap 中則以 object 為 key,AssociationsMap 為 Value,
AssociationsMap 則以我們傳入的 key 為 key, ObjectAssociation 為 value,
ObjectAssociation 中則存儲了我們傳入的 value 和內(nèi)存管理規(guī)則。

其他

objc_removeAssociatedObjects 會移除所有關(guān)聯(lián)對象,如果只需要單獨移除某個只要在 objc_setAssociatedObject 時 value 為 nil 即可。

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

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

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