美文网首页
Runtime知识随记

Runtime知识随记

作者: 城市之光 | 来源:发表于2016-01-08 11:26 被阅读166次

    以前在学习Runtime,概念性东西太多,总是有种朦胧的感觉。现在潜心研究Runtime。发现越深入了解,越能接触到很多深入底层的东西,有种学无止境的感觉。

    Runtime是一套比较底层的纯C语言的API, 属于C语言库, 包含了很多底层的C语言API。 在我们平时编写的iOS代码中, 最终都是转成了Runtime的C语言代码。所谓运行时,也就是在编译时是不存在的,只是在运行过程中才去确定对象的类型、方法等。利用Runtime机制可以在程序运行时动态修改类、对象中的所有属性、方法等。譬如C++也是运行时动态的识别类的对象,简称对象的动态绑定等。

    相信我们都遇到过这样一个问题:我想在扩展(category)中添加一个属性,iOS是不允许给扩展类扩展属性的,那怎么办呢?答案就是使用Runtime机制和关联属性。还有就是我们在网络请求数据处理时,调用了-setValuesForKeysWithDictionary:方法来设置模型值。这里其实也是通过Runtime机制来Finish,内部会遍历模型类的所有属性名,然后设置与key对应的属性名的值。在使用Runtime前,先要包含头文件:#import <objc/runtime.h>。如果是Swift就不需要包含头文件,就可以直接使用了。

    1.消息(Message)
    为什么叫消息呢?因为面向对象编程中,对象调用方法叫做发送消息。在编译时,应用的源代码就会被编将对象发送消息转换成runtime的objc_msgSend函数调用。

    在Objective-C,消息在运行时并不要求实现。编译器会转换消息表达式:

    [receiver message];
    

    在编译时会转换成类似这样的函数调用:

    objc_msgSend(receiver, selector);
    或
    ((void (*)(id, SEL, id))objc_msgSend)(self, @selector(method), param)
    

    具体会转换成哪个,我们来看看官方的原话:

    When it encounters a method call, the compiler generates a call to one of the
    *  functions \c objc_msgSend, \c objc_msgSend_stret, \c objc_msgSendSuper,    or \c objc_msgSendSuper_stret.
    *  Messages sent to an object’s superclass (using the \c super keyword) are sent using \c objc_msgSendSuper; 
    *  other messages are sent using \c objc_msgSend. Methods that have data structures as return values
    *  are sent using \c objc_msgSendSuper_stret and \c objc_msgSend_stret.
    

    也就是说,我们是通过编译器来自动转换成运行时代码时,它会根据类型自动转换成下面的其它一个函数:

    objc_msgSend:其它普通的消息都会通过该函数来发送
    objcmsgSendstret:消息中需要有数据结构作为返回值时,会通过该函数来发送消息并接收返回值
    objcmsgSendSuper:与objcmsgSend函数类似,只是它把消息发送给父类实例
    objcmsgSendSuperstret:与objcmsgSendstret函数类似,只是它把消息发送给父类实例并接收数组结构作为返回值
    

    另外,如果函数返回值是浮点类型,官方说明如下:

    * arm:    objc_msgSend_fpret not used
    * i386:   objc_msgSend_fpret used for `float`, `double`, `long double`.
    * x86-64: objc_msgSend_fpret used for `long double`.
    *
    * arm:    objc_msgSend_fp2ret not used
    * i386:   objc_msgSend_fp2ret not used
    * x86-64: objc_msgSend_fp2ret used for `_Complex long double`.
    

    其实这是一个条件编译,我们不用担心是哪种处理器上,我们只需要调用objc_msgSend_fpret函数即可。

    当消息被发送到实例对象时,它是如何处理的:

    image

    我们的根类是NSObject,它会一层一层的传递,直接找到要处理该消息的对象,若都没有找到,正常情况下会出现Unreconized selector ...这样的崩溃提示了。

    1.1 Message Forwarding
    当发送消息给一个不处理该消息的对象是错误的。然后在宣布错误之前,运行时系统给了接收消息的对象处理消息的第二个机会。

    当某对象不处理某消息时,可以通过重写-forwardInvocation:方法来提供一个默认的消息响应或者避免出错。当对象中找不到方法实现时,会按照类继承关系一层层往上找。我们看看类继承关系图:

    class-diagram.jpg

    所有元类中的isa指针都指向根元类,而根元类的isa指针则指向自身。根元类是继承于根类的,与根类的结构体成员一致,都是objc_class结构体,不同的是根元类的isa指针指向自身,而根类的isa指针为nil

    我们再看看消息处理流程:

    QQ20141113-1@2x.png

    当对象查询不到相关的方法,消息得不到该对象处理,会启动“消息转发”机制。消息转发还分为几个阶段:先询问receiver或者说是它所属的类是否能动态添加方法,以处理当前这个消息,这叫做“动态方法解析”,runtime会通过+resolveInstanceMethod:判断能否处理。如果runtime完成动态添加方法的询问之后,receiver仍然无法正常响应则Runtime会继续向receiver询问是否有其它对象即其它receiver能处理这条消息,若返回能够处理的对象,Runtime会把消息转给返回的对象,消息转发流程也就结束。若无对象返回,Runtime会把消息有关的全部细节都封装到NSInvocation对象中,再给receiver最后一次机会,令其设法解决当前还未处理的这条消息。

    消息处理越往后,开销也就会越大,因此最好直接在第一步就可以得到消息处理。

    我们看看类结构体:

    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;
    /* Use `Class` instead of `struct objc_class *` */
    

    我们可以看到每个类结构体都会有一个isa指针,它是指向元类的。它还有一个父类指针super_class,指针父类。包含了类的名称name、类的版本信息version、类的一些标识信息info、实例大小instance_size、成员变量地址列表ivars、方法地址列表methodLists、缓存最近使用的方法地址cache、协议列表protocols`。

    我们在使用时,经常使用到Class,它就是:

    typedef struct objc_class *Class;
    

    当类为根类时,它的super_class就会是nil。普通的Class存储的是实例成员,如-号方法、属性、成员变量,而isa则指向元类,而元类存储的是静态成员,如+号方法、static成员。

    2.我们可以利用Runtime机制来实现类似拷贝copy,mutableCopy功能,那就是使用object_copy来实现对象拷贝。object_copy方法,随之对应的方法是object_dispose方法,不过这两个方法在ARC下无法通过编译器,OBJC_ARC_UNAVAILABLE。
    API:id object_copy(id obj, size_t size)和id object_dispose(id obj)。

    3.class_addMethod 一般用在消息自动解析时动态新增方法来进行消息的响应,从而达到消息发送的目的。

    动态方法解析
    你可以动态地提供一个方法的实现。例如我们可以用@dynamic关键字在类的实现文件中修饰一个属性:
    @dynamic propertyName;
    这表明我们会为这个属性动态提供存取方法,也就是说编译器不会再默认为我们生成setPropertyName:和propertyName方法,而需要我们动态提供。我们可以通过分别重载resolveInstanceMethod:和resolveClassMethod:方法分别添加实例方法实现和类方法实现。因为当 Runtime 系统在Cache和方法分发表中(包括超类)找不到要执行的方法时,Runtime会调用resolveInstanceMethod:或resolveClassMethod:来给程序员一次动态添加方法实现的机会。我们需要用class_addMethod函数完成向特定类添加特定方法实现的操作:

    void dynamicMethodIMP(id self, SEL _cmd) {
        // implementation ....
    }
    @implementation MyClass
    + (BOOL)resolveInstanceMethod:(SEL)aSEL
    {
          if (aSEL == @selector(resolveThisMethodDynamically)) {
                class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
                return YES;
          }
          return [super resolveInstanceMethod:aSEL];
    }
    

    @end
    上面的例子为resolveThisMethodDynamically方法添加了实现内容,也就是dynamicMethodIMP方法中的代码。其中 “v@:” 表示返回值和参数,这个符号涉及 Type Encoding。

    参数解析:
    BOOL class_addMethod(Class cls,SEL name,IMP imp, const char *types)
    Class 可以理解为要添加方法的目标类的名称
    SEL name 可以理解为 要添加的方法名
    IMP imp 这个可以理解为实现的方法名称,至少要包含2个参数,分别是 id self, SEL _cmd
    Const char *types 可以理解为参数的属性。
    其中types参数为"i@:@“,按顺序分别表示:

     i    表示 返回值类型int,若是v则表示void
     @    表示 参数id(self)
     :    表示 SEL(_cmd)
     @    表示 id(str)
    
     Type Encoding
     编码值       含意
     c        代表char类型
     i        代表int类型
     s        代表short类型
     l        代表long类型,在64位处理器上也是按照32位处理
     q        代表long long类型
     C        代表unsigned char类型
     I        代表unsigned int类型
     S        代表unsigned short类型
     L        代表unsigned long类型
     Q        代表unsigned long long类型
     f        代表float类型
     d        代表double类型
     B        代表C++中的bool或者C99中的_Bool
     v        代表void类型
     *        代表char *类型
     @      代表对象类型
     #        代表类对象 (Class)
     :        代表方法selector (SEL)
     [array type]   代表array
     {name=type…}   代表结构体
     (name=type…)   代表union
     bnum      A bit field of num bits
     type      A pointer to type
     ?        An unknown type (among other things, this code is used for function pointers)
    

    我们想要通过运行时处理各种类型,那么我们必须要知道哪种字符代表什么类型。
    4.Method Swizzling
    这个是Swizzling技术,可以通过它进行"骗取"系统(其实就是调换方法,重新映射方法对应的实现),达到"偷天换日"的效果。
    #pragma mark - method swizzling
    - (void)methodExchange {
    Method m1 = class_getInstanceMethod([NSString class],@selector(lowercaseString));
    Method m2 = class_getInstanceMethod([NSString class],@selector(uppercaseString));
    method_exchangeImplementations(m1, m2);
    NSLog(@"%@", [@"helloWorld" lowercaseString]);
    NSLog(@"%@", [@"heLLoworLd" uppercaseString]);
    }
    运行结果:
    2015-01-08 11:02:36.937 rumtimeDemo[1367:681073] HELLOWORLD
    2015-01-08 11:02:36.937 rumtimeDemo[1367:681073] helloworld

    举个摘自老外博客的例子
    #import <objc/runtime.h>
    @implementation UIViewController (Tracking)
    + (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    Class aClass = [self class];
    SEL originalSelector = @selector(viewWillAppear:);
    SEL swizzledSelector = @selector(xxx_viewWillAppear:);

               Method originalMethod = class_getInstanceMethod(aClass, originalSelector); 
               Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector); 
        
               // When swizzling a class method, use the following:
               // Class aClass = object_getClass((id)self);
               // ...
               // Method originalMethod = class_getClassMethod(aClass, originalSelector);
               // Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);
    
               BOOL didAddMethod = 
               class_addMethod(aClass, 
                            originalSelector, 
                            method_getImplementation(swizzledMethod), 
                            method_getTypeEncoding(swizzledMethod)); 
    
               if (didAddMethod) { 
                     class_replaceMethod(aClass, 
                     swizzledSelector, 
                     method_getImplementation(originalMethod), 
                     method_getTypeEncoding(originalMethod)); 
               } else { 
                     method_exchangeImplementations(originalMethod, swizzledMethod); 
               } 
          }); 
    } 
    
    #pragma mark - Method Swizzling 
    - (void)xxx_viewWillAppear:(BOOL)animated { 
          [self xxx_viewWillAppear:animated]; 
          NSLog(@"viewWillAppear: %@", self); 
    } 
    @end
    

    5.获取对象所有属性名
    利用runtime获取对象的所有属性名是可以的,但是变量名获取就得用另外的方法了。我们可以通过class_copyPropertyList方法获取所有的属性名称。
    - (void)getClassAllPropertyList
    {
    u_int count;
    objc_property_t properties=class_copyPropertyList([UIViewController class], &count);
    for (int i = 0; i < count ; i++)
    {
    const char
    propertyName =property_getName(properties[i]);
    NSString *strName = [NSString stringWithCString:propertyName encoding:NSUTF8StringEncoding];
    NSLog(@"propertyName:%@",strName);
    }
    // 注意,这里properties是一个数组指针,就是二级指针的意思,是C的语法,
    // 我们需要使用free函数来释放内存,否则会造成内存泄露
    free(properties);
    }

    6.获取对象的成员变量名称
    要获取对象的成员变量,可以通过class_copyIvarList方法来获取,通过ivar_getName来获取成员变量的名称。对于属性,会自动生成一个成员变量。

    - (NSArray *)allMemberVariables {
           unsigned int count = 0;
           Ivar *ivars = class_copyIvarList([self class], &count);
    
           NSMutableArray *results = [[NSMutableArray alloc] init];
           for (NSUInteger i = 0; i < count; ++i) {
                Ivar variable = ivars[i];
               const char *name = ivar_getName(variable);
               NSString *varName = [NSString stringWithUTF8String:name];
               [results addObject:varName];
           }
    
          return results;
     }
    

    7.获取类的所有方法
    - (void)getClassAllMethod
    {
    u_int count;
    Method* methods= class_copyMethodList([UIViewController class], &count);
    for (int i = 0; i < count ; i++)
    {
    SEL name = method_getName(methods[i]);
    NSString *strName = [NSString stringWithCString:sel_getName(name)encoding:NSUTF8StringEncoding];
    NSLog(@"methodName:%@",strName);
    }
    }

    8.Category扩展属性
    iOS的category是不能扩展存储属性的,但是我们可以通过运行时关联来扩展“属性”。
    假设扩展下面的“属性”:

    // 由于扩展不能扩展属性,因此我们这里在实现文件中需要利用runtime实现。

    typedef void(^CallBack)();
    @property (nonatomic, copy) CallBack callback;
    

    在实现文件中,我们用一个静态变量作为key:

    static char CallbackKey;
    
    - (void)setCallback:(CallBack)callback {
        objc_setAssociatedObject(self, &CallbackKey, callback, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    
    - (CallBack)callback {
        return objc_getAssociatedObject(self, &CallbackKey);
    }
    

    其实就是通过objc_getAssociatedObject取得关联的值,通过objc_setAssociatedObject设置关联。

    对于动态获取属性的名称、属性值使用较多的地方一般是在使用第三方库中,比如MJExtension。该第三方库是通过动态获取模型类的属性,并通过KVC的setValue:forKey来写入到模型的,实现两者间的转换(即将Model转换成字典,或者将字典转换成Model)。
    待续。。。。。。
    这里有一篇中文版挺不错的讲解Runtime博客。地址:http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/

    相关文章

      网友评论

          本文标题:Runtime知识随记

          本文链接:https://www.haomeiwen.com/subject/fzifkttx.html