iOS runtime详解

引导

runtime是运行时,对于从事iOS开发,想要深入学习OC的人,runtime是必须熟悉掌握的东西。

runtime的概念

Objective-C 是基于 C 的,它为 C 添加了面向对象的特性。它将很多静态语言在编译和链接时期做的事放到了runtime 运行时来处理,可以说runtime是我们Objective-C幕后工作者。

  • runtime(简称运行时),是一套 纯C(C和汇编写的) 的API。而OC就是运行时机制,也就是在运行时候的一些机制,其中最主要的是消息机制
  • 对于 C 语言,函数的调用在编译的时候会决定调用哪个函数。
  • OC的函数调用成为消息发送,属于动态调用过程。在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。
  • 在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要声明过就不会报错,只有当运行的时候才会报错,这是因为OC是运行时动态调用的。而C语言调用未实现的函数就会报错。

runtime的消息机制

  • 我们写的OC代码运行的时候也是转换成了runtime方式运行的。任何方法调用本质:就是发送一个消息(用runtime发送消息,OC底层实现通过runtime实现)。
  • 消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现。
  • 每一个OC的方法,底层必然有一个与之对应的runtime方法。

简单示例:
验证:方法调用,是否真的是转换为消息机制?
必须要导入头文件 #import<objc/message.h>
注解1:我们导入系统的头文件,一般用尖括号。
注解2:OC 解决消息机制方法提示步骤【查找build setting -> 搜索msg -> objc_msgSend(YES --> NO)】
注解3:最终生成消息机制,编译器做的事情,最终代码,需要把当前代码重新编译,用xcode编译器,【clang -rewrite-objc main.m 查看最终生成代码】,示例:cd main.m --> 输入前面指令,就会生成 .opp文件(C++代码)
注解4:这里一般不会直接导入<objc/runtime.h>
示例代码:OC 方法-->runtime 方法

说明: eat(无参) 和 run(有参) 是 Person模型类中的私有方法「可以帮我调用私有方法」;

// Person *p = [Person alloc];
// 底层的实际写法
Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));

// p = [p init];
p = objc_msgSend(p, sel_registerName("init"));

// 调用对象方法(本质:让对象发送消息)
//[p eat];

// 本质:让类对象发送消息
objc_msgSend(p, @selector(eat));
objc_msgSend([Person class], @selector(run:),20);

//--------------------------- <#我是分割线#> ------------------------------//
// 也许下面这种好理解一点

// id objc = [NSObject alloc];
id objc = objc_msgSend([NSObject class], @selector(alloc));

// objc = [objc init];
objc = objc_msgSend(objc, @selector(init));

runtime 方法调用流程「消息机制」

消息机制方法调用流程

怎么去调用类方法和实例方法,实例方法:(保存到类对象的方法列表) ,类方法:(保存到元类(Meta Class)中方法列表)。
1.OC在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象对应的或其父类中查找方法。
2.注册方法编号(这里用方法编号的好处,可以快速查找)。
3.根据方法编号去查找对应方法。
4.找到只是最终函数实现地址,根据地址去方法区调用对应函数。

一个objc 对象的 isa 的指针指向什么?有什么作用?

每一个对象内部都有一个isa指针,这个指针是指向它的真实类型,根据这个指针就能知道将来调用哪个类的方法。

runtime 常见作用

  • 动态交换两个方法的实现
  • 动态添加属性
  • 实现字典转模型的自动转换
  • 发送消息
  • 动态添加方法
  • 拦截并替换方法
  • 实现 NSCoding 的自动归档和解档

runtime 常用开发应用场景「工作掌握」

runtime 交换方法

应用场景:当第三方框架 或者 系统原生方法功能不能满足我们的时候,我们可以在保持系统原有方法功能的基础上,添加额外的功能。
需求:加载一张图片直接用[UIImage imageNamed:@"image"];是无法知道到底有没有加载成功。给系统的imageNamed添加额外功能(是否加载图片成功)。
方案一:继承系统的类,重写方法.(弊端:每次使用都需要导入)
方案二:使用 runtime,交换方法.
实现步骤:
1.给系统的方法添加分类
2.自己实现一个带有扩展功能的方法
3.交换方法,只需要交换一次

- (void)viewDidLoad {
    [super viewDidLoad];
    // 方案一:先搞个分类,定义一个能加载图片并且能打印的方法+ (instancetype)imageWithName:(NSString *)name;
    // 方案二:交换 imageNamed 和 ln_imageNamed 的实现,就能调用 imageNamed,间接调用 ln_imageNamed 的实现。
    UIImage *image = [UIImage imageNamed:@"123"];
}

#import <objc/message.h> 
@implementation UIImage (Image)
/**
 load方法: 把类加载进内存的时候调用,只会调用一次
 方法应先交换,再去调用
 */
+ (void)load {

    // 1.获取 imageNamed方法地址
    // class_getClassMethod(获取某个类的方法)
    Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
    // 2.获取 ln_imageNamed方法地址
    Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:));

    // 3.交换方法地址,相当于交换实现方式;「method_exchangeImplementations 交换两个方法的实现」
    method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod);
}

/**
 看清楚下面是不会有死循环的
 调用 imageNamed => ln_imageNamed
 调用 ln_imageNamed => imageNamed
 */
// 加载图片 且 带判断是否加载成功
+ (UIImage *)ln_imageNamed:(NSString *)name {

    UIImage *image = [UIImage ln_imageNamed:name];
    if (image) {
        NSLog(@"runtime添加额外功能--加载成功");
    } else {
        NSLog(@"runtime添加额外功能--加载失败");
    }
    return image;
}

/**
 不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super
 所以第二步,我们要 自己实现一个带有扩展功能的方法.
 + (UIImage *)imageNamed:(NSString *)name {

 }
 */
@end

// 打印输出
2017-02-17 17:52:14.693 runtime[12761:543574] runtime添加额外功能--加载成功 

总结:我们交换两个方法地址指向,必须在系统的imageNamed:方法调用前,所以讲代码卸载分类的load方法中,最后当运行的时候系统的方法就会去找我们的方法的实现。

runtime给分类动态添加属性

原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。

应用场景:给系统的类添加属性的时候,可以使用runtime动态添加属性方法。
注解:系统 NSObject 添加一个分类,我们知道在分类中是不能够添加成员属性的,虽然我们用了@property,但是仅仅会自动生成get和set方法的声明,并没有带下划线的属性和方法实现生成。但是我们可以通过runtime就可以做到给它方法的实现。

需求:给系统 NSObject 类动态添加属性 name 字符串。

案例代码:方法+调用+打印

@interface NSObject (Property)

// @property分类:只会生成get,set方法声明,不会生成实现,也不会生成下划线成员属性
@property NSString *name;
@property NSString *height;
@end

@implementation NSObject (Property)

- (void)setName:(NSString *)name {

    // objc_setAssociatedObject(将某个值跟某个对象关联起来,将某个值存储到某个对象中)
    // object:给哪个对象添加属性
    // key:属性名称
    // value:属性值
    // policy:保存策略
    objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)name {
    return objc_getAssociatedObject(self, @"name");
}

// 调用
NSObject *objc = [[NSObject alloc] init];
objc.name = @"123";
NSLog(@"runtime动态添加属性name==%@",objc.name);

// 打印输出
2017-02-17 19:37:10.530 runtime[12761:543574] runtime动态添加属性--name == 123

总结:给属性赋值的本质其实就是让属性与一个对象产生关联,所以要个NSObject的分类的name属性赋值就是让name和NSObject产生关联,runtime可以做到这一点。

runtime字典转模型

字典转模型的方式:

  • 一个一个给模型属性赋值

  • 字典转模型KVC实现
    1、KVC字典转模型弊端:必须保证,模型中的属性和字典中的key一一对应
    2、如果不一致,就会调用[<Status 0x7fa74b545d60> setValue:forUndefinedKey:]key找不到的错。
    3、分析:模型中的属性和字典中的key不一一对应,系统就会调用setValue:forUndefinedKey:报错。
    4、解决:重写对象的setValue:forUndefinedKey:,把系统的方法覆盖,就能继续使用KVC字典转模型。

  • 字典转模型Runtime实现
    思路:利用运行时,遍历模型中的所有属性,根据模型中的属性名,去字典中查找key,取出对应的值,给模型的属性赋值(注:字典中的取值,不一定会全部取出来)。

考虑情况:
1、当字典中的key 和模型的属性匹配不上。
2、模型中嵌套模型(模型属性是另一个模型对象)。
3、模型的属性是一个数组,数组中是一个个模型对象。

注解:字典中的key和模型的属性不对应的情况有两种,一种是字典的键值对大于模型的属性数量,这时候我们不需要任何处理,因为runtime是先遍历模型所有属性,再去字典中根据属性名找对应的值进行赋值,多余的键值对不需要去看;另外一种情况是模型属性数量大于字典中的键值对,这时候由于属性没有对应值会被赋值为nil,就会导致crash,只需加一个判断即可。

实现步骤:提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类实现字典转模型。

MJExtension字典转模型实现也是通过底层对runtime进行封装,才可以把模型中所有属性遍历出来。

字典转模型Runtime方式实现

1、runtime字典转为模型 -- 字典中的key和模型的属性不匹配(模型属性数量大于字典键值对),代码如下:

// Runtime:根据模型中属性,去字典中取出对应的value给模型属性赋值
// 思路:遍历模型中所有属性->使用运行时
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
    // 1.创建对应的对象
    id objc = [[self alloc] init];

    // 2.利用runtime给对象中的属性赋值
    /**
     class_copyIvarList: 获取类中的所有成员变量
     Ivar:成员变量
     第一个参数:表示获取哪个类中的成员变量
     第二个参数:表示这个类有多少成员变量,传入一个Int变量地址,会自动给这个变量赋值
     返回值Ivar *:指的是一个ivar数组,会把所有成员属性放在一个数组中,通过返回的数组就能全部获取到。
     count: 成员变量个数
     */
    unsigned int count = 0;
    // 获取类中的所有成员变量
    Ivar *ivarList = class_copyIvarList(self, &count);

    // 遍历所有成员变量
    for (int i = 0; i < count; i++) {
        // 根据角标,从数组取出对应的成员变量
        Ivar ivar = ivarList[i];

        // 获取成员变量名字
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];

        // 处理成员变量名->字典中的key(去掉 _ ,从第一个角标开始截取)
        NSString *key = [ivarName substringFromIndex:1];

        // 根据成员属性名去字典中查找对应的value
        id value = dict[key];

        // 【如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil】
        // 而报错 (could not set nil as the value for the key age.)
        if (value) {
            // 给模型中属性赋值
            [objc setValue:value forKey:key];
        }

    }

    return objc;
}

这里在获取模型类中的所有属性名,是采取 class_copyIvarList 先获取成员变量(以下划线开头) ,然后再处理成员变量名->字典中的key(去掉 _ ,从第一个角标开始截取) 得到属性名。
原因:
Ivar:成员变量,以下划线开头Property 属性
获取类里面属性 class_copyPropertyList
获取类中的所有成员变量 class_copyIvarList

{
    int _a; // 成员变量
}

@property (nonatomic, assign) NSInteger attitudes_count; // 属性

这里有成员变量,就不会漏掉属性;如果有属性,可能会漏掉成员变量;

使用runtime字典转模型获取模型属性名的时候,最好获取成员属性名Ivar因为可能会有个属性是没有settergetter方法的。

2、runtime字典转模型--模型中嵌套模型(模型属性是另外一个模型对象),代码如下:

+ (instancetype)modelWithDict2:(NSDictionary *)dict
{
    // 1.创建对应的对象
    id objc = [[self alloc] init];

    // 2.利用runtime给对象中的属性赋值
    unsigned int count = 0;
    // 获取类中的所有成员变量
    Ivar *ivarList = class_copyIvarList(self, &count);

    // 遍历所有成员变量
    for (int i = 0; i < count; i++) {
        // 根据角标,从数组取出对应的成员变量
        Ivar ivar = ivarList[i];

        // 获取成员变量名字
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        // 获取成员变量类型
        NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];

        // 替换: @\"User\" -> User
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];

        // 处理成员属性名->字典中的key(去掉 _ ,从第一个角标开始截取)
        NSString *key = [ivarName substringFromIndex:1];

        // 根据成员属性名去字典中查找对应的value
        id value = dict[key];

        //--------------------------- <#我是分割线#> ------------------------------//
        //
        // 二级转换:如果字典中还有字典,也需要把对应的字典转换成模型
        // 判断下value是否是字典,并且是自定义对象才需要转换
        if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {

            // 字典转换成模型 userDict => User模型, 转换成哪个模型
            // 根据字符串类名生成类对象
            Class modelClass = NSClassFromString(ivarType);

            if (modelClass) { // 有对应的模型才需要转
                // 把字典转模型
                value = [modelClass modelWithDict2:value];
            }
        }

        // 给模型中属性赋值
        if (value) {
            [objc setValue:value forKey:key];
        }
    }
    return objc;
}

3、runtime字典转模型--数组中装着模型(模型的属性是一个数组,数组中是字典模型对象),代码如下:

// Runtime:根据模型中属性,去字典中取出对应的value给模型属性赋值
// 思路:遍历模型中所有属性->使用运行时
+ (instancetype)modelWithDict3:(NSDictionary *)dict
{
    // 1.创建对应的对象
    id objc = [[self alloc] init];

    // 2.利用runtime给对象中的属性赋值
    unsigned int count = 0;
    // 获取类中的所有成员变量
    Ivar *ivarList = class_copyIvarList(self, &count);

    // 遍历所有成员变量
    for (int i = 0; i < count; i++) {
        // 根据角标,从数组取出对应的成员变量
        Ivar ivar = ivarList[i];

        // 获取成员变量名字
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];

        // 处理成员属性名->字典中的key(去掉 _ ,从第一个角标开始截取)
        NSString *key = [ivarName substringFromIndex:1];

        // 根据成员属性名去字典中查找对应的value
        id value = dict[key];


        //--------------------------- <#我是分割线#> ------------------------------//
        //

        // 三级转换:NSArray中也是字典,把数组中的字典转换成模型.
        // 判断值是否是数组
        if ([value isKindOfClass:[NSArray class]]) {
            // 判断对应类有没有实现字典数组转模型数组的协议
            // arrayContainModelClass 提供一个协议,只要遵守这个协议的类,都能把数组中的字典转模型
            if ([self respondsToSelector:@selector(arrayContainModelClass)]) {

                // 转换成id类型,就能调用任何对象的方法
                id idSelf = self;

                // 获取数组中字典对应的模型
                NSString *type =  [idSelf arrayContainModelClass][key];

                // 生成模型
                Class classModel = NSClassFromString(type);
                NSMutableArray *arrM = [NSMutableArray array];
                // 遍历字典数组,生成模型数组
                for (NSDictionary *dict in value) {
                    // 字典转模型
                    id model =  [classModel modelWithDict3:dict];
                    [arrM addObject:model];
                }

                // 把模型数组赋值给value
                value = arrM;

            }
        }

        // 如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil,而报错
        if (value) {
            // 给模型中属性赋值
            [objc setValue:value forKey:key];
        }
    }
    return objc;
}

总结:我们既然能获取到属性类型,那就可以拦截到模型的那个数组属性,进而对数组中每个模型遍历并字典转模型,但是我们不知道数组中的模型都是什么类型,我们可以声明一个方法,该方法目的不是让其调用,而是让其实现并返回模型的类型。

runtime添加方法

应用场景:如果一个类的方法非常多,加载类到内存的时候比较耗资源,需要给每个方法生成映射表,可以使用动态给某个类添加方法解决。

注解:OC中使用的懒加载,当用到的时候才去加载它,实际上只要一个类实现了某个方法,就会被加载到内存。当我们不想加载那么多方法的时候,就可以使用runtime动态的添加方法。

需求:runtime动态添加方法处理调用一个未实现的方法和去除报错。

案例代码:方法+调用+打印输出

- (void)viewDidLoad {
    [super viewDidLoad];   
    Person *p = [[Person alloc] init];
    // 默认person,没有实现run:方法,可以通过performSelector调用,但是会报错。
    // 动态添加方法就不会报错
    [p performSelector:@selector(run:) withObject:@10];
}

@implementation Person
// 没有返回值,1个参数
// void,(id,SEL)
void aaa(id self, SEL _cmd, NSNumber *meter) {
    NSLog(@"跑了%@米", meter);
}

// 任何方法默认都有两个隐式参数,self,_cmd(当前方法的方法编号)
// 什么时候调用:只要一个对象调用了一个未实现的方法就会调用这个方法,进行处理
// 作用:动态添加方法,处理未实现
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    // [NSStringFromSelector(sel) isEqualToString:@"run"];
    if (sel == NSSelectorFromString(@"run:")) {
        // 动态添加run方法
        // class: 给哪个类添加方法
        // SEL: 添加哪个方法,即添加方法的方法编号
        // IMP: 方法实现 => 函数 => 函数入口 => 函数名(添加方法的函数实现(函数地址))
        // type: 方法类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
        class_addMethod(self, sel, (IMP)aaa, "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
@end

// 打印输出
2017-02-17 19:05:03.917 runtime[12761:543574] runtime动态添加方法--跑了10米

动态变量控制

现在有一个Person的类,创建xiaoming对象

  • 动态获取xiaoming类中的所有属性(包括私有的)
    Ivar *ivar = class_copyIvarList([self.xiaoming class], &count);

  • 遍历属性找到对应的name字段
    const char *varName = ivar_getName(var);

  • 修改对应的字段值为20
    object_setIvar(self.xiaoMing, var, @"20");

  • 代码

-(void)answer{
  unsigned int count = 0;
  Ivar *ivar = class_copyIvarList([self.xiaoMing class], &count);
  for (int i = 0; i<count; i++) {
      Ivar var = ivar[i];
      const char *varName = ivar_getName(var);
      NSString *name = [NSString stringWithUTF8String:varName];
      if ([name isEqualToString:@"_age"]) {
          object_setIvar(self.xiaoMing, var, @"20");
          break;
      }
  }
  NSLog(@"XiaoMing's age is %@",self.xiaoMing.age);
}

实现NSCoding的自动归档和解档

实现自定义的模型持久化的过程,如果一个模型有许多个属性,需要对每个属性都实现一遍encodeObjectdecodeObjectForKey方法,当遇到这样的模型有很多个,这是一件十分麻烦的事情,下面介绍简单的实现方法。

假设现在有一个Movie类,有3个属性。先看下 .h文件

// Movie.h文件
//1. 如果想要当前类可以实现归档与反归档,需要遵守一个协议NSCoding
@interface Movie : NSObject<NSCoding>  
@property (nonatomic, copy) NSString *movieId;
@property (nonatomic, copy) NSString *movieName;
@property (nonatomic, copy) NSString *pic_url;
@end

如果是正常写法, .m 文件应该是这样的:

// Movie.m文件
@implementation Movie
- (void)encodeWithCoder:(NSCoder *)aCoder
{
   [aCoder encodeObject:_movieId forKey:@"id"];
   [aCoder encodeObject:_movieName forKey:@"name"];
   [aCoder encodeObject:_pic_url forKey:@"url"];
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
   if (self = [super init]) {
       self.movieId = [aDecoder decodeObjectForKey:@"id"];
       self.movieName = [aDecoder decodeObjectForKey:@"name"];
       self.pic_url = [aDecoder decodeObjectForKey:@"url"];
   }
   return self;
}
@end

如果这里有100个属性,难道我们也只能把100个属性都给写一遍吗。
使用runtime让我们有更简便的方法

#import "Movie.h"
#import <objc/runtime.h>
@implementation Movie
- (void)encodeWithCoder:(NSCoder *)encoder
{
   unsigned int count = 0;
   Ivar *ivars = class_copyIvarList([Movie class], &count);

   for (int i = 0; i<count; i++) {
       // 取出i位置对应的成员变量
       Ivar ivar = ivars[i];
       // 查看成员变量
       const char *name = ivar_getName(ivar);
       // 归档
       NSString *key = [NSString stringWithUTF8String:name];
       id value = [self valueForKey:key];
       [encoder encodeObject:value forKey:key];
   }
   free(ivars);
}
- (id)initWithCoder:(NSCoder *)decoder
{
   if (self = [super init]) {
       unsigned int count = 0;
       Ivar *ivars = class_copyIvarList([Movie class], &count);
       for (int i = 0; i<count; i++) {
       // 取出i位置对应的成员变量
       Ivar ivar = ivars[i];
       // 查看成员变量
       const char *name = ivar_getName(ivar);
      // 归档
      NSString *key = [NSString stringWithUTF8String:name];
     id value = [decoder decodeObjectForKey:key];
      // 设置到成员变量身上
       [self setValue:value forKey:key];

       }
       free(ivars);
   } 
   return self;
}
@end

这样的方式实现,不管有多少个属性,写这几行代码就搞定了。如果嫌代码有点多,有更加简便的方法:两句代码搞定。

#import "Movie.h"
#import <objc/runtime.h>
#define encodeRuntime(A) \
\
unsigned int count = 0;\
Ivar *ivars = class_copyIvarList([A class], &count);\
for (int i = 0; i<count; i++) {\
Ivar ivar = ivars[i];\
const char *name = ivar_getName(ivar);\
NSString *key = [NSString stringWithUTF8String:name];\
id value = [self valueForKey:key];\
[encoder encodeObject:value forKey:key];\
}\
free(ivars);\
\
#define initCoderRuntime(A) \
\
if (self = [super init]) {\
unsigned int count = 0;\
Ivar *ivars = class_copyIvarList([A class], &count);\
for (int i = 0; i<count; i++) {\
Ivar ivar = ivars[i];\
const char *name = ivar_getName(ivar);\
NSString *key = [NSString stringWithUTF8String:name];\
id value = [decoder decodeObjectForKey:key];\
[self setValue:value forKey:key];\
}\
free(ivars);\
}\
return self;\
\
@implementation Movie
- (void)encodeWithCoder:(NSCoder *)encoder
{
   encodeRuntime(Movie)
}
- (id)initWithCoder:(NSCoder *)decoder
{
   initCoderRuntime(Movie)
}
@end

优化:上面是encodeWithCoder 和 initWithCoder这两个方法抽成宏。我们可以把这两个宏单独放到一个文件里面,这里以后需要进行数据持久化的模型都可以直接使用这两个宏。

runtime下Class的各项操作

下面是 runtime 下Class的常见方法 及 带有使用示例代码。各项操作,【转载原著】http://www.jianshu.com/p/46dd81402f63

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(@"property---->%@", [NSString stringWithUTF8String:propertyName]);
}
  • 获取方法列表
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i; i<count; i++) {
   Method method = methodList[i];
   NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
}
  • 获取成员变量列表
Ivar *ivarList = class_copyIvarList([self class], &count);
for (unsigned int i; i<count; i++) {
    Ivar myIvar = ivarList[i];
    const char *ivarName = ivar_getName(myIvar);
    NSLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]);
}
  • 获得协议列表
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (unsigned int i; i<count; i++) {
    Protocol *myProtocal = protocolList[i];
    const char *protocolName = protocol_getName(myProtocal);
    NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);
}

现在有一个Person类,和person创建的xiaoming对象,有test1和test2两个方法

  • 获得类方法
Class PersonClass = object_getClass([Person class]);
SEL oriSEL = @selector(test1);
Method oriMethod = _class_getMethod(xiaomingClass, oriSEL);
  • 获得实例方法
Class PersonClass = object_getClass([xiaoming class]);
SEL oriSEL = @selector(test2);
Method cusMethod = class_getInstanceMethod(xiaomingClass, oriSEL);
  • 添加方法
BOOL addSucc = class_addMethod(xiaomingClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
  • 替换原方法实现
class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
  • 交换原方法实现
method_exchangeImplementations(oriMethod, cusMethod);

常用方法

// 得到类的所有方法
    Method *allMethods = class_copyMethodList([Person class], &count);
// 得到所有成员变量
    Ivar *allVariables = class_copyIvarList([Person class], &count);
// 得到所有属性
    objc_property_t *properties = class_copyPropertyList([Person class], &count);
// 根据名字得到类变量的Ivar指针,但是这个在OC中好像毫无意义
Ivar oneCVIvar = class_getClassVariable([Person class], name);
// 根据名字得到实例变量的Ivar指针
    Ivar oneIVIvar = class_getInstanceVariable([Person class], name);
// 找到后可以直接对私有变量赋值
    object_setIvar(_per, oneIVIvar, @"Mike");//强制修改name属性
/* 动态添加方法:
     第一个参数表示Class cls 类型;
     第二个参数表示待调用的方法名称;
     第三个参数(IMP)myAddingFunction,IMP是一个函数指针,这里表示指定具体实现方法myAddingFunction;
     第四个参数表方法的参数,0代表没有参数;
     */
    class_addMethod([_per class], @selector(sayHi), (IMP)myAddingFunction, 0);
// 交换两个方法
    method_exchangeImplementations(method1, method2);

// 关联两个对象
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
/*
 id object                     :表示关联者,是一个对象,变量名理所当然也是object
 const void *key               :获取被关联者的索引key
 id value                      :被关联者,这里是一个block
 objc_AssociationPolicy policy : 关联时采用的协议,有assign,retain,copy等协议,一般使用OBJC_ASSOCIATION_RETAIN_NONATOMIC
*/

runtime 几个参数概念

1、objc_msgSend
这是个最基本的用于发送消息的函数
其实编译器会根据情况在objc_msgSendobjc_msgSend_stret,objc_msgSendSuper, 或objc_msgSendSuper_stret 四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有Super的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有stret的函数。

2、SEL
objc_msgSend函数第二个参数类型为SEL,它是selectorObjc中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的ID,而这个ID的数据结构是SEL:
typedef struct objc_selector *SEL;
其实它就是个映射到方法的C字符串,你可以用Objc编译器命令@selector()或者Runtime系统的sel_registerName函数来获得一个SEL类型的方法选择器。

3、id
objc_msgSend第一个参数类型为id,大家对它都不陌生,它是一个指向类实例的指针:
typedef struct objc_object *id;
objc_object又是啥呢:
struct objc_object { Class isa; };
objc_object结构体包含一个isa指针,根据isa指针就可以顺藤摸瓜找到对象所属的类。

4、runtime.h中Class的定义

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;//每个Class都有一个isa指针

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;//父类
    const char *name                                         OBJC2_UNAVAILABLE;//类名
    long version                                             OBJC2_UNAVAILABLE;//类版本
    long info                                                OBJC2_UNAVAILABLE;//!*!供运行期使用的一些位标识。如:CLS_CLASS (0x1L)表示该类为普通class; CLS_META(0x2L)表示该类为metaclass等(runtime.h中有详细列出)
    long instance_size                                       OBJC2_UNAVAILABLE;//实例大小
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;//存储每个实例变量的内存地址
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;//!*!根据info的信息确定是类还是实例,运行什么函数方法等
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;//缓存
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;//协议
#endif

} OBJC2_UNAVAILABLE;

可以看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议。
在objc_class结构体中:ivarsobjc_ivar_list指针;methodLists是指向objc_method_list指针的指针。也就是说可以动态修改*methodLists的值来添加成员方法,这也是Category实现的原理。

面试题

1、什么是 method swizzling(俗称黑魔法)

  • 简单说就是进行方法交换

  • 在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的

  • 每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系,selector的本质其实就是方法名,IMP有点类似函数指针,指向具体的Method实现,通过selector就可以找到对应的IMP。


    1738688-2b0484c79165c45b.jpeg
  • 交换方法的几种实现方式
    • 利用 method_exchangeImplementations 交换两个方法的实现
    • 利用 class_replaceMethod 替换方法的实现
    • 利用 method_setImplementation 来直接设置某个方法的IMP。


      1738688-d1d965b31b69ab01.jpeg

参考:
Runtime Method Swizzling开发实例汇总(持续更新中)
OC运行时黑魔法 Method Swizzling

2、下面的代码输出什么?

@implementation Son : NSObject
- (id)init
{
    self = [super init];
    if (self) {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
}
@end

答案:都输出 Son

  • class获取当前方法的调用者的类,superClass获取当前方法的调用者的父类,super仅仅是一个编译指示器,就是给编译器看的,不是一个指针。

  • 本质:只要编译器看到super这个标志,就会让当前对象去调用父类方法,本质还是当前对象在调用这个题目主要是考察关于objc中对selfsuper的理解:

  • self是类的隐藏参数,指向当前调用方法的这个类的实例。而super本质是一个编译器标示符,和self是指向的同一个消息接受者

  • 当使用self调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;

  • 而当使用super时,则从父类的方法列表中开始找。然后调用父类的这个方法

  • 调用[self class]时,会转化成objc_msgSend函数

id objc_msgSend(id self, SEL op, ...)
- 调用 `[super class]`时,会转化成 `objc_msgSendSuper` 函数.

id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
第一个参数是 objc_super 这样一个结构体,其定义如下
 struct objc_super {
 __unsafe_unretained id receiver;
 __unsafe_unretained Class super_class;
 };

第一个成员是 receiver, 类似于上面的 objc_msgSend函数第一个参数self
第二个成员是记录当前类的父类是什么,告诉程序从父类中开始找方法,找到方法后,最后内部是使用 objc_msgSend(objc_super->receiver, @selector(class))去调用, 此时已经和[self class]调用相同了,故上述输出结果仍然返回 Son

objc Runtime 开源代码对- (Class)class方法的实现
-(Class)class { return object_getClass(self); 
}

runtime模块推荐阅读文章

西木 http://www.jianshu.com/p/6b905584f536
天口三水羊 http://www.jianshu.com/p/9e1bc8d890f9
夜千寻墨 http://www.jianshu.com/p/46dd81402f63
袁峥Seemygo http://www.jianshu.com/p/e071206103a4
郑钦洪_ http://www.jianshu.com/p/bd24c3f3cd0a
HenryCheng http://www.jianshu.com/p/f6300eb3ec3d

程序员的最爱(不点进去你会后悔的)

【译文 & 源码】
【工具类】

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • Runtime的特性主要是消息(方法)传递,如果消息(方法)在对象中找不到,就进行转发,具体怎么实现的呢。我们从下...
    jackyshan阅读 137,746评论 79 765
  • 引导 对于从事 iOS 开发人员来说,所有的人都会答出「 Runtime 是运行时 」,什么情况下用 Runtim...
    Winny_园球阅读 4,185评论 3 75
  • 对于从事 iOS 开发人员来说,所有的人都会答出【runtime 是运行时】什么情况下用runtime?大部分人能...
    梦夜繁星阅读 3,697评论 7 64
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 前言,文章是转载的,因为之前收藏的,今天突然发现没了。不知道什么原因,简书搜索不到了,有几篇同样转载的,但是代码没...
    安静就好_阅读 2,615评论 6 42