IOS Low-level exploration series

  • IOS low-level exploration – alloc & init
  • IOS underlayer exploration – Calloc and Isa
  • IOS Low-level exploration – classes
  • IOS Low-level exploration – cache_t
  • IOS Low-level exploration – Methods
  • IOS Low-level exploration – message lookup
  • IOS Low-level exploration – Message forwarding
  • IOS Low-level exploration – app loading
  • IOS low-level exploration – class loading
  • IOS Low-level exploration – classification loading
  • IOS low-level exploration – class extension and associated objects
  • IOS Low-level exploration – KVC
  • IOS Basics – KVO

IOS leak check and fill series

  • IOS leak fix – PerfromSelector
  • IOS bug fix – Threads
  • – RunLoop for iOS
  • IOS – LLVM & Clang

Earlier we explored the loading of classes and categories in iOS. There are still some holes left to fill in for classes, such as class extension and associated objects. Today let’s fill in this hole.

First, class expansion

1.1 What is Class extension?

For a specific definition of class extension, you can refer directly to Apple’s notes on class extension.

A class extension bears some similarity to a category, but it can only be added to a class for which you have the source code at compile time (the class is compiled at the same time as the class extension).

Class extension is similar to classification, but only if you have the source code of the original class attached to it at compile time. (Class and class extension compiled simultaneously)

Structure of class extension:

@interface ClassName ()
 
@end
Copy the code

Because no name is given in the parentheses, class extensions are often referred to as anonymous categories. Because nothing is filled in parentheses, class extensions are also referred to as anonymous classifications.

When we create objective-type files in Xcode, we can choose between empty files, categories, protocols, and class extensions.

If we select Extension, Xcode will generate a header file with the NSObject + Extension, that is, the class Extension will be named with the class name _ Extension.h

In fact, we rarely do such operations, we generally declare the current class extension in the.m file, basically we will declare some private attributes, methods in the class extension. For example, declare a read-only property in a.h file and then override the property to make it readable and writable in a.m class extension.

Why don’t we use LLDB printing to see if class extensions are attached to the class at compile time?

Are 1.2 class extensions determined at compile time?

Create a new class Person under objC-DEBUG in the objC-756 source code, add a property name to this class, and add a property mName and method extM_method in the.m class extension. Create a Person class to extend the Person+ extension.h file:

// Person.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end

NS_ASSUME_NONNULL_END

// Person.m
#import "Person.h"
#import "Person+Extension.h"

@interface Person ()
@property (nonatomic, copy) NSString *mName;

- (void)extM_method;
@end

@implementation Person

+ (void)load{
    NSLog(@"%s",__func__);
}

- (void)extM_method{
    NSLog(@"%s",__func__);
}

- (void)extH_method{
    NSLog(@"%s",__func__);
}

@end

// Person+Extension.h
#import <AppKit/AppKit.h>
#import "Person.h"

NS_ASSUME_NONNULL_BEGIN

@interface Person ()
@property (nonatomic, copy) NSString *ext_name;
@property (nonatomic, copy) NSString *ext_subject;

- (void)extH_method;
@end

NS_ASSUME_NONNULL_END
Copy the code

Let’s test it in main.m:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import "Person.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [Person alloc];
        NSLog(@"%@ - %p", p, p);
    }
    return 0;
}
Copy the code

We make a breakpoint on the line where the Person instantiates object P and run the project. Next LLDB print at the console:

Because the properties and methods of the object are stored on the class object, and because the ro in the class structure is determined at compile time, So we just need to print out whether the ro structure of the class object has mName and extM_method in the class extension and ext_name and ext_subject in the class extension and extH_method

1.3 LLDB validation

  • throughx/4gxCommand printLGPersonThe memory address of the class object, printed in hexadecimal format, in 4 segments

  • Because the memory address of the class object starts withisa, followed bysuperclass, and then thecache_t. We have already analyzed that in the defaultarm64In processor architecture,isaIt’s eight bytes,superclassIt’s 8 bytes, andcache_tThe three attributes of “add up to 8 + 4 + 4 = 16 bytes, so to getbitsWe need to shift 8 + 8 + 16 = 32 bytes of memory, but this is hexadecimal, so we need to move 0x20 memory addresses, i.e0x100002420 + 0x20 = 0x100002440

  • Because of the class objectdata()Property returnsbits.data()So I’m going to print what I just picked upbitsdata()Properties, andbitsdata()Property actually returnsrw.
struct objc_class : objc_object {
    class_rw_t *data() { 
        returnbits.data(); }}struct class_data_bits_t {
    class_rw_t* data() {
        return (class_rw_t*)(bits & FAST_DATA_MASK); }}Copy the code

  • Then printrwThe properties of thero, and then we try to read firstbaseMethodListProperty, which stores all the methods of the class as determined at compile time.

  • becausebaseMethodListA property is aListType of container that we use directlyget(index)To obtain theindexPhi, the result of what we’re looking forextH_methodextM_methodThere we are, but we’re not done yet, we haven’t verified the two properties declared in the class extension, let’s print them outrobaseProperties

  • And we clearly see that,mName.ext_nameext_subjectDoes that mean that class extension is determined at compile time? We also missed the three propertiesgettersetterLet’s go backbaseMethodListLet’s look it up

  • Bingo! Our class extends the attributes definedgettersetterMethods are also generated, and at this point we are completely sure that the class extension will be loaded into the class at compile timeroIn the.

One thing to note here is that if we do not introduce a separate class extension header file in the class header or source file, the properties and methods in the separate class extension header file will not be loaded onto the class.

1.4 Differences between class extension and classification

The research object Loading time Action object Can you declare a property to generate getters and setters via @Property
Classification (implements load method) The runtime rw No, you need to use associated objects to do this
Classification (load method not implemented) Compile time ro No, you need to use associated objects to do this
Class expand Compile time ro can

2. Associated objects

In the last video we explored class extension and the difference between class extension and classification, and we saw that we could declare properties in class extension, and the compiler would help us generate getters and setters for those properties, But classes declare properties as @property and do not generate getters and setters. There’s actually a way in iOS to add properties that have getters and setters, and that’s — Associated Objects.

2.1 Definition of Associated Objects

The official definition of the association object can be found in the Official Apple documentation.

Associative references, available starting in OS X v10.6, simulate the addition of object instance variables to an existing class. Using associative references, you can add storage to an object without modifying the class declaration. This may be useful if you do not have access to the source code for the class, or if for binary-compatibility reasons you cannot alter the layout of the object.

Associated references, enabled since OS X 10.6, simulate adding object instance variables to an existing class. By using associative references, you can add content to an object without modifying the class declaration. This can be useful if you do not have access to the source code for the class, or if you cannot change the layout of the object for binary compatibility reasons.

Associations are based on a key. For any object you can add as many associations as you want, each using a different key. An association can also ensure that the associated object remains valid for at least the lifetime of the source object.

The associative reference mechanism is based on keys. For any object, you can add as many association references as you need, each with a different key. An associative reference also ensures that the associated object remains valid for at least the declaration cycle of the source object.

Best practices for Associated Objects can be found in the nshipster-Associated Objects article.

As you can see from apple’s official documentation, associated references are not only used in categories, but are more commonly used in categories for our daily development. Most developers already know how to use associative references. Indeed, associative references are simple to use in two ways:

// Set objc_setAssociatedObject() // Get associateDobject ()Copy the code

If we want to set an associative object for a property in a class, we need to override the setter method for the property and use objc_setAssociatedObject:

- (void)setXXX:(associated value data type) associated value objc_setAssociatedObject(self, associated key, associated value, associated object memory management policy); }Copy the code

Then you also need to override the getter method and use objc_getAssociatedObject:

- (associative value data type) Associative value {return objc_getAssociatedObject(self, associative key); }Copy the code

The associated object memory management strategy is shown in the following table:

Related policies Equivalent @ property describe
OBJC_ASSOCIATION_ASSIGN @property (assign) or @property (unsafe_unretained)Specifies a weak reference to an associated object. Specifies a weak reference to an associated object.
OBJC_ASSOCIATION_RETAIN_NONATOMIC @property (nonatomic, strong) Specifies a strong reference to an associated object that cannot be used atomically.
OBJC_ASSOCIATION_COPY_NONATOMIC @property (nonatomic, copy) Specifies a copy reference to an associated object that cannot be used atomically.
OBJC_ASSOCIATION_RETAIN @property (atomic, strong) Specifies a strong reference to an associated object that can be used atomically.
OBJC_ASSOCIATION_COPY @property (atomic, copy) Specifies a copy reference to an associated object that can be used atomically.

2.2 Underlying Principles of Associated Objects

On the underlying principles of associative objects, there is a lighthouse DraVENESS blog post on AssociatedObject that fully parses AssociatedObject and is well worth reading.

Of course, if you can also follow the author to explore the underlying principle of the associated object. Let’s start with the most intuitive objc_setAssociatedObject method:

2.3 objc_setAssociatedObject

// objc-runtime.mm
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

The implementation of the objc_setAssociatedObject method wraps up another layer as _object_set_associative_reference

The implementation of the _object_set_associative_reference method is quite long, so let’s explore it in sections here.

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

If both object and key are nil, the value is returned. This is done to avoid a crash when null values are passed in.

// objc-references.mm
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));

// objc-runtime-new.h        
bool forbidsAssociatedObjects(a) {
    return (data()->flags & RW_FORBIDS_ASSOCIATED_OBJECTS);
}
Copy the code

To determine whether associative references are disabled for the object to be associated, use the flags attribute of the object’s ISA and the previous RW_FORBIDS_ASSOCIATED_OBJECTS macro.

    // retain the new value (if any) outside the lock.
    ObjcAssociation old_association(0, nil);
Copy the code

Initializes an ObjcasSociety object to hold the existing association objects

id new_value = value ? acquireValue(value, policy) : nil;
Copy the code

AcquireValue = acquireValue (); acquireValue ();

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

You can see that acquireValue sends a retain or copy message based on the association policy

        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
Copy the code

Initialize an AssociationsManager object, then obtain an AssociationsHashMap hash table, and use the DISGUISE method as the key to look up the hash table. The DISGUISE here actually carries out the reverse bitwise operation.

inline disguised_ptr_t DISGUISE(id value) { return ~uintptr_t(value); }
Copy the code

If the value of the associated object passed in exists, the assignment operation is performed. If the value of the associated object passed in does not exist, the operation is null. Let’s first look at the assignment process:

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 = newObjectAssociationMap; associations[disguised_object] = refs; (*refs)[key] = ObjcAssociation(policy, new_value); object->setHasAssociatedObjects(); }}Copy the code

1. Query the AssociationsHashMap hash table based on the bit-inverse result of the previous step. In this case, query the AssociationsHashMap hash table by iterator. The internal store is the key-value pair 2 of the value passed in by the _object_set_associative_reference method. If no, no associated object has been set on the current class. Initialize an ObjectAssociationMap and set the current object’s ISA has_ASsoc property to true 3 with setHasAssociatedObjects. If yes, the associated object has been set on the current class. Then check whether the key exists. If the key exists, update the original associated object. If the key does not exist, you need to add an associated object

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

Because new_value is nil, it means to delete the associated object. The internal logic is basically the same as above, but the last step is to erase the node corresponding to the key in the ObjectAssociationMap

    // release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
Copy the code

Finally, old_association is determined if it has a value, and if it does, it is released, provided that the old policy of the associated object is OBJC_ASSOCIATION_SETTER_RETAIN

struct ReleaseValue {
Copy the code
void operator() (ObjcAssociation &association) {
    releaseValue(association.value(), association.policy());
}
Copy the code

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

Copy the code

2.4 objc_getAssociatedObject

With the objc_setAssociatedObject method out of the way, let’s move on to another important method, objc_getAssociatedObject:

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

As you can see, just like objc_setAssociatedObject, objc_getAssociatedObject has another layer wrapped around it, which is implemented as _object_get_associative_reference, This method is simpler than the _object_set_associative_reference method in the previous section, so we’ll just post the complete code

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

1. Initialize an empty value and policy 2 whose policy is OBJC_ASSOCIATION_ASSIGN. Initialize an AssociationsManager associated object management class and get the AssociationsHashMap object, which is static 3 at the bottom of the AssociationsManager. Then query AssociationsHashMap 4 with the result of the reverse bit of the object as the key. If I’m stuck in AssociationsHashMap, ObjectAssociationMap, ObjectAssociationMap, objectCasSociation, ObjectAssociationMap, objectCasSociation, ObjectAssociationMap, ObjectAssociationMap, objectCasSociation, ObjectAssociationMap Assign the value and policy to the two temporary variables declared in the method entry, and then determine whether the obtained policy of the associated object is OBJC_ASSOCIATION_GETTER_RETAIN. If so, retain operation 5 is required on the associated value. Finally, if the associated value exists and the policy is OBJC_ASSOCIATION_GETTER_AUTORELEASE, objC_autoRelease needs to be called torelease the associated value 6. Finally return the associated value

2.5 objc_removeAssociatedObjects

The objc_removeAssociatedObjects method is something we probably don’t use very often, but literally, it should be used to remove associated objects. We come to its definition:

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

_object_remove_assocations _object_remove_assocations _object_remove_assocations _object_remove_assocations _object_remove_assocations _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 ! = 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

We add all associated objects that the object contains into a vector, and then call ReleaseValue() on all objCassociety objects to release values that are no longer needed.

Third, summary

  • Class extension is an anonymous classification that is loaded at compile time
  • Class extension can add attributes and methods as well as instance variables. Classification can only add methods and attributes, but it needs to be generated by using associated objectsgettersetterAnd classes cannot declare instance variables
  • The associative object at the bottom is actuallyObjcAssociationObject structure
  • There’s a global oneAssociationsManagerThe management class stores a static hash tableAssociationsHashMapThe hash table stores Pointers to objects and values of all objects associated with the objectObjectAssociationMapTo store the
  • ObjectAssociationMapThe storage structure iskeyAs the key,ObjcAssociationFor the value
  • Quickly determine whether an object has an associated object, you can directly fetch the objectisahas_assoc

Iv. Reference materials

Apple – Class extension

Apple – Associated object

NSHipster – Associated Objects

Drap – The AssociatedObject is fully resolved