zoukankan      html  css  js  c++  java
  • Objective-C runtime 机制

    runtime使用C语言结构体表示对象,用C语言函数表示方法,这些C语言函数和结构体被Runtime封装后,我们就可以在程序中执行创建,检查,修改类和对象和他们的方法

    runtime 是 OC底层的一套C语言的API(引入 <objc/runtime.h> 或<objc/message.h>),编译器最终都会将OC代码转化为运行时代码,通过终端命令编译.m 文件:clang -rewrite-objc xxx.m可以看到编译后的xxx.cpp(C++文件)。
    比如我们创建了一个对象

    int main(int argc, char * argv[]) {
        @autoreleasepool {
            JKPerson *person = [[JKPerson alloc] init];
            [person doSomething];
        }
        return 0;
    }

    最终被转换为几万行代码,截取最关键的一句可以看到底层是通过runtime创建的对象

    int main(int argc, char * argv[]) {
    
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
            JKPerson *person = ((JKPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((JKPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("JKPerson"), sel_registerName("alloc")), sel_registerName("init"));
            ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("doSomething"));
        }
        return 0;
    }

    runtime
    1、是由C、C++、汇编写成的api
    2、OC运行时,装载到内存

    相对应的编译时,源代码翻译

    OC SWIFT JAVA 高级语言,不被机器所识别,需要编译成响应的机器语言,二进制

    Objective-c程序有三种途径和运行时系统交互
    1、通过Objective-c源代码,如@selector()
    2、通过Foundation框架中NSObject的方法,如 iskindof
    3、通过调用运行时系统给我们提供的api接口,如objc_msgSend,objc_getClass

    OC对象本质是结构体
    OC调用方法就是发送消息 objc_msgSend
    消息的组成:((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("doSomething"));
    第一个参数person消息的接收者,第二个参数sel_registerName("doSomething")方法编号
    imp 函数实现的指针,sel找到imp

    查看关系图

    OC的Class其实是一个objc_class结构体的指针,下面是Class类的定义

    typedef struct objc_class *Class;

    查看objc/runtime.h中objc_class结构体的定义如下

    struct objc_class {
     Class isa OBJC_ISA_AVAILABILITY; //isa指针   
    #if !__OBJC2__ Class
    super_class OBJC2_UNAVAILABLE; // 父类
    const char *name OBJC2_UNAVAILABLE; // 类名
    long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
    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;

    这个isa指针的指向就是该类对象的元类,每一个类都是它的元类的对象,元类是对类对象的描述,就像类是普通实例对象的描述一样。

    每一个类里面声明的类方法,其本质就是把该类方法放到元类的方法列表上面,所以类在调用类方法时,可以想象成是元类的对象在调用一个实例方法。

    A的父类是B,A的元类的父类是B的元类,B的父类是NSObject,NSObject的父类是nil,B元类的父类是NSObject的元类;特别注意的一点,NSObject的元类的父类是NSObject,NSObject的isa指针又指向NSObject的元类,所以在NSObject里面的所有方法,NSObject的元类也都拥有,1、所以用NSObject 调用任意NSObject里面的实例方法都是可以成功的,

    类和元类是一个闭环,实例指向类,类指向元类,元类指向跟元类,跟元类指向自身,根元类的父类是NSObject

    元类是 Class 对象的类。每个类(Class)都有自己独一无二的元类(每个类都有自己第一无二的方法列表)。这意味着所有的类对象都不同。

    NSObject里面的所有实力方法,任意类都可以通过类方法调用。

    所有的meta-class使用基类的meta-class作为自己的基类,对于顶层基类的meta-class也是一样,只是它指向自己而已

    [obj foo] 等同于 obj_msgSend(obj,@selector(foo))

    objc 在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类的方法列表以及其父类方法列表中寻找方法运行。

    iOS 消息查找流程 https://www.jianshu.com/p/e6b253c8c76c

    如果在层层的寻找中均位找到方法的实现,就会抛出unrecognized selector sent to XXX的异常,导致程序奔溃.
     在这奔溃前,oc运行时提供了三次拯救程序的机会
     
     1、Method resolution ,动态方法解析阶段
     对应的具体方法是+(BOOL)resolveInstanceMethod:(SEL)sel 和+(BOOL)resolveClassMethod:(SEL)sel,
     当方法是实例方法时调用前者,当方法为类方法时,调用后者。这个方法设计的目的是为了给类利用 class_addMethod 添加方法的机会。

    // void(*)()
    // 默认方法都有两个隐式参数,
    void eat(id self,SEL sel)
    {
        NSLog(@"%@ %@",self,NSStringFromSelector(sel));
    }
    pragma mark 消息转发第一步(实例) 如此便达到了,当此类调用未定义的实例方法时,自动调用eat函数,而避免了崩溃的情况。
    // 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
    // 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法
    + (BOOL)resolveInstanceMethod:(SEL)sel
    {
    
        if (sel == @selector(eat)) {
            // 动态添加eat方法
    
            // 第一个参数:给哪个类添加方法
            // 第二个参数:添加方法的方法编号
            // 第三个参数:添加方法的函数实现(函数地址)
            // 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
            class_addMethod(self, @selector(eat), (IMP)eat, "v@:");
    
        }
    
        return [super resolveInstanceMethod:sel];
    }


     2、fowarding 方法转发,备援接收者阶段
     对象的具体方法是-(id)forwardingTargetForSelector:(SEL)aSelector ,
     此时,运行时询问能否把消息转给其他接收者处理,也就是此时系统给了个将这个 SEL 转给其他对象的机会。

      #pragma mark 消息转发第二步, 第一步失败后执行                     
     #pragma mark 其实只要返回对象不为self 和 nil 就会把消息转发给返回的对象 
     - (id)forwardingTargetForSelector:(SEL)aSelector { 
     NSString * str = NSStringFromSelector(aSelector); 
     NSString * obj = [NSString stringWithFormat:@"testClass"]; 
     NSLog(@"方法 %@ 即将转发给 Class %@",str,[obj class]); 
     return obj; 
     }


     3、 fowarding 方法转发,完整消息转发阶段
     1)、首先会调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 方法,倘若返回值为nil,则runtime会发出doesNotRecognizeSelector:消息,引发异常,程序崩溃。

    2)、如果返回了一个合理的函数签名,Runtime就会创建一个NSInvocation对象并发送-forwardInvocation:消息给目标对象。参数 anInvocation 中包含未处理消息的各种信息(selector arget参数...)。
     在这个方法中,可以把 anInvocation 转发给多个对象,与第二步不同,第二步只能转给一个对象

    代码范例:策略模式 NSInvocation 路由机制原理-消息转发 HXInvocationHelper
     
     4、如果上述3个方法都没有来处理这个消息,就会进入 NSObject 的-(void)doesNotRecognizeSelector:(SEL)aSelector方法中,抛出异常

    总结一下整个消息转发的流程:

     

    代码:

    #import "ViewController.h"
    
    @interface ViewController ()
    
    @property (weak, nonatomic) IBOutlet UILabel *displayLabel;
    - (IBAction)buttonTest:(UIButton *)sender;
    @end
    
    @implementation ViewController
    - (IBAction)buttonTest:(UIButton *)sender {
        NSLog(@"--1--");
        [self performSelector:@selector(setText:) withObject:@"hello"];
    }
    +(BOOL)resolveInstanceMethod:(SEL)sel
    {
        NSLog(@"--2--");
        return NO;
    }
    //+(BOOL)resolveClassMethod:(SEL)sel
    //{
    //    NSLog(@"--2--");
    //    return NO;
    //}
    
    -(id)forwardingTargetForSelector:(SEL)aSelector
    {
        NSLog(@"--3--");
        return nil;
    }
    
    -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
    {
        NSLog(@"--4--");
       NSMethodSignature *signature= [super methodSignatureForSelector:aSelector];
        if (!signature) {
            signature=[self.displayLabel methodSignatureForSelector:aSelector];
        }
        
        return signature;
    }
    -(void)forwardInvocation:(NSInvocation *)anInvocation
    {
        NSLog(@"--5--");
        SEL seletor=[anInvocation selector];
        if([self.displayLabel respondsToSelector:seletor]){
            [anInvocation invokeWithTarget:self.displayLabel];
        }
    }
    @end

     问题:那我们只用最后一个接盘侠方法多好啊,为什么还需要前2个呢?
    其实还与这3个方法的用途不同有关:
    运行期添加方法,用1;
    转发给另1个对象、改变方法时,用2;
    需要转发给多个对象时,用3;

    参考:

    链接:OC最实用的runtime总结,面试、工作你看我就足够了!

    链接:iOS防止崩溃机制以及底层原理

  • 相关阅读:
    查看mysql数据库容量大小
    通过shell监控网页是否正常,然后促发邮件告警
    Linux shell标准输入,标准输出,错误输出
    linux资源管理命令之-----vmstat
    linux基础命令--lsof
    squid之------ACL控制
    JDK的二进制安装
    重置grafana密码
    CentOS 7添加开机启动服务脚本
    LVM逻辑卷理论及配置
  • 原文地址:https://www.cnblogs.com/dhui69/p/6413212.html
Copyright © 2011-2022 走看看