It started on my personal blog

Start by adding attributes to categories

In our article on categorizing Cateogry in iOS, we posed the question,

Can you add member variables to a Category? If so, how do YOU add member variables to a Category?

  • You cannot add member variables to a Category directly, but you can achieve the effect of a Category having member variables indirectly, using the associative object technique

So, here are the details

What do you actually do when you add attributes

First of all, let’s recall that adding a property actually does three things

  • Generating member variables
  • Generate declarations for the set and GET methods
  • Generate implementations of the set and GET methods

Eg: Define a YZPerson class and define the age attribute

#import <Foundation/Foundation.h>

@interface YZPerson : NSObject

@property (assign, nonatomic) int age;


@end

Copy the code

That’s like doing three things

  • Generating member variables_age
  • generatesetMethods andgetMethod declaration
  • generatesetMethods andgetThe implementation of the method is as follows
#import <Foundation/Foundation.h>


@interface YZPerson : NSObject

{
    int _age;
}
- (void)setAge:(int)age;
- (int)age;

@end



#import "YZPerson.h"

@implementation YZPerson
- (void)setAge:(int)age{
    _age = age;
}

- (int)age{
    return _age;
}
@end

Copy the code

So why not add attributes to categories?

Say first conclusion

  • Generating member variables_age
  • Will not generatesetMethods andgetMethod declaration
  • Will not generatesetMethods andgetMethod implementation

Will not generatesetMethods andgetMethod implementation

Define a category YZPerson+ ext.h and add the attribute weight

#import "YZPerson.h"
@interface YZPerson (Ext)
@property (nonatomic ,assign)  int weight;
@end
Copy the code

use

YZPerson *person = [[YZPerson alloc] init];
person.weight = 10;
Copy the code

Will report an error directly,

IOS - Associated object [1009:10944] *** Terminating app due to uncaught exception'NSInvalidArgumentException', 
reason: '-[YZPerson setWeight:]: unrecognized selector sent to instance 0x10182bd10'
*** First throw call stack:
(
	0   CoreFoundation                      0x00007fff3550d063 __exceptionPreprocess + 250
	1   libobjc.A.dylib                     0x00007fff6ac8e06b objc_exception_throw + 48
	2   CoreFoundation                      0x00007fff355961bd -[NSObject(NSObject) __retain_OA] + 0
	3   CoreFoundation                      0x00007fff354b34b4 ___forwarding___ + 1427
	4   CoreFoundation                      0x00007fff354b2e98 _CF_forwarding_prep_0 + 120
	
	6   libdyld.dylib                       0x00007fff6c0183f9 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
Program ended with exit code: 9
Copy the code

Reason: ‘-[YZPerson setWeight:]: unrecognized selector sent to instance 0x10182bd10

Will be generatedsetMethods andgetMethod declaration

#import "YZPerson+Ext.h"

@implementation YZPerson (Ext)
- (void)setWeight:(int)weight{
    
}
- (int)weight{
    return 100;
}
@end

Copy the code

And then call

YZPerson *person = [[YZPerson alloc] init];
person.age = 25;
person.weight = 10;
NSLog(@"person.age = %d",person.age);
NSLog(@"person.weight = %d",person.weight);
Copy the code

The output

2019-07-10 08:28:04.406972+0800 iOS- Associated object [1620:18520] person. Age = 25 2019-07-10 08:28:04.407291+0800 IOS - Associated object [1620:18520] person.weight = 100Copy the code

Further proof that implementations of the set and GET methods are not generated, but declarations of the set and GET methods are, because the method cannot be called without the declarations of the set and GET methods.

We can also declare the weight in the YZPerson+ ext. h file, and then write the implementation in YZPerson+ ext. m, we will be prompted

All the more proof that there is a claim.

Member variables cannot be defined directly in a classification

#import "YZPerson.h"@interface YZPerson (Ext) { int _weight; Instance variables may not be placedin categories
}
@property (nonatomic ,assign)  int weight;
@end

Copy the code

Instance variables may not be placed in categories

Source proof

Objc-runtime-new. h classifiescateogry. Objc-runtimenew. h classifiescateogry

struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
Copy the code

In this structure, there are no arrays for member variables, only properties, protocols, etc.

How to improve attributes

Is there any way to add attributes to a category that has the same effect as adding attributes to a class? The answer is yes

Plan one uses global variables

Class YZPerson+ ext.m defines the global variable _weight


#import "YZPerson+Ext.h"

@implementation YZPerson (Ext)

int _weight;

- (void)setWeight:(int)weight{
    _weight = weight;
}
- (int)weight{
    return _weight;
}
@end

Copy the code

When using

YZPerson *person = [[YZPerson alloc] init];
person.weight = 103;
NSLog(@"person.weight = %d",person.weight);
Copy the code

The output is

IOS - Associated object [1983:23793] person.weight = 103Copy the code

It looks like it does, but actually we can’t use it that way, because global variables are shared, so let’s say we have two persons, and the second Person modifies the weight property, and then prints the first person.weight

YZPerson *person = [[YZPerson alloc] init];
person.weight = 103;
NSLog(@"person.weight = %d",person.weight);

YZPerson *person2 = [[YZPerson alloc] init];
person2.weight = 10;
NSLog(@"person.weight = %d",person.weight);
Copy the code

The output is

Ios-associated object [1983:23793] person.weight = 103 ios-associated object [1983:23793] person.weight = 10Copy the code

Changing person. weight changes the value of person. weight because it is a global variable. So that’s not going to work

Plan two uses the dictionary

Since the previous scheme doesn’t work because global variables share a copy, shouldn’t we just guarantee one-to-one relationships?

Define the dictionary weights_ to store and use the address value of the object as the key and the weight value as the value


#import "YZPerson+Ext.h"@implementation YZPerson (Ext) NSMutableDictionary *weights_; + (void)load{ static dispatch_once_t onceToken; {dispatch_once(&onceToken, ^{dispatch_once(&onceToken, ^{); }); } - (void)setWeight:(int)weight{
    NSString *key = [NSString stringWithFormat:@"%p",self]; Weights_ [key] = @(weight); } - (int)weight{NSString *key = [NSString stringWithFormat:@"%p",self];
    return  [weights_[key] intValue];
}

@end
Copy the code

In this way, when used, there is no interference between different objects and the result is as follows

Existing problems

  • Because it is global, there are memory leaks
  • Thread-safety issues, if multiple threads access at the same time, thread-safety issues
  • Too much code. If you add a property every time, you’re writing too much code. Adverse to maintenance

Associated object scheme

Use of associated objects

The following is a brief explanation of the use of associated objects

Dynamically add

objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
Copy the code
  • A parameter:id objectTo add an attribute to an object, useself.
  • Parameters of the two:void * == id key : keyValue, gets the value of the attribute of the associated object based on key, inobjc_getAssociatedObjectIn time,keyGets the value of the property and returns.
  • Three parameters:id value: The associated value, i.esetThe value passed by the method is saved to the property.
  • Four parameters:objc_AssociationPolicy policy: policy, in what form attributes are stored.
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) { OBJC_ASSOCIATION_ASSIGN = 0, // Specify a weak reference to the associated object OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // specify a strong reference to the associated object, Non-atomic OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // specifies that the associated object is copied, non-atomic OBJC_ASSOCIATION_RETAIN = 01401, // Specifies a strong reference to the associated object, Atomicity OBJC_ASSOCIATION_COPY = 01403 // Specifies that the associated object is copied, atomicity};Copy the code

The table is as follows

objc_AssociationPolicy Corresponding modifier
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC strong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC copy, nonatomic
OBJC_ASSOCIATION_RETAIN strong, atomic
OBJC_ASSOCIATION_COPY copy, atomic

Eg: The use of OBJC_ASSOCIATION_RETAIN_NONATOMIC in our code is equivalent to the use of nonatomic and strong modifiers.

Notice that in the above list, there is no policy corresponding to the weak modification, because the object is transformed into the DISGUised_ptr_T type DISGUised_object through the DISGUISE function.

disguised_ptr_t disguised_object = DISGUISE(object);
Copy the code

The weak attribute will be destroyed when no object is owned and the pointer is set to nil. After the destruction of the object, the AssociationsHashMap corresponding to the value object still exists in the map, but the object address has been set to nil. It will cause bad address access and cannot be transformed into disguised_object according to the address of the object object. This paragraph can be seen again after the disguised_object.

The values

objc_getAssociatedObject(id object, const void *key);
Copy the code
  • A parameter:id object: Gets the associated property of which object.
  • Parameters of the two:void * == id key: What attributes, withobjc_setAssociatedObjectIn thekeyThe corresponding is throughkeyValue to take out thevalue.

Removing associated Objects

- (void)removeAssociatedObjects {// Remove the associated object objc_removeAssociatedObjects(self); }Copy the code

The specific application


#import "YZPerson.h"

@interface YZPerson (Ext)
@property (nonatomic,strong) NSString *name;
@end


#import "YZPerson+Ext.h"
#import <objc/runtime.h>
@implementation YZPerson (Ext)

const void *YZNameKey = &YZNameKey;

- (void)setName:(NSString *)name{
    objc_setAssociatedObject(self, YZNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name{
   return objc_getAssociatedObject(self, YZNameKey);
}

- (void)dealloc
{
    objc_removeAssociatedObjects(self);
}

@end
Copy the code

When you use it, just use it normally

YZPerson *person = [[YZPerson alloc] init];
person.name = @"jack";

YZPerson *person2 = [[YZPerson alloc] init];
person2.name = @"rose";
        
NSLog(@"person.name = %@",person.name);
NSLog(@"person2.name = %@",person2.name);
Copy the code

The output

Ios-associated object [4266:52285] person.name = Jack ios-associated object [4266:52285] person2.name = RoseCopy the code

It’s that simple to use

Principle of Associated Objects

Four core objects

The core objects that implement associative object technology are

  • AssociationsManager
  • AssociationsHashMap
  • ObjectAssociationMap
  • ObjcAssociation

The source code interpretation

The source of the associated object is in the Runtime source

objc_setAssociatedObject

Looking at the objC-Runtime. mm class, first find the objc_setAssociatedObject function and see its implementation

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

_object_set_associative_reference

To view


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

As is shown in

Inside the _object_set_associative_reference function we can find the four core objects that implement the associative object technique we described above. Let’s take a look at the internal implementations one by one and see how they relate to each other.

AssociationsManager

AssociationsManager static AssociationsHashMap *_map;


class AssociationsManager {
    // associative references: object pointer -> PtrPtrHashMap.
    static AssociationsHashMap *_map;
public:
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }
    
    AssociationsHashMap &associations() {
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return*_map; }};Copy the code

AssociationsHashMap

Now look at AssociationsHashMap

AssociationsHashMap (unordered_map) AssociationsHashMap (unordered_map) AssociationsHashMap (unordered_map

Parameter _Key and _Tp correspond to the Key and Value in the unordered_map. It can be seen that the pass in _Key is UNORDERED_MAP < DISGUised_ptr_t, and the pass in _Tp is ObjectAssociationMap *.

ObjectAssociationMap = ObjectAssociationMap = ObjectAssociationMap = ObjectAssociationMap We can know that object Association is also stored in ObjectAssociationMap in the form of key and Value.

ObjcAssociation

And then we go to objC Society, and you can see that

class ObjcAssociation { uintptr_t _policy; // policy id _value; Public: ObjcAssociation(uintptr_t policy, id value) : _policy(policy), _value(value) {} ObjcAssociation() : _policy(0), _value(nil) {} uintptr_t policy() const {return _policy; }
        id value() const { return _value; }
        
        bool hasValue() { return _value != nil; }
    };
Copy the code

From the above code, we can see that objcasSociety stores _policy and _value, which we can see are the values passed in by calling objc_setAssociatedObject, In other words we pass in value and policy in calling objc_setAssociatedObject and those two values are ultimately stored in the ObjcasSociety.

Now that we have a preliminary understanding of the relationship between the four core objects AssociationsManager, AssociationsHashMap, ObjectAssociationMap, and ObjcasSociety, we will continue to read the source code carefully. In what object do the four parameters passed in the objc_setAssociatedObject function serve

read_object_set_associative_reference

_object_set_associative_reference



void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    // retain the new value (ifany) outside the lock. ObjcAssociation old_association(0, nil); // Obtain new_value id new_value = value? acquireValue(value, policy) : nil; { AssociationsManager manager; AssociationsHashMap & Associations (manager.associations()); // The object is transformed into disguised_object DISGUised_ptr_T type disguised_Ptr_T DISGUised_object = disguised_object (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[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{// come here to say, The value to null / / setting the association to nil breaks the association. The 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

**acquireValue** The internal implementation returns different values by judging the policy

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;
}
Copy the code
  • First of all, based on what we passed invalueafteracquireValueFunction processing returnsnew_value.acquireValueInternally, the function returns different values based on policy judgments
 typedef uintptr_t disguised_ptr_t;
    inline disguised_ptr_t DISGUISE(id value) { return ~uintptr_t(value); }
    inline id UNDISGUISE(disguised_ptr_t dptr) { return id(~dptr); }
Copy the code
  • After creatingAssociationsManager manager,managerThe inside of theAssociationsHashMapnamelyassociations. Then we see the first argument we pass inobjectafterDISGUISEThe function is converted to thetadisguised_ptr_tThe type ofdisguised_object.
typedef uintptr_t disguised_ptr_t;
inline disguised_ptr_t DISGUISE(id value) { return ~uintptr_t(value); }
inline id UNDISGUISE(disguised_ptr_t dptr) { return id(~dptr); }
Copy the code
  • And then it’s processed intonew_valuethevalue, andpolicyIt was deposited togetherObjcAssociationIn the. whileObjcAssociationCorresponding to what we passed inkeyIs placed into theObjectAssociationMapIn the.disguised_objectandObjectAssociationMapwithkey-valueCorresponds to the form stored inassociationsThere is also inAssociationsHashMapIn the.

The value is null

If the value passed in is null, the associated object is deleted

// Come here to say, The value to null / / setting the association to nil breaks the association. The 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

References for this article:

Table summarizes

The relationships between these core classes are summarized in a table below

summary

  • The associated object is not stored in the associated object’s own memory, but has a global unifiedAssociationsManagerIn the
  • One instance object corresponds to oneObjectAssociationMap.
  • whileObjectAssociationMapObject that stores multiple associated objects of this instance objectkeyAs well asObjcAssociation.
  • ObjcAssociationThat stores the associated objectvalueandpolicystrategy

objc_getAssociatedObject

Objc_getAssociatedObject internally calls _object_get_associative_reference

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

_object_get_associative_referencefunction

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); / / find disguised_object AssociationsHashMap: : iterator I = associations. The find (disguised_object);if(i ! = associations.end()) { ObjectAssociationMap *refs = i->second; / / to check the key and the value ObjectAssociationMap: : iterator j = refs - > find (key);if(j ! = refs->end()) { ObjcAssociation &entry = j->second; value = entry.value(); policy = entry.policy(); // If there are keys and values, fetch the corresponding valuesif(policy & OBJC_ASSOCIATION_GETTER_RETAIN) { objc_retain(value); }}}}if(value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {objc_autoRelease (value); }return value;
}
Copy the code

The key code has been commented out above

objc_removeAssociatedObjectsfunction

The objc_removeAssociatedObjects function is used to remove all associated objects and internally calls _object_remove_assocations

void objc_removeAssociatedObjects(id object) 
{
    if(object && object->hasAssociatedObjects()) { _object_remove_assocations(object); }}Copy the code

_object_remove_assocations

Take a look at _object_remove_ASsocations

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 ! = AssociationsHashMap (); // 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; / / delete associations. Erase (I); } } // the calls to releaseValue() happen outside of the lock. for_each(elements.begin(), elements.end(), ReleaseValue()); }Copy the code

As you can see in the code, take an object and iterate over it to delete all of its associated objects

conclusion

The relationships between these core classes are summarized in a table below

  • The associated object is not stored in the associated object’s own memory, but has a global unifiedAssociationsManagerIn the
  • One instance object corresponds to oneObjectAssociationMap.
  • whileObjectAssociationMapObject that stores multiple associated objects of this instance objectkeyAs well asObjcAssociation.
  • ObjcAssociationThat stores the associated objectvalueandpolicystrategy
  • Accept one when deletingobjectObject, and then iterate over and delete all associated objects for that object
  • Setting an Associated Object_object_set_associative_referenceIt’s time if the incomingvalueIf null, delete the associated object

References for this article:

Github address github

The Runtime source

Basic principles of iOS

More information, welcome to pay attention to the individual public number, not regularly share a variety of technical articles.