我们在前面的文章讲到类和分类的加载原理,今天我们来探索下类扩展和关联对象。
在这之前我们下来看看类扩展和分类的区别:
1:category
:类别、分类
· 专门用来给类添加新的方法
· 不能给类添加成员属性,添加了成员变量,也无法取到
· 注意:其实可以通过runtime
给分类添加属性
· 分类中用 @property
定义变量,只会生成变量的getter
、setter
方法的声明,不能生成方法实现和带下划线的成员变量。
2:extension
:类扩展
· 可以说成是特殊的分类,也称作匿名分类
· 可以给类添加成员属性,但是是私有变量
· 可以给类添加方法,也是私有方法
类扩展
下面我们直接在main.m
文件里写一个ZYTeacher
类 并且写一个他的分类如下图:
所以分类是我们在开发中使用非常频繁的一个东西,下面我们利用clang
命令clang -rewrite-objc main.m -o main.cpp
将这个main.m
文件转换成c++
文件main.cpp
,然后去查看下c++
的实现是什么样的。
我们分别看下属性和方法:
我们可以发现类扩展的属性和方法都是存在的,实现都和类的属性方法一样,并没并没有出现像前面文章里分析分类
category
时候出现的分类特出处理情况。
那我们不禁要想一下,类扩展和分类与这样的区别那类扩展是否会像分类一样影响主类的加载呢?(因为前面我们发现分类添加load方法与否会影响主类的加载)我们探索下:
我们创建一个ZYPerson
类实现几个方法,然后创建一个ZYPerson
的类扩展文件命名为ZYPerson (EXTA)
并且声明两个方法,然后将分类头文件引入到ZYPerson.m
里最后实现分类声明的方法。我们来在objc818源码
里运行这几个文件(在mian.m
里调用方法),跟踪一下加载流程。
ZYPerson .h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface ZYPerson : NSObject
- (void)zyEatSugar;
+ (void)sayHappy;
- (void)zyShowTime;
@end
NS_ASSUME_NONNULL_END
ZYPerson .m
#import "ZYPerson.h"
#import "ZYPerson+EXTA.h"
@implementation ZYPerson
+(void)load{
}
- (void)zyEatSugar
{
NSLog(@"%s",__func__);
}
+ (void)sayHappy
{
NSLog(@"%s",__func__);
}
- (void)zyShowTime
{
NSLog(@"%s",__func__);
}
- (void)zyT_extA_instanceMethod1
{
NSLog(@"%s",__func__);
}
+ (void)zyT_extA_classMethod
{
NSLog(@"%s",__func__);
}
@end
ZYPerson (EXTA).h
#import "ZYPerson.h"
NS_ASSUME_NONNULL_BEGIN
@interface ZYPerson ()
- (void)zyT_extA_instanceMethod1;
+ (void)zyT_extA_classMethod;
@end
NS_ASSUME_NONNULL_END
最后我们到main.m
文件调用主类的方法:
我们同样效仿之前探索类加载的方法在之前添加特殊打印和断点的地方都加上。然后运行程序在每一个点去获取ro
,看看什么时候类扩展的方法加载进去了:
结论:类扩展的方法在编译阶段就已经添加到了类里面也就是
data()
里面了。类扩展和分类是不一样的。
关联对象
我们知道分类/category
不能添加属性/成员变量
。开篇也有说明。并且给出了解决这个问题的方案就是利用runtime
来实现添加属性。这也是我们熟悉的关联对象
。那么关联对象的实现原理是怎么样的呢?我们来探究下:
直接上代码:
ZYPerson+ZYA.h
#import "ZYPerson.h"
NS_ASSUME_NONNULL_BEGIN
@interface ZYPerson (ZYA)
@property (nonatomic, copy) NSString *zyA_name;
@property (nonatomic, copy) NSString *zyA_age;
- (void)saySomething;
- (void)zyA_instanceMethod1;
- (void)zyA_instanceMethod2;
+ (void)zyA_classMethod1;
+ (void)zyA_classMethod2;
@end
NS_ASSUME_NONNULL_END
ZYPerson+ZYA.m
#import "ZYPerson+ZYA.h"
#import <objc/runtime.h>
@implementation ZYPerson (ZYA)
+(void)load
{
}
+ (void)zyA_classMethod1
{
NSLog(@"%s",__func__);
}
+ (void)zyA_classMethod2
{
NSLog(@"%s",__func__);
}
- (void)saySomething
{
NSLog(@"%s",__func__);
}
- (void)zyA_instanceMethod1
{
NSLog(@"%s",__func__);
}
- (void)zyA_instanceMethod2
{
NSLog(@"%s",__func__);
}
/*
* 关联对象
**/
- (void)setZyA_name:(NSString *)zyA_name
{
/**
1: 对象
2: 标识符
3: value
4: 策略
*/
objc_setAssociatedObject(self, "zyA_nameKey", zyA_name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)zyA_name
{
return objc_getAssociatedObject(self, "zyA_nameKey");
}
- (void)setZyA_age:(NSString *)zyA_age
{
objc_setAssociatedObject(self, "zyA_ageKey", zyA_age, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
-(NSString *)zyA_age
{
return objc_getAssociatedObject(self, "zyA_ageKey");
}
@end
mian.m
#import <Foundation/Foundation.h>
#import "ZYPerson.h"
#import "ZYPerson+ZYA.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
ZYPerson *person = [ZYPerson alloc];
person.zyA_name = @"Wayne";
person.zyA_age = @"20";
}
return 0;
}
探索:
我们直接到ZYPerson+ZYA.m
文件关联对象方法里。直接command+点击
查看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)
{
// 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/44094390
if (!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));
DisguisedPtr<objc_object> disguised{(objc_object *)object};
ObjcAssociation association{policy, value};
// retain the new value (if any) outside the lock.
association.acquireValue();
bool isFirstAssociation = false;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.get());
if (value) {
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 */
auto &refs = refs_result.first->second;
auto result = refs.try_emplace(key, std::move(association));
if (!result.second) {
association.swap(result.first->second);
}
} else {
auto refs_it = associations.find(disguised);
if (refs_it != associations.end()) {
auto &refs = refs_it->second;
auto it = refs.find(key);
if (it != refs.end()) {
association.swap(it->second);
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.
if (isFirstAssociation)
object->setHasAssociatedObjects();
// release the old value (outside of the lock).
association.releaseHeldValue();
}
上面这个方法 总共60多行代码,我们稍微分析下看看哪些是重点。
1,首先传进来的参数:
object
:关联对象(ZYPerson(ZYA)
);
key
: 我们给属性设置的key
(zyA_nameKey
);
value
:我们属性的值(Wayne
);
policy
: 策略 (存储的策略)
2,第一部分,头部代码:
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/44094390
if (!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 包装成 一个统一的 数据结构 ptr
DisguisedPtr<objc_object> disguised{(objc_object *)object};
//策略 policy 、 value
ObjcAssociation association{policy, value};
// retain the new value (if any) outside the lock.
association.acquireValue();
//省略下方代码
}
这一段代码主要做了两件事:
第一件: DisguisedPtr<objc_object> disguised{(objc_object *)object};
将传入的对象/object
进行处理,变成格式统一的ptr
数据结构形式。方便统一管理。
第二件: ObjcAssociation association{policy, value};
将传入的策略/policy
和值/value
进行处理,变成一个对象形式的association
。
3,第二部分,尾部代码:
void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
// 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.
if (isFirstAssociation)
object->setHasAssociatedObjects();
// release the old value (outside of the lock).
association.releaseHeldValue();
}
这部分代码主要是做最后的释放处理,所以也不是我们寻找的目标。
4,第三部分,中间代码:
void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
bool isFirstAssociation = false;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.get());
if (value) {
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 */
auto &refs = refs_result.first->second;
auto result = refs.try_emplace(key, std::move(association));
if (!result.second) {
association.swap(result.first->second);
}
} else {
auto refs_it = associations.find(disguised);
if (refs_it != associations.end()) {
auto &refs = refs_it->second;
auto it = refs.find(key);
if (it != refs.end()) {
association.swap(it->second);
refs.erase(it);
if (refs.size() == 0) {
associations.erase(refs_it);
}
}
}
}
}
}
这一段代码是重点,因为从其他两部分代码分析知道所有的存储和获取逻辑都在这一段代码里。我们先在AssociationsHashMap &associations(manager.get());
这行代码打一个断点然后运行程序让程序到这个方法里来。
从上图也可以看到lldb
调试出来的前面包装的disguised
和association
的数据结构
#######方法:AssociationsManager manager;
我们再看看AssociationsManager manager;
这句代码,这句代码的作用是调用构造函数。作用域就是上面代码的最外层{}
。实现了在这个作用域每次来就调用一次并且上锁的作用,如下代码(构造函数和析构函数我们在补充里有例子解析)
方法:AssociationsManager
class AssociationsManager {
using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
static Storage _mapStorage;
public:
AssociationsManager() { AssociationsManagerLock.lock(); }
~AssociationsManager() { AssociationsManagerLock.unlock(); }
AssociationsHashMap &get() {
return _mapStorage.get();
}
static void init() {
_mapStorage.init();
}
};
#######方法:AssociationsHashMap &associations(manager.get());
点击get
进入到上方的AssociationsManager
实现方法可以看到里面有这里两部分代码:
AssociationsHashMap &get() {
return _mapStorage.get();
}
static Storage _mapStorage;
方法:AssociationsHashMap
typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;
上方代码就是表明AssociationsHashMap
是一个单例,因为static Storage _mapStorage;
表明是全局静态变量,这个地方的两句代码很容易让人搞混淆,容易让人觉得AssociationsManager
是个单例然而恰恰相反 内部实现可以发现原来这个AssociationsHashMap
才是一个单例。每次AssociationsManager
创建manager
然后去操作HashMap
的时候就去调用AssociationsHashMap
这个单例保证每次进来调用的都是同一张表。
验证单例:
我们利用创建多个manager
和多个associations
打印地址来验证。
因为class AssociationsManager {}
对于mananger
创建有加锁,如果创建多个的话会导致崩溃那我们就去掉锁:
接下来我们看看assouciations
和 refs_result
的数据结构:
assouciations
数据类型比较简单,但是这个refs_result
的类型是个什么鬼?这么长?幸好内容比较简单。那我们看看refs_result
这个值的获取方法associations.try_emplace(disguised, ObjectAssociationMap{})
// 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.
template <typename... Ts>
std::pair<iterator, bool> try_emplace(const KeyT &Key, Ts &&... Args) {
BucketT *TheBucket;
if (LookupBucketFor(Key, TheBucket))
return std::make_pair(
makeIterator(TheBucket, getBucketsEnd(), true),
false); // Already in map.
// Otherwise, insert the new element.
TheBucket = InsertIntoBucket(TheBucket, Key, std::forward<Ts>(Args)...);
return std::make_pair(
makeIterator(TheBucket, getBucketsEnd(), true),
true);
}
这里传进来一个disguised
和一个ObjectAssociationMap{}
,所以这里的key
是个对象也就是我们的object
封装后的disguised
。然后创建了一个TheBucket
。然后调用了一个LookupBucketFor
的方法传入这个key
和TheBucket
。我们看看这个LookupBucketFor
方法,发现有两个:
第一个:
template<typename LookupKeyT>
bool LookupBucketFor(const LookupKeyT &Val,
const BucketT *&FoundBucket) const {
//省略内部代码 太长了
}
第二个:
template <typename LookupKeyT>
bool LookupBucketFor(const LookupKeyT &Val, BucketT *&FoundBucket) {
const BucketT *ConstFoundBucket;
bool Result = const_cast<const DenseMapBase *>(this)
->LookupBucketFor(Val, ConstFoundBucket);
FoundBucket = const_cast<BucketT *>(ConstFoundBucket);
return Result;
}
分析:从传入的参数TheBucket
我们可以知道我们应该看第二个方法,因为第一个方法对于这个TheBucket
的要求有const
修饰。而我们在上面的方法看到创建的并没有。
我们从第二个 LookupBucketFor
的处理方法可以知道最终还是调用了第一个方法的LookupBucketFor
获取到Result
值返回,同时需要注意的是这里传入的TheBucket
是个指针传递,为什么呢?因为我们看这行代码:
FoundBucket = const_cast<BucketT *>(ConstFoundBucket);
这句代码做的处理就是把传进来的TheBucket/FoundBucket
处理之后又进行了赋值。就是说他可以把值带回上一个方法。
所以我们下一步就要回到上面的第一个LookupBucketFor
方法
方法:bool LookupBucketFor(const LookupKeyT &Val, const BucketT *&FoundBucket) const
/// 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.
template<typename LookupKeyT>
bool LookupBucketFor(const LookupKeyT &Val,
const BucketT *&FoundBucket) const {
const BucketT *BucketsPtr = getBuckets();
const unsigned NumBuckets = getNumBuckets();
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();
const KeyT TombstoneKey = getTombstoneKey();
assert(!KeyInfoT::isEqual(Val, EmptyKey) &&
!KeyInfoT::isEqual(Val, TombstoneKey) &&
"Empty/Tombstone value shouldn't be inserted into map!");
unsigned BucketNo = getHashValue(Val) & (NumBuckets-1);
unsigned ProbeAmt = 1;
while (true) {
const BucketT *ThisBucket = BucketsPtr + BucketNo;
// Found Val's bucket? If so, return it.
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.
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 += ProbeAmt++;
BucketNo &= (NumBuckets-1);
}
}
上面这个方法 一开始就是一些判断、创建变量之类的然后到下面这句代码:
unsigned BucketNo = getHashValue(Val) & (NumBuckets-1);
这句代码其实作用就是利用hash
函数得到 bucketNo
即bucket
的下标 同cache_t
查找bucket
时候获取那个index
一样。
然后就是下方的一个while(true){}
的死循环。这个死循环就跟之前那个cache_t
里查找bucket
的do while
循环一样的。根据查找结果最后出去回到方法template <typename... Ts> std::pair<iterator, bool> try_emplace(const KeyT &Key, Ts &&... Args)
。
方法:template <typename... Ts> std::pair<iterator, bool> try_emplace(const KeyT &Key, Ts &&... Args)
:
// 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.
template <typename... Ts>
std::pair<iterator, bool> try_emplace(const KeyT &Key, Ts &&... Args) {
BucketT *TheBucket;
if (LookupBucketFor(Key, TheBucket))
return std::make_pair(
makeIterator(TheBucket, getBucketsEnd(), true),
false); // Already in map.
// Otherwise, insert the new element.
TheBucket = InsertIntoBucket(TheBucket, Key, std::forward<Ts>(Args)...);
return std::make_pair(
makeIterator(TheBucket, getBucketsEnd(), true),
true);
}
找到了就走if
然后return
返回结果,如果没找到就走下面的代码
将上面创建的空的bucket
调用方法InsertIntoBucket
插入一个表。
方法:InsertIntoBucket
template <typename KeyArg, typename... ValueArgs>
BucketT *InsertIntoBucket(BucketT *TheBucket, KeyArg &&Key,
ValueArgs &&... Values) {
TheBucket = InsertIntoBucketImpl(Key, Key, TheBucket);
TheBucket->getFirst() = std::forward<KeyArg>(Key);
::new (&TheBucket->getSecond()) ValueT(std::forward<ValueArgs>(Values)...);
return TheBucket;
}
方法:InsertIntoBucketImpl
:
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.
//
// 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.
unsigned NewNumEntries = getNumEntries() + 1;
unsigned NumBuckets = getNumBuckets();
if (LLVM_UNLIKELY(NewNumEntries * 4 >= NumBuckets * 3)) {
this->grow(NumBuckets * 2);
LookupBucketFor(Lookup, TheBucket);
NumBuckets = getNumBuckets();
} else if (LLVM_UNLIKELY(NumBuckets-(NewNumEntries+getNumTombstones()) <=
NumBuckets/8)) {
this->grow(NumBuckets);
LookupBucketFor(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.
if (KeyInfoT::isEqual(TheBucket->getFirst(), getEmptyKey())) {
// Replacing an empty bucket.
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;
}
在插入方法中也是按照3/4 两倍扩容
的方式来插入,这里不做过多分析(跟前面的很多扩容一样)在插入之后就返回这个bucket
:
然后就是去调用方法make_pair
并且利用迭代器makeIterator
来实现TheBucket
和getBucketsEnd()
配对。最后我们看到第二个参数返回的是个true
。这个参数就是前面refs_result
里的refs_result.second
。用来if
判断的。
到这里我们disguised
已经做了处理,但是至此,我们保存value
的associations
还没有做处理和关联,所以有了下面的步骤
然后利用refs.try_emplace(key, std::move(association));
把key
和association
做关联.
我们可以看到上图的LookupBucketFor
的判断还是为false
不走if
。因为此时打印的TheBucket
是我们前面创建的一个空的桶子所以不走if
,看图
所以会走下面的insert
,然后就走了上面创建桶子插入空桶子同样的流程就不做重复解释了。最后返回一个结果 到上一个方法:
流程总结:
1,_object_set_associative_reference
方法被调用传进来一个对象object
、一个键key
、一个值value
、一个策略policy
。
2,先对对象object
做处理包装成一个统一格式的disguised
。DisguisedPtr<objc_object> disguised{(objc_object *)object};
3,对值value
和策略policy
进行处理包装成一个association
。ObjcAssociation association{policy, value};
4,构造一个函数mananger
。AssociationsManager manager;
5,创建一个AssociationsHashMap
的单例查找到一个总的hash表
。AssociationsHashMap &associations(manager.get());
6,对disguised
进行处理并判断是否为第一次进来。auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
查看bucket
存在不不存在就创建一个空的bucket
返回(第一次进来),存在就直接返回。
7,将封装好的association
和key
进行关联,插入到bucket
里。auto result = refs.try_emplace(key, std::move(association));
总结:
关联对象: 设值流程
1: 创建⼀个 AssociationsManager
管理类
2: 获取唯⼀的全局静态哈希Map
3: 判断是否插⼊的关联值是否存在:
3.1: 存在⾛第4步
3.2: 不存在就⾛ : 关联对象插⼊空流程
4: 创建⼀个空的 ObjectAssociationMap
去取查询的键值对
5: 如果发现没有这个key
就插⼊⼀个 空的 BucketT
进去 返回
6: 标记对象存在关联对象
7: ⽤当前 修饰策略 和 值 组成了⼀个ObjcAssociation
替换原来 BucketT
中的空
8: 标记⼀下 ObjectAssociationMap
的第⼀次为false
关联对象插⼊空流程:
1: 根据 DisguisedPtr
找到 AssociationsHashMap
中的 iterator
迭代查询器
2: 清理迭代器
3: 其实如果插⼊空置 相当于清除
关联对象: 取值流程:
1: 创建⼀个 AssociationsManager
管理类
2: 获取唯⼀的全局静态哈希Map
3: 根据DisguisedPtr
找到 AssociationsHashMap
中的 iterator
迭代查询器
4: 如果这个迭代查询器不是最后⼀个 获取 : ObjectAssociationMap
(这⾥有策略
和value
)
5: 找到ObjectAssociationMap
的迭代查询器获取⼀个经过属性修饰符修饰的value
6: 返回_value
总结: 其实就是两层
哈希map
, 存取的时候两层处理(类似⼆位数组)
关联对象总结图:
补充
构造函数、析构函数
这里直接写了个例子,可以直观的明白构造函数和析构函数的结构以及调用: