OC runtime常见的使用场景和具体实现

简介

runtime 是OC一个很重要的机制,运行时机制,我们平时写的OC代码都会被转成runtime代码, runtime是一套比较底层的纯C语言的API。 runtime最主要的应该就是消息机制了,OC调用函数为消息发送,属于动态调用,编译的时候不能决定真正调用的哪个函数,这个和C,C++不同,C,C++在一个函数没有实现的时候编译也是通过不了的,而OC则不会。

查看runtime代码

查看runtime代码需要在终端中,cd 到想要查看文件的指定路径,输入clang -rewrite-objc xxx.m,但是记不清从Xcode那个版本之后,输入这个命令会报
UIKit/UIKit.h' file not found 错误,后来经过查找资料,编译之前,需要一些编译环境 和库的参数配置,或者是三方库头文件,需要借助 xcrun 命令。

xcrun -sdk iphonesimulator clang -rewrite-objc xxx.m

这样,编译成功的话,在文件中就会生成一个.cpp的文件,打开就能看到指定文件的OC代码转换成的runtime代码 (C++)。

runtime使用场景

使用runtime需要 #import 
1.给分类添加属性

我们应该都试过,直接在分类中是不能添加属性的,运行会crash,但是我们可以利用runtime给分类添加属性。

#例如给项目中的控制器添加一个别名 -- 关键代码

static const char aliasKey;
- (void)setName:(NSString *)alias {
    //将值和对象关联起来
    objc_setAssociatedObject(self, &aliasKey, alias, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)alias {
    return objc_getAssociatedObject(self, &aliasKey);
}
2.交换两个方法的实现

比较常用的是拦截系统自带方法调用(Swizzling),如果我们想统一给项目中的控制器自定义返回按钮样式,则可以在UIViewController的分类中Swizzling 控制器的ViewDidLoad方法,在新的方法中处理导航栏。
:统一设置项目中的返回按钮,常用的方法还有自定义导航控制器,重写导航控制器的push方法统一设置。这里主要是介绍OC的黑魔法 runtime Swizzling使用。

//当程序装载到内存中的时候调用
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method originalMethod = class_getInstanceMethod([UIViewController class], @selector(viewDidLoad));
        Method swizzledMethod = class_getInstanceMethod([UIViewController class], @selector(swizzling_viewDidLoad));
        
        method_exchangeImplementations(originalMethod, swizzledMethod);
    });
}

- (void)swizzling_viewDidLoad {
    if (self.navigationController) {
        UIImage *buttonNormal = [[UIImage imageNamed:@"nav_back_normal"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
        [self.navigationController.navigationBar setBackIndicatorImage:buttonNormal];
        [self.navigationController.navigationBar setBackIndicatorTransitionMaskImage:buttonNormal];
        UIBarButtonItem *backItem = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil];
        self.navigationItem.backBarButtonItem = backItem;
    }
    [self swizzling_viewDidLoad];
}

3.获取某个类的所有成员变量和方法

具体的使用,放到runtime动态创建类和后面实现归档的例子中体现。

  • 获得所有成员变量
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
  • 获得所有方法
Method *class_copyMethodList(Class cls, unsigned int *outCount)
  • 获得成员变量名
 const char *ivar_getName(Ivar v)
  • 获得成员变量类型
const char *ivar_getTypeEncoding(Ivar v)
4.动态生成一个类

在运行时的机制中,我们可以动态生成一个类,例如 KVO的底层实现,在这里不介绍KVO是如何通过动态创建一个类来实现监听对象的改变,感兴趣的可以去网上了解一下,相关资料很多。

  • 动态创建类
    参数:
    1.superclass = 要动态创建类的父类
    2.name = 动态创建的类名
    3.extraBytes = 值通常为0
Class objc_allocateClassPair(Class superclass, const char *name, 
                                         size_t extraBytes)
  • 添加成员变量
    参数:
    1.cls = 为哪个类添加成员变量
    2.name = 成员变量的名字
    3.size = 成员变量的字节数
    4.alignment = 对其方式
    5.type = 成员变量的类型
BOOL class_addIvar(Class cls, const char *name, size_t size, 
                               uint8_t alignment, const char *types)
  • 添加方法
    参数:
    1.cls = 为哪个类添加方法
    2.name = 方法的名字
    3.imp = 对应的方法
    4.types = 描述方法参数类型的字符数组
BOOL class_addMethod(Class cls, SEL name, IMP imp, 
                                 const char *types)
  • 注册到运行时环境
    必须将创建的类注册到运行时环境才能生效。
void objc_registerClassPair(Class cls)

下面的例子是在Animal类中,动态创建了它的子类Dog类,并打印出了Dog类及其父类的所有成员变量。

- (void)printDogIvars {
    unsigned int varCount = 0;
    Class c = NSClassFromString(@"Dog");
    while (c && c != [NSObject class]) {
        Ivar *vars = class_copyIvarList(c, &varCount);
        // class_copyPropertyList 获得是当前类的所有属性,不包括成员变量hhh
        
        for (int i = 0; i < varCount; i ++) {
            Ivar var = vars[i];
            //获得成员变量的名字
            const char *varName = ivar_getName(var); //“_type”,"hhh"
            //获得成员变量的类型
            const char *varType = ivar_getTypeEncoding(var);//"NSString"
            NSLog(@"varName = %s,varType = %s", varName, varType);
        }
        c = [c superclass];
        free(vars);
    }
}

- (void)dymicCreateDogClass {
    const char *className = "Dog";
    
    Class Dog = object_getClass(NSClassFromString([NSString stringWithUTF8String:className]));
    if (!Dog) {
        //动态创建一个继承自Animal的Dog类
        Dog = objc_allocateClassPair([Animal class], className, 0);
        //给创建的Dog类添加成员变量
        if (class_addIvar(Dog, "header", sizeof(NSString *), 0, "@")) {
            NSLog(@"success");
        }
        class_addMethod(Dog, @selector(printDogIvarsValue), (IMP)printDogIvarsValue, "");
        //注册到运行时环境
        objc_registerClassPair(Dog);
    }
    
    id smallDog = [[Dog alloc] init];
//    //给变量赋值
    [smallDog setValue:@"smallHeader" forKey:@"header"];
    
    [smallDog printDogIvarsValue];
}

//这个方法实际上没有调用,但是必须实现这个方法,才能调用下面的方法
- (void)printDogIvarsValue {
    
}

//调用这个方法,self和_cmd 参数是必须的,在之后也可以随便添加参数
static void printDogIvarsValue(id self, SEL _cmd) {
    Ivar var = class_getInstanceVariable([self class], "header");
    id value = object_getIvar(self, var);
    
    NSLog(@"动态创建的Dog类的成员变量header的值==%@", value);
}
5.利用runtime实现对象的归档

利用runtime实现对象的归档是很常见的,我们平时在实现对象归档的时候通常的做饭是,实现归档和解档的两个方法 encodeWithCoder:initWithCoder:,在里面对要归档和解档的属性进行重复的decodeObjectForKey:encodeObject:,如果属性很多的话就要写很多重复的代码,这样并不好。现在可以利用runtime获取全部的成员变量来进行归档操作。

//不需要进行归解档的属性
- (NSArray *)ignoredNames {
    return @[@"_one",@"_tow",@"_three"];
}

//解档
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        unsigned int varCount = 0;
        Ivar *vars = class_copyIvarList([self class], &varCount);
        for (int i = 0; i < varCount; i ++) {
            Ivar var = vars[i];
            const char *varName = ivar_getName(var);
            NSString *key = [NSString stringWithUTF8String:varName];
            // 忽略不需要解档的属性
            if ([[self ignoredNames] containsObject:key]) {
                continue;
            }
            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
        free(vars);
    }
    return self;
}

//归档
- (void)encodeWithCoder:(NSCoder *)aCoder {
    unsigned int varCount = 0;
    Ivar *vars = class_copyIvarList([self class], &varCount);
    for (int i = 0; i < varCount; i ++) {
        Ivar var = vars[i];
        const char *varName = ivar_getName(var);
        NSString *key = [NSString stringWithUTF8String:varName];
        // 忽略不需要归档的属性
        if ([[self ignoredNames] containsObject:key]) {
            continue;
        }
        id value = [self valueForKey:key];
        [aCoder encodeObject:value forKey:key];
    }
    free(vars);
}

另外还有一种处理方式是给NSObject添加一个分类,提供归档和解档的方法,将上面归解档操作放到相应的方法里,如果多个类都需要归解档时,就可以直接调用分类中的方法,实现了对代码的封装,简化了代码。

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

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

归解档的操作,建议去看一下MJExtension的实现,代码封装的很好,很值得学习,这里只是讲解runtime的使用,没有花时间去封装代码,还请见谅!

最后

感谢您的阅读,希望我的这篇文章能帮助到您!

代码在这里

在这个时代根本就没有怀才不遇, 关键是你真的必须具备才华,所以请你一定要强大自己!

你可能感兴趣的:(OC runtime常见的使用场景和具体实现)