前言
前面几篇文章主要分析了类的加载过程,那么这篇文章主要分析分类中属性的存储,也就是大家常说的关联对象,以及类扩展的探究。
准备工作
类扩展
- 为某个类附加
额外的属性,成员变量,方法声明 - 网上许多博客都写到类扩展是一种特殊的匿名分类,这种说法我是不认同,
分类和类扩展是有本质的区别的 - 分类的底层是category_t结构题类型,而类扩展底层是直接编译到主类中的
- 一般的类扩展写到
.m文件中,一般的私有属性或者想要独立区别的属性写到类扩展
类扩展格式
.m文件中的格式
@interface XJLTeacher()
{
NSInteger height; //成员变量
}
@property(nonatomic,assign)NSInteger lw_age;//属性
-(void)helloWord;//对象方法
+(void)helloClass;//类方法
@end
这种声明在.m文件的类扩展,基本上每天都在用,但是我们确很少留意到这就是类扩展, 注意的是类扩展必须写在类的声明和类的实现直接。
注意:类的声明是在.h文件中的,在编译时.h文件会被展开放在.m文件中。
Extension格式
#import "XJLTeacher.h"
@interface XJLTeacher ()
@property(nonatomic,assign)int age;
@end
创建类扩展时发现只有一个.h文件,没有与之对应的.m。类扩展的所有的实现都是在一个.m文件中实现的,其实Extension就是相当于把.m文件中类扩展的方式写到一个单独的头文件中,其实没有区别。
类扩展底层探究
把XJLTeacher写在main.m目的是为了更好的探究.cpp文件,代码如下:
@interface XJLTeacher : NSObject
- (void)sayHello;
@end
@interface XJLTeacher()
{
NSInteger height;
}
@property(nonatomic,assign)NSInteger lw_age;
-(void)helloWord;
+(void)helloClass;
@end
@implementation XJLTeacher
+(void)load{
}
- (void)sayHello{
}
- (void)helloWord{
}
+(void)helloClass{
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
XJLTeacher * teacher = [[XJLTeacher alloc] init];
[teacher sayHello];
}
return 0;
}
通过xcrun命令行将main.m文件生成main.cpp文件如下:

图中显示类扩展的
变量和方法都是在编译时就已经确定,其实就是存储在类中。下面就验证下是否是在编译期就确定的,在_read_images方法中下断点,如图所示:
图中很明显
类扩展的方法也是在ro中的,ro是在编译期就确定的。所以现在得出结论类可以有多个类扩展,但是所有的实现都是在.m文件中实现的。
关联对象 - set
关联对象应用的场景一般是在分类中添加属性,现在就探究下关联对象底层实现。创建LWTeacher+HH分类和LWPerson+LWA分类,源码如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
LWPerson * person = [[LWPerson alloc] init];
person.lw_name = @"luwenhan";
person.lw_showHello = @"hello";
person.lw_showHello1 = @"H1111";
person.lw_showHello2 = @"H2222";
LWTeacher * teacher = [[LWTeacher alloc] init];
teacher.lw_teacher = @"teacher";
}
return 0;
}
//LWPerson+LWA
@implementation LWPerson (LWA)
+(void)load{
}
-(void)setLw_name:(NSString *)lw_name{
objc_setAssociatedObject(self, "lw_name", lw_name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
-(NSString *)lw_name{
return objc_getAssociatedObject(self, "lw_name");
}
...
// LWTeacher+HH
@implementation LWTeacher (HH)
+(void)load{
}
-(void)setLw_teacher:(NSString *)lw_teacher{
objc_setAssociatedObject(self, "lw_teacher", lw_teacher, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
-(NSString *)lw_teacher{
return objc_getAssociatedObject(self, "lw_teacher");
}
@end
在LWPerson+LWA分类定义4个属性,在LWTeacher+HH分类中创建定义1个属性。在objc_setAssociatedObject中添加断点,运行源码,进入objc_setAssociatedObject,如下:
void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
_object_set_associative_reference(object, key, value, policy);
}
进入_object_set_associative_reference方法里面的代码比较多,查看核心代码的实现如下:
void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
if (!object && !value) return; //没有值直接返回
if (object->getIsa()->forbidsAssociatedObjects())
...
//将 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` = false ,如果找不到就创建result = 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 对应的ObjectAssociationMap
auto refs_it = associations.find(disguised);
//如果找到对应的 ObjectAssociationMap 对象关联表
if (refs_it != associations.end()) {
//获取 refs_it->second 里面存放了association类型数据
auto &refs = refs_it->second;
//根据key查询对应的association
auto it = refs.find(key);
if (it != refs.end()) {
//如果找到更新旧的association里面的值
//旧的association会存放在association中
association.swap(it->second);
//value= nil时释放关联对象表中存的`association`
refs.erase(it);
//如果该对象关联表中所有的关联属性数据被清空,那么该对象关联表会被释放
if (refs.size() == 0) {
associations.erase(refs_it);
}
}
}
}
}
//每一次第一个关联对象调用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在关联表中查询ObjectAssociationMap如果没有就去开辟内存创建ObjectAssociationMap,创建的规则就是在3/4时,进行两倍扩容 - 将根据
key查询到相关的association就是关联的数据value和policy,如果查询到直接更新里面的数据,如果没有则去获取空的asociation类型然后将值存放进去,如果ObjectAssociationMap中是第一次去关联数据或者关联的数据的个数满足了3/4时,进行两倍扩容 - 扩容的规则和
cache方法存储的规则时一样的
AssociationsManager manager可能大家有疑问为什么不是单例,而AssociationsHashMap &associations是单例,探究下AssociationsManager和AssociationsHashMap结构如下:
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,是全局的静态变量,只是在AssociationsManager内部 可以使用,如果你把它放在外面一样可以使用,AssociationsManager有一个构造函数和一个析构函数。AssociationsManager manager就相当于调用了构造函数加锁的功能,AssociationsManager的作用域结束会自动调用析构函数进行解锁功能。AssociationsManager就是为了防止多线程访问出现混乱,AssociationsHashMap就是全局的静态变量获取的只调用一次
验证下AssociationsManager是否为单例

AssociationsManager是class类型,此时的manager还没有初始化赋值,但是manager是AssociationsManager类型。&manager变成是AssociationsManager*类型既指针类型,&manager存放的就是AssociationsManager的对象类型,&manager、&manager1和&manager2不同的地址说明实例化后的地址不是同的,不是同一个对象。
验证AssociationsHashMap &associations是否为单例

很明显
AssociationsHashMap &associations是单例,只有单例才能值开辟一次内存只有一个地址,其它指针存的都是这一块地址。
try_emplace 方法
使用断点调试的方法一步步走下去:

template <typename... Ts>
std::pair<iterator, bool> try_emplace(const KeyT &Key, Ts &&... Args) {
BucketT *TheBucket;
//根据key去查找对应的Bucket
if (LookupBucketFor(Key, TheBucket))
//通过make_pair生成相应的键值对
return std::make_pair(
makeIterator(TheBucket, getBucketsEnd(), true),
false); //false 表示往哈希关联表已经存在bucket
//如果没有查询到 将数据插入bucket中,返回bucket
TheBucket = InsertIntoBucket(TheBucket, Key, std::forward<Ts>(Args)...);
// 通过 make_pair生成相应的键值对
return std::make_pair(
makeIterator(TheBucket, getBucketsEnd(), true),
true);//true表示第一次往哈希关联表中添加bucket
}
- 首先去根据
key去查找有没有对应的bucket,如果有将bucket进行封装返回 - 如果
没有将去找空的bucket,如果空的bucket也没有就会去创建bucket,然后将数据存在bucket中
注意:Key就是系统对object进行封装的结构,Args是传过来空的 ObjectAssociationMap{}对象关联表。
LookupBucketFor探究
进入LookupBucketFor方法发现有两个相同的方法,源码如下:

这两个方法的区别就是第二个参数一个带
const修饰一个不带,很明显是第二个调用第一个方法。而且第二个参数值是指针类型,也就是常说的指针传递。
- 首先根据
key获取到的bucket地址存放在ConstFoundBucket指针中,然后将ConstFoundBucket指针的地址赋值给&FoundBucket指针中,这样FoundBucket存放的数据就会实时更新 - 如果查询到就返回
true
template<typename LookupKeyT>
bool LookupBucketFor(const LookupKeyT &Val,
const BucketT *&FoundBucket) const {
//获取buckets的首地址
const BucketT *BucketsPtr = getBuckets();
//获取当前buckets的个数
const unsigned NumBuckets = getNumBuckets();
// 如果NumBuckets = 0 返回 false
if (NumBuckets == 0) {
FoundBucket = nullptr;
return false;
}
// FoundTombstone - Keep track of whether we find a tombstone while probing.
const BucketT *FoundTombstone = nullptr;
const KeyT EmptyKey = getEmptyKey();// 获取肯空bucket的key
const KeyT TombstoneKey = getTombstoneKey();
assert(!KeyInfoT::isEqual(Val, EmptyKey) &&
!KeyInfoT::isEqual(Val, TombstoneKey) &&
"Empty/Tombstone value shouldn't be inserted into map!");
//hash 获取下标和cache中的很像
unsigned BucketNo = getHashValue(Val) & (NumBuckets-1);
unsigned ProbeAmt = 1;
// 进行while循环
while (true) {
//根据下标找到对应的`bucket`内存偏移 ThisBucket = 首地址 + 第几个
const BucketT *ThisBucket = BucketsPtr + BucketNo;
// Found Val's bucket? If so, return it.
// 如果查询到`bucket`的`key`和`Val`相等 返回当前的bucket说明查询到了
if (LLVM_LIKELY(KeyInfoT::isEqual(Val, ThisBucket->getFirst()))) {
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的 目的是可以向空的bucket插入数据
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.
if (KeyInfoT::isEqual(ThisBucket->getFirst(), TombstoneKey) &&
!FoundTombstone)
FoundTombstone = 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.
if (ProbeAmt > NumBuckets) {
FatalCorruptHashTables(BucketsPtr, NumBuckets);
}
// BucketNo ++
BucketNo += ProbeAmt++;
//在hash获取下标
BucketNo &= (NumBuckets-1);
}
}
在LookupBucketFor下断点进行调试源码如下

因为是
第一进行关联对象还没有创建关联对象表所以地址是空的,也没有bucket,所以返回false FoundBucket = nil。
在try_emplace方法中的InsertIntoBucket下断点,查看写TheBucket是否为nil

TheBucket确实为nil,下面进入插入流程。
InsertIntoBucket探究
Btemplate <typename KeyArg, typename... ValueArgs>
BucketT *InsertIntoBucket(BucketT *TheBucket, KeyArg &&Key,
ValueArgs &&... Values) {
//获取空的`bucket`
TheBucket = InsertIntoBucketImpl(Key, Key, TheBucket);
//将key插入TheBucket中的key为first中
TheBucket->getFirst() = std::forward<KeyArg>(Key);
//如果的将value值插入TheBucket中的key为second中
::new (&TheBucket->getSecond()) ValueT(std::forward<ValueArgs>(Values)...);
return TheBucket;
}
核心功能获取空的bucket然后进行键值的匹配存储将key和value插入到bucket中。
InsertIntoBucketImpl
template <typename LookupKeyT>
BucketT *InsertIntoBucketImpl(const KeyT &Key, const LookupKeyT &Lookup,
BucketT *TheBucket) {
// NewNumEntries 表示将要插入一个 bucket
unsigned NewNumEntries = getNumEntries() + 1;
//获取bucket总个数
unsigned NumBuckets = getNumBuckets();
//如果当前要插入的个数 大于等于总个数的3/4 进行两倍扩容
if (LLVM_UNLIKELY(NewNumEntries * 4 >= NumBuckets * 3)) {
this->grow(NumBuckets * 2);//进行两倍扩容,但是如果NumBuckets = 0 默认是开辟4个buckeet
LookupBucketFor(Lookup, TheBucket);
NumBuckets = getNumBuckets();
} else if (LLVM_UNLIKELY(NumBuckets-(NewNumEntries+getNumTombstones()) <=
NumBuckets/8)) {
//这种情况是负载小于1/8,这种情况比较复杂暂不讨论 很少会出现
this->grow(NumBuckets);
LookupBucketFor(Lookup, TheBucket);
}
ASSERT(TheBucket);
//
if (KeyInfoT::isEqual(TheBucket->getFirst(), getEmptyKey())) {
// Replacing an empty bucket.
// 当前bucket被占用的数量 + 1
incrementNumEntries();
} else if (KeyInfoT::isEqual(TheBucket->getFirst(), getTombstoneKey())) {
// Replacing a tombstone.
incrementNumEntries();
decrementNumTombstones();
} else {
// we should be purging a zero. No accounting changes.
ASSERT(ValueInfoT::isPurgeable(TheBucket->getSecond()));
TheBucket->getSecond().~ValueT();
}
return TheBucket;
}
核心功能就是开辟内存然后获取空的bucket,开辟内存是this->grow(NumBuckets),获取空的bucket是通过LookupBucketFor,在this->grow(NumBuckets)下断点:

进入
两倍扩容流程grow(NumBuckets * 2),跟断点流程如下进入grow方法:
void grow(unsigned AtLeast) {
static_cast<DerivedT *>(this)->grow(AtLeast);
}
断点进入grow(unsigned AtLeast)方法:
#define MIN_BUCKETS 4
void grow(unsigned AtLeast) {
unsigned OldNumBuckets = NumBuckets;
BucketT *OldBuckets = Buckets;
// MIN_BUCKETS = 4
allocateBuckets(std::max<unsigned>(MIN_BUCKETS, static_cast<unsigned>(NextPowerOf2(AtLeast-1))));
ASSERT(Buckets);
if (!OldBuckets) {
this->BaseT::initEmpty();
return;
}
this->moveFromOldBuckets(OldBuckets, OldBuckets+OldNumBuckets);
// Free the old table.
operator delete(OldBuckets);
}
- allocateBuckets方法就是根据bucket的类型和Num开辟的个数去开辟内存空间
-
bucket的个数是MIN_BUCKETS和NextPowerOf2(AtLeast-1))取最大值。MIN_BUCKETS是一个宏值为4 -
BaseT::initEmpty()初始化空的bucket具体类型根据T的类型注意空的桶子key为first对应的值是1 -
moveFromOldBuckets将旧的buckets移动到新的buckets中,这点和cache扩容是不一样 - 释放旧的
buckets
断点进入NextPowerOf2方法:
// 32位
/// NextPowerOf2 - 返回 2 的下一个幂(32 位)
/// 严格大于 A。溢出时返回零。
inline uint32_t NextPowerOf2(uint32_t A) {
A |= (A >> 1);
A |= (A >> 2);
A |= (A >> 4);
A |= (A >> 8);
A |= (A >> 16);
return A + 1;
}
// 64位
/// NextPowerOf2 - 返回 2 的下一个幂(64 位)
/// 严格大于 A。溢出时返回零。
inline uint64_t NextPowerOf2(uint64_t A) {
A |= (A >> 1);
A |= (A >> 2);
A |= (A >> 4);
A |= (A >> 8);
A |= (A >> 16);
A |= (A >> 32);
return A + 1;
}
在macOS中断点进入的是uint32_t NextPowerOf2方法,严格大于A,超过的话就返回0。且参数A是无符号整型。目前传来的参数是-1,-1转换成二进制的规则是:1的原码=0x01,取反=0xfe,再加1=0xff,如果是32位,-1的二进制是0xffffffff。如果是64位-1的二进制是0xffffffffffffffff,在进行A |= (A >> n),其实就是等于其本身,然后A+1溢出等于0,当然你也可以理解为-1 + 1 = 0,因为是两倍扩容,而二进制之间也是2倍,所以只要不超过范围就是等于传进来的值。
断点进入initEmpty方法:
void initEmpty() {
setNumEntries(0);
setNumTombstones(0);
ASSERT((getNumBuckets() & (getNumBuckets()-1)) == 0 &&
"# initial buckets must be a power of two!");
//设置空的key
const KeyT EmptyKey = getEmptyKey();
for (BucketT *B = getBuckets(), *E = getBucketsEnd(); B != E; ++B)
::new (&B->getFirst()) KeyT(EmptyKey);
}
断点进入getEmptyKey,getEmptyKey方法很多最后进入的方法如下:
static inline DisguisedPtr<T> getEmptyKey() {
return DisguisedPtr<T>((T*)(uintptr_t)-1);
}
DisguisedPtr我们应该很熟悉,object也是封装成DisguisedPtr类型
template** <typename T>
class DisguisedPtr {
uintptr_t value;
static uintptr_t disguise(T* ptr) {
return -(uintptr_t)ptr;
}
...
}
ptr = (uintptr_t)-1,再次经过-(uintptr_t)ptr计算。就等于-(uintptr_t)((uintptr_t)-1) = 1 所以最后的value = 1,也就意味着空的bucket中key为first的值是value = 1。
扩容完成以后开始调用LookupBucketFor(Lookup, TheBucket)。Lookup就是封装好的object,作为参数传进来的。

调用
LookupBucketFor返回的是一个空的bucket。就是扩容以后创建的buckets中的一个.此时bucket的总个数是4个
断点继续调试会进入incrementNumEntries()方法:

incrementNumEntries()方法就是获取当前的有数据buckets的个数,之前是0,调用以后变成1。
void incrementNumEntries() {
// getNumEntries() 是原来有数据的个数
// getNumEntries() +1 表示将要插入的
// 设置NumEntries的个数
setNumEntries(getNumEntries() + 1);
}
源码显示将原有的NumEntries的数量加1。incrementNumEntries()执行完以后返回bucket。
TheBucket的first赋值
//获取键值里面的first键
KeyT &getFirst() { return std::pair<KeyT, ValueT>::first; }
TheBucket->getFirst() = std::forward<KeyArg>(Key)把Key赋值给TheBucket中的first。

的确是将
Key赋值给TheBucket的first。
TheBucket的second赋值

TheBucket的second默认可能有脏数据,将value赋值给second 赋值成功后返回通过make_pair封装好的bucket。
isFirstAssociation 第一次关联

secound = true是make_pair方法中的第二个参数为true赋值的,意思就是第一次关联该对象。
获取ObjectAssociationMap

根据
disguised通过refs_result.first->second获取AssociationsHashMap表中的ObjectAssociationMap。插入
association的数据或者是更新ObjectAssociationMap中的已有的数据:
result.second = ture表示在ObjectAssociationMap中没有该数据是第一次存储,如果result.second = false表示ObjectAssociationMap中有该数据然后更新数据。此时存储的第一个属性是XJLPerson类中的lw_name,存储的值是luwenhan。
ObjectAssociationMap的结构和cache结构是比较相似,不仅可以存储值,还可以有其它的变量配合使用。
设置isa中是关联对象属性
...
if (isFirstAssociation)
object->setHasAssociatedObjects()
...
isFirstAssociation第一次给关联对象设置值的时候才会调用setHasAssociatedObjects方法:
objc_object::setHasAssociatedObjects()
{
if (isTaggedPointer()) return;
...
isa_t newisa, oldisa = LoadExclusive(&isa.bits);
do {
newisa = oldisa;
if (!newisa.nonpointer || newisa.has_assoc) {
ClearExclusive(&isa.bits);
return;
}
//将isa中的has_assoc 设置为true
newisa.has_assoc = true;
} while (slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)));
}
通过setHasAssociatedObjects方法设置对象存在关联对象,即isa指针的has_assoc属性设置为true,最后通过releaseHeldValue方法释放旧值。
关联对象存储流程图

关联对象 - get
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方法,关联对象取值就是比较简单的了就是查表:
id
_object_get_associative_reference(id object, const void *key)
{
//创建空的关联对象
ObjcAssociation association{};
{ //创建一个管理类
AssociationsManager manager;
//获取全局唯一的HashMap表
AssociationsHashMap &associations(manager.get());
// iterator是个迭代器实际上相当于找到objc_object和对应的ObjectAssociationMap
// 类似{first = objc_object,second =ObjectAssociationMap}结构
AssociationsHashMap::iterator i = associations.find((objc_object *)object);
//如果这个迭代器封装的一个结构不是最后一个进入判断流程
if (i != associations.end()) {
//获取 ObjectAssociationMap
ObjectAssociationMap &refs = i->second;
//获取 ObjectAssociationMap 的迭代器
ObjectAssociationMap::iterator j = refs.find(key);
//如果这个迭代器封装的一个结构不是最后一个进入判断流程
if (j != refs.end()) {
//获取 association
association = j->second;
//retain 新值
association.retainReturnedValue();
}
}
}
//release 旧值,返回新值
return association.autoreleaseReturnedValue();
}
直接调试查看最后的结果:

关联对象取值其实就是一个查表取值的过程,然后返回存储的值。相比较关联对象存值,取值流程也更加容易理解。
总结
这篇文章只要是理解查表的存取值过程,从而更深入理解关联对象的底层原理,需要花好多时间区分析源码,可能说关联对象的底层我们极少用到,但是对探究过程你对苹果底层的封装思想以及哈希表都会有一个深刻的理解,大家再接再厉把!!?










