Runtime知识随记

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

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

相信我们都遇到过这样一个问题:我想在扩展(category)中添加一个属性,iOS是不允许给扩展类扩展属性的,那怎么办呢?答案就是使用Runtime机制和关联属性。还有就是我们在网络请求数据处理时,调用了-setValuesForKeysWithDictionary:方法来设置模型值。这里其实也是通过Runtime机制来Finish,内部会遍历模型类的所有属性名,然后设置与key对应的属性名的值。在使用Runtime前,先要包含头文件:#import 。如果是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:方法来提供一个默认的消息响应或者避免出错。当对象中找不到方法实现时,会按照类继承关系一层层往上找。我们看看类继承关系图:

Runtime知识随记_第1张图片
class-diagram.jpg

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

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

当对象查询不到相关的方法,消息得不到该对象处理,会启动“消息转发”机制。消息转发还分为几个阶段:先询问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
@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知识随记)