introduce

AssociatedObject is a feature of the Objective-C 2.0 runtime that allows developers to add custom attributes to existing classes in extensions. In the actual production process, the more common way is to add member variables to categories.

example

#import <objc/runtime.h>

@interface NSObject (AssociatedObject)

@property (nonatomic.strong) id property;

@end

@implementation NSObject (AssociatedObject)
@dynamic property;

- (id)property {
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setProperty:(NSString *)property {
    objc_setAssociatedObject(self.@selector(property), property, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end
Copy the code

With a little bit of implementation code analysis, objc_getAssociatedObject takes the unchanging pointer address (the example passes selector as an argument, which is actually void*) and gets the desired object from the instance. Objc_setAssociatedObject Saves the specified object based on the passed parameter protocol.

Parameters of the agreement

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. */
};
Copy the code

As a result, OBJC_ASSOCIATION_ASSIGN is equal to a weak reference, but it is also equal to assign/unsafe_unretained.

The difference between weak and weak is beyond the scope of this article. The obvious difference is that weak will empty the reference after the variable is released, and unsafe_unretained memory address. Once obtained, the wild pointer may flash back.

conclusion

We know that if a class wants to add variables, only addIvar is between objc_allocateClassPair and objc_registerClassPair. After the class is registered, the variable structure is not allowed to change. This is to prevent two instances of the same class with different variables from causing confusion.

It is a good idea to add variables to an instance at Runtime without changing the internal variable structure of the class.


Implementation of associated objects

External methods

//Sets an associated value for a given object using a given key and association policy.
void objc_setAssociatedObject(id object, const void * key, id value, objc_AssociationPolicy policy);

//Returns the value associated with a given object for a given key.
id objc_getAssociatedObject(id object, const void * key);

//Removes all associations for a given object.
void objc_removeAssociatedObjects(id object);
Copy the code

Objc_removeAssociatedObjects = objc_removeAssociatedObjects = objc_removeAssociatedObjects;

Apple’s documentation explains that this method is mainly used to restore an object to its original class state. It removes all associations, including those added by other modules, so objc_setAssociatedObject(.. ,nil,..) To uninstall.


Setter implementation

Objc_setAssociatedObject actually calls _object_set_associative_reference

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    // retain the new value (if any) outside the lock.
    ObjcAssociation old_association(0.nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // break any existing association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if(i ! = associations.end()) {// secondary table exists
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if(j ! = refs->end()) { old_association = j->second; j->second = ObjcAssociation(policy, new_value); }else{ (*refs)[key] = ObjcAssociation(policy, new_value); }}else {
                // create the new association (first time).ObjectAssociationMap *refs = new ObjectAssociationMap; associations[disguised_object] = refs; (*refs)[key] = ObjcAssociation(policy, new_value); object->setHasAssociatedObjects(); }}else {
            // setting the association to nil breaks the association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if(i ! = associations.end()) { ObjectAssociationMap *refs = i->second; ObjectAssociationMap::iterator j = refs->find(key);if(j ! = refs->end()) { old_association = j->second; refs->erase(j); }}}}// release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
}
Copy the code

Memory management

static id acquireValue(id value, uintptr_t policy) {
    switch (policy & 0xFF) {
    case OBJC_ASSOCIATION_SETTER_RETAIN:
        return objc_retain(value);
    case OBJC_ASSOCIATION_SETTER_COPY:
        return ((id(*) (id, SEL))objc_msgSend)(value, SEL_copy);
    }
    return value;
}

static void releaseValue(id value, uintptr_t policy) {
    if (policy & OBJC_ASSOCIATION_SETTER_RETAIN) {
        return objc_release(value);
    }
}

ObjcAssociation old_association(0.nil);
id new_value = value ? acquireValue(value, policy) : nil;
{
    old_association = ...
}
if (old_association.hasValue()) ReleaseValue()(old_association);
Copy the code

We will extract the code related to object memory for careful analysis. First, we will retain/copy the newly passed object according to the protocol, obtain the old value during the assignment, and release it before the end of the method.


The assignment

AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
    // An assignment is required
    AssociationsHashMap::iterator i = associations.find(disguised_object);
    if(i ! = associations.end()) {// Find the associated table for this object
        ObjectAssociationMap *refs = i->second;
        ObjectAssociationMap::iterator j = refs->find(key);
        if(j ! = refs->end()) {// Find the object associated with this key
            old_association = j->second;
            j->second = ObjcAssociation(policy, new_value);
        } else {
            // Add a new association(*refs)[key] = ObjcAssociation(policy, new_value); }}else {
        // Create a new associated tableObjectAssociationMap *refs = new ObjectAssociationMap; associations[disguised_object] = refs; (*refs)[key] = ObjcAssociation(policy, new_value); object->setHasAssociatedObjects(); }}Copy the code

Take a look at AssociationsManager and AssociationsHashMap

class AssociationsManager {
    static AssociationsHashMap *_map;
public:
    AssociationsHashMap &associations() {
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return*_map; }};class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap *, DisguisedPointerHash, DisguisedPointerEqual, AssociationsHashMapAllocator>;

class ObjectAssociationMap : public std::map<void*, ObjcAssociation, ObjectPointerLess, ObjectAssociationMapAllocator >;Copy the code

AssociationsManager manages all associated objects in an application through a hash table with pointer addresses as the primary key and values as the associated table.

First, the pointer address of the object is used to find the associated table, and then the associated relationship is searched by the specified key value, so as to obtain the associated object.

delete

AssociationsHashMap::iterator i = associations.find(disguised_object);
if(i ! = associations.end()) { ObjectAssociationMap *refs = i->second; ObjectAssociationMap::iterator j = refs->find(key);if (j != refs->end()) {
        old_association = j->second;
        refs->erase(j);
    }
}
Copy the code

The erase method is similar to the erase method of the hash table.


Getter implementation

Objc_getAssociatedObject actually calls _object_get_associative_reference

id _object_get_associative_reference(id object, void *key) { id value = nil; uintptr_t policy = OBJC_ASSOCIATION_ASSIGN; { AssociationsManager manager; AssociationsHashMap &associations(manager.associations()); disguised_ptr_t disguised_object = DISGUISE(object); AssociationsHashMap::iterator i = associations.find(disguised_object); if (i ! = associations.end()) { ObjectAssociationMap *refs = i->second; ObjectAssociationMap::iterator j = refs->find(key); if (j ! = refs->end()) { ObjcAssociation &entry = j->second; value = entry.value(); policy = entry.policy(); if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) { objc_retain(value); } } } } if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) { objc_autorelease(value); } return value; }Copy the code

Finding a hash table is the same as a Setter, except that if a retain and an autorelease are required in the policy, they will be handled. So how do you agree on these strategies?

enum { 
    OBJC_ASSOCIATION_SETTER_ASSIGN      = 0,
    OBJC_ASSOCIATION_SETTER_RETAIN      = 1,
    OBJC_ASSOCIATION_SETTER_COPY        = 3.// NOTE:  both bits are set, so we can simply test 1 bit in releaseValue below.
    OBJC_ASSOCIATION_GETTER_READ        = (0 << 8), 
    OBJC_ASSOCIATION_GETTER_RETAIN      = (1 << 8), 
    OBJC_ASSOCIATION_GETTER_AUTORELEASE = (2 << 8)};typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
    OBJC_ASSOCIATION_RETAIN = 01401, 
    OBJC_ASSOCIATION_COPY = 01403
};
Copy the code

OBJC_ASSOCIATION_RETAIN = 01401, where 01401 starts with 0 and is an octal number, which translates to binary 0000 0011 0000 0001, The bit determination is OBJC_ASSOCIATION_SETTER_RETAIN OBJC_ASSOCIATION_GETTER_RETAIN OBJC_ASSOCIATION_GETTER_AUTORELEASE.

Retain is required when it is saved, and when it is retrieved, the reference count needs to be retained before autorelease waits for release to achieve atomicity.

Remove implementation

Objc_removeAssociatedObjects determines whether the object is associated and then executes _object_set_associative_reference

void _object_remove_assocations(id object) { vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements; { AssociationsManager manager; AssociationsHashMap &associations(manager.associations()); if (associations.size() == 0) return; disguised_ptr_t disguised_object = DISGUISE(object); AssociationsHashMap::iterator i = associations.find(disguised_object); if (i ! = associations.end()) { // copy all of the associations that need to be removed. ObjectAssociationMap *refs = i->second;  for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j ! = end; ++j) { elements.push_back(j->second); } // remove the secondary table. delete refs; associations.erase(i); } } // the calls to releaseValue() happen outside of the lock. for_each(elements.begin(), elements.end(), ReleaseValue()); }Copy the code

The implementation can also be seen as why it is not recommended in the introduction, because it will iterate over all associated objects and release them all, which may cause defects in other modules.

Judge associated objects

The interesting thing is to determine whether an object has an implementation of an associated object.

inline bool objc_object::hasAssociatedObjects()
{
    if (isTaggedPointer()) return true;
    if (isa.nonpointer) return isa.has_assoc;
    return true;
}
Copy the code
inline void objc_object::setHasAssociatedObjects()
{
    if (isTaggedPointer()) return;

 retry:
    isa_t oldisa = LoadExclusive(&isa.bits);
    isa_t newisa = oldisa;
    if(! newisa.nonpointer || newisa.has_assoc) { ClearExclusive(&isa.bits);return;
    }
    newisa.has_assoc = true;
    if(! StoreExclusive(&isa.bits, oldisa.bits, newisa.bits))goto retry;
}
Copy the code

The default return is true, and only on 64-bit systems will a flag bit be saved. I guess this is done to speed up the release cycle. When the object is destructed, this method is used to determine whether the associated object needs to be released. If you query the hash table every time, the execution efficiency will be reduced. It is better to pass the hash table first and then do the processing.

On 64-bit systems, pointer addresses are stored not only as memory addresses, but also as other marker information, including has_ASsoc, which is covered in this article.

TaggedPointer is an optimization strategy that stores simple numeric or string information directly in pointer addresses to speed up performance without requiring additional memory.

conclusion

The implementation of the association object is not complicated, and the way of saving is a global hash table, and the access is performed by searching the table to find the association. The characteristic of hash tables is to sacrifice space for time, so execution speed can be guaranteed.


Question and answer

What are the applications of associated objects?

Associated objects can bind a lifetime variable to a given object at run time.

1. Since the implementation of the original class is not changed, the native class or packaged library can be extended, generally with the Category to achieve complete functions.

2. Variables defined by ObjC classes are exposed externally due to runtime features. Associated objects can be used to hide key variables and ensure security.

3. It can be used for KVO. The associated object can be used as an observer to avoid the loop caused by observing itself.

How does the system manage associated objects?

The system manages a global hash table and obtains associated objects through the object pointer address and the fixed parameter address passed. Manage the life cycle of the object based on the parameter protocol passed in by the setter.

Do I need to manually null its pointer when it is released?

When an object is released, if the protocol is set to OBJC_ASSOCIATION_ASSIGN, the reference count of its associated object will not be reduced. All other protocols will be reduced to release the associated object.

Unsafe_unretain It is generally considered that the object is not handled because of external object control. Therefore, no matter what protocol is used, the associated object does not need to be manually empty when the object is released.