当前位置:   article > 正文

iOS中的runtime

ios中的runtime

iOS中的Runtime

引言

  • 对于C语言,函数调用在编译的时候会决定调用哪个函数,编译完成之后直接顺序执行
  • 对于OC语言, 属于动态函数调用,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用
  • 事实证明:在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要声明过就不会报错。而C语言在编译阶段就会报错

Runtime简介

Runtime简称运行时,Runtime就是执行已编译好的代码,在其底层,OC就是通过Runtime这个库把方法调用转化为消息发送

消息驱动机制

在OC中方法调用:

 [object doSomething];

在编译时RunTime会将上述代码转化为:

 objc_msgSend(id object,selector); 
 //object:方法的调用者
 //selector:方法选择器

动态绑定

当我们在调用一个实例方法的时候,其过程:
1、Runtime系统会把方法[object doSomething];调用转化为消息发送objc_msgSend(object, @selector (doSomething));,即objc_msgSend,并且把方法的调用者和方法选择器,当做参数传递过去。
分为以下俩种:

方法无参数:objc_msgSend(id objec,selector);
方法有参数:objc_msgSend(id objec,selector, arg1.arg2...);

@selector (doSomething)返回一个SEL数据类型,即方法选择器。SEL主要作用是快速的通过方法名字(doSomething)查找到对应方法的函数指针,然后调用其函数。SEL其本身是一个int型的地址,地址中存放着方法的名字。在一个类中,每一个方法对应着一个SEL。iOS类中不能存在两个名称相同的方法,即使参数类型不同,因为SEL是根据方法名字生成的,相同的方法名称只能对应一个SEL。

2、在objc_msgSend函数中,首先通过obj的isa指针找到obj对应的class。在class中,有一块最近调用的方法的指针缓存,所以先去cache通过selector查找对应的method。
3、若cache中未找到,再去method list中查找,若method list中未找到,则去superClass中查找。若能找到,则将method加入到cache中,以方便下次查找,
4、通过method中的函数指针跳转到对应的函数中去执行。

和RunTime交互的三种方式

通过Objective-C源代码

大部分情况下你就只管写你的Objc代码就行,runtime 系统自动在幕后辛勤劳作着。
还记得引言中举的例子吧,消息的执行会使用到一些编译器为实现动态语言特性而创建的数据结构和函数,Objc中的类、方法和协议等在 runtime 中都由一些数据结构来定义。

通过Foundation框架中类NSObject的方法

Cocoa 中大多数类都继承于NSObject类,也就自然继承了它的方法。最特殊的例外是NSProxy,它是个抽象超类,它实现了一些消息转发有关的方法,可以通过继承它来实现一个其他类的替身类或是虚拟出一个不存在的类,说白了就是领导把自己展现给大家风光无限,但是把活儿都交给幕后小弟去干。
有的NSObject中的方法起到了抽象接口的作用,比如description方法需要你重载它并为你定义的类提供描述内容。NSObject还有些方法能在运行时获得类的信息,并检查一些特性,比如class返回对象的类;isKindOfClass:和isMemberOfClass:则检查对象是否在指定的类继承体系中;respondsToSelector:检查对象能否响应指定的消息;conformsToProtocol:检查对象是否实现了指定协议类的方法;methodForSelector:则返回指定方法实现的地址。

通过直接调用运行时系统的函数

Runtime 系统是一个由一系列函数和数据结构组成,具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下。许多函数允许你用纯C代码来重复实现 Objc 中同样的功能。虽然有一些方法构成了NSObject类的基础,但是你在写 Objc 代码时一般不会直接用到这些函数的,除非是写一些 Objc 与其他语言的桥接或是底层的debug工作。在中Objective-C Runtime Reference有对 Runtime 函数的详细文档。

Runtime作用

动态加载属性、方法

1、动态添加属性
  • 使用场景:准备用一个系统的类,但是系统的类并不能满足你的需求,你需要额外添加一个属性。这种情况的一般解决办法就是继承。但是,只增加一个属性,就去继承一个类,总是觉得太麻烦类。这个时候,runtime的关联属性,也叫加载属性
  • 简单使用:
  
   @implementation ViewController
	//动态添加属性
    // 给系统NSString类动态添加属性name
    NSString *str = [[NSString alloc]init];
    str.name = @"动态添加属性";
    NSLog(@"%@",str.name);


	#import "NSString+AddProperty.h"
	#import <objc/objc-runtime.h>
	// 定义关联的key
	static const char *key = "name";
	@implementation NSString (AddProperty)

	- (NSString *)name
	{
	    // 根据关联的key,获取关联的值。
	    return objc_getAssociatedObject(self, key);
	}

	- (void)setName:(NSString *)name
	{
	    // 参数1:给哪个对象添加属性赋值
	    // 参数2:关联的key,通过这个key获取
	    // 参数3:关联的value
	    // 参数4:关联的策略,下面枚举(手机开发一般都选择NONATOMIC//    enum {
	//        OBJC_ASSOCIATION_ASSIGN = 0,//assign策略
	//        OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
	//        OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
	//        OBJC_ASSOCIATION_RETAIN = 01401,// retain策略
	//        OBJC_ASSOCIATION_COPY = 01403//copy策略
	//    };
	    objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
	}
2、动态添加方法
  • 使用场景:一个类方法非常多,一次性加载到内存,比较耗费资源
  • 简单使用:

  @implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //创建Addmethod的实例,并调用不存在的方法
    AddMethod *addmethod = [[AddMethod alloc]init];
    //调用实例方法
    [addmethod performSelector:@selector(test)];
    [addmethod performSelector:@selector(eat)];
    //调用类方法
    [AddMethod performSelector:@selector(read)];

}


#import <objc/objc-runtime.h>
@implementation AddMethod
//有俩个隐含参数id、SEL

void test(id self,SEL sel)
{
    NSLog(@"添加test实例方法");
}
void(^writeBlock)(id,SEL) = ^(id objc_self,SEL objc_cmd){
    NSLog(@"添加writeBlock实例方法");
};
void(^readBlock)(id,SEL) = ^(id objc_self,SEL objc_cmd){
    NSLog(@"添加readBlock类方法");
};

#pragma -mark:动态解析实例方法(如果一个实例的方法选择器没有再方法列表里找打就会进入)
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    //可以添加函数指针或block

    //判断若方法名是test则添加,别则不添加
    if (sel == @selector(test)) {
        // 参数1:给哪个类添加方法
        // 参数2:方法名
        // 参数3:添加方法的函数实现(函数地址)
        // 参数4:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
        class_addMethod([self class], @selector(test), test, "v@:");
    }else{
        //判断若是其他则添加
        class_addMethod([self class], sel, imp_implementationWithBlock(writeBlock), "v@:");
    }
    printf("%s\n",sel_getName(sel));
    return [super resolveInstanceMethod:sel];
}


#pragma -mark:动态解析类方法(如果一个类的方法选择器没有再方法列表里找打就会进入)
+(BOOL)resolveClassMethod:(SEL)sel{
    
    //创建类方法的实现
    
    /*为指定类添加方法
     *参数1:需要添加新方法的类
     *参数2:方法名
     *参数3:由编译器生成的、指向实现方法的指针。也就是说,这个指针指向的方法就是我们要添加的方法。
     *参数4:添加的方法的返回值和参数,函数的类型编码(结构为:返回值、第一个参数、第二个参数。。。)
     */
    class_addMethod(object_getClass([self class]), sel, imp_implementationWithBlock(readBlock), "v@:");

    printf("%s\n",sel_getName(sel));
    return [super resolveInstanceMethod:sel];;
}

消息转发

消息转发只是将其他类引入消息链,而不是继承链。当一个对象或类调用了一个不存在的方法,程序就会crash,为了程序不crash,有个层级方法可以避免程序crash。

  • 第一层级
    给程序动态添加方法,对应的具体方法是+(BOOL)resolveInstanceMethod:(SEL)sel+(BOOL)resolveClassMethod:(SEL)sel,当方法是实例方法时调用前者,当方法为类方法时,调用后者。这个方法设计的目的是为了给类利用 class_addMethod 添加方法的机会,在返回值中,无论返回 YES 还是 NO,系统都会尝试用 SEL 来寻找方法实现,如果找到函数实现,则执行,所以无论返回 YES\NO都会进入第二层级。
  • 第二层级
    第二个层级是备援接收者阶段,对象的具体方法是-(id)forwardingTargetForSelector:(SEL)aSelector,此时,运行时询问能否把消息转给其他接收者处理,也就是此时系统给了个将这个 SEL 转给其他对象的机会。在返回值中,当返回值是非self\非nil 时,消息被转给新对象执行,就不会进入第三层级。如果是self\nil就会进入第三层级。
@interface ViewController ()
    //消息转发
    ForwardInvocationA *f =[[ForwardInvocationA alloc]init];
    f.objectB = [[ForwardInvocationB alloc]init];
    [f performSelector:@selector(learnOC)];

------------------------------

@implementation ForwardInvocationA
//第二层---当第一层没有添加动态方法就会进入第二层
#pragma maerk-将消息转给某对象
-(id)forwardingTargetForSelector:(SEL)aSelector{
    
    NSLog(@"myObject---%@",NSStringFromSelector(_cmd));
    
    if ([self.objectB respondsToSelector:aSelector]){
        return nil ;
    }
    return [super forwardingTargetForSelector:aSelector];
}
  • 第三层级
    第三层级是完整消息转发阶段,对应方法-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector-(void)forwardInvocation:(NSInvocation *)anInvocation`,这是消息转发流程的最后一个环节。在这个方法中,可以把 anInvocation 转发给多个对象,与第二号层级不同,二号只能转给一个对象。
@interface ViewController ()
    //消息转发
    ForwardInvocationA *f =[[ForwardInvocationA alloc]init];
    f.objectB = [[ForwardInvocationB alloc]init];
    [f performSelector:@selector(learnOC)];

------------------------------

//第三层---当第二层返回值为selfnil就会进入第三层
#pragma maerk-得到方法名
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    
    if(aSelector == @selector(learnOC)){
        return [self.objectB methodSignatureForSelector:aSelector];
    }
    return [super methodSignatureForSelector:aSelector];
}
#pragma maerk-实现消息转发
-(void)forwardInvocation:(NSInvocation *)anInvocation{
    
    if (anInvocation.selector == @selector(learnOC)){
        
        [anInvocation invokeWithTarget:self.objectB];
    }
}
  • 都不对
    如果都不中,调用doesNotRecognizeSelector抛出异常,程序crash。

注:
最后说一下 warning 的事。编译器很好心的报的那个 warning 咋办呢,不管那个小黄条不是一个爱整洁的程序员的风格,所以我们要想办法把它去掉。比较暴力,通过在配置文件中把 Complier Flag 加-w,对该类去除所有 warning。
这里写图片描述

方法交换

  • 使用场景:需要扩张一个功能又不想改变原始代码可以使用方法交换

  • 概念
    +(void)initialize:当类第一次被调用的时候就会调用该方法,整个程序运行中只会调用一次
    + (void)load:当程序启动的时候就会调用该方法,换句话说,只要程序一启动就会调用load方法,整个程序运行中只会调用一次

  • 简单使用:


//方法调用
NSURL *url = [NSURL URLWithString:@"http://www.baidu.com/中文"];
    
NSURLRequest *request = [[NSURLRequest alloc]initWithURL:url];

------------------------------

    //利用runtime交换方法
#import "NSURL+ExchangeMethod.h"
#import <objc/objc-runtime.h>
@implementation NSURL (ExchangeMethod)

//当这个类被加载时调用
+(void)load{
    NSLog(@"-------");
    
    //利用runtime交换方法
    
    // 获取方法地址
    Method  urlWithStr = class_getClassMethod([NSURL class], @selector(URLWithString:));
    Method  XYURLWithStr = class_getClassMethod([NSURL class], @selector(XYURLWithStr:));
    
    // 交换方法地址,相当于交换实现方式
    method_exchangeImplementations(urlWithStr,XYURLWithStr);
}

//不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super.


+(instancetype)XYURLWithStr:(NSString*)str{
    
    /**
     1. 此时调用的方法 'XYURLWithStr' 相当于调用系统的 'URLWithString' 方法,原因是在load方法中进行了方法交换.
     2. 注意:此处并没有递归操作.
     */
    NSURL *url = [NSURL XYURLWithStr:str];//调用系统方法实现
    if (url == nil){
        NSLog(@"自定义url为nil");
    }
    return url;
}

归档–反归档(序列化–反序列化)

  • 使用场景:归档是保存数据方式之一,一般用于保存模型对象,模型属性个数很多,使用runtime归档会简单很多
  • 简单使用:

//方法调用
 //归档
    PersonEncode *per = [[PersonEncode alloc]init];
    per.name = @"XY";
    per.age = 12;
    
    NSString *temp = NSTemporaryDirectory();
    NSString *filepath = [temp stringByAppendingPathComponent:@"xy.xy"];
    [NSKeyedArchiver archiveRootObject:per toFile:filepath];
    
    
    //反归档并输出
    PersonEncode *per1 =[NSKeyedUnarchiver unarchiveObjectWithFile:filepath];
    NSLog(@"%@---%d",per1.name,per1.age);
------------------------------
#import <Foundation/Foundation.h>

@interface PersonEncode : NSObject<NSCoding>
@property(nonatomic, strong)NSString *name;
@property(nonatomic, assign)NSInteger age;
@end

------------------------------

#import "PersonEncode.h"
#import <objc/objc-runtime.h>

@implementation PersonEncode
-(void)encodeWithCoder:(NSCoder *)aCoder{
    //定义一个实例变量的个数
    unsigned int ivarCount = 0;
    //首先获取这个类的实例变量列表
    /*
     *C语言中,如果传基本数据类型的指针,那么一般都是需要在函数内部改变他的值
     */
    Ivar *vars = class_copyIvarList([self class], &ivarCount);
    
    //遍历实例变量列表
    for (int i = 0; i< ivarCount; i++) {
        
        //获取实例变量名
        NSString *strName = [NSString stringWithUTF8String:ivar_getName(vars[i])];
        //通过KVC获取实例变量值
        id value = [self valueForKey:strName];
        //以实例变量名作为key进行归档
        [aCoder encodeObject:value forKey:strName];
    }
    //C语言中,凡是看到new,creat,copy函数需要释放指针
    //释放内存
    free(vars);
}

-(id)initWithCoder:(NSCoder *)aDecoder{
    
    if (self = [self init]){
        
        //首先获取这个类的实例变量列表
        //定义一个实例变量的个数
        unsigned int ivarCount = 0;
        Ivar *vars = class_copyIvarList([self class], &ivarCount);
        
        //遍历实例变量列表
        for (int i = 0; i< ivarCount; i++) {
            
            //获取实例变量名
            NSString *strName = [NSString stringWithUTF8String:ivar_getName(vars[i])];
            //以实例变量的名字作为key进行反归档
            id value = [aDecoder decodeObjectForKey:strName];
            
            //通过KVC对实例变量进行赋值
            [self setValue:value forKey:strName];
        }
        //C语言中,凡是看到new,creat,copy函数需要释放指针
        //释放内存
        free(vars);
    }
    
    return self;
}
@end

最后,附上以上的demo,git:(https://github.com/hejiasu/Runtime)

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/小舞很执着/article/detail/1022500?site
推荐阅读
相关标签
  

闽ICP备14008679号