• This post was originally posted on my personal blog:”Unruly Pavilion”
  • Article links:portal
  • This article was updated on July 31, 2019 at 20:29:30

This article introduces how to obtain class properties and methods through “Runtime” in iOS development. Through this article, you will learn:

  1. Gets class detailed attributes, method descriptions
  2. Get class details properties, methods (list of member variables, list of properties, list of methods, list of protocols followed)
  3. Application Scenario 3.1 Modifying Private Attributes 3.2 Universal Controller Jump 3.3 Converting dictionaries to models 3.4 Improving iOS Archiving and Archiving

Bujige/Ysc-class-detaillist-demo


1. Obtain detailed class attributes and method descriptions

Only a small number of publicly available properties and methods are available in the classes officially provided by Apple. Some of the properties and methods we need are hidden from us by the authorities and not provided to us directly.

So how do we get all the variables and methods in a class to see if there are any that are useful to us?

Fortunately, the Runtime provides a set of apis to get Ivar, Property, Method, Protocol, and so on. We can use these methods to traverse a list of member variables, attributes, methods, and protocols in a class. To find the variables and methods we need.

Let’s say you have a requirement to change the color and size of the UITextField placeholder text. The implementation code refers to the example in 3.1 Modifying private Attributes.

Let’s start by explaining how to get the class properties and methods in code.


2. Obtain detailed class attributes and methods

Note: #import

is required in the header file.

2.1 Get the list of member variables of the class

// Prints a list of member variables
- (void)printIvarList {
    unsigned int count;
    
    Ivar *ivarList = class_copyIvarList([self class], &count);
    for (unsigned int i = 0; i < count; i++) {
        Ivar myIvar = ivarList[i];
        const char *ivarName = ivar_getName(myIvar);
        NSLog(@"ivar(%d) : %@", i, [NSString stringWithUTF8String:ivarName]);
    }
    
    free(ivarList);
}
Copy the code

2.2 Getting the property list of the class

// Prints the property list
- (void)printPropertyList {
    unsigned int count;
    
    objc_property_t *propertyList = class_copyPropertyList([self class], &count);
    for (unsigned int i = 0; i < count; i++) {
        const char *propertyName = property_getName(propertyList[i]);
        NSLog(@"propertyName(%d) : %@", i, [NSString stringWithUTF8String:propertyName]);
    }
    
    free(propertyList);
}
Copy the code

2.3 Getting the method list of the class

// Prints a list of methods
- (void)printMethodList {
    unsigned int count;
    
    Method *methodList = class_copyMethodList([self class], &count);
    for (unsigned int i = 0; i < count; i++) {
        Method method = methodList[i];
        NSLog(@"method(%d) : %@", i, NSStringFromSelector(method_getName(method)));
    }
    
    free(methodList);
}
Copy the code

2.4 Obtaining a list of protocols that the class complies with

// Prints the protocol list
- (void)printProtocolList {
    unsigned int count;
    
    __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
    for (unsigned int i = 0; i < count; i++) {
        Protocol *myProtocal = protocolList[i];
        const char *protocolName = protocol_getName(myProtocal);
        NSLog(@"protocol(%d) : %@", i, [NSString stringWithUTF8String:protocolName]);
    }
    
    free(protocolList);
}
Copy the code

3. Application scenarios

3.1 Modifying private Attributes

Requirement: Change the color and size of UITextField placeholder text

Let’s think about several ways to do this:

Method 1: Modify by attributedPlaceholder attribute

We know that UITextField has placeholder properties and attributedPlaceholder properties. You can only change placeholder text via the placeholder property; you cannot change the font or color of placeholder text. And with the attributedPlaceholder property we can change the color and size of the UITextField placeholder.

Mymyplaceholderinrect: myMyPlaceholderInRect: myMyPlaceholderinRect: myMyPlaceholderinRect: myMyPlaceholderinRect: myMyMyReturn

Implementation steps:

  1. Custom XXTextField inherits from UITextField;
  2. DrawPlaceholderInRect: myquery ();
  3. Set the placeholder property in the drawPlaceholderInRect method.
- (void)drawPlaceholderInRect:(CGRect)rect {
    
    // Calculate the Size of the placeholder text
    NSDictionary *attributes = @{
                                 NSForegroundColorAttributeName : [UIColor lightGrayColor],
                                 NSFontAttributeName : [UIFont systemFontOfSize:15]};CGSize placeholderSize = [self.placeholder sizeWithAttributes:attributes];
    
    [self.placeholder drawInRect:CGRectMake(0, (rect.size.height - placeholderSize.height)/2, rect.size.width, rect.size.height) withAttributes: attributes];
}
Copy the code

Method 3: Using the Runtime, find and modify the private attributes of the UITextfield

Implementation steps:

  1. Print all attributes and member variables of the UITextfield by obtaining the class attribute list and member variable list method;
  2. Find the private member variable_placeholderLabel;
  3. Use KVC on_placeholderLabelModify.
// Prints all attributes and member variables of the UITextfield
- (void)printUITextFieldList {
    unsigned int count;
    
    Ivar *ivarList = class_copyIvarList([UITextField class], &count);
    for (unsigned int i = 0; i < count; i++) {
        Ivar myIvar = ivarList[i];
        const char *ivarName = ivar_getName(myIvar);
        NSLog(@"ivar(%d) : %@", i, [NSString stringWithUTF8String:ivarName]);
    }
    
    free(ivarList);
    
    objc_property_t *propertyList = class_copyPropertyList([UITextField class], &count);
    for (unsigned int i = 0; i < count; i++) {
        const char *propertyName = property_getName(propertyList[i]);
        NSLog(@"propertyName(%d) : %@", i, [NSString stringWithUTF8String:propertyName]);
    }
    
    free(propertyList);
}

// Change the placeholder color and font by modifying the private property of the UITextfield
- (void)createLoginTextField {
    UITextField *loginTextField = [[UITextField alloc] init];
    loginTextField.frame = CGRectMake(15, (self.view.bounds.size.height- 52- 50) /2.self.view.bounds.size.width- 6018.52);
    loginTextField.delegate = self;
    loginTextField.font = [UIFont systemFontOfSize:14];
    loginTextField.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter;
    loginTextField.textColor = [UIColor blackColor];
    
    loginTextField.placeholder = @" Username/email";
    [loginTextField setValue:[UIFont systemFontOfSize:15] forKeyPath:@"_placeholderLabel.font"];
    [loginTextField setValue:[UIColor lightGrayColor]forKeyPath:@"_placeholderLabel.textColor"];
    
    [self.view addSubview:loginTextField];
}
Copy the code

3.2 Universal controller jump

Requirements:

  1. Different banners of a page, click to jump to a different page.
  2. Push notification, click to jump to the specified page.
  3. Two-dimensional code scanning, according to different content, jump to different pages.
  4. WebView page, according to the URL click different, jump to different native page.

Let’s think about a couple of solutions.

Method 1: Write a bunch of judgment statements and jump statements at each place you want to jump.

Method 2: Extract judgment statements and jump statements and write them to the base class or corresponding Category.

Method 3: Using the Runtime, customize a universal jump Controller tool.

Implementation steps:

  1. Discuss with the server in advance, define rules for jumping to different controllers, and let the server return the relevant parameters of the corresponding rules. For example, to jump to controller A, the server needs to pass back the class name of controller A, and controller A needs to pass in property parameters (ID, type, and so on).
  2. According to the class name returned by the server, create the corresponding controller object;
  3. Iterate through the parameters returned by the server, using the Runtime to iterate through the property list of the controller object;
  4. If the controller object has this property, it is assigned using KVC.
  5. Jump.

First, define jump rules, as shown below. XXViewController is the name of the controller class to jump to. The property dictionary holds the property parameters required by the controller.

// define rule NSDictionary *params = @{@"class" : @"XXViewController"The @"property" : @{
                                 @"ID" : @"123"The @"type" : @"XXViewController1"}};Copy the code

Then, add a utility class XXJumpControllerTool to add jump related class methods.

/ * * * * * * * * * * * * * * * * * * * * * XXJumpControllerTool. H file * * * * * * * * * * * * * * * * * * * * * /#import <Foundation/Foundation.h>@interface XXJumpControllerTool : NSObject + (void)pushViewControllerWithParams:(NSDictionary *)params; @ the end / * * * * * * * * * * * * * * * * * * * * * XXJumpControllerTool m file * * * * * * * * * * * * * * * * * * * * * /#import "XXJumpControllerTool.h"
#import <UIKit/UIKit.h>
#import <objc/runtime.h>@ implementation XXJumpControllerTool + (void) pushViewControllerWithParams params: (NSDictionary *) {/ / remove the controller class name nsstrings *classNameStr = [NSString stringWithFormat:@"% @", params[@"class"]]. const char *className = [classNameStr cStringUsingEncoding:NSASCIIStringEncoding]; Class newClass = objc_getClass(className);if(! NewClass) {// Create a Class Class superClass = [NSObject Class]; newClass = objc_allocateClassPair(superClass, className, 0); // Register the class you created objc_registerClassPair(newClass); } // create object (controller object) id instance = [[newClass alloc] init]; NSDictionary *propertys = params[@"property"]; [propertys enumerateKeysAndObjectsUsingBlock: ^ (id key, id obj, BOOL * stop) {/ / detect this object is the presence of this attributeif([XXJumpControllerTool checkIsExistPropertyWithInstance: instance verifyPropertyName: key]) {/ / use KVC attribute assignment of controller object [instancesetValue:obj forKey:key]; }}]; / / jump to the corresponding controller [[XXJumpControllerTool topViewController]. NavigationController pushViewController: instance animated: YES]; } / / the existence of the test object attribute + (BOOL) checkIsExistPropertyWithInstance: (id) instance verifyPropertyName: (verifyPropertyName nsstrings *)  { unsigned int count, i; Objc_property_t *properties = class_copyPropertyList([instance class], &count);for(i = 0; i < count; i++) { objc_property_t property =properties[i]; NSString *propertyName = [[NSString alloc] initWithCString:property_getName(property) encoding:NSUTF8StringEncoding]; // Check whether the attribute existsif ([propertyName isEqualToString:verifyPropertyName]) {
            free(properties);
            return YES;
        }
    }
    free(properties);
    
    returnNO; } // Get the current ViewController (UIViewController *)topViewController {UIViewController *resultVC = [XXJumpControllerTool _topViewController:[[UIApplication sharedApplication].keyWindow rootViewController]];while (resultVC.presentedViewController) {
        resultVC = [XXJumpControllerTool _topViewController:resultVC.presentedViewController];
    }
    return resultVC;
}

+ (UIViewController *)_topViewController:(UIViewController *)vc {
    if ([vc isKindOfClass:[UINavigationController class]]) {
        return [XXJumpControllerTool _topViewController:[(UINavigationController *)vc topViewController]];
    } else if ([vc isKindOfClass:[UITabBarController class]]) {
        return [XXJumpControllerTool _topViewController:[(UITabBarController *)vc selectedViewController]];
    } else {
        return vc;
    }
    return nil;
}

@end
Copy the code

Test code:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // universal jump controller
    [self jumpController];
}
Copy the code

3.3 Realize dictionary model transformation

In daily development, converting JSON data from network requests to data models is an essential operation. Usually we use third-party frameworks such as YYModel, JSONModel, or MJExtension to implement this process. At the heart of these framework implementations are the Runtime and KVC, and getters/setters.

The general idea is as follows: With the help of Runtime, you can dynamically obtain the member list feature, iterate through all the attributes in the model, and then find the corresponding value in the JSON dictionary based on the obtained attribute name key. Then assign each corresponding value to the model by using KVC or calling Getter/Setter directly to accomplish the purpose of dictionary transformation.

Requirement: Convert the JSON dictionary returned by the server to the data model.

Prepare a JSON data to be parsed as follows:

{
    "id": "123412341234"."name": "Walking Boy."."age": "18"."weight": 120,
    "address": {
        "country": "China"."province": "Beijing"
    },
    "courses": [{"name": "Chinese"."desc": "Chinese Class"
        },
        {
            "name": "Math"."desc": "Math class"
        },
        {
            "name": "English"."desc": "English class"}}]Copy the code

Suppose this is the JSON data returned by the server, with information about a student. Now we need to turn the JSON dictionary into a data model that is easy to develop.

As you can see from this JSON, the dictionary contains values in addition to strings, arrays and dictionaries. Then, when converting dictionaries into data models, we should consider the case that models nest models and models nest model arrays. The specific steps are as follows:

3.3.1 Creating a model

After analysis, we need three models in total: XXStudentModel, XXAdressModel and XXCourseModel.

/ * * * * * * * * * * * * * * * * * * * * * XXStudentModel. H file * * * * * * * * * * * * * * * * * * * * * /#import <Foundation/Foundation.h>
#import "NSObject+XXModel.h"@class XXAdressModel, XXCourseModel; @interface XXStudentModel: NSObject <XXModel> /* name */ @property (nonatomic, copy) NSString *name; /* student id */ @property (nonatomic, copy) NSString *uid; /* age */ @property (nonatomic, assign) NSInteger age; /* weight */ @property (nonatomic, assign) NSInteger weight; */ @property (nonatomic, strong) XXAdressModel *address; */ @property (nonatomic, strong) NSArray *courses; @ the end / * * * * * * * * * * * * * * * * * * * * * XXStudentModel m file * * * * * * * * * * * * * * * * * * * * * /#import "XXStudentModel.h"
#import "XXCourseModel.h"ModelContainerPropertyGenericClass @ implementation XXStudentModel + (NSDictionary *) {/ / need special handling propertiesreturn @{
             @"courses" : [XXCourseModel class],
             @"uid" : @"id"}; } @ the end / * * * * * * * * * * * * * * * * * * * * * XXAdressModel. H file * * * * * * * * * * * * * * * * * * * * * /#import <Foundation/Foundation.h>@interface XXAdressModel: NSObject /* nationality */ @property (nonatomic, copy) NSString *country; /* province */ @property (nonatomic, copy) NSString * type; /* city */ @property (nonatomic, copy) NSString *city; @ the end / * * * * * * * * * * * * * * * * * * * * * XXAdressModel m file * * * * * * * * * * * * * * * * * * * * * /#import "XXAdressModel.h"@ implementation XXAdressModel @ the end / * * * * * * * * * * * * * * * * * * * * * XXCourseModel. H file * * * * * * * * * * * * * * * * * * * * * /#import <Foundation/Foundation.h>@interface XXCourseModel: NSObject /* class name */ @property (nonatomic, copy) NSString *name; / / @property (nonatomic, copy) NSString *desc; @ the end / * * * * * * * * * * * * * * * * * * * * * XXCourseModel m file * * * * * * * * * * * * * * * * * * * * * /#import "XXCourseModel.h"

@implementation XXCourseModel

@end
Copy the code

3.3.2 Implement dictionary transformation model in NSObject classification

If you are careful, you may have noticed: #import “NSObject+ xxModel. h” and follow the

protocol. And in XXStudentModel. M file implements the protocol + modelContainerPropertyGenericClass method (NSDictionary *).

NSObject+ xxModel. h, NSObject+ xxModel. m are the categories we created to solve the dictionary transformation, The + (NSDictionary *) of the agreement modelContainerPropertyGenericClass classification method is used to tell special field processing rules, such as id – > uid.

/ * * * * * * * * * * * * * * * * * * * * * NSObject + XXModel. H file * * * * * * * * * * * * * * * * * * * * * /
#import <Foundation/Foundation.h>

/ / XXModel protocol
@protocol XXModel <NSObject>

@optional
// protocol method: returns a dictionary indicating the processing rules for a particular field
+ (nullable NSDictionary<NSString *, id> *)modelContainerPropertyGenericClass;

@end;

@interface NSObject (XXModel)

// dictionary to model method
+ (instancetype)xx_modelWithDictionary:(NSDictionary *)dictionary;

@end
Copy the code
/ * * * * * * * * * * * * * * * * * * * * * NSObject + XXModel. M file * * * * * * * * * * * * * * * * * * * * * /#import "NSObject+XXModel.h"
#import <objc/runtime.h>@implementation NSObject (XXModel) + (instancetype)xx_modelWithDictionary:(NSDictionary *)dictionary {// create current model object id object = [[self alloc] init]; unsigned int count; Objc_property_t *propertyList = class_copyPropertyList([self class], &count); // Iterate over all properties in the propertyList with the property name key and look up value in the dictionaryfor(unsigned int i = 0; i < count; I ++) {objc_property_t property = propertyList[I]; const char *propertyName = property_getName(property); NSString *propertyNameStr = [NSString stringWithUTF8String:propertyName]; / / get a JSON the value of the attribute value id value = [dictionary objectForKey: propertyNameStr]; NSString *propertyType; unsigned int attrCount; objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);for (unsigned int i = 0; i < attrCount; i++) {
            switch (attrs[i].name[0]) {
                case 'T': { // Type encoding
                    if(attrs[i].value) { propertyType = [NSString stringWithUTF8String:attrs[i].value]; // Remove the escape character: @\"NSString\" -> @"NSString" propertyType = [propertyType stringByReplacingOccurrencesOfString:@"\"" withString:@""]; / / remove the @ symbol propertyType = [propertyType stringByReplacingOccurrencesOfString: @"@" withString:@""]; }}break;
                default: break; } // if the current class implements the protocol method, get the protocol method specified in the special attribute NSDictionary *perpertyTypeDic;if([self respondsToSelector:@selector(modelContainerPropertyGenericClass)]){ perpertyTypeDic = [self performSelector:@selector(modelContainerPropertyGenericClass) withObject:nil]; Id -> uid id anotherName = perpertyTypeDic[propertyNameStr];if(anotherName && [anotherName isKindOfClass:[NSString class]]){ value = dictionary[anotherName]; } // Handle: model nested model caseif([value isKindOfClass:[NSDictionary class]] && ! [propertyType hasPrefix:@"NS"]) {
            Class modelClass = NSClassFromString(propertyType);
            if(modelClass ! = nil) {/ / will be nested dictionary data into the Model value = [modelClass xx_modelWithDictionary: value]; // Check that the current value is an array and there is a protocol method that returns perpertyTypeDicif([value isKindOfClass:[NSArray class]] && perpertyTypeDic) { Class itemModelClass = perpertyTypeDic[propertyNameStr]; NSMutableArray *itemArray = @[]. MutableCopy;for (NSDictionary *itemDic  invalue) { id model = [itemModelClass xx_modelWithDictionary:itemDic]; [itemArray addObject:model]; } value = itemArray; } // Use the KVC method to update value to objectif(value ! = nil) { [objectsetValue:value forKey:propertyNameStr];
        }
        
    }
    free(propertyList);
    
    return object;
}

@end
Copy the code

3.3.3 Test code

- (void)parseJSON { NSString *filePath = [[NSBundle mainBundle] pathForResource:@"Student" ofType:@"json"]; NSData *jsonData = [NSData dataWithContentsOfFile:filePath]; / / read the JSON data NSDictionary * JSON = [NSJSONSerialization JSONObjectWithData: jsonData options:NSJSONReadingMutableContainers error:nil]; NSLog(@"%@",json); Studentmodel *student = [XXStudentModel xx_modelWithDictionary: JSON]; NSLog(@"student.uid = %@", student.uid); NSLog(@"student.name = %@", student.name); for (unsigned int i = 0; i < student.courses.count; i++) { XXCourseModel *courseModel = student.courses[i]; NSLog(@"courseModel[%d].name = %@ .desc = %@", i, courseModel.name, courseModel.desc); }}Copy the code

The effect is as follows:

Of course, if you need to consider caching, performance issues, object type checking, etc., it is recommended to use a well-known third-party framework like YYModel, or build your own wheels.


3.4 Improve iOS archiving and archiving

Archiving is a common lightweight file storage method. In a project, if the data model needs to be stored locally, archiving and archiving are commonly used. But if there are multiple attributes in the data model, we have to deal with each attribute, which is a tedious process.

Here we can refer to the previous “dictionary to model” code. Obtain the attribute list of the class through the Runtime to realize automatic archiving and archiving. Archive operation and file operation will mainly use two methods: encodeObject: forKey: and decodeObjectForKey:.

First add the following code to NSObject+ xxmodel.h, NSObject+ xxmodel.m:

/ / solution
- (instancetype)xx_modelInitWithCoder:(NSCoder *)aDecoder {
    if(! aDecoder)return self;
    if (!self) {
        return self;
    }
    
    unsigned int count;
    objc_property_t *propertyList = class_copyPropertyList([self class], &count);
    for (unsigned int i = 0; i < count; i++) {
        const char *propertyName = property_getName(propertyList[i]);
        NSString *name = [NSString stringWithUTF8String:propertyName];
        
        id value = [aDecoder decodeObjectForKey:name];
        [self setValue:value forKey:name];
    }
    free(propertyList);
    
    return self;
}

/ / archive
- (void)xx_modelEncodeWithCoder:(NSCoder *)aCoder {
    if(! aCoder)return;
    if (!self) {
        return;
    }
    unsigned int count;
    objc_property_t *propertyList = class_copyPropertyList([self class], &count);
    for (unsigned int i = 0; i < count; i++) {
        const char *propertyName = property_getName(propertyList[i]);
        NSString *name = [NSString stringWithUTF8String:propertyName];
        
        id value = [self valueForKey:name];
        [aCoder encodeObject:value forKey:name];
    }
    free(propertyList);
}
Copy the code

Then add the -initWithcoder: and -encodeWithcoder: methods to the model where you want to implement archiving.

#import "XXPerson.h"
#import "NSObject+XXModel.h"

@implementation XXPerson

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super init];
    if (self) {
        [self xx_modelInitWithCoder:aDecoder];
    }
    return self;
}

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

@end
Copy the code

Test the archive unfile code:

XXPerson *person = [[XXPerson alloc] init];
person.uid = @ "123412341234";
person.name = @" Walking Boy";
person.age = 18;
person.weight = 120;

/ / archive
NSString *path = [NSString stringWithFormat:@"%@/person.plist".NSHomeDirectory()];
[NSKeyedArchiver archiveRootObject:person toFile:path];

/ / solution
XXPerson *personObject = [NSKeyedUnarchiver unarchiveObjectWithFile:path];

NSLog(@"personObject.uid = %@", personObject.uid);
NSLog(@"personObject.name = %@", personObject.name);
Copy the code

Of course, the above code is only a demonstration of the Runtime for archiving and file optimization, the real logic used in development is far more responsible than the above sample, also refer to the implementation of YYModel.


The resources

  • CoyoteK: Runtime from Beginner to Master (nine) – Universal interface jump
  • Run-time apps for iOS
  • Lehman’s classmate: https://www.jianshu.com/p/361c9136cf3a
  • Ibireme: iOS JSON model transformation library review

IOS Development: Runtime

  • IOS development: “Runtime” details (1) Basic knowledge
  • IOS development: “Runtime” details (ii) Method Swizzling
  • IOS development: “Runtime” details (3) Category underlying principles
  • IOS development: “Runtime” detailed explanation (4) obtain the class detailed attributes, methods

Not yet completed:

  • IOS development: “Runtime” details (5) Crash protection system
  • IOS development: “Runtime” (6) Objective-C 2.0
  • IOS development: “Runtime” details (seven) KVO low-level implementation