Runtime知识随记

以前在学习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/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,670评论 0 9
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,161评论 0 7
  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,521评论 33 466
  • 参考链接: http://www.cnblogs.com/ioshe/p/5489086.html 简介 Runt...
    乐乐的简书阅读 2,125评论 0 9
  • 看到很多新闻说到暴力事件,仔细分析,很多都是发生在亲人朋友间,值得反思。 我们平时听到的都是赞美的话,比如“打虎亲...
    东兴之阅读 128评论 0 0