preface

We explored the loading of categories in the basic principles of IOS and the loading of categories. Today we’re going to look at the storage of attributes in categories, also known as associated objects, and the exploration of class extension

The preparatory work

  • Objc – 818.2 –
  • Iced watermelon

Class extensions

Class extension

  • Attach additional attributes, member variables, and method declarations to a class
  • Many blogs on the web write that class extension is a special kind of anonymous classification. I do not agree with this statement. Classification and class extension are fundamentally different
  • The bottom of the classification iscategory_tStruct type, while the class extension layer is compiled directly into the main class
  • General class extension write.mIn a file, general private attributes or attributes that you want to distinguish independently are written to the class extension

Class extension format

.mFormat in file

@interface LWTeacher(a)
{
    NSInteger  height; // Member variables
}
@property(nonatomic,assign)NSInteger  lw_age;/ / property- (void)helloWord;// Object method+ (void)helloClass;/ / class methods
@end
Copy the code

This declaration of class extensions in.m files is something that developers use almost every day, but few of us know it as a class extension. Note that class extensions must be written directly in the class declaration and the class implementation

Question: there are no class declarations in the.m file, only the extension and implementation class declarations are in the.h file, which will be expanded into the.m file at compile time

Extension

#import "LWTeacher.h"

@interface LWTeacher (a)
@property(nonatomic,assign)int  age;
@end
Copy the code

There is only one.h file and no corresponding.m when creating the class extension. All of the implementation of a class Extension is implemented in a.m file. Extension means that the Extension method of the.m file is written into a separate header file

Class extension low-level exploration

The purpose of writing LWTeacher in main.m is to better explore the. CPP file, the code is as follows

 @interface LWTeacher : NSObject
- (void)sayHello;
@end

@interface LWTeacher(a)
{
    NSInteger height;
}

@property(nonatomic,assign)NSInteger lw_age; - (void)helloWord; + (void)helloClass;
@end

@implementation LWTeacher
+(void)load{
}
- (void)sayHello{
}
- (void)helloWord{   
}
+(void)helloClass{    
}
@end
 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LWTeacher * teacher = [[LWTeacher alloc] init];
        [teacher sayHello];
    }
    return 0;
}
Copy the code

Generate main. CPP file from main.m file

The variables and methods shown in the figure for class extensions are determined at compile time and are stored in the class. To verify that this is done at compile time, set a breakpoint in the _read_images method

It is clear from the diagram that the method of class extension is also in RO, which is determined at compile time. So now it follows that a class can have multiple class extensions, but all implementations are implemented in a.m file

Associated object –set

The application scenario of the associated object is generally adding attributes in the classification. Now we will explore the underlying implementation of the associated object. Create LWTeacher+HH and 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

Copy the code

Define 4 attributes in LWPerson+LWA classification and create define 1 attribute in LWTeacher+HH classification. Add a breakpoint in objc_setAssociatedObject, run the source code, and go to objc_setAssociatedObject

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

Enter the _object_set_associative_reference method inside the code is more, look at the implementation of the core code

void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
  
    if(! object && ! value)return; // No value is returned directly
    if (object->getIsa()->forbidsAssociatedObjects())
        ...
    // Object is encapsulated into DisguisedPtr to facilitate the unified processing at the lower level
    DisguisedPtr<objc_object> disguised{(objc_object *)object};
    // Encapsulate policy and value into objcassociety to facilitate unified processing at the bottom level
    ObjcAssociation association{policy, value};

    // retain the new value (if any) outside the lock.
    // Determine whether to retain or copy according to the policy
    association.acquireValue(a);bool isFirstAssociation = false;
    {   // Instantiate AssociationsManager note that this is not a singleton
        AssociationsManager manager;
         // instantiate the global AssociationsHashMap
        AssociationsHashMap &associations(manager.get());
        // If value has a value
        if (value) {
            //AssociationsHashMap: associative table ObjectAssociationMap: associative table of objects
            // First find out if there are any object associated tables in the backup of the object encapsulated file
            // Return the table directly if there is one, and create the object associated tables in 'backup' if there is not
            When (The number of objects +1 is greater than or equal to 3/4, expand the capacity twice
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            if (refs_result.second) {
                /* it's the first association we make */
                // The object is associated for the first time
                isFirstAssociation = true;
            }

            /* establish or replace the association */
            // Get the address of the value stored in ObjectAssociationMap
            auto &refs = refs_result.first->second;
            // Store the values that need to be stored in the address where the values are stored in the associated table
            If 'result' = false, if 'result' = true '
            // Create association type When the number of associations +1 exceeds 3/4, the system performs double capacity expansion
            auto result = refs.try_emplace(key, std::move(association));
            if(! result.second) {// Exchange 'association' with 'association'
                // Replace the old value with the new value
                association.swap(result.first->second); }}else {
            //value does not have a value
            // Find a backup of the corresponding ObjectAssociationMap
            auto refs_it = associations.find(disguised);
            // If the corresponding ObjectAssociationMap object association table is found
            if(refs_it ! = associations.end()) {
                Refs_it ->second contains association type data
                auto &refs = refs_it->second;
                // Query the association based on the key
                auto it = refs.find(key);
                if(it ! = refs.end()) {
                    // Update the old association value
                    // The old association is stored in the association
                    association.swap(it->second);
                    If value= nil, free 'association' stored in the associated object table
                    refs.erase(it);
                    // If all attribute data in the object association table is cleared, the object association table will be released
                    if (refs.size() = =0) {
                        associations.erase(refs_it);

                    }
                }
            }
        }
    }
    SetHasAssociatedObjects is called every time the first associated object is called
    // Set the 'has_assoc' property of 'isa pointer' to 'true' by using the setHasAssociatedObjects method to mark the presence of an associated object
    if (isFirstAssociation)
        object->setHasAssociatedObjects(a);// release the old value (outside of the lock).
    // Release old values because any old values are swapped to 'association'
    // The new value of 'association' is stored in the object association table
    association.releaseHeldValue(a); }Copy the code

_object_set_associative_reference has two core functions

  • According to theobjectQuery in the associated tableObjectAssociationMapIf not, open up memory creationObjectAssociationMap, the rules are created inThree quarters of, perform double capacity expansion
  • Based on thekeyNoneassociationIt’s the associated datavalueandpolicy, if the query found directly update the data inside, if not to fetch emptyasociationType and then put the value in, ifObjectAssociationMapIs the first time to disassociate data or the number of associated data is satisfiedThree quarters of, perform double capacity expansion
  • Capacity expansion rules andcacheMethod stores the same rules

AssociationsManager Manager you might wonder why AssociationsHashMap & Associations is a singleton, Explore the AssociationsManager and AssociationsHashMap structures

class AssociationsManager {
    using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
    static Storage _mapStorage;

public:
    // The constructor locks
    AssociationsManager()   { AssociationsManagerLock.lock(a); }// Destructor unlocks
    ~AssociationsManager()  { AssociationsManagerLock.unlock(a); }// Get a global AssociationsHashMap
    AssociationsHashMap &get(a) {
        return _mapStorage.get(a); }static void init(a) {
        _mapStorage.init();
    }
};
Copy the code

Static Storage _mapStorage is a global static variable that can only be used inside the AssociationsManager. If you place it outside the AssociationsManager, the AssociationsManager has a constructor and a destructor. AssociationsManager is equivalent to calling the constructor lock function. When the scope of AssociationsManager ends, the destructor is automatically called to unlock the AssociationsManager. AssociationsManager is designed to prevent multithreaded access from being messy, and AssociationsHashMap is a global static variable fetch that is called only once

Verify whether AssociationsManager is a singleton

AssociationsManager is of class type. The MManager has not been initialized yet, but the Manager is of AssociationsManager type. And manager becomes AssociationsManager*, which is the pointer type, and manager stores the object type of AssociationsManager, The &manager, &Manager1, and &Manager2 addresses are different

Verify whether AssociationsHashMap & Associations is a singleton

obviouslyAssociationsHashMap &associationsIs a singleton, only singleton value open memory only one address, other Pointers are stored in this piece of address

try_emplacemethods

The following use breakpoint debugging with the flow of the way to explore

  template <typename. Ts>std::pair<iterator, bool> try_emplace(const KeyT &Key, Ts &&... Args) {
    BucketT *TheBucket;
    // Find the corresponding Bucket according to the key
    if (LookupBucketFor(Key, TheBucket))
      // Use make_pair to generate the corresponding key-value pairs
      return std::make_pair(
               makeIterator(TheBucket, getBucketsEnd(), true),
               false); //false indicates that the hash associated table already exists in a bucket

    // If there is no query to insert data into bucket, return bucket
    TheBucket = InsertIntoBucket(TheBucket, Key, std::forward<Ts>(Args)...) ;// Use make_pair to generate the corresponding key-value pairs
    return std::make_pair(
             makeIterator(TheBucket, getBucketsEnd(), true),
             true);//true indicates that buckets are added to the hash association table for the first time
  }
Copy the code
  • First go according tokeyLet’s see if there’s a matchbucket, if there will bebucketPackage return
  • If not, go to find emptybucketIf no empty bucket exists, it will be createdbucketAnd then save the databucketIn the

Args is an empty ObjectAssociationMap{} object association table

LookupBucketForTo explore the

Enter the LookupBucketFor method and you’ll find two identical methods, source code below

The difference between these two methods is that the second argument, one with const modifier and the other without const modifier, is obviously the second call to the first method. And the second parameter value is a pointer type, also known as pointer pass.

  • First obtained by keybucketAddress depositConstFoundBucketPointer, and then theConstFoundBucketThe address of the pointer assigned to&FoundBucketThe pointer looks like thisFoundBucketThe stored data is updated in real time
  • Return if foundtrue
template<typename LookupKeyT>
  bool LookupBucketFor(const LookupKeyT &Val,
                       const BucketT *&FoundBucket) const {
    // Get buckets first address
    const BucketT *BucketsPtr = getBuckets(a);// Get the current number of buckets
    const unsigned NumBuckets = getNumBuckets(a);// NumBuckets = 0 returns 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(a);// Get the bucket key
    const KeyT TombstoneKey = getTombstoneKey(a);assert(! KeyInfoT::isEqual(Val, EmptyKey) && ! KeyInfoT::isEqual(Val, TombstoneKey) &&
           "Empty/Tombstone value shouldn't be inserted into map!");
    // Hash retrives the subscript just like in cache
    unsigned BucketNo = getHashValue(Val) & (NumBuckets- 1);
    unsigned ProbeAmt = 1;
    // Do the while loop
    while (true) {
      ThisBucket = the first address + the number of bytes
      const BucketT *ThisBucket = BucketsPtr + BucketNo;
      // Found Val's bucket? If so, return it.
      // If 'key' of 'bucket' is equal to 'Val', return the current 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.
      // An empty bucket can be inserted into an empty 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++;
      // Get the subscript in hash
      BucketNo &= (NumBuckets- 1); }}Copy the code

In LookupBucketFor breakpoint under debugging source code as follows

Because it’s the first associated object that hasn’t created the associated object table yet, so the address is empty, and there’s no bucket, so return false FoundBucket = nil

InsertIntoBucket sets a breakpoint in the try_emplace method to see if writing TheBucket is nil

TheBucket is indeed nil, so let’s go into the insert process

InsertIntoBucket

  Btemplate <typename KeyArg, typename. ValueArgs>BucketT *InsertIntoBucket(BucketT *TheBucket, KeyArg &&Key, ValueArgs &&... Values) {
    // Get an empty bucket
    TheBucket =  InsertIntoBucketImpl(Key, Key, TheBucket);
    // insert the key into TheBucket
    TheBucket->getFirst() = std::forward<KeyArg>(Key);
    // insert value into key second in TheBucket: :new (&TheBucket->getSecond()) ValueT(std::forward<ValueArgs>(Values)...) ;return TheBucket;
  }
Copy the code

The core function retrieves an empty bucket and then performs a key-value matching store to insert the key and value into the bucket

InsertIntoBucketImpl

 
  template <typename LookupKeyT>
  BucketT *InsertIntoBucketImpl(const KeyT &Key, const LookupKeyT &Lookup,
                                BucketT *TheBucket) {
    // NewNumEntries indicate that a bucket will be inserted
    unsigned NewNumEntries = getNumEntries() + 1;
    // Get the total number of buckets
    unsigned NumBuckets = getNumBuckets(a);// If the number to be inserted is greater than or equal to 3/4 of the total number, double the capacity
    if (LLVM_UNLIKELY(NewNumEntries * 4 >= NumBuckets * 3)) {
      this->grow(NumBuckets * 2);// Bucket = 0; // Bucket = 0; // Bucket = 0
      LookupBucketFor(Lookup, TheBucket);
      NumBuckets = getNumBuckets(a); }else if (LLVM_UNLIKELY(NumBuckets-(NewNumEntries+getNumTombstones()) <=
                             NumBuckets/8)) {
      // The load is less than 1/8
      this->grow(NumBuckets);
      LookupBucketFor(Lookup, TheBucket);
    }
    ASSERT(TheBucket);

    //
    if (KeyInfoT::isEqual(TheBucket->getFirst(), getEmptyKey())) {
      // Replacing an empty bucket.
      // The number of buckets currently occupied + 1
      incrementNumEntries(a); }else if (KeyInfoT::isEqual(TheBucket->getFirst(), getTombstoneKey())) {
      // Replacing a tombstone.
      incrementNumEntries(a);decrementNumTombstones(a); }else {
      // we should be purging a zero. No accounting changes.
      ASSERT(ValueInfoT::isPurgeable(TheBucket->getSecond()));
      TheBucket->getSecond(). ~ValueT(a); }return TheBucket;
  }
Copy the code

The core function is to open memory and get an empty bucket. Open memory is this->grow(NumBuckets), and get an empty bucket by LookupBucketFor, which is a breakpoint under this->grow(NumBuckets)

Enter the double capacity expansion process grow(NumBuckets * 2). Follow the breakpoint process to enter the grow method

void grow(unsigned AtLeast) {
    static_cast<DerivedT *>(this) - >grow(AtLeast);
  }
Copy the code

Breakpoint into grow(unsigned AtLeast) method

  #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(a);return;
    }

    this->moveFromOldBuckets(OldBuckets, OldBuckets+OldNumBuckets);

    // Free the old table.
    operator delete(OldBuckets);
  } 
Copy the code
  • allocateBucketsThe method is based onbucketThe type andNumNumber of open up to open up memory space
  • bucketThe number of isMIN_BUCKETSNextPowerOf2(AtLeast-1))Take the maximum.MIN_BUCKETSIs a macro with a value of zero4
  • BaseT::initEmpty()Initialize emptybucketSpecific type basisTNote the type of empty bucketkeyforfirstThe corresponding value is theta1
  • moveFromOldBucketsWill the oldbucketsMove to newbucketsPhi, this sumcacheExpansion is different
  • The release of the oldbuckets

Breakpoint into NextPowerOf2 method

/ / 32 bits
NextPowerOf2 - returns the NextPowerOf2 (32 bits)
/// strictly greater than A. Returns zero on overflow.
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 - returns the NextPowerOf2 (64 bits)
/// strictly greater than A. Returns zero on overflow.
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;
}
Copy the code

The breakpoint in macOS is the Uint32_t NextPowerOf2 method, which is strictly greater than A and returns 0 if it is greater. And parameter A is an unsigned integer. The current parameter passed is -1. The rule for converting -1 to binary is: 1= 0x01, reverse =0xfe, plus 1= 0xFF, and if it is 32 bits, the binary of -1 is 0xFFFFFFFF. If it is 64-1 is A binary 0 XFFFFFFFFFFFFFFFF, on A | = (A > > n), is equal to itself, then A + 1 overflow is equal to zero, of course, you can also be interpreted as 1 + 1 = 0, because is two times the capacity, and also between binary 2 times, So as long as it’s within the range it’s going to be equal to the value that’s passed in

Breakpoint into initEmpty

 void initEmpty(a) {
    setNumEntries(0);
    setNumTombstones(0);

    ASSERT((getNumBuckets() & (getNumBuckets(a)- 1)) = =0 &&
           "# initial buckets must be a power of two!");
    // Set an empty key
    const KeyT EmptyKey = getEmptyKey(a);for (BucketT *B = getBuckets(), *E = getBucketsEnd(a); B ! = E; ++B) ::new (&B->getFirst()) KeyT(EmptyKey);
  }

Copy the code

The breakpoint goes into getEmptyKey, getEmptyKey method and there are a number of methods that come in at the end as follows

static inline DisguisedPtr<T> getEmptyKey(a) {
    return DisguisedPtr<T>((T*)(uintptr_t)- 1);
  }
Copy the code

DisguisedPtr Everyone should be familiar with the Object is also encapsulated into the DisguisedPtr type

   template* * (typename T>
   class DisguisedPtr {
    uintptr_t value;
    static uintptr_t disguise(T* ptr) {
        return- (uintptr_t)ptr; }... }Copy the code

PTR = (uintptr_t)-1, uintptr_t) PTR Uintptr_t (uintptr_t)-1) = 1 uintptr_t = 1

After capacity expansion, call LookupBucketFor(Lookup, TheBucket). A Lookup is a wrapped object passed in as a parameter

Calling LookupBucketFor returns an empty bucket. It is one of buckets created after expansion. The total number of buckets is four

Breakpoints continue debugging by entering the incrementNumEntries() method

The incrementNumEntries() method gets the current number of buckets, 0 and 1

 void incrementNumEntries(a) {
    // getNumEntries() is the original number of entries
    // getNumEntries() +1 indicates the number to be inserted
    // Set the number of NumEntries
    setNumEntries(getNumEntries() + 1); 
  }
Copy the code

The source code shows that the number of existing NumEntries is increased by one. IncrementNumEntries () returns bucket after execution

TheBucketthefirstThe assignment

// Get the first key in the key value
KeyT &getFirst(a) { return std::pair<KeyT, ValueT>::first; }
Copy the code

TheBucket->getFirst() = STD ::forward

(Key) assign Key to first in TheBucket

Assign Key to first of TheBucket

TheBucketthesecondThe assignment

Second of TheBucket may have dirty data by default. Assigning value to second successfully returns TheBucket wrapped in make_pair

isFirstAssociationFirst association

Secound = true is assigned to the second argument in the make_pair method with true, which means that the object is associated for the first time

To obtainObjectAssociationMap

Obtain the ObjectAssociationMap in the AssociationsHashMap table in backup (refs_result.first->second)

Insert association data or update existing data in ObjectAssociationMap

Result. second = true indicates that the data is not stored in ObjectAssociationMap for the first time. If result.second = false indicates that the data is present in ObjectAssociationMap for further details. The first property stored at this point is lw_name in the LWPerson class, and the value stored is luwenhan

The structure of ObjectAssociationMap is similar to that of cache. It can store values as well as other variables

Set up theisaIs the associated object property

.if (isFirstAssociation)
        object->setHasAssociatedObjects()
  ...
Copy the code

The setHasAssociatedObjects method is called when isFirstAssociation first sets the value of the associated object

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;
        }
        // Set has_assoc in ISA to true
        newisa.has_assoc = true;
    } while (slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)));
}
Copy the code

The setHasAssociatedObjects method sets the object to have an associated object, that is, the has_assoc property of the ISA pointer is set to true and the old value is released by the releaseHeldValue method

Flow chart of associating object storage

Associated object –get

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

Objc_getAssociatedObject calls _object_get_associative_reference. In the _object_get_associative_reference method, the associated object value is relatively easy to look up the table

id
_object_get_associative_reference(id object, const void *key)
{   
    // Create an empty associated object
    ObjcAssociation association{};

    {   // Create a management class
        AssociationsManager manager;
        // Get the globally unique HashMap table
        AssociationsHashMap &associations(manager.get());
        // Iterator is an iterator that actually finds objc_Object and its corresponding ObjectAssociationMap
        {first = objc_object,second =ObjectAssociationMap
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        
        // If the iterator encapsulates a structure that is not the last to enter the judgment flow
        if(i ! = associations.end()) {
            / / get ObjectAssociationMap
            ObjectAssociationMap &refs = i->second;
            // Get the iterator of ObjectAssociationMap
            ObjectAssociationMap::iterator j = refs.find(key);
            // If the iterator encapsulates a structure that is not the last to enter the judgment flow
            if(j ! = refs.end()) {
                / / for association
                association = j->second; 
                / / retain new values
                association.retainReturnedValue(a); }}}//release old value, return new value
    return association.autoreleaseReturnedValue(a); }Copy the code

Comments have been written very clearly, do not repeat again, direct debugging to see the final result

Associative objects are simply a process of looking up the value of a table and returning the stored value. Compared with the associated object store value, the value process is easier to understand

conclusion

We may think that the underlying exploration of the associated object is not commonly used, there is no need to explore, in fact, you will have a deep understanding of apple’s underlying packaging ideas and hash tables during the exploration process