前言
我們都知道 category 可以為某個(gè)類來添加方法虱朵,但是是否可以添加屬性呢?如何添加钓账?
category 分析
category 可以為我們重寫類的方法碴犬,但是我們?nèi)绻ㄟ^他來添加屬性,會(huì)發(fā)生什么
首先梆暮,我們創(chuàng)建了一個(gè) Person 類服协。當(dāng)前此類只有一個(gè)屬性 name,而當(dāng)我們聲明一個(gè)屬性的時(shí)候啦粹,實(shí)際上是生成了一個(gè)成員變量和它的 set get 方法偿荷,這樣我們?cè)谑褂玫臅r(shí)候才能正常的拿到對(duì)應(yīng)的值窘游。
那么當(dāng)我們?cè)?category 里聲明這個(gè)屬性的時(shí)候,能否同樣給我們生成這些內(nèi)容呢跳纳?
我們創(chuàng)建一個(gè) Person 的 category 并為它添加一個(gè)屬性 age 忍饰, 然后我們來為這個(gè) age 賦值。
可以發(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 時(shí)崩潰如下
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)喘批,我們雖然可以使用這個(gè)屬性撩荣,但實(shí)際上這個(gè)屬性并沒有生成對(duì)應(yīng)的 set get 方法, 所以會(huì)產(chǎn)生崩潰铣揉。
如何正確的給類添加屬性
這個(gè)時(shí)候我們就需要使用到 runtime api 為我們提供的一套機(jī)制
/**
* 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#>)
這個(gè)方法是添加關(guān)聯(lián)對(duì)象。
參數(shù)一:指我們?yōu)檎l(shuí)來添加關(guān)聯(lián)對(duì)象餐曹,因?yàn)槲覀兪窃?category 中寫的逛拱,是為 person 添加對(duì)象,所以此處直接寫 self 即可台猴。
參數(shù)二:指的是我們存儲(chǔ)值得時(shí)候?qū)?yīng)的 Key 是什么朽合,當(dāng)我們需要添加多個(gè)屬性時(shí),需要用 key 來做區(qū)分饱狂,所以我們要保證 key 值唯一曹步,這樣添加多個(gè)屬性才能區(qū)分我們?nèi)〉玫哪膫€(gè)屬性的值。我們可以通過定義變量取其地址來定義
static const void* kAge = &kAge;
或者
static const char kAge; // 使用的時(shí)候直接使用 &kAge, char 相比地址更節(jié)省內(nèi)存休讳。
參數(shù)三:指的是我們需要關(guān)聯(lián)的值是哪個(gè)讲婚?當(dāng)前我們?yōu)?age 添加,所以此處寫 age 即可俊柔,此處需要使用對(duì)象筹麸,所以我們使用 @(age);
參數(shù)四:指的是關(guān)聯(lián)策略,其實(shí)就是我們對(duì)象寫屬性時(shí)的內(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
這個(gè)時(shí)候可以看出我們已經(jīng)可以正常運(yùn)行了
原理解讀
Q:通過 category 存儲(chǔ)的內(nèi)容是否會(huì)存儲(chǔ)到原有對(duì)象的數(shù)據(jù)結(jié)構(gòu)中物赶?如果存儲(chǔ)的?
A:不會(huì)影響原來結(jié)構(gòu)
通過查看 runtime 源碼可以發(fā)現(xiàn)留晚,重要的類和數(shù)據(jù)結(jié)構(gòu)有下面幾個(gè)
AssociationsManager
AssocaitionsHashMap
AssociationsMap
ObjectAssociation
通過 AssociationsManager 來管理 AssocaitionsHashMap酵紫,
而 AssocaitionsHashMap 中則以 object 為 key,AssociationsMap 為 Value错维,
AssociationsMap 則以我們傳入的 key 為 key憨闰, ObjectAssociation 為 value,
ObjectAssociation 中則存儲(chǔ)了我們傳入的 value 和內(nèi)存管理規(guī)則需五。
其他
objc_removeAssociatedObjects 會(huì)移除所有關(guān)聯(lián)對(duì)象鹉动,如果只需要單獨(dú)移除某個(gè)只要在 objc_setAssociatedObject 時(shí) value 為 nil 即可。