关于iOS动态运行时学习笔记

一、前言

1、什么是静态语言:
所谓静态语言,就是在程序运行前决定了所有的类型判断,类的所有成员、方法,在编译阶段就确定好了内存地址。即所有类对象只能访问属于自己的成员变量和方法,否则编译器会直接报错。

2、什么是动态语言:
所谓动态语言,指类型的判断、类的成员变量、方法的内存地址,都是在程序的运行阶段才最终确定,并且还能动态的添加成员变量和方法。也就意味着你调用一个不存在的方法时,编译也能通过,甚至一个对象它是什么类型的并不是表面我们所看到的那样,只有运行之后才能决定其真正的类型。


3、什么是运行时:
所谓运行时,就是程序在运行时做的一些事。苹果提供了一套纯C语言的api,即Runtime。

二、Runtime数据结构
在Objective-C中,使用 [receiver message] 语法时,并不会马上执行 receiver 对象的 message 方法的代码,而是向 receiver 发送一条 message 消息,这条消息可能有 receiver 来处理,也可能转发给其他对象来处理,也可能假装没有接收到这条消息而没有处理。

[receiver message] 被编译器转化为: id objc_msgSend(id self, SEL op, … );



1、SEL:
SEL 是函数 objc_msgSend 第二个参数的数据类型,表示方法选择器。

// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

// 获取一个SEL类型的方法选择器
SEL sel1 = @selector(funcname);
SEL sel2 = sel_registerName("funcname");

// 将SEL转化为字符串
NSString *funcString = NSStringFromSelector(sel1);



2、id:
id 是 objc_msgSend 第一个参数的数据类型,id 是通用类型指针,能够表示任何对象,它其实就是一个指向 objc_object 结构体指针,它包含一个 Class isa 成员,根据 isa 指针就可以顺藤摸瓜找到对象所属的类

/// Represents an instance of a class.
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;



3、Class:
isa 指针的数据类型就是 Class, Class 表示对象所属的类, Class 其实也是一个 objc_class 结构体指针。

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

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 表示一个对象的Class。
  • super_class 表示示例对象对应的父类。
  • name 表示类名。
  • ivars 表示多个成员变量,它指向 objc_ivar_list 结构体,objc_ivar_list 其实就是一个链表,存储多个 objc_ivar,而 objc_ivar 结构体存储类的单个成员变量信息。
  • methodLists 表示方法列表,它指向 objc_method_list 结构体的二级指针, objc_method_list也是一个链表,存储多个objc_method,而objc_method结构体存储类的某个方法的信息(可以动态修改 *methodLists的值来添加成员方法,也是Category的实现原理,同样也解析Category不能添加实例变量的原因)。
  • cache 用来缓存经常访问的方法,它指向 objc_cache 结构体。
  • protocols 用来表示遵循哪些协议。



4、Method:
Method 表示类中的某个方法,它指向 objc_method 结构体指针,它存储了方法名(method_name),方法类型(method_types),方法实现(method_imp)等信息

/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
} 



5、Ivar:
Ivar 表示类中的实例变量,它指向 objc_ivar 结构体指针,包含了变量名(ivar_name),变量类型(ivar_type)等信息。

/// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar;

struct objc_ivar {
    char *ivar_name                                          OBJC2_UNAVAILABLE;
    char *ivar_type                                          OBJC2_UNAVAILABLE;
    int ivar_offset                                          OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
} 



6、IMP:
IMP 本质上就是一个函数指针,指向方法的实现。当你向某个对象发送一条消息,可以由这个函数指针指定方法的实现,它最终就会执行那段代码,这样可以绕开消息传递阶段而去执行另一个方法实现。

 /// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id (*IMP)(id, SEL, ...); 
#endif



7、Cache:
Cache 其实就是一个存储 Method 的链表,用来缓存经常调用的方法,主要是为了优化方法调用的性能,当调用方法时,优先在Cache查找,如果没有找到,再到 methodLists 查找。


三、消息发送

在Objective-C中,任何方法的调用,本质都是发送消息。也就是说,我们OC调用一个方法是,其实质就是转换为Runtime中的一个函数 objc_msgSend(),这个函数的作用是向obj对象,发送了一条消息,告诉它,你该去执行某个方法。


1、objc_msgSend函数:

  • 当对象 receiver 调用方法 message 时,首先根据对象 receiver 的 isa 指针查找它对应的类 class。
  • 优先在类 class 的 Cache 中查找 message 方法。
  • 如果没找到,就在类的 methodLists 中搜索方法。
  • 如果还没有找到,就使用 super_class 指针到父类中的 methodLists 查找。
  • 一旦找到 message 这个方法,就执行它实现的 IMP。
  • 如果还没找到,有可能消息转发,也有可能忽略了该方法。


四、消息转发

[receiver message] 调用方法时,如果 message 方法在 receiver 对象的类继承体系中,没有找到方法,一般情况下,程序在运行时就会Crash掉,抛出 unrecognized selector sent to.. 类似的异常信息。但在抛出异常之前,还有三次机会按以下顺序让你拯救程序。

  • Method Resolution: 由于Method Resolution不能像消息转发那样可以交给其他对象处理,所以只适用于在原来的类中代替掉。
  • Fast Forwarding: 可以将消息处理转发给其他对象,使用范围更广,不只是限于原来的对象。
  • Normal Forwarding: 跟Fast Forwarding一样可以消息转发,但它能能过NSInvocation对象获取更多消息发送的信息,如:target、selector、arguments和返回值等信息。



1、Method Resolution:
Objective-C在运行时调用 + resolveInstanceMethod: 或 + resolveClassMethod: 方法,让你添加方法的实现。如果你添加方法并返回YES,那系统在运行时就会重新启动一次消息发送的过程。如果返回NO,运行时就跳转到下一步: 消息转发(Message Forwarding)

 // 1、正常情况下
@interface Message : NSObject
- (void)sendMessage:(NSString *)word;
@end

@implementation Message
- (void)sendMessage:(NSString *)word {
    NSLog(@"normal way : send message = %@",word);
}
@end

// 2、注释掉 sendMessage 的实现方法后,覆盖 resolveInstanceMethod 方法:
@interface Message : NSObject
- (void)sendMessage:(NSString *)word;
@end

@implementation Message
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(sendMessage:)) {
        class_addMethod([self class],
                        sel,
                        imp_implementationWithBlock(^(id self, NSString *word){
            NSLog(@"method resolution way : send message = %@",word);
        }), "v@*");
    }
    return YES;
}
@end

其中 v@* 表示方法的返回值和参数,详情参考 Type Encodings 。



2、Fast Forwarding:
如果目标对象实现 - forwardingTargetForSelector: 方法,系统会在运行时调用这个方法,只要这个方法返回的不是nil或者self,也会重启消息发送的过程,把该消息转发给其他对象来处理。否则,就会继续Normal Forwarding.

// 3、注释掉 sendMessage 的实现方法后,覆盖 forwardingTargetForSelector 方法:
@interface Message : NSObject
- (void)sendMessage:(NSString *)word;
@end

@implementation Message
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(sendMessage:)) {
        return [[MessageForwarding alloc] init];
    }
    return nil;
}
@end

// 转发给该对象处理
@interface MessageForwarding : NSObject
- (void)sendMessage:(NSString *)word;
@end

@implementation MessageForwarding
- (void)sendMessage:(NSString *)word {
    NSLog(@"fast forwarding way : send message = %@",word);
}
@end



3、Normal Forwarding:
如果没有使用Fast Forwarding来消息转发,最后只有使用 Normal Forwarding来进行消息转发。

它首先调用 - methodSignatureForSelector: 方法来获取函数的参数和返回值。如果返回nili,程序会Crash掉,并抛出unrecognized selector sent to instance异常信息。如果返回一个函数签名,系统就会创建一个NSInvocation对象并调用 - forwardInvocation: 方法。

// 4、注释掉 sendMessage 的实现方法后,通过 methodSignatureForSelector 方法获取函数签名,并通过 forwardInvocation 方法来转发处理:

@interface Message : NSObject
- (void)sendMessage:(NSString *)word;
@end

@implementation Message
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
    if (!methodSignature) {
        methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
    }
    return methodSignature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    MessageForwarding *messageForwarding = [[MessageForwarding alloc] init];
    if ([messageForwarding respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:messageForwarding];
    }
}
@end

// 转发给该对象处理
@interface MessageForwarding : NSObject
- (void)sendMessage:(NSString *)word;
@end

@implementation MessageForwarding
- (void)sendMessage:(NSString *)word {
    NSLog(@"fast forwarding way : send message = %@",word);
}
@end


五、我们可以用运行时做什么

1、互换方法的实现:
因为 selector 和 IMP 之间的关系是在运行时才决定的,所以我们可以通过 void method_exchangeImplementations(Method m1, Method m2) 方法来改变 selector 和 IMP 的对应关系。

@implementation NSObject (ExchangeMethod)

// 此方法会在此类第一次被加进内存是调用,且仅调用一次
+ (void)load {
    // 获取系统的dealloc方法
    Method m1 = class_getInstanceMethod(self, NSSelectorFromString(@"dealloc"));
    // 获取自己声明的my_dealloc
    Method m2 = class_getInstanceMethod(self, @selector(my_dealloc));
    
    // 交换两个方法的实现,即调用dealloc方法时,会实现my_dealloc,调用my_dealloc方法时,才会调用
    method_exchangeImplementations(m1, m2);
}

- (void)my_dealloc {
    // do something
    NSLog(@"my_dealloc");
    
    // 这里需要调用自己,调用自己就是调用原来的dealloc进行释放操作
    [self my_dealloc];
}

@end



2、动态添加方法:
动态语言调用一个没有的方法时,编译阶段不会报错,但是运行时便会抛出异常闪退,但我们可以动态为某个了添加方法。这里用到的其实就是上面提到的,消息转发时的拯救程序机制。



3、动态添加属性:
有时我们想为系统的类或者一些不便修改的第三方框架的类,增加一些自定义的属性,以满足开发的需求。比如使用类目,但是类目只能为一个类添加方法,不能添加属性。这是便可用到下面的方法:

#import "UIView+TintColor.h"
#import <objc runtime="runtime">


@implementation UIView (TintColor)

- (nullable NSString *)tintColorName {
    return objc_getAssociatedObject(self, @selector(tintColorName));
}

- (void)setTintColorName:(NSString *)tintColorName {
    if (!tintColorName || tintColorName.length == 0) {
        return;
    }
    UIColor *color;
    SEL sel = NSSelectorFromString(tintColorName);
    if ([UIColor respondsToSelector:sel]) {
        color = [UIColor performSelector:sel];
    }
    if (!color) {
        return;
    }
    self.tintColor = color;
    
    objc_setAssociatedObject(self,
                             @selector(tintColorName),
                             tintColorName,
                             OBJC_ASSOCIATION_COPY_NONATOMIC);
}

@end



4、获取类中所有的成员变量和属性:
在开发中,有时会遇到想要改变系统自带类的某一个值,却找不到与之对应的api,便可用运行时来获取该类的所有成员变量。

// 获取类中所有的成员变量
Ivar *class_copyIvarList(Class cls, unsigned int *outCount) 
// 获取类中所有的属性
objc_property_t class_getProperty(Class cls, const char *name)

unsigned int count;
    Ivar *ivars = class_copyIvarList([UIButton class], &count);
    for (NSInteger i = 0; i < count; i++) {
        // 取出成员变量
        Ivar ivar = ivars[i];
        // 获取属性名字
        NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)];
        // 获取属性类型
        NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        
        NSLog(@"%@:%@",type,name);
    }


六、结束语

将XCode升级到6后,报Too many arguments to function call, expected 0, have *,在XCode5.1里能编译通过的,到xcode6就报错

objc_msgSend(self.beginRefreshingTaget, self.beginRefreshingAction, self);

Too many arguments to function call, expected 0, have *

解决办法:
选中项目 - Project - Build Settings - Enable Strict Checking of objc_msgSend Calls 将其设置为 NO 即可

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • runtime 和 runloop 作为一个程序员进阶是必须的,也是非常重要的, 在面试过程中是经常会被问到的, ...
    SOI阅读 21,776评论 3 63
  • runtime 和 runloop 作为一个程序员进阶是必须的,也是非常重要的, 在面试过程中是经常会被问到的, ...
    made_China阅读 1,200评论 0 7
  • runtime 运行时语言,实现Object-C的C语言库,将OC转换成C进行编译的过渡者。 作为一门动态编程语言...
    夜雨聲煩_阅读 541评论 0 0
  • 1 古人通常多久洗一次澡?他们用什么来洗头呢? 古人比我们想像中讲卫生多了。早在先秦时期,古人便“三日一洗头,五日...
    看鉴阅读 432评论 0 0