分类、扩展和关联对象

  • 一、分类与扩展的区别
    • 1、`category`类别(分类)
    • 2、`extension`(类扩展)
  • 二、关联对象的实现
    • 1.关联对象的实现步骤:
    • 2.关联对象分析:
  • 三、关联对象-设值流程
    • 1.四个核心对象:
    • 2.`objc_setAssociatedObject`解析:
      • 内存策略:
      • `_object_set_associative_reference`源码如下:
    • 3.`AssociationsManager`解析:
      • `AssociationsManager`的源码如下:
    • 4.`try_emplace`方法探究:
    • 5.`LookupBucketFor`方法:
    • 6.`InsertIntoBucket`方法:
    • 7.InsertIntoBucketImpl方法分析:
    • 8.`isFirstAssociation`首次关联对象:
    • 9.关联对象的数据结构:
  • 四、关联对象-取值流程
    • 1.`objc_getAssociatedObject`方法:
    • 2.`_object_get_associative_reference`方法:
  • 五、关联对象-移除流程
    • 1.`objc_removeAssociatedObjects`方法:
    • 2.`_object_remove_assocations`源码:
    • 3.对象销毁dealloc时,销毁相关的关联对象:
  • 六、总结

一、分类与扩展的区别

1、category类别(分类)

  • 专门用来给类添加新的方法。
  • 不能给类添加成员属性,添加了成员属性,也无法取到。
  • 【注意】:其实可以通过runtime给分类添加属性,即属性关联,重写settergetter方法。
  • 分类中用@property定义的变量,只会生成变量的settergetter方法的声明,不能生成方法实现和带下划线的成员变量。

2、extension(类扩展)

  • 可以说成是特殊的分类,也可称作匿名分类。
  • 可以给类添加成员属性,但是是私有变量。
  • 可以给类添加方法,也是私有方法。

二、关联对象的实现

关联对象(AssociatedObject)是一种运用runtime在分类中添加"属性"的方法。那么关联是怎样实现添加"属性"的呢?

1.关联对象的实现步骤:

  • 创建一个分类,并在其.h文件中声明一个属性。
  • 添加头文件#import <objc/runtime.h>
  • .m中声明该属性为动态加载@dynamic
  • 实现settergetter方法。

举例:
UIViewController+Test.h

#import <UIKit/UIKit.h>@interface UIViewController (Test)//定义一个属性
@property (nonatomic, strong) NSString *tempString;@end

UIViewController+Test.m

#import "UIViewController+Test.h"
//一定要记得加头文件
#import <objc/runtime.h>@implementation UIViewController (Test)//用@dynamic修饰属性,这样编译器不会自动实现setter和getter方法
@dynamic tempString;//实现该属性的setter和getter方法
- (void)setTempString:(NSString *)tempString {objc_setAssociatedObject(self, @"setObj", tempString, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}- (NSString *)tempString {return objc_getAssociatedObject(self, @"setObj");
}@end

2.关联对象分析:

从代码能看出,实际上关联对象并没有往原有类中添加成员变量。当然了,实际上类的空间编译的时候就已经是确定好的了,分类的属性实际上就是setget方法的实现。

那么我们只要看懂setget方法内部的实现就能搞清楚究竟关联对象是怎么做的。

三、关联对象-设值流程

1.四个核心对象:

  • AssociationsManager
  • AssociationsHashMap
  • ObjectAssociationMap
  • ObjcAssociation

在源码分析之前,我们需要去苹果官方下载runtime库我们才可以看其源码。

2.objc_setAssociatedObject解析:

我们在源码库中可以看到其实现部分:

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {_object_set_associative_reference(object, key, value, policy);
}

我们在调用此方法的时候,一共传递了四个参数:

参数名称 解释
id object 需要关联的对象
void *key 对应的key
id value 对应的值
objc_AssociationPolicy policy 内存管理策略
内存策略:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {//等效于assignOBJC_ASSOCIATION_ASSIGN = 0,           /**< 指定关联对象的弱引用。 *///等效于nonatomic,retainOBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< 指定对关联对象的强引用。*   关联不是以原子方式建立的。 *///等效于nonatiomic,copyOBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< 指定复制关联对象。 *   关联不是以原子方式建立的。 *///等效于retainOBJC_ASSOCIATION_RETAIN = 01401,       /**< 指定对关联对象的强引用。*   关联是原子式的。 *///等效于copyOBJC_ASSOCIATION_COPY = 01403          /**< 指定复制关联对象。*   关联是原子式的。 */
};

对于四个参数理解完了之后让我们看看它真正的实现函数_object_set_associative_reference

_object_set_associative_reference源码如下:
void _object_set_associative_reference(id object, const void *key, id value, uintptr_t policy) {// This code used to work when nil was passed for object and key. Some code// probably relies on that to not crash. Check and handle it explicitly.// rdar://problem/44094390if (!object && !value) return;if (object->getIsa()->forbidsAssociatedObjects())_objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));// 将 object 封装成 DisguisedPtr 目的是方便底层统一处理DisguisedPtr<objc_object> disguised{(objc_object *)object};// 将 policy和value 封装成ObjcAssociation,目的是方便底层统一处理ObjcAssociation association{policy, value};// retain the new value (if any) outside the lock.// 根据policy策略去判断是进去 retain 还是 copy 操作association.acquireValue();bool isFirstAssociation = false;//用来判断是否是,第一次关联该对象{// 实例化 AssociationsManager 注意这里不是单例AssociationsManager manager;// 实例化 全局的关联表 AssociationsHashMap 这里是单例AssociationsHashMap &associations(manager.get());//如果传过来的value有值if (value) {// AssociationsHashMap:关联表 ObjectAssociationMap:对象关联表// 首先根据对象封装的disguised去关联表中查找有没有对象关联表// 如果有直接返回结果,如果没有则根据`disguised`去创建对象关联表// 创建ObjectAssociationMap时当(对象的个数+1大于等于3/4,进行两倍扩容)auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});if (refs_result.second) {/* it's the first association we make */// 表示第一次关联该对象isFirstAssociation = true;}/* establish or replace the association */// 获取ObjectAssociationMap中存储值的地址auto &refs = refs_result.first->second;// 将需要存储的值存放在关联表中存储值的地址中// 同时会根据key去查找,如果查找到`result.second` = false ,如果找不到就创建`result.second` = true// 创建association时,当(association的个数+1)超过3/4,就会进行两倍扩容auto result = refs.try_emplace(key, std::move(association));//查找到了if (!result.second) {// 交换association和查询到的`association`// 其实可以理解为更新查询到的`association`数据,新值替换旧值association.swap(result.first->second);}} else { // value没有值走else流程// 查找disguised 对应的ObjectAssociationMapauto refs_it = associations.find(disguised);// 如果找到对应的 ObjectAssociationMap 对象关联表if (refs_it != associations.end()) {// 获取 refs_it->second,里面存放了association类型数据auto &refs = refs_it->second;// 根据key查询对应的associationauto it = refs.find(key);if (it != refs.end()) {// 如果找到,更新旧的association里面的值association.swap(it->second);// value = nil时释放关联对象表中存的`association`refs.erase(it);if (refs.size() == 0) {// 如果该对象关联表中所有的关联属性数据被清空,那么该对象关联表会被释放associations.erase(refs_it);}}}}}// Call setHasAssociatedObjects outside the lock, since this// will call the object's _noteAssociatedObjects method if it// has one, and this may trigger +initialize which might do// arbitrary stuff, including setting more associated objects.// 首次关联对象调用setHasAssociatedObjects方法// 通过setHasAssociatedObjects方法`标记对象存在关联对象`设置`isa指针`的`has_assoc`属性为`true`if (isFirstAssociation)object->setHasAssociatedObjects();// release the old value (outside of the lock).// 释放旧值因为如果有旧值会被交换到`association`中// 原来`association`的新值会存放到对象关联表中association.releaseHeldValue();
}

_object_set_associative_reference方法主要有下列两步操作:

  • 根据object全局关联表(AssociationsHashMap)中查询ObjectAssociationMap,如果没有就去开辟内存创建ObjectAssociationMap,创建的规则就是在3/4时,进行两倍扩容,扩容的规则和cache方法存储的规则是一样的。`
  • 将根据key查询到相关的association(即关联的数据 value和policy),如果查询到直接更新里面的数据,如果没有则去获取空的association类型然后将值存放进去,扩容的规则和cache方法存储的规则是一样的。

_object_set_associative_reference函数内部我们可以找到我们上面说过的实现关联对象技术的四个核心对象。接下来我们来一个一个看其内部实现原理探寻他们之间的关系。

3.AssociationsManager解析:

AssociationsManager manager并不是单例,通过AssociationsHashMap &associations(manager.get());获取的关联表是全局唯一的。

AssociationsManager的源码如下:
class AssociationsManager {using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;static Storage _mapStorage;public:// 构造函数(在作用域内加锁)AssociationsManager()   { AssociationsManagerLock.lock(); }// 析构函数(离开作用域,解锁)~AssociationsManager()  { AssociationsManagerLock.unlock(); }// 获取全局的一张AssociationsHashMap表AssociationsHashMap &get() {return _mapStorage.get();}static void init() {_mapStorage.init();}
};

从源码我们可以发现:static Storage _mapStorage;_mapStorage是全局静态变量,因此获取的AssociationsHashMap关联表也是全局唯一的一份。

AssociationsManager的构造函数AssociationsManager()和析构函数~AssociationsManager()主要是在相应作用域内加锁,为了防止多线程访问出现混乱。

4.try_emplace方法探究:

try_emplace方法的作用就是去表中查找Key相应的数据,不存在就创建:(我的理解就是哈希表,通过key去查找相应桶中的数据)

  • 通过LookupBucketFor方法去表中查找Key对应的TheBucket是否有存在,如果存在对TheBucket进行包装然后返回。
  • 如果不存在,通过InsertIntoBucket方法插入新值,扩容的规则和cache方法存储的规则是一样的。
// Inserts key,value pair into the map if the key isn't already in the map.
// The value is constructed in-place if the key is not in the map, otherwise
// it is not moved.
//如果键不在map中,则将键值对插入到map中。
//如果键不在map中,该值将被就地构造,否则不会移动。
template <typename... Ts>
std::pair<iterator, bool> try_emplace(KeyT &&Key, Ts &&... Args) {BucketT *TheBucket;// 根据key去查找对应的TheBucketif (LookupBucketFor(Key, TheBucket))// 通过make_pair生成相应的键值对return std::make_pair(makeIterator(TheBucket, getBucketsEnd(), true),false); // Already in map.表示【表中】已经存在bucket// Otherwise, insert the new element.// 如果没有查询到 将数据(键值)插入TheBucket中TheBucket =InsertIntoBucket(TheBucket, std::move(Key), std::forward<Ts>(Args)...);// 通过make_pair生成相应的键值对return std::make_pair(makeIterator(TheBucket, getBucketsEnd(), true),true); // true表示第一次往哈希关联表中添加bucket
}

5.LookupBucketFor方法:

这个方法就是 根据Key去表中查找Bucket,如果已经缓存过,返回true,否则返回false

/// LookupBucketFor - Lookup the appropriate bucket for Val, returning it in
/// FoundBucket.  If the bucket contains the key and a value, this returns
/// true, otherwise it returns a bucket with an empty marker or tombstone and
/// returns false.
/// LookupBucketFor - 为Val查找相应的桶,并在FoundBucket中返回。如果桶中包含键和值,则返回true,否则返回带有空标记或空tombstone的桶并返回false。
template<typename LookupKeyT>
bool LookupBucketFor(const LookupKeyT &Val,const BucketT *&FoundBucket) const {// 获取buckets的首地址const BucketT *BucketsPtr = getBuckets();// 获取可存储的buckets的总数const unsigned NumBuckets = getNumBuckets();// 如果NumBuckets = 0 返回 falseif (NumBuckets == 0) {FoundBucket = nullptr;return false;}// FoundTombstone - 在探查的时候留意我们是否找到了tombstone。const BucketT *FoundTombstone = nullptr;const KeyT EmptyKey = getEmptyKey();const KeyT TombstoneKey = getTombstoneKey();assert(!KeyInfoT::isEqual(Val, EmptyKey) &&!KeyInfoT::isEqual(Val, TombstoneKey) &&"Empty/Tombstone value shouldn't be inserted into map!");// 计算hash下标unsigned BucketNo = getHashValue(Val) & (NumBuckets-1);unsigned ProbeAmt = 1;while (true) {// 内存平移:找到hash下标对应的Bucketconst BucketT *ThisBucket = BucketsPtr + BucketNo;// Found Val's bucket?  If so, return it.if (LLVM_LIKELY(KeyInfoT::isEqual(Val, ThisBucket->getFirst()))) {// 如果查询到`Bucket`的`key`和`Val`相等 返回当前的Bucket说明查询到了FoundBucket = ThisBucket;return true;}// If we found an empty bucket, the key doesn't exist in the set.// Insert it and return the default value.// 如果bucket为空,说明当前key还不在表中,返回false,后续进行插入操作if (LLVM_LIKELY(KeyInfoT::isEqual(ThisBucket->getFirst(), EmptyKey))) {// If we've already seen a tombstone while probing, fill it in instead// of the empty bucket we eventually probed to.FoundBucket = FoundTombstone ? FoundTombstone : ThisBucket;return false;}// If this is a tombstone, remember it.  If Val ends up not in the map, we// prefer to return it than something that would require more probing.// Ditto for zero values.//如果这是tombstone,记住它。如果Val最终不在地图中,我们宁愿返回它,而不是需要更多探测的东西。对于零值也是如此。if (KeyInfoT::isEqual(ThisBucket->getFirst(), TombstoneKey) &&!FoundTombstone)//记录发现的第一块tombstoneFoundTombstone = ThisBucket;  // Remember the first tombstone found.if (ValueInfoT::isPurgeable(ThisBucket->getSecond())  &&  !FoundTombstone)FoundTombstone = ThisBucket;// Otherwise, it's a hash collision or a tombstone, continue quadratic// probing.//否则,它是一个哈希冲突或tombstone,继续二次探索。if (ProbeAmt > NumBuckets) {FatalCorruptHashTables(BucketsPtr, NumBuckets);}// 重新计算hash下标BucketNo += ProbeAmt++;BucketNo &= (NumBuckets-1);}
}

6.InsertIntoBucket方法:

template <typename KeyArg, typename... ValueArgs>
BucketT *InsertIntoBucket(BucketT *TheBucket, KeyArg &&Key,ValueArgs &&... Values) {// 根据Key 找到TheBucket的内存地址TheBucket = InsertIntoBucketImpl(Key, Key, TheBucket);// 将 Key 和 Values保存到TheBucket中TheBucket->getFirst() = std::forward<KeyArg>(Key);::new (&TheBucket->getSecond()) ValueT(std::forward<ValueArgs>(Values)...);return TheBucket;
}

7.InsertIntoBucketImpl方法分析:

主要的工作都是在InsertIntoBucketImpl方法中完成的:

  • 计算实际占用buckets的个数,如果超过负载因子(3/4),进行扩容操作this->grow(NumBuckets * 2);
  • 找到TheBucket的内存地址:LookupBucketFor(Lookup, TheBucket);
  • 更新占用的容量个数:incrementNumEntries();
template <typename LookupKeyT>
BucketT *InsertIntoBucketImpl(const KeyT &Key, const LookupKeyT &Lookup,BucketT *TheBucket) {// If the load of the hash table is more than 3/4, or if fewer than 1/8 of// the buckets are empty (meaning that many are filled with tombstones),// grow the table.//如果哈希表的加载量大于3/4,或者小于1/8的桶是空的(这意味着很多桶都装满了tombstones),那么就增加哈希表。//// The later case is tricky.  For example, if we had one empty bucket with// tons of tombstones, failing lookups (e.g. for insertion) would have to// probe almost the entire table until it found the empty bucket.  If the// table completely filled with tombstones, no lookup would ever succeed,// causing infinite loops in lookup.//后一种情况比较棘手。例如,如果我们有一个空桶,里面有大量的tombstone,那么失败的查找(例如插入)将不得不探测几乎整个表,直到找到空桶。如果表完全被tombstone填满,那么任何查找都无法成功,导致无限循环的查找。// 计算实际占用buckets的个数,如果超过负载因子(3/4),进行扩容操作unsigned NewNumEntries = getNumEntries() + 1;// 获取buckets的总容量unsigned NumBuckets = getNumBuckets();if (LLVM_UNLIKELY(NewNumEntries * 4 >= NumBuckets * 3)) {// 如果哈希表的负载大于等于3/4,进行二倍扩容this->grow(NumBuckets * 2); // 首次分配 4 的容量//查找BucketLookupBucketFor(Lookup, TheBucket);NumBuckets = getNumBuckets();} else if (LLVM_UNLIKELY(NumBuckets-(NewNumEntries+getNumTombstones()) <=NumBuckets/8)) {this->grow(NumBuckets);//查找BucketLookupBucketFor(Lookup, TheBucket);}ASSERT(TheBucket);// Only update the state after we've grown our bucket space appropriately// so that when growing buckets we have self-consistent entry count.// If we are writing over a tombstone or zero value, remember this.//只有在适当增加桶空间之后才更新状态,这样当增加桶时,我们就有了自一致的条目计数。如果我们写在tombstone上或者零值上,记住这个。if (KeyInfoT::isEqual(TheBucket->getFirst(), getEmptyKey())) {// Replacing an empty bucket.// 更换空桶。// 更新占用的容量个数incrementNumEntries();} else if (KeyInfoT::isEqual(TheBucket->getFirst(), getTombstoneKey())) {// Replacing a tombstone.// 更换tombstoneincrementNumEntries();decrementNumTombstones();} else {// we should be purging a zero. No accounting changes.// 我们应该清除一个零。没有占用变更。ASSERT(ValueInfoT::isPurgeable(TheBucket->getSecond()));TheBucket->getSecond().~ValueT();}return TheBucket;
}

8.isFirstAssociation首次关联对象:

首次关联对象,需要更新对象isa的标志位has_assoc,表示是否有关联对象。

// Call setHasAssociatedObjects outside the lock, since this
// will call the object's _noteAssociatedObjects method if it
// has one, and this may trigger +initialize which might do
// arbitrary stuff, including setting more associated objects.
// 首次关联对象调用setHasAssociatedObjects方法
// 通过setHasAssociatedObjects方法`标记对象存在关联对象`设置`isa指针`的`has_assoc`属性为`true`
if (isFirstAssociation)object->setHasAssociatedObjects();

查看setHasAssociatedObjects方法:

inline void
objc_object::setHasAssociatedObjects() {if (isTaggedPointer()) return;if (slowpath(!hasNonpointerIsa() && ISA()->hasCustomRR()) && !ISA()->isFuture() && !ISA()->isMetaClass()) {void(*setAssoc)(id, SEL) = (void(*)(id, SEL)) object_getMethodImplementation((id)this, @selector(_noteAssociatedObjects));if ((IMP)setAssoc != _objc_msgForward) {(*setAssoc)((id)this, @selector(_noteAssociatedObjects));}}isa_t newisa, oldisa = LoadExclusive(&isa.bits);do {newisa = oldisa;if (!newisa.nonpointer  ||  newisa.has_assoc) {ClearExclusive(&isa.bits);return;}newisa.has_assoc = true;} while (slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)));
}

发现它执行了newisa.has_assoc = true;即标记了这个对象存在关联对象。
通过setHasAssociatedObjects方法设置对象存在关联对象,即isa指针的has_assoc位域设置为true最后通过releaseHeldValue方法释放旧值。

9.关联对象的数据结构:

四、关联对象-取值流程

1.objc_getAssociatedObject方法:

id objc_getAssociatedObject(id object, const void *key) {return _object_get_associative_reference(object, key);
}

我们可以直观的看到,objc_getAssociatedObject调用了_object_get_associative_reference方法。进入_object_get_associative_reference方法,关联对象取值就是比较简单的了就是查表,源码如下:

2._object_get_associative_reference方法:

id _object_get_associative_reference(id object, const void *key) {// 创建空的关联对象ObjcAssociation association{};{// 实例化 AssociationsManager 注意这里不是单例AssociationsManager manager;// 实例化 全局的关联表 AssociationsHashMap 这里是单例AssociationsHashMap &associations(manager.get());// iterator是个迭代器,实际上相当于找到object和对应的ObjectAssociationMap(对象关联表)AssociationsHashMap::iterator i = associations.find((objc_object *)object);//找到了object对应的ObjectAssociationMap(对象关联表)if (i != associations.end()) {// 获取ObjectAssociationMap(对象关联表)ObjectAssociationMap &refs = i->second;// 迭代获取key对应的数据ObjectAssociationMap::iterator j = refs.find(key);//找到了key对应的数据if (j != refs.end()) {// 获取 associationassociation = j->second;// retain 新值association.retainReturnedValue();}}}// release旧值,返回新值return association.autoreleaseReturnedValue();
}

五、关联对象-移除流程

关联对象的移除流程分类两种情况:

  • 手动调用objc_removeAssociatedObjects方法进行移除。
  • 对象销毁时,系统会自动移除关联对象。

1.objc_removeAssociatedObjects方法:

void objc_removeAssociatedObjects(id object) {if (object && object->hasAssociatedObjects()) {_object_remove_assocations(object, /*deallocating*/false); ///*deallocating*/对象是否正在销毁}
}

2._object_remove_assocations源码:

// Unlike setting/getting an associated reference,
// this function is performance sensitive because of
// raw isa objects (such as OS Objects) that can't track
// whether they have associated objects.
//与设置/获取关联引用不同,此函数对性能敏感,因为原始isa对象(如OS对象)不能跟踪它们是否有关联对象。
void _object_remove_assocations(id object, bool deallocating) {ObjectAssociationMap refs{};{// 实例化 AssociationsManager 注意这里不是单例AssociationsManager manager;// 实例化 全局的关联表 AssociationsHashMap 这里是单例AssociationsHashMap &associations(manager.get());// iterator是个迭代器,实际上相当于找到object和对应的ObjectAssociationMap(对象关联表)AssociationsHashMap::iterator i = associations.find((objc_object *)object);//找到了object对应的ObjectAssociationMap(对象关联表)if (i != associations.end()) {refs.swap(i->second);// If we are not deallocating, then SYSTEM_OBJECT associations are preserved.//如果我们没有回收,那么SYSTEM_OBJECT关联会被保留。bool didReInsert = false;if (!deallocating) {for (auto &ref: refs) {if (ref.second.policy() & OBJC_ASSOCIATION_SYSTEM_OBJECT) {i->second.insert(ref);didReInsert = true;}}}if (!didReInsert)associations.erase(i);}}// Associations to be released after the normal ones.//在正常关联之后释放关联。SmallVector<ObjcAssociation *, 4> laterRefs;// release everything (outside of the lock).//释放锁外的所有内容。for (auto &i: refs) {if (i.second.policy() & OBJC_ASSOCIATION_SYSTEM_OBJECT) {// If we are not deallocating, then RELEASE_LATER associations don't get released.//如果我们没有释放,那么RELEASE_LATER关联不会被释放。if (deallocating)laterRefs.append(&i.second);} else {i.second.releaseHeldValue();}}for (auto *later: laterRefs) {later->releaseHeldValue();}
}

3.对象销毁dealloc时,销毁相关的关联对象:

调用流程:dealloc --> _objc_rootDealloc --> rootDealloc --> object_dispose --> objc_destructInstance --> _object_remove_assocations

六、总结

总的来说,关联对象主要就是两层哈希map的处理,即存取时都是两层处理,类似于二维数组。

  • 关联对象并不存储在被关联对象本身内存中,而是有一个全局统一的AssociationsManager
  • 一个实例对象就对应一个ObjectAssociationMap
  • ObjectAssociationMap中存储着多个此实例对象的关联对象的key以及ObjcAssociation
  • ObjcAssociation中存储着关联对象的valuepolicy策略
  • 删除的时候接收一个object对象,然后遍历删除该对象所有的关联对象
  • 设置关联对象_object_set_associative_reference的是时候,如果传入的value为空就删除这个关联对象

文章参考:iOS底层原理20:类扩展与关联对象底层原理探索

【iOS】—— 分类、扩展和关联对象相关推荐

  1. 【iOS】—— 分类,扩展和关联对象

    分类,扩展和关联对象 文章目录 分类,扩展和关联对象 分类和扩展 分类概念 扩展概念 两者区别 分类的实质 关联对象 通过关联对象给分类添加属性 关联对象的用处 关联对象的API 给声明的属性添加se ...

  2. 【iOS】——分类、扩展和关联对象

    目录 一.分类Category和扩展Extension 1.分类Category(运行期) 2.扩展Extension(编译期) 3.分类和扩展的区别 二.分类Category的实质 1.分类的结构 ...

  3. iOS Runtime特性之关联对象

    前言 现在你准备用一个系统的类或者是你写的类,但是这个类并不能满足你的需求,你需要额外添加一个属性. 一般解决办法要么是extends(继承),要么使用category(类别). 而我并不推荐使用ex ...

  4. [iOS]-Category、Extension和关联对象

    目录: 参考的博客: 前言 一.Category分类 Extension扩展 Category的实质 Category结构体 将分类转成C++看起 对象方法列表结构体 类方法列表结构体 协议列表结构体 ...

  5. [OC学习笔记]分类和关联对象源码解析

    我们平时在开发的时候经常会使用分类来添加方法.协议.属性,但在添加属性的时候属性是不会自动生成成员变量的,这时候我们就需要关联对象来动态存储属性值. 分类 @interface NSObject(St ...

  6. 【iOS高级资深工程师面试篇】②、2022年,金九银十我为你准备了《iOS高级资深工程师面试知识总结》 Objective-C语言特性部分1/2 分类-关联对象-扩展-代理

    iOS高级资深工程师面试篇系列 - 已更新3篇 UI部分1/3 -UITableView-事件传递&视图响应 UI部分2/3 -图像显示原理-UI卡顿&掉帧 UI部分3/3 -UIVi ...

  7. 【iOS开发】——Category底层原理、Extension、关联对象

    [iOS开发]--Category底层原理.Extension.关联对象 Category是什么?它可以用来干什么? Category特点 Category的实质以及实现过程 Category结构体 ...

  8. iOS runtime实战应用:关联对象

    在开始之前建议先阅读iOS runtime的基础理解篇:iOS内功篇:runtime 有筒子在面试的时候,遇到这样一个问题:"如何給NSArray添加一个属性(不能使用继承)",筒 ...

  9. iOS Category 添加属性实现原理 - 关联对象

    iOS Category 添加属性实现原理 - 关联对象 RunTime为Category动态关联对象 使用RunTime给系统的类添加属性,首先需要了解对象与属性的关系.对象一开始初始化的时候其属性 ...

最新文章

  1. Android官方命令深入分析之Device Monitor
  2. opencv精要(1)-opencv简介
  3. Attention注意力机制的前世今身
  4. MVC3 中使用Unity实现依赖注入
  5. HEVC/H265 HM10.0 分析(二)TComDataCU.cpp
  6. KubeVela v1.3 多集群初体验,轻松管理应用分发和差异化配置
  7. Invitation Cards POJ 1511
  8. linux 自定义安装软件,在/ usr中跟踪Linux上自定义软件安装的最佳实践?
  9. the server is not ready for publishing.Please check if the Publishing Tools on the server
  10. sizeof,真正终结版GCC与VC
  11. win10删除开机密码_教你电脑如何设置开机密码_win10教程
  12. 使用Spring自定义注解实现任务路由
  13. C语言程序设计流程图详解
  14. PL/SQL详细的安装和配置教程(附带网盘下载链接,以及PL/SQL的基本操作与注意事项)
  15. 网站整站下载工具推荐【Z】
  16. 值得留意在线支持插件
  17. php exif_read_data orientation,PHP exif_read_data Illegal IFD size
  18. day030进程的两种创建方法,验证进程的空间隔离,join等待子进程
  19. 补交20145226蓝墨云班课 -- Arrays和String单元测试
  20. chm打开秒退_CHM文件打开方式

热门文章

  1. 喝茶学问多分季节、看体质
  2. 像素格式之YUV(基于海思Hisi35xx平台)
  3. window.print打印指定div-打印网页指定区域
  4. CodeCraft第一步
  5. 北京大学 软件与微电子学院 集成电路工程 电子信息工程 数电专业课 资料 百度云盘...
  6. UVa_1585 Score 算法竞赛入门经典
  7. 计算机科学与技术专业,学什么,怎么学,才能找到好工作?
  8. 8个优秀图片素材网站,免费/商用/高分辨率。
  9. html文件手机怎么改名,手机怎么改无线网名称?
  10. python — 项目命名规范