preface
We discussed the loading process of iOS categories in our last article, but today we’ll look at iOS extension classes and associated objects.
Learn the key
Class extension loading
Difference between classification and class extension
The implementation principle of associated objects
1. Class Extension
1.1 class extensions
Extension classes are often used in our development process. The red box below is a ViewController extension.
Class extensions are actually a special class, also known as anonymous classes, that are created with only.h files and no.m files, as shown below:
Without creating a file, the code for a class extension can only be written between the class declaration and the class implementation, as shown in the red box below:
1.2 The difference between classification and class extension
It differs from classification as follows:
category
Category n.
- Add new methods specifically to the class.
- You cannot add member attributes to a class, and even if you do add member variables, you cannot get them.
- You can add attributes to a classification through the Runtime.
- Classes that use @property to define variables only generate getters and declarations of setter methods, not method implementations and underlined member variables.
extension
: class extensions
- It’s called a special category, also known as an anonymous category.
- You can add member attributes to a class, but as private variables.
- You can add methods to a class, which are also private.
1.3 Loading of class extensions
Start by writing the following code in the main.m file:
@interface Animal : NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) int age; - (void)eat; + (void)class_method; @end @interface Animal () @property (nonatomic, copy) NSString *ext_type; - (void)ext_sleep; + (void)ext_class_method; @end @implementation Animal + (void)load { NSLog(@"%s", __func__); } - (void)eat { NSLog(@"%s\n", __func__); } - (void)ext_sleep { NSLog(@"%s\n", __func__); } + (void)class_method { NSLog(@"%s\n", __func__); } + (void)ext_class_method { NSLog(@"%s\n", __func__); } @end int main(int argc, const char * argv[]) { Animal *animal = [[Animal alloc] init]; Animal. Name = @"; Animal. ext_type = @"; [animal ext_sleep]; return 0; }Copy the code
Use the clang command to compile the main.m file into a C++ file, and check the source code. In fact, the attributes of the class extension are no different from those defined in the main class, as shown below:
Methods defined in class extensions are no different from methods defined in the main class, as shown in the following figure:
So let’s see how the class extension data is loaded, compile and run the program, using the map_images function to call realizeClassWithoutSwift indirectly, as shown below:
Print all the methods in the Animal class using the command, as shown below:
By printing the results, we can see that the methods and attributes in the class extension are not loaded as the class data is loaded, but as the methods and attributes defined in the main class are loaded.
2. Associated objects
2.1 Associated objects and their basic usage
In our daily in the process of development, sometimes we need to add attributes to a class of systems or a library, but we can’t change the source code, so we have to create such a classification, because we defined in the classification of attributes in fact can only be used for computing the, but if you want to access attributes in an instance of the class object, Instead of adding new properties to the class’s property list, you can use the Runtime API to associate the object with the value of the associated object by hash map twice.
Suppose we wanted to associate an Animal object in the Person category CA, we could write code like this:
@interface Person: NSObject @property (nonatomic, copy) NSString *name; #import "person. h" @implementation Person @end // animal. h @implementation Animal: NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) int age; - (void)eat; #import "animal. h" @implementation Animal - (void)eat {NSLog(@"%@ eat, %s\n", self.name, __func__); @interface Person (CA) <NSObject> @property (nonatomic, strong) Animal * ANI; #import "Person+ ca. h" #import <objc/runtime.h> static NSString *kAnimal = @"Animal"; @implementation Person (CA) - (Animal *)ani { return objc_getAssociatedObject(self, kAnimal); } - (void)setAni:(Animal *)dog { objc_setAssociatedObject(self, kAnimal, dog, OBJC_ASSOCIATION_RETAIN); H > #import "Person. H "#import "Person+ ca.h" int main(int argc, const char * argv[]) { Animal *dog = [[Animal alloc] init]; Dog. name = @" dog.name "; dog.age = 1; Person *p = [[Person alloc] init]; [p setAni:dog]; P.name = @" wuhu "; [p.ani eat]; NSLog(@"%@ --- %@ \n", p.ani.name, p.name); Animal *cat = [[Animal alloc] init]; Cat. name = @" cat "; cat.age = 1; [p setAni:cat]; [p.ani eat]; NSLog(@"%@ --- %@ \n", p.ani.name, p.name); return 0; }Copy the code
Compile and run the program, and the printed information is as follows:
2.2 Implementation Principles of Associated Objects
Once we know how to associate objects, let’s explore the implementation principle of the association object. First, let’s look at the principle of setting the association object.
2.2.1 Set values of associated objects
Check out the objc_setAssociatedObject function, which has the following source code:
void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
_object_set_associative_reference(object, key, value, policy);
}
Copy the code
The _object_set_associative_reference function code is as follows:
In this function, we first explore the DisguisedPtr C++ template class, part of which is shown in the figure below:
The backup variable created in the _object_set_associative_reference function calls the DisguisedPtr constructor, passing in the object address of the associated object, Disguised by the disguise function (the pointer value is converted to decimal if positive, bitwise reversed, plus one at the end, and the pointer value is converted to decimal if negative, and then bitwise reversed) to the backup member variable value.
The association variable is then created as objcassociety C++ class type, and its constructor is initialized to store the associated object and the associated policy in its member variables _value and _policy, as shown in the red box below:
Association then calls the function acquireValue that holds the associated object, and the code in this function looks like this:
AcquireValue is stored differently depending on the association policy if the association policy is of type OBJC_ASSOCIATION_RETAIN or OBJC_ASSOCIATION_RETAIN_NONATOMIC, The objc_retain function is used to hold the object, and the reference count of the object is increments by one. Print the reference count of isa before and after the objc_retain function is called, and verify as follows:
You can see that the last eight bits of isa in this object have changed from 1 to 2, which means that the reference count has increased by 1. This can also be called a shallow copy. OBJC_ASSOCIATION_SETTER_COPY requires that the class that this object belongs to implement the copyWithZone instance method in the NSCopying protocol.
However, if the associated value is not a class object, but a primitive data type or TaggedPointer, then the value is stored directly.
Next, we define the isFirstAssociation variable to determine whether it is the first association. Then we define a C++ variable manager with class type AssociationsManager. The class definition code of AssociationsManager is shown in the following figure:
The variable _mapStorage shown in the red box is a local static variable of type Storage (alias of the ExplicitInitDenseMap class), that is, _mapStorage is initialized only once, and its life cycle is from initialization until the end of application execution. So no matter how many AssociationsManager variables are created later, _mapStorage is initialized only once and always exists, like a singleton, while ExplicitInitDenseMap C++ class code looks like this:
There is very little code, but the main code is in its parent class, ExplicitInit, as shown below:
In the ExplicitInit template class there is a uint8_t (unsigned 8-bit integer, 1 byte size) array member variable _storage, which is initialized to the size of the type passed in by the template class.
We then get AssociationsHashMap variable ‘associations’ by calling get in the Manager variable. Associations is actually a hash table. The key-value pair are respectively the wrapped DisguisedPtr and the hash table that stores the object information associated with the associated object. If the value _value of the associated object is not empty, The associations try_emplace function is first called to try to set the key values in the backup of the association values hash table to an empty ObjectAssociationMap because the associated objects have not yet been set to the association objects. ObjectAssociationMap is not added to the hash table of associated values. If it is added for the first time, an empty ObjectAssociationMap variable needs to be created for it. In fact, ObjectAssociationMap is also a hash table, which is used to store information about its associated objects.
Both types belong to the DenseMap C++ template class, but they correspond to different templates. The DenseMap template class defines several member variables as shown in the figure below.
The try_emplace function code looks like this:
LookupBucketFor (); LookupBucketFor (); LookupBucketFor (); LookupBucketFor (); LookupBucketFor ()
The code for the first LookupBucketFor function looks like this:
The logic in this function is to find the Buckets corresponding to the Val Key from Buckets and obtain the first address of the Buckets and the number of Buckets. If Buckets are 0, If the value of AssociationsHashMap is not empty, false is returned. If the value of AssociationsHashMap is not empty, the hash function is used to obtain the location where Val is stored by Buckets. If the Bucket exists and the Key value is Val, then the ObjectAssociationMap corresponding to this Val has been created. The Bucket is fetched and returns true. If the Bucket is empty, If the Bucket exists but does not equal the Bucket corresponding to Val’s Key, then a hash collision is generated. Hash and fetch the next Bucket until the number of Buckets is greater than the number of Buckets. If no Bucket corresponding to Val is found, an error occurs.
After try_emplace calls LookupBucketFor, if the Bucket for Val is found, it returns pair
, which is like a tuple. Iterator is a C++ template class named DenseMapIterator. The try_emplace call returns an iterator whose Ptr is a pointer to the Bucket that corresponds to Val. End is the pointer to the last Bucket in Buckets at AssociationsHashMap. The second bool set to true in pari indicates that the Bucket is found, but if LookupBucketFor fails to find the corresponding Bucket in Val, The InsertIntoBucket function is called to insert a new value into Buckets, that is, the Bucket where Val is initialized, as follows:
InsertIntoBucketImpl (); InsertIntoBucketImpl ();
This function determines whether Buckets at AssociationsHaseMap should be expanded. If Buckets at AssociationsHaseMap’s storage capacity exceeds three quarters (load factor), it calls grow to create a hash table with twice the current capacity. Add the Bucket from the old table to the new hash table, call LookupBucketFor to retrieve the Bucket based on the passed Key (in this case, the wrapped object), and add one to the total Buckets to return the Bucket. After the Bucket is obtained in the InsertIntoBucket function, the Key and value of the Bucket are assigned, and the wrapped associated object is assigned to the Key through the function forward. Assign the newly created ObjectAssociationMap to Value by wrapper.
Return to the _object_set_associative_reference function. After calling try_emplace and retrieving the refs_result variable, If isFirstAssociation is set to true, then the ObjectAssociationMap (refS) of the associated object is obtained. If this is the first time an associated object is added to the associated object, the refs table must be empty. The refs function try_emplace is called to get the associated object information for the passed key (i.e., ObjcAssociation wraps the types of policy and value) and passes in the current association as an argument. Refs calls try_emplace in the same way as associations calls try_emplace in the same way. If the refs calls try_emplace and gets a Bucket that already exists, In this case, the management policies in the Association, the management policies of the value (Object Society type) in the bucket corresponding to the key in the refS, and the associated objects need to be exchanged.
IsFirstAssociation is true. If true, it calls the setHasAssociatedObjects function to set the isa value of the associated object. Has_assoc is set to true. The releaseHeldValue function is then called to release the _value in the old association, which is the process of setting the value of the associated object.
2.2.2 Deleting a Single associated object
If you want to delete an associated object of an associated object, all you need to do is call objc_setAssociatedObject and set the value that’s passed to nil, The _object_set_associative_reference function will determine whether the value is empty. If the value is not empty, the process of setting the value of the associated object is executed; if the value is empty, the process of deleting the associated object is executed, as shown in the following figure:
Associations’ find function is called to find the AssociationHashMap of the associated object, as shown below:
LookupBucketFor returns iterator if it is found, or the value returned after the end() call if it is not found. Return to the _object_set_associative_reference function, and then determine whether the ObjectAssociationMap table corresponding to the associated object can be found in AssociationHashMap. If not, do not do anything. If the corresponding ObjectAssociationMap table is found, find is called to find the value of the corresponding key in the ObjectAssociationMap table. If not, do nothing. If it does, it sets policy and value to the policy and nil value of the value corresponding to the key in the ObjectAssociationMap and then calls erase to erase the pair of keys in the ObjectAssociationMap. Then check whether Buckets is empty in the ObjectAssociationMap. If Buckets is empty, erase will be called to erase the AssociationHashMap and ObjectAssociationMap.
2.2.3 Associated Object Values
If you want to get the value of the associated object, you can get it by calling the objc_getAssociatedObject function, which looks like this:
In the objc_getAssociatedObject function, the value is given by _object_get_associative_reference, and its code is shown below:
It can be found that the value process of the associated object is very similar to the deletion process of the associated object. The difference lies in that during the value process, if the packaging type association of the associated object is obtained, it will judge whether the associated object is an OC object and whether it is not; if it is not empty and is an OC object, If the association policy is of type OBJC_ASSOCIATION_RETAIN, the turnedvalue value of the association object is increased by one, but the autoreleaseredvalue function is called later, but the turnedvalue of the association object is not reduced by one, as shown in the following figure:
This is because, instead of sending an autoRelease message, the rootAutorelease function is called during the call to the bottom of the autoreleaseredvalue function, as shown in the figure below:
And then finally return the value of the associated object, and that’s the whole process of evaluating the associated object.
2.2.4 Deleting All Associated Objects
Now that we know how to set, delete, and value a single associated object, what happens to all associated objects when an associated object is released by calling the dealloc method? Objc_removeAssociatedObjects = objc_removeAssociatedObjects = objc_removeAssociatedObjects
If true, the _object_remove_assocations function is called, with the code shown below:
In this function, we first create a temporary variable refs of type ObjectAssociationMap, whose member variables are initialized to the corresponding null values, The global hash table AssociationsHashMap (which holds all associated objects and their corresponding ObjectAssociationMap) is then obtained according to the get() function of the manager variable. Then check whether the incoming associated object has a corresponding ObjectAssociationMap table in this table. If found, exchange the data in the data domain Refs of the ObjectAssociationMap table of this associated object. Initialize a variable of type bool, didReInsert, with an initial value of false, and determine whether the parameter dealLocating is false, or re-insert if it is not. Each Bucket of Buckets in refS (ObjectAssociationMap table, which stores the key passed in when setting the associated object and the corresponding object Association variable) is iterated. Call the policy() function of the second Bucket pair to obtain the policy of the associated objects in the Bucket. If the value of OBJC_ASSOCIATION_SYSTEM_OBJECT is true, it means that the associated object is a system object. Insert the associated object back into the associationMap of the associated object in the AssociationsHashMap table, and then assign didReInsert to true, If there is no system object to reinstate after traversing the entire objCasSociety Map, call erase to erase the associated object from the AssociationsHashMap. After iterating through all the keys in the refs table and the corresponding associated object information, releaseHeldValue is called to subtract 1 from the reference count of the associated objects of all non-system objects, and a SmallVector laterRefs is defined to store all system associated objects. Finally, all associated object reference counts in laterRefs are reduced by one.
But when is the _object_remove_assocations function called? Let’s do the reverse lookup, search globally for the _object_remove_assocations keyword, and find that it is called in the following function:
The objc_destructInstance function is called in object_Dispose, as shown below:
The object_Dispose function is called in the objc_object::rootDealloc function, as shown below:
The objc_object::rootDealloc function is called in the _objc_rootDealloc function, as shown below:
Finally, the _objc_rootDealloc function is called in the dealloc method. When the reference count of the object is 0, the system calls its dealloc method. This is the call process for deleting all associated objects of the object.
3. Summary
After the above discussion, we already know clearly that the storage of an object’s association is actually a two-layer hash table structure in the bottom layer, as shown in the following figure: