天天看点

#iOS Runtime 从初识到实践

#iOS Runtime 从初识到实践

xcode.png

** 本文适用于没有揭开runtime神秘面纱的读者参考, 大神看到这里的话还希望向小弟甩点墨水, 增长学识! **

Runtime简介:

它是个运行时的库,基本是C和汇编写的。可以把一些工作从编译推迟到运行时处理, 也就是说是运行时系统来执行编译后的代码. 因为其动态性, 所以我们可以在程序运行的时候为类添加,修改,匹配方法等.

初识Runtime:

我们所创建的对象或者说类在runtime中是结构体的形式存在的, 进入runtime的头文件即可获悉

#if !OBJC_TYPES_DEFINED

/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method; // 描述一个方法

/// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar; // 实例变量

/// An opaque type that represents a category.
typedef struct objc_category *Category; // 分类

/// An opaque type that represents an Objective-C declared property.
typedef struct objc_property *objc_property_t; // 类中属性

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY; //指针, 实例的指针指向类对象象,类对象的指针指向元类

#if !__OBJC2__
    Class super_class //指向父类                              OBJC2_UNAVAILABLE;
    const char *name      //类的名称                                   OBJC2_UNAVAILABLE;
    long version           //类的版本信息                                  OBJC2_UNAVAILABLE;
    long info                   // 类的信息                             OBJC2_UNAVAILABLE;
    long instance_size                //该类实例变量的大小                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars        //成员变量列表                     OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists     //方法列表               OBJC2_UNAVAILABLE;
    struct objc_cache *cache            //缓存列表(对于已经调用的方法会存入其中,下次调用优先从缓存找)                     OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols       //协议列表              OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
           

相信看到这如果我问你category为什么不能添加属性?(不能在.h中写成员变量,却可以写属性,但调用的时候会crash) 你应该可以知道了吧! 但可以通过其它方式间接实现,详情一会会写到**

Runtime作用

1. 发送消息

方法的调用即是让对象发送消息, 运行时会转换成objc_msgSend, 使用消息机制须#import <objc/message.h>

消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现

Cat *cat = [Cat new];
// 直接调用
[self eatFish];

// 运行时会转成
objc_msgSend(cat, @selector(eatFish));
           

2. 动态添加方法

当我们调用[cat performSelector:@selector(play)];的时候程序会carsh, 因为没有实现play方法, 怎么办呢? 可以通过下面的方式解决

@implementation Cat

void c_play(id self,SEL sel) // 如果不写括号里的self和_cmd会隐式添加 self为方法调用者 _cmd为方法编号
{
  NSLog(@"自己玩");
}
 
//消息接收者没有找到对应的方法时候,会先调用此方法,我们拦截没实现的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
 
  if (sel == @selector(play)) {
     //这里我们添加play方法
    // 参数1:哪个类添加方法 2:方法编号 3:方法实现的函数地址 4:函数类型
    //函数类型解释:v代表没有返回值void, @:代表对象, :代表SEL
    class_addMethod(self, @selector(play), c_play, "v@:");
 
  }
 
  return [super resolveInstanceMethod:sel];
}
@end
           

这样调用[cat performSelector:@selector(play)]程序跑起来的时候就不会crash了, 因为我们已经动态添加了;若添加类方法为resolveClassMethod, 同理的.

3. 方法的交换

**若有这样一个需求, 觉得系统的功能不太受用, 在无法改变系统方法的前提下, 为这个方法添加一些功能, 比如即使数组中插入nil时也不让其crash, 用imageNamed:这个方法时候知道图片到底加载成功了没 **

你有可能想到的是继承或者重写该方法(但分类中无法调用super,重写会覆盖之前功能), 下面我们用runtime试试看

@implementation UIImage (Image)
// 分类在内存中的时候很早就被调用的一个方法,一般都在它中实现方法的交换
+ (void)load
{
 // 获取类方法imageNamed:
  Method m1 = class_getClassMethod([UIImage class], @selector(imageNamed:));
 // // 获取类方法rt_imageNamed:
  Method m2 = class_getClassMethod([UIImage class], @selector(rt_imageNamed:));
 
  // 交换彼此方法的实现
  method_exchangeImplementations(m1, m2);
}
 
// 需要交换的方法实现
+ (UIImage *)rt_imageNamed:(NSString *)imageName
{
  // 可能有读者会问,这不是死循环了吗, no, 我们在load里已经交换了方法实现了哦, 所以看似调用自己而已
  UIImage *image = [UIImage rt_imageNamed:imageName];
  //功能
  if (image == nil) {
    // do something
  }
 
  return image;
}

@end

           

4. 添加属性

我们之前说过如何变相的给category添加属性, 答案是objc_getAssociatedObject, 也是大家常用的一个函数, 即关联.

值得注意的是关联对象不是为类\对象添加属性或者成员变量(因为在设置关联后也无法通过ivarList或者propertyList取得, 至于后面这两个函数我们稍后实践)

使用场景: 例如给NSArray添加一个name属性,我们这里不用继承实现

@interface NSArray (TestAssociated)

@property (nonatomic, copy)NSString *name;
@end
#import "NSArray+TestAssociated.h"

@implementation NSArray (TestAssociated)

static char associatedKey;

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

//参数1: 给谁添加关联 2: 关联的key(通过key获取关联的对象) 3: 被关联的对象 4: 关联策略(点进入看一下就知道了)
    objc_setAssociatedObject(self, &associatedKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name{

// 同上
    return objc_getAssociatedObject(self, &associatedKey);
}
@end

           

这样我们就不仅能用tag值了, 使用就这样

NSArray *arr = @[];
    arr.name = @"111";
    
    NSLog(@"%@", arr.name);//打印的值为111
           

5. 打印对象的属性等

unsigned int i = 0;
    // 打印当前对象的属性
    objc_property_t *pros = class_copyPropertyList([self class], &i);
    
    for (int j = 0; j < i; j++) {
        
        NSString *name = @(property_getName(pros[j]));
        NSLog(@":%@", name);
           
    }

// 同理我们可以打印更多的东西从头文件可知
objc_ivar_list //成员变量列表
objc_method_list //方法列表
...

           

如果你看到了这里, 你现在一定恍然大悟, 原来那些大神们写的json转model就是这么实现的啊!

使用总结: 对对象进行操作的方法一般以object_开头

对类进行操作的方法一般以class_开头

对类或对象的方法进行操作的方法一般以method_开头

对成员变量进行操作的方法一般以ivar_开头

对属性进行操作的方法一般以property_开头开头

对协议进行操作的方法一般以protocol_开头

总结:关于runtime的使用还有更多更多, 我只介绍了冰山一角,或者说助你看清了轮廓. 实际做项目中我们很少时候用到它, 但实现强大的功能时,往往却离不开它, 例如: json转model功能, 调用私有函数并且绕过苹果审核等!