The most practical runtime summary of IOS OC

preface

There are a lot of runtime materials on the Internet, some of which are obscure and difficult to understand. I summarize them through my own learning methods, mainly talking about some common methods and functions, mainly practical. I think it is the most impressive to use them. Moreover, the last two demo s are also the implementation principle of mjextense, and I can talk more during the interview.
In addition, there is a lot of knowledge about runtime. If you want to know more, you can see my translation Official documents (a little boring), the demo of this article Download address

What is runtime?

Runtime is a set of C language API at the bottom of OC (Introducing < objc / runtime. H > or < objc / message. H >), and the compiler will eventually convert the OC code into runtime code. Compile the. m file through the terminal command: clang - rewrite objc XXX. m, you can see the compiled xxx.cpp (c + + file).
For example, we created an object [[NSObject alloc]init], which was eventually converted into tens of thousands of lines of code. By intercepting the most critical sentence, we can see that the underlying object is created through runtime

Delete some coercion statements. You can see that the essence of the calling method is to send messages. The [[NSObject alloc]init] statement sends messages twice, the first alloc message and the second init message. Using this function, we can explore the underlying, such as the implementation principle of block.
It should be noted that objc is used_ msgSend() sel_ The registername() method needs to import the header file < objc / message. H >

In addition, the use of runtime can do some functions that OC is not easy to implement

  • Implementation of two methods of dynamic switching (especially the method provided by the switching system)
  • Dynamically add member variables and member methods of objects
  • Get all member methods and all member variables of a class

How do I apply the runtime?

1. Turn some OC code into runtime code and explore the underlying, such as the implementation principle of block (as mentioned above);
2. Intercept the system's own method calls (Swizzle black magic), such as imageNamed:, viewDidLoad, alloc;
3. You can also add attributes to implement classification;
4. Realize automatic archiving and automatic de filing of NSCoding;
5. Realize the automatic conversion of dictionary and model.

Now I'll explain it one by one through the demo

1, Exchange the implementation of the two methods and intercept the method call function of the system

Methods to be used < objc / runtime. H >

  • Get the class method of a class
Method class_getClassMethod(Class cls , SEL name)
  • Get the instance object method of a class
Method class_getInstanceMethod(Class cls , SEL name)
  • Implementation of switching two methods
void method_exchangeImplementations(Method m1 , Method m2)

Case 1: simple method exchange

Create a Person class that implements the following two class methods and declares them in the. h file

+ (void)run {
    NSLog(@"run");
}

+ (void)study {
    NSLog(@"study");
}

When the controller is called, run and print learning first.

[Person run];
[Person study];

Next, the method exchange is implemented through runtime, and the class method uses class_getClassMethod, the object method uses class_getInstanceMethod

// Gets the class methods of two classes
Method m1 = class_getClassMethod([Person class], @selector(run));
Method m2 = class_getClassMethod([Person class], @selector(study));
// Start switching method implementation
method_exchangeImplementations(m1, m2);
// After the exchange, print the study first, and then print the run!
[Person run];
[Person study];

Case 2: interception system method

Requirements: for example, version adaptation is required after iOS6 is upgraded to iOS7. Different styles of pictures (materialization and flattening) are used according to different systems. How can we add version judgment statements to this method without manually modifying the imageNamed: method of each UIImage one by one?

Steps:
1. Create a category for UIImage (UIImage+Category)
2. Implement a user-defined method in the classification. The method writes the statements to be added to the system method, such as version judgment

+ (UIImage *)xh_imageNamed:(NSString *)name {
    double version = [[UIDevice currentDevice].systemVersion doubleValue];
    if (version >= 7.0) {
        // If the system version is above 7.0, use another set of file names, ending with ''_ Flat picture of os7 '
        name = [name stringByAppendingString:@"_os7"];
    }
    return [UIImage xh_imageNamed:name];
}

3. Rewrite the load method of UIImage in the classification to realize the method exchange (as long as it can execute the method exchange statement once, load is perfect)

+ (void)load {
    // Gets the class methods of two classes
    Method m1 = class_getClassMethod([UIImage class], @selector(imageNamed:));
    Method m2 = class_getClassMethod([UIImage class], @selector(xh_imageNamed:));
    // Start switching method implementation
    method_exchangeImplementations(m1, m2);
}

Note: at the end of the custom method, we must call the system method to make it have the function of loading pictures. However, due to the method exchange, the system method name has become our custom method name (a little around, that is, we can call the system method with our name and our method with the system name), which realizes the interception of the system method!

Using the above ideas, we can also add classifications to NSObject, count how many objects have been created, add classifications to controllers, and count how many controllers have been created. Especially when the company's requirements always change, we can add a function to some original controls or modules. This method is recommended!

2, Set properties in classification, and set properties for any object

As we all know, attributes cannot be set in the classification. If @ property is written in the declaration of the classification, it can only generate the declaration of get and set methods, but cannot generate member variables. That is, although the point syntax can be called, the program will crash after execution. Someone would think of using global variables? For example:

int _age;

- (int )age {
    return _age;
}

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

However, there is only one global variable in the memory during the whole execution of the program. When we create multiple objects and modify their attribute values, we will modify the same variable. Therefore, it is impossible to ensure that each object has its own attribute values like attributes. At this time, we need to add attributes to the classification with the help of runtime.

Methods to be used < objc / runtime. H >

  • set method to associate the value with the object (store the value in the object)
    Parameter object: to which object is the attribute set
    Parameter key: an attribute corresponds to a key. In the future, the stored value can be retrieved through the key. The key can be of any type: double, int, etc. it is recommended to use char to save bytes
    Parameter value: the value set for the attribute
    Parameter policy: storage policy (assign, copy and retain are strong)
void objc_setAssociatedObject(id object , const void *key ,id value ,objc_AssociationPolicy policy)
  • Use the parameter key to get the corresponding value stored in the object
id objc_getAssociatedObject(id object , const void *key)

Steps:
1. Create a category, such as adding a name attribute to any object, that is, adding a category to NSObject (NSObject+Category)
2. First declare the get and set methods in @ property in. h to facilitate the syntax call

@property(nonatomic,copy)NSString *name;

3. Rewrite the set and get methods in. m, and internally use runtime to assign and value attributes

char nameKey;

- (void)setName:(NSString *)name {
    // Associate a value with an object and store a value in an object
    objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

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

3, Get all member variables of a class

The most typical usage is that all attributes of an object need to be decodeObjectForKey: and encodeObject: in the encodeWithCoder and initWithCoder: methods of archiving and unmarshalling. No matter how many attributes are written in the runtime declaration, there is no need to modify the code in the implementation.

Methods to be used < objc / runtime. H >

  • Get all member variables of a class (outCount returns the total number of member variables)
    Parameters:
    1. Which class
    2. Put an address to receive the value, which is used to store the number of attributes
    3. Return value: store all the obtained properties. You can call up the name and type through the following two methods
Ivar *class_copyIvarList(Class cls , unsigned int *outCount)
  • Gets the name of the member variable
const char *ivar_getName(Ivar v)
  • Gets the type of the member variable
const char *ivar_getTypeEndcoding(Ivar v)

Case 1: get the names and types of all member variables in the Person class

unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([Person class], &outCount);

// Traverse all member variables
for (int i = 0; i < outCount; i++) {
    // Get the member variable corresponding to the i position
    Ivar ivar = ivars[i];
    const char *name = ivar_getName(ivar);
    const char *type = ivar_getTypeEncoding(ivar);
    NSLog(@"Member variable name:%s Member variable type:%s",name,type);
}
// Pay attention to freeing memory!
free(ivars);

Case 2: using the runtime to obtain all attributes to override the archive de filing method

// Set properties that do not need to be resolved
- (NSArray *)ignoredNames {
    return @[@"_aaa",@"_bbb",@"_ccc"];
}

// De filing method
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super initWithCoder:aDecoder]) {
        // Get all member variables
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList([self class], &outCount);
        
        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            // Converts each member variable name to an NSString object type
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            
            // Ignore properties that do not need to be unmarked
            if ([[self ignoredNames] containsObject:key]) {
                continue;
            }
            
            // The value is retrieved according to the variable name, regardless of the type
            id value = [aDecoder decodeObjectForKey:key];
            // The extracted value is set to the property
            [self setValue:value forKey:key];
            // These two steps are equivalent to the previous self. Age = [adecodeobjectforkey: @ "_age"];
        }
        free(ivars);
    }
    return self;
}

// Archive call method
- (void)encodeWithCoder:(NSCoder *)aCoder {
     // Get all member variables
    unsigned int outCount = 0;
    Ivar *ivars = class_copyIvarList([self class], &outCount);
    for (int i = 0; i < outCount; i++) {
        Ivar ivar = ivars[i];
        // Converts each member variable name to an NSString object type
        NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        // Ignore attributes that do not need to be archived
        if ([[self ignoredNames] containsObject:key]) {
            continue;
        }
        
        // Get the value of the member variable through the member variable name
        id value = [self valueForKeyPath:key];
        // Then archive the values
        [aCoder encodeObject:value forKey:key];
        // These two steps are equivalent to [aCoder encodeObject:@(self.age) forKey:@"_age"];
    }
    free(ivars);
}

According to the above principles, we can classify NSObject, so that we don't need to write such a long string of code every time. As long as we implement a small piece of code, an object can have the ability to resolve files.

Note that in the following code, I changed a method name (otherwise the original method of the system will be overwritten!), added a judgment to ignore whether the attribute method is implemented, and added a file reduction loop for the parent attribute.

NSObject+Extension.h

#import <Foundation/Foundation.h>

@interface NSObject (Extension)

- (NSArray *)ignoredNames;
- (void)encode:(NSCoder *)aCoder;
- (void)decode:(NSCoder *)aDecoder;

@end

NSObject+Extension.m

#import "NSObject+Extension.h"
#import <objc/runtime.h>

@implementation NSObject (Extension)

- (void)decode:(NSCoder *)aDecoder {
    // Look up the parent classes one by one, and execute the file reduction method for the properties of the parent class
    Class c = self.class;
    while (c &&c != [NSObject class]) {
        
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList(c, &outCount);
        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            
            // If the method is implemented, call it again
            if ([self respondsToSelector:@selector(ignoredNames)]) {
                if ([[self ignoredNames] containsObject:key]) continue;
            }
            
            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
        free(ivars);
        c = [c superclass];
    }
    
}

- (void)encode:(NSCoder *)aCoder {
    // Look up the parent classes one by one, and execute the file reduction method for the properties of the parent class
    Class c = self.class;
    while (c &&c != [NSObject class]) {
        
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList([self class], &outCount);
        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            
            // If the method is implemented, call it again
            if ([self respondsToSelector:@selector(ignoredNames)]) {
                if ([[self ignoredNames] containsObject:key]) continue;
            }
            
            id value = [self valueForKeyPath:key];
            [aCoder encodeObject:value forKey:key];
        }
        free(ivars);
        c = [c superclass];
    }
}
@end

Usage method of the above classification: implement the following method in the object that needs to be classified and solved:

// Set properties to ignore
- (NSArray *)ignoredNames {
    return @[@"bone"];
}

// Call our method within the system method
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        [self decode:aDecoder];
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [self encode:aCoder];
}

In this way, we have to write the same code every time. We can encapsulate the two methods of resolving files into macros. We can do it with a macro where necessary. If there are properties that do not need to be resolved files, we can implement the ignoredNames method. For details, see my demo. This is also the implementation principle of resolving files with a macro in mjextense.

Case 3: use runtime to obtain all attributes to convert dictionary to model

In the past, we used KVC to convert the dictionary to the model, but it still has some limitations. For example, if the model attribute does not correspond to the key value pair, it will crash (although the setValue: forundefined key: method can be rewritten to prevent error reporting), and it is difficult to handle when the model attribute is an object or array, so in terms of efficiency and function, It is a good choice to use runtime to convert dictionary to model.

We need to consider three special cases for the dictionary to model:
1. When the key of the dictionary does not match the attribute of the model
2. Nested model in model (model attribute is another model object)
3. There are models in the array (the attributes of the model are an array, and there are model objects in the array)

According to the above three special cases, we deal with them one by one. First, the key of the dictionary does not correspond to the attribute of the model.
No, there should be two. One is that the key value of the dictionary is greater than the number of model attributes. At this time, we do not need any processing, because the runtime first traverses all the attributes of the model, and then finds the corresponding value in the dictionary according to the attribute name for assignment. Of course, redundant key value pairs will not be viewed; The other is that the number of model attributes is greater than the key value pairs of the dictionary. At this time, because the attribute has no corresponding value, it will be assigned nil, which will lead to crash. We only need to add a judgment. The JSON data and sample are as follows:

- (void)setDict:(NSDictionary *)dict {
    
    Class c = self.class;
    while (c &&c != [NSObject class]) {
        
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList(c, &outCount);
        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            
            // Convert member variable name to property name (remove underscore)
            key = [key substringFromIndex:1];
            // Fetch dictionary values
            id value = dict[key];
            
            // If the number of model attributes is greater than the dictionary key value pair, the model attribute will be assigned nil and an error will be reported
            if (value == nil) continue;
            
            // Set the values in the dictionary to the model
            [self setValue:value forKeyPath:key];
        }
        free(ivars);
        c = [c superclass];
    }
}

The second case is that the attribute of the model is another model object

At this time, we need to use the Ivar of runtime_ The gettypeencoding method obtains the model object type, and then converts the model object type from dictionary to model, that is, recursion. Note that we need to exclude the object type of the system, such as NSString. In the following methods, I add a class method to facilitate recursion.

You can see each attribute type when printing

#import "NSObject+JSONExtension.h"
#import <objc/runtime.h>

@implementation NSObject (JSONExtension)

- (void)setDict:(NSDictionary *)dict {
    
    Class c = self.class;
    while (c &&c != [NSObject class]) {
        
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList(c, &outCount);
        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            
            // Convert member variable name to property name (remove underscore)
            key = [key substringFromIndex:1];
            // Fetch dictionary values
            id value = dict[key];
            
            // If the number of model attributes is greater than the dictionary key value pair, the model attribute will be assigned nil and an error will be reported
            if (value == nil) continue;
            
            // Gets the type of the member variable
            NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
            
            // If the property is an object type
            NSRange range = [type rangeOfString:@"@"];
            if (range.location != NSNotFound) {
                // Then the name of the intercepted object (for example, @ "Dog", intercepted as Dog)
                type = [type substringWithRange:NSMakeRange(2, type.length - 3)];
                // Exclude system object types
                if (![type hasPrefix:@"NS"]) {
                    // Convert the object name to the object type, and convert the new object dictionary to the model (recursion)
                    Class class = NSClassFromString(type);
                    value = [class objectWithDict:value];
                }
            }
            
            // Set the values in the dictionary to the model
            [self setValue:value forKeyPath:key];
        }
        free(ivars);
        c = [c superclass];
    }
}

+ (instancetype )objectWithDict:(NSDictionary *)dict {
    NSObject *obj = [[self alloc]init];
    [obj setDict:dict];
    return obj;
}

In the third case, the attribute of the model is an array, and there are model objects in the array. For example, I can get the C language programming through books[0].name

Since we can get the attribute type, we can intercept the array attribute of the model, and then traverse each model in the array and dictionary to model. However, we don't know what type the model in the array is. We can declare a method. The purpose of this method is not to make it call, but to make it implement and return the type of the model.
This language may not be explained clearly. You can refer to my demo and run it directly.

NSObject+JSONExtension.h

// Returns what type of model objects are in the array
- (NSString *)arrayObjectClass ;

NSObject+JSONExtension.m

#import "NSObject+JSONExtension.h"
#import <objc/runtime.h>

@implementation NSObject (JSONExtension)

- (void)setDict:(NSDictionary *)dict {
    
    Class c = self.class;
    while (c &&c != [NSObject class]) {
        
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList(c, &outCount);
        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            
            // Convert member variable name to property name (remove underscore)
            key = [key substringFromIndex:1];
            // Fetch dictionary values
            id value = dict[key];
            
            // If the number of model attributes is greater than the dictionary key value pair, the model attribute will be assigned nil and an error will be reported
            if (value == nil) continue;
            
            // Gets the type of the member variable
            NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
            
            // If the property is an object type
            NSRange range = [type rangeOfString:@"@"];
            if (range.location != NSNotFound) {
                // Then the name of the intercepted object (for example, @ "Dog", intercepted as Dog)
                type = [type substringWithRange:NSMakeRange(2, type.length - 3)];
                // Exclude system object types
                if (![type hasPrefix:@"NS"]) {
                    // Convert the object name to the object type, and convert the new object dictionary to the model (recursion)
                    Class class = NSClassFromString(type);
                    value = [class objectWithDict:value];
                    
                }else if ([type isEqualToString:@"NSArray"]) {
                    
                    // If it is an array type, dictionary each model in the array to model, and first create a temporary array to store the model
                    NSArray *array = (NSArray *)value;
                    NSMutableArray *mArray = [NSMutableArray array];
                    
                    // Gets the type of each model
                    id class ;
                    if ([self respondsToSelector:@selector(arrayObjectClass)]) {
                        
                        NSString *classStr = [self arrayObjectClass];
                        class = NSClassFromString(classStr);
                    }
                    // Dictionary all models in the array to model
                    for (int i = 0; i < array.count; i++) {
                        [mArray addObject:[class objectWithDict:value[i]]];
                    }
                    
                    value = mArray;
                }
            }
            
            // Set the values in the dictionary to the model
            [self setValue:value forKeyPath:key];
        }
        free(ivars);
        c = [c superclass];
    }
}

+ (instancetype )objectWithDict:(NSDictionary *)dict {
    NSObject *obj = [[self alloc]init];
    [obj setDict:dict];
    return obj;
}

@end


Author: I'm Mr. Teng
Link: https://www.jianshu.com/p/ab966e8a82e2
Source: Jianshu
The copyright belongs to the author. For commercial reprint, please contact the author for authorization, and for non-commercial reprint, please indicate the source.

Tags: Runtime

Posted on Sat, 02 Oct 2021 21:31:42 -0400 by GRUMBLE