Frequently Asked Points in iOS Interviews - RunTime

1. Introduction to RunTime

RunTime is short for runtime. OC is the runtime mechanism, that is, some mechanisms at runtime, the most important of which is the message mechanism.

For the C language, the function call will determine which function to call at compile time, and an error will be reported if the unimplemented function is called. For the OC language, it belongs to the dynamic calling process. It is not possible to decide which function to call at the time of compilation. Only when it is actually running will it find the corresponding function to call according to the name of the function. In the compilation phase, OC can call any function, even if the function is not implemented, as long as it is declared, no error will be reported.

2. RunTime message mechanism

The message mechanism is the most important mechanism in the runtime. The essence of calling any method in OC is to send a message. When using the runtime, the framework needs to be imported to send a message <objc/message.h>and after xcode5, Apple does not recommend using the underlying method. If you want to use the runtime, you need to turn off the strict check of the objc_msgSend call, and BuildSetting->Search msg to NO.

Let's take a look at the underlying implementation of instance method calls

Person *p = [[Person alloc] init];
[p eat];
// 底层会转化成
//SEL:方法编号,根据方法编号就可以找到对应方法的实现。
[p performSelector:@selector(eat)];
//performSelector本质即为运行时,发送消息,谁做事情就调用谁 
objc_msgSend(p, @selector(eat));
// 带参数
objc_msgSend(p, @selector(eat:),10);

The bottom layer of the class method invocation

// 本质是会将类名转化成类对象,初始化方法其实是在创建类对象。
[Person eat];
// Person只是表示一个类名,并不是一个真实的对象。只要是方法必须要对象去调用。
// RunTime 调用类方法同样,类方法也是类对象去调用,所以需要获取类对象,然后使用类对象去调用方法。
Class personclass = [Persion class];
[[Persion class] performSelector:@selector(eat)];
// 类对象发送消息
objc_msgSend(personclass, @selector(eat));

**@selector (SEL): is a SEL method selector. The main function of **SEL is to quickly find the function pointer of the corresponding method through the method name, and then call its function. SEL itself is an address of type Int, and the name of the method is stored in the address. for a class. Each method corresponds to an SEL. Therefore, two methods with the same name cannot exist in a class, even if the parameter types are different, because SEL is generated according to the method name, and the same method name can only correspond to one SEL.

The underlying implementation of sending messages at runtime Each class has a method list Method List, which saves all the methods in this class, and finds the method according to the method number passed in by SEL, which is equivalent to the value-key mapping. Then find the implementation of the method. Go to the implementation of the method to implement it. as the picture shows.

Low-level implementation of sending messages at runtime

So how does it dynamically find the corresponding method internally? First of all, we know that all classes inherit from the NSObject class, and there is a Class isa pointer in NSObjcet.

typedef struct objc_class *Class;
@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

We come to objc_class to see, which contains some basic information of the class.

struct objc_class {
  Class isa; // 指向metaclass
  
  Class super_class ; // 指向其父类
  const char *name ; // 类名
  long version ; // 类的版本信息,初始化默认为0,可以通过runtime函数class_setVersion和class_getVersion进行修改、读取
  long info; // 一些标识信息,如CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含对象方法和成员变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
  long instance_size ; // 该类的实例变量大小(包括从父类继承下来的实例变量);
  struct objc_ivar_list *ivars; // 用于存储每个成员变量的地址
  struct objc_method_list **methodLists ; // 与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储对象方法,如CLS_META (0x2L),则存储类方法;
  struct objc_cache *cache; // 指向最近使用的方法的指针,用于提升效率;
  struct objc_protocol_list *protocols; // 存储该类遵守的协议
}

Next, we will use the eat method of the p instance to see how to dynamically find the corresponding method after a specific message is sent.

  1. The instance method [p eat];calls the method at the bottom [p performSelector:@selector(eat)];, and the compiler converts the code intoobjc_msgSend(p, @selector(eat));
  2. in the objc_msgSendfunction. First find the corresponding pointer through pthe pointer . Find the corresponding function in the middle first , if found, jump to the corresponding function through the function pointer in the middle to execute.isapclassClasscacheSELmethodmethod
  3. If cachenot found. Find methodListit again. If it can be found, it will be methodadded to cachein order to facilitate the next search, and methodjump to the corresponding function to execute through the function pointer in .
  4. If methodlistit is not found, go superClassto find it. If it can be found, it will be methodadded to cachein order to facilitate the next search, and methodjump to the corresponding function to execute through the function pointer in .

3. Use the RunTime exchange method:

When the function of the method that comes with the system is not enough, you need to extend some functions to the method that comes with the system, and keep the original function, you can use the RunTime exchange method to achieve. Here, when adding a picture to the image, it will automatically determine whether the image is empty. If it is empty, it will remind that the picture does not exist. Method 1: Use categories

+ (nullable UIImage *)xx_ccimageNamed:(NSString *)name
{
    // 加载图片    如果图片不存在则提醒或发出异常
   UIImage *image = [UIImage imageNamed:name];
    if (image == nil) {
        NSLog(@"图片不存在");
    }
    return image;
}

Disadvantages: The header file needs to be imported every time it is used, and if the project is relatively large, all the methods used before need to be changed.

Method 2: RunTime exchange method The essence of the exchange method is to exchange the implementation of the two methods, that is, to exchange the xx_ccimageNamed and imageName methods, so that calling xx_ccimageNamed is actually the purpose of calling the imageNamed method.

So first of all, you need to understand where the method is exchanged, because the exchange only needs to be done once, so in the load method of the classification, the method can be exchanged when the classification is loaded.

 +(void)load
{
    // 获取要交换的两个方法
    // 获取类方法  用Method 接受一下
    // class :获取哪个类方法 
    // SEL :获取方法编号,根据SEL就能去对应的类找方法。
    Method imageNameMethod = class_getClassMethod([UIImage class], @selector(imageNamed:));
    // 获取第二个类方法
    Method xx_ccimageNameMrthod = class_getClassMethod([UIImage class], @selector(xx_ccimageNamed:));
    // 交换两个方法的实现 方法一 ,方法二。
    method_exchangeImplementations(imageNameMethod, xx_ccimageNameMrthod);
    // IMP其实就是 implementation的缩写:表示方法实现。
}

The internal implementation of the exchange method:

  1. Find the method in Method according to the SEL method number, both methods are found
  2. The implementation of the swap method, the pointer cross points. as the picture shows:
    The internal implementation of the exchange method

Note: When the method is exchanged, the imageNamed method cannot be called in the xx_ccimageNamed method, because calling the imageNamed method is essentially equivalent to calling the xx_ccimageNamed method, which will cause circular references to cause an infinite loop.

RunTime also provides methods for obtaining object methods and method implementations.

// 获取方法的实现
class_getMethodImplementation(<#__unsafe_unretained Class cls#>, <#SEL name#>) 
// 获取对象方法
class_getInstanceMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>)

At this point, when the imageNamed: method is called, the xx_ccimageNamed: method is called to add a picture to the image and determine whether the picture exists. If it does not exist, it will remind that the picture does not exist.

4. Dynamic Add Method

If a class has many methods, many of them may not be used temporarily. When loading class methods into memory, it is necessary to generate a mapping table for each method, which is more resource-intensive. At this point, you can use RunTime to dynamically add methods

Dynamically adding a method to a class is equivalent to a lazy loading mechanism. Many methods in the class are temporarily unavailable, so they will not be loaded first, and the methods will be loaded when they are used.

The method of dynamically adding methods: First of all, we do not implement the object method, and then dynamically load the method when the performSelector: method is called. Create the Person class as above and use performSelector: to call the eat method of the Person class object.

Person *p = [[Person alloc]init];
// 当调用 P中没有实现的方法时,动态加载方法
[p performSelector:@selector(eat)];

At this time, no error will be reported when compiling, but only when the program is running, because the eat method is not implemented in the Person class. When you go to the Method List in the class and find that the eat method cannot be found, it will report an error that the eat method cannot be found. .

Error message: Not sent to instance by selector

When the corresponding method cannot be found, it will come to intercept the call, and the method called before the program crashes when the method called cannot be found. The ** method is called when an unimplemented object method is called +(BOOL)resolveInstanceMethod:(SEL)sel. When a class method that is not implemented is called, the+(BOOL)resolveClassMethod:(SEL)sel ** method is called.

First, let's go to the API and look at Apple's instructions, search for Dynamic Method Resolution to come to dynamic method resolution.

Dynamic Method Resolution

The API of Dynamic Method Resolution has been explained very clearly. We can implement methods resolveInstanceMethod:or resolveClassMethod:methods, and dynamically add methods and method implementations to instance methods or class methods.

So through these two methods, you can know which methods are not implemented, so as to dynamically add methods. The parameter sel means a method that is not implemented.

An Objective-C method is ultimately a C function, and by default any method takes two parameters. self : method caller_cmd : calling method number. We can add a method and implementation to a class using the function class_addMethod.

Following the example given by the API, dynamically add the eat object to the P instance

+(BOOL)resolveInstanceMethod:(SEL)sel
{
    // 动态添加eat方法
    // 首先判断sel是不是eat方法 也可以转化成字符串进行比较。    
    if (sel == @selector(eat)) {
    /** 
     第一个参数: cls:给哪个类添加方法
     第二个参数: SEL name:添加方法的编号
     第三个参数: IMP imp: 方法的实现,函数入口,函数名可与方法名不同(建议与方法名相同)
     第四个参数: types :方法类型,需要用特定符号,参考API
     */
      class_addMethod(self, sel, (IMP)eat , "v@:");
        // 处理完返回YES
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

Focus on the class_addMethod method

class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types)

Four parameters in class_addMethod. The first and second parameters are easier to understand, and the focus is on the third and fourth parameters.

  1. cls : Indicates which class to add a method to, here to add a method to the Person class, self represents Person.
  2. SEL name : Indicates the number of the add method. Because there is only one method that needs to be added dynamically, and it was determined by judgment that sel is the eat method, sel can be used here.
  3. IMP imp : Indicates the implementation of the method, the function entry, the function name can be different from the method name (it is recommended to be the same as the method name), you need to implement this function yourself. Each method has two implicit parameters by default self : method caller _cmd : the label of the calling method , which can be written or not.
void eat(id self ,SEL _cmd)
{
      // 实现内容
      NSLog(@"%@的%@方法动态实现了",self,NSStringFromSelector(_cmd));
}
  1. types : Indicates the method type and requires a specific symbol. The example provided by the system uses ** "v@:", let's come to the API to see"v@:" what type of method ** specifies.
    Objective-C type encodings
    As can be seen from the figure

v-> void means no return value @-> object means id parameter :-> method selector means SEL

So far, the dynamic addition of the eat method of the P instance has been completed. Output when P calls the eat method

output when calling the eat method

Dynamically add a method with parameters If it is a method with parameters, you need to make some changes to the implementation of the method and the method type parameters in the class_addMethod method. Method implementation: Because in the C language function, the object parameter type can only be replaced by id. Method type parameter: Because an id parameter is added, the method type should be ** "v@:@"** Let's take a look at the code

+(BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(eat:)) {
        class_addMethod(self, sel, (IMP)aaaa , "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
void aaaa(id self ,SEL _cmd,id Num)
{
    // 实现内容
    NSLog(@"%@的%@方法动态实现了,参数为%@",self,NSStringFromSelector(_cmd),Num);
}

call eat:function

Person *p = [[Person alloc]init];
[p performSelector:@selector(eat:)withObject:@"xx_cc"];

The output is

p output when the eat: method is called

5. Dynamically add properties to RunTime

To use RunTime to add attributes to system classes, you first need to understand the relationship between objects and attributes.

relationship between objects and properties

When the object is initialized at the beginning, its attribute name is nil. Assigning a value to an attribute is actually to make the name attribute point to a piece of memory that stores strings, so that the attribute of the object is associated with this memory. Personally, I understand that the attribute of an object is a pointer , which points to a memory area.

So if you want to add properties dynamically, it is actually to generate some kind of association dynamically. If you want to add attributes to the class of the system, you can only use the classification.

Here, add the name property to NSObject, to create the category of NSObject, we can use @property to add properties to the category

@property(nonatomic,strong)NSString *name;

Although you can write @property to add properties in the classification, private properties will not be automatically generated, nor will the implementation of set and get methods be generated, only the declaration of set and get will be generated, which needs to be implemented by ourselves.

Method 1: We can add attributes to categories by using static global variables

static NSString *_name;
-(void)setName:(NSString *)name
{
    _name = name;
}
-(NSString *)name
{
    return _name;
}

However, the _name static global variable is not associated with the class. Regardless of whether the object is created or destroyed, the _name variable exists as long as the program is running, and it is not an attribute in the true sense.

Method 2: Use RunTime to dynamically add properties RunTime provides methods for dynamically adding properties and obtaining properties.

-(void)setName:(NSString *)name
{
    objc_setAssociatedObject(self, @"name",name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSString *)name
{
    return objc_getAssociatedObject(self, @"name");    
}
  1. Dynamically add properties
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);

Parameter one: id object: Which object to add attributes to, here to add attributes to yourself, use self. Parameter two: void * == id key: attribute name, obtain the value of the attribute of the associated object according to the key, and obtain the value of the attribute objc_getAssociatedObjectthrough the secondary key in ** and return it. Parameter three:id value ** : The associated value, that is, the value passed in by the set method, is saved to the attribute. Parameter four: objc_AssociationPolicy policy: Strategy, in what form is the attribute saved. There are the following

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     // 指定相关的对象被复制,原子性   
};
  1. get attribute
objc_getAssociatedObject(id object, const void *key);

Parameter one: id object: Get the associated property in which object. Parameter two: void * == id key: What attribute objc_setAssociatedObjectcorresponds to the key in ** **, that is, the value is retrieved through the key value.

At this point, the name attribute has been successfully added to NSObject, and the NSObject object can assign values ​​to attributes through dot syntax.

NSObject *objc = [[NSObject alloc]init];
objc.name = @"xx_cc";
NSLog(@"%@",objc.name);

6. RunTime dictionary to model

In order to facilitate future reuse, here by adding a classification to NSObject, declare and implement the class method that uses the RunTime dictionary to convert the model.

+ (instancetype)modelWithDict:(NSDictionary *)dict

First, let's take a look at the difference between KVC dictionary-to-model and RunTime dictionary-to-model

KVC: The implementation principle of KVC dictionary conversion model is to traverse all keys in the dictionary, and then go to the model to find the corresponding attribute name. It is required that the attribute name and the key must be in one-to-one correspondence, and all keys in the dictionary must exist in the model. RunTime: The implementation principle of RunTime dictionary to model is to traverse all the attribute names in the model, and then go to the dictionary to find the corresponding Key, which is based on the model. What attributes are in the model, go to the dictionary to find those attributes.

The advantage of RunTime dictionary to model: When the server returns too much data, and we only use a small part of it, there is no need to define useless attributes as attributes to waste unnecessary resources. Only the most useful properties are saved.

The process of converting RunTime dictionary to model first needs to understand that the attribute is defined in the class, then there is an attribute list in the class, and the attribute list exists in the form of an array. According to the attribute list, all attribute names in the class can be obtained, so traverse the attribute list, It can also iterate over all the property names in the model. So the process of converting RunTime dictionary to model is very clear.

  1. Create model objects
id objc = [[self alloc] init];
  1. Use the ** class_copyIvarList** method to copy the member property list
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList(self, &count);

Parameter one: __unsafe_unretained Class cls: Get the list of member properties of which class. Here is self, because whoever calls the class method in the category is self. Parameter two: unsigned int *outCount: An unsigned int pointer, where an unsigned int count is created, &count is its address, and it is guaranteed that the address of the count can be obtained in the method to assign a value to the count. The value passed out is the total number of member attributes. Return value: Ivar *: Returns a pointer of type Ivar. By default, the pointer points to the 0th element of the array, and the pointer +1 will move an Ivar unit byte to the high address, which is to point to the first element. Ivar represents a member property. 3. Traverse the member attribute list to get the attribute list

for (int i = 0 ; i < count; i++) {
        // 获取成员属性
        Ivar ivar = ivarList[i];
}
  1. Use ** ivar_getName(ivar)** to get the member attribute name, because the member attribute name returns a C language string, convert it into an OC string
NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(ivar)];

ivar_getTypeEncoding(ivar)Member property types can also be obtained through ** **. 5. Because the obtained member attribute name is a member attribute with _, it is necessary to remove the underscore to obtain the attribute name, which is the key of the dictionary.

// 获取key
NSString *key = [propertyName substringFromIndex:1];
  1. Get the Value corresponding to the key in the dictionary.
// 获取字典的value
id value = dict[key];
  1. Assign values ​​to model properties and return the model
if (value) {
 // KVC赋值:不能传空
[objc setValue:value forKey:key];
}
return objc;

So far, the dictionary has been successfully converted into a model.

7. Secondary conversion of RunTime dictionary to model

Model nesting is often used in the development process, that is, there is another model in the model. Here, we try to use RunTime to perform the second-level conversion of the model. The realization idea is actually relatively simple and clear.

  1. First get the type of the member property in the first-level model
// 成员属性类型
NSString *propertyType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
  1. It is judged that the second-level conversion is required only when the value in the first-level dictionary is a dictionary and the member attribute type in the first-level model is not NSDictionary. First of all, it is necessary to convert the value to a dictionary, because we usually convert the dictionary into a model. Second, the member attribute type is not a system class, indicating that the member attribute is our custom class, that is, the secondary model to be converted. When the member attribute type is NSDictionary, it means that we want the member attribute to be a dictionary, and no model conversion is required.
id value = dict[key];
if ([value isKindOfClass:[NSDictionary class]] && ![propertyType containsString:@"NS"]) 
{ 
      // 进行二级转换。
}
  1. To get the model type to be converted, we need to do some processing on the propertyType member property type, because the propertyType returns us the member property type is ** @\"Mode\", we need to intercept it asMode **. It should be noted here that \ is just an escape character, not a placeholder.
// @\"Mode\"去掉前面的@\"
NSRange range = [propertyType rangeOfString:@"\""];
propertyType = [propertyType substringFromIndex:range.location + range.length];
// Mode\"去掉后面的\"
range = [propertyType rangeOfString:@"\""];
propertyType = [propertyType substringToIndex:range.location];
  1. Get the class object whose class needs to be converted, and convert the string to the class name.
Class modelClass =  NSClassFromString(propertyType);
  1. It is judged that if the class name is not empty, the modelWithDict method of the classification is called, the value dictionary is passed, the secondary model is converted, and the secondary model is returned to be assigned to the value.
if (modelClass) {
      value =  [modelClass modelWithDict:value];
}  

There may be some twists here. Let’s rethink it. We judge that the value is a dictionary and need to perform secondary conversion, then convert the value dictionary into a model and return it, and reassign it to the value, and finally assign the model value to the corresponding key in the primary model. The conversion of the second-level dictionary to the model can be completed.

Finally, the complete method of the secondary conversion is attached

+ (instancetype)modelWithDict:(NSDictionary *)dict{
    // 1.创建对应类的对象
    id objc = [[self alloc] init];
    // count:成员属性总数
    unsigned int count = 0;
   // 获得成员属性列表和成员属性数量
    Ivar *ivarList = class_copyIvarList(self, &count);
    for (int i = 0 ; i < count; i++) {
        // 获取成员属性
        Ivar ivar = ivarList[i];
        // 获取成员名
       NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        // 获取key
        NSString *key = [propertyName substringFromIndex:1];
        // 获取字典的value key:属性名 value:字典的值
        id value = dict[key];
        // 获取成员属性类型
        NSString *propertyType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        // 二级转换
        // value值是字典并且成员属性的类型不是字典,才需要转换成模型
        if ([value isKindOfClass:[NSDictionary class]] && ![propertyType containsString:@"NS"]) {
            // 进行二级转换
            // 获取二级模型类型进行字符串截取,转换为类名
            NSRange range = [propertyType rangeOfString:@"\""];
            propertyType = [propertyType substringFromIndex:range.location + range.length];
            range = [propertyType rangeOfString:@"\""];
            propertyType = [propertyType substringToIndex:range.location];
            // 获取需要转换类的类对象
           Class modelClass =  NSClassFromString(propertyType);
           // 如果类名不为空则进行二级转换
            if (modelClass) {
                // 返回二级模型赋值给value
                value =  [modelClass modelWithDict:value];
            }
        }
        if (value) {
            // KVC赋值:不能传空
            [objc setValue:value forKey:key];
        }
    }
    // 返回模型
    return objc;
}

The above is just a simple understanding of RunTime, which is enough to deal with some problems of Runtime in the iOS interview process.


I have summarized some learning video book materials for iOS in the "Code Farmer Style" public account, and friends who need it can get it by themselves.

Scan the code to follow

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=326690910&siteId=291194637