【ios学习】浅谈Runtime

2018-04-23  本文已影响16人  软件iOS开发

一、Runtime基石:Objective-C对象模型

1、对象

每一个对象都是类的实例, 类中保存对象的方法列表;当一个对象方法被调用时,类会首先查找它本身是否有该方法的实现,如果没有,则会向它的父类查找该方法,直到NSObject(根类);

类是元类 (metaclass) 的实例;元类保存类方法列表;当一个类方法被调用时,元类会首先查找它本身是否有该类方法的实现,如果没有,则会向它的父类查找该方法,直到NSObject(根类);

2、isa指针

对象的isa指针指向所属的类,类的isa指针指向所属的元类;所有的元类的isa指针都会指向一个根元类 (root metaclass)。根元类的isa指针指向自己,行成了一个闭环。

在64 位 CPU 下,isa 的内部结构有变化。具体查看用 isa 承载对象的类信息

对象、isa指针、类、元类、根元类的关系如下图:

3、对象布局

实例变量(包括父类)都保存在对象本身的存储空间内;实例方法保存在中,类方法保存在元类中;父类的实例方法保存在各级 super class 中,父类的类方法保存在各级 super meta class;

//对象组成 --start--

isa pointer

rootClass's vars

penultimate superClass's vars

...

superClass's vars

Class's vars

//对象组成 --end--

typedef struct objc_class *Class;

 //类的结构

 struct objc_class{

  struct objc_class* isa;                              //指向元类

  struct objc_class* super_class;                //指向父类

  const char* name;

  long version;

  long info;

  long instance_size;

  struct objc_ivar_list* ivars;                         //实例变量列表

  struct objc_method_list** methodLists;      //方法列表

  struct objc_cache* cache;

  struct objc_protocol_list* protocols;           //协议列表

};

//实例变量的结构

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

}

说明1:对象中保存指向类的isa指针 以及 各级的 实例变量(ivar),这个内存结构在编译时就确定下来了,不能在编译时给对象增加实例变量。

说明2:类的内存布局有isa指针、super_class指针、实例变量列表、方法列表和协议列表,其中实例变量(var)包含了变量的名称、类型、偏移等。

二、Runtime核心:消息发送和转发

Runtime赋予了OC了诸多动态特性,使其可以在运行时可以做一些事情;主要表现为:动态类型(在运行时才检查对象类型)和动态绑定(接到消息后,由运行环境决定执行哪部分代码)

1、消息发送(Message)

Objective-C 中的方法调用,实质上是在底层用objc_msgSend()实现消息发送,其核心在于:根据SEL(选择器)开始找到IMP;其中SEL是实例方法的指针,可以看做方法名字符串;IMP是函数指针,指向方法实现的地址。

//调用方法  

[obj doSomething];

//在编译时候转换

objc_msgSend(obj,@selector(doSomething))

objc_msgSend的定义如下:

// self是接收者,接收该消息的类的实例

    // _cmd是选择器,要处理的消息的selector

     // ... 是需传入的参数,参数个数不定

    objc_msgSend(id self, SEL _cmd, ...)

objc_msgSend的发送流程:先在Class中的缓存查找imp(没缓存则初始化缓存),如果没找到,则向父类的Class查找。如果一直查找到根类仍旧没有实现,就走消息转发(_objc_msgForward)了。

给nil发送消息不会有什么作用,但是返回值有些区别,具体如下:

a) 如果方法返回值是 对象,返回nil

b) 如果方法返回值是 指针类型,其指针大小为小于或者等于sizeof(void*),float,double,long double 或者 long long 的整型标量

c) 如果方法返回值是 结构体,发送给 nil 的消息将返回0。结构体中各个字段的值将都是0。

d) 如果方法返回值不是 上述提到的几种情况,那么发送给 nil 的消息的返回值将是未定义的。

2-1、消息转发(Message Forwarding)

消息转发解决的是:查找IMP(方法实现)失败后的处理;经历动态方法解析备用接收者完整的消息转发三个过程,其流程如下图:

动态方法解析:接收到未知消息时,Runtime向当前类发送+resolveInstanceMethod:或+resolveClassMethod:消息,在这里可以添加缺失的方法,返回YES,重新发送消息,否则继续下一步;

备用接收者动态方法解析中没能处理,Runtime会向forwardingTargetForSelector:发消息,如果该方法返回了一个非nil或非self对象,恰好该对象实现了这个方法,那么该对象就成了消息的接收者,消息就被分发到该对象。

完整消息转发:前两个都没能处理好,Runtime发送methodSignatureForSelector:消息,获取selector对应方法的签名;如果有方法签名返回,则根据方法签名创建描述消息的NSInvocation,向当前对象发送forwardInvocation:消息;如果没有方法签名返回,返回nil,向当前对象发送doesNotRecognizeSelector:消息,应用Crash退出。

2-2、避免消息转发的办法

在消息转发三个过程中,未知消息的处理过程越往后,代价越大;一般我们可以这么做 尽可能避免消息转发,可以这么做:

调用delegate 方法前检查方法是否实现(respondsToSelector:), 只有实现了(respondsToSelector:返回YES) ,才去真正调用delegate 方法。

if([self.delegate respondsToSelector: @selector(sayHello)]) {

    [self.delegate sayHello];

}

直接调用方法,少用performSelector:;因为在直接调用方法时,编译自动校验,如果方法不存在,编译器会直接报错;而使用performSelector:的话一定是在运行时候才能发现,如果此方法不存在就会崩溃。

//直接使用方法调用,少使用performSelector

[dog sayHello];

// [dog performSelector:@selector(sayHello) withObject:nil];

使用performSelector:,最好先判断方法是否实现(respondsToSelector:),只有实现了(respondsToSelector:返回YES) ,才去调用performSelector:方法。

//respondsToSelector:和performSelector:组合使用

    if ([dog respondsToSelector:@selector(sayHello)])         {

    [dog performSelector:@selector(sayHello)];

 }

强制类型转换,先判断对象是否属于强制转换后的类

if([data isKindOfClass:[NSDictionary class]]){

  //

}

三、Runtime特性和应用

1、分类(Category)

原理:对象的方法定义都保存在类的可变区域中,修改methodLists指针指向的指针的值,就可以实现动态地为某一个类增加成员方法。(但是对象布局在编译时候就固定了,结构体的大小并不能动态变化,在运行时不能增加实例变量)。

通过关联objc_setAssociatedObject 和 objc_getAssociatedObject方法可以变相地给对象增加实例变量,并不会真正改变了对象的内存结构。

通过Category新增的方法,会插入到方法列表的前部;如果有和原来方法重名,在运行时,顺序查找时,一旦找到对应名字的方法,就不再查找,导致原来方法得不到机会,这是Category新增的方法和原方法重名,原有方法失效的原因

作用:给现有的类添加方法;将一个类的实现拆分成多个独立的源文件;声明私有的方法。

2、关联对象(Associated Objects)

原理:Category不能给一个已有类添加实例变量,但是可以通过关联对象添加属性;但是关联对象不会改变对象的内存布局,新增的属性是添加到和对象地址关联的哈希表中;

Associated Objects 相关的三个方法

objc_setAssociatedObject    //添加关联对象

objc_getAssociatedObject    //获取关联对象

objc_removeAssociatedObjects  // 删除所有关联对象

作用:为现有的类添加私有变量以帮助实现细节;为现有的类添加公有属性;为 KVO 创建一个关联的观察者

3、方法混写(Method Swizzling)

原理:在运行时交换方法实现(IMP)

作用:可以利用它hook原有的方法,插入自己的业务需求,

4、键值观察(KVO)

观察者模式在Objective-C的应用之一,借助Runtime特性,实现自动键值观察;使用了isa swizzling机制。具体描述如下:

当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个子类,在这个子类中重写基类中被观察属性的 setter 方法,实现真正的通知机制;

派生类还重写了 class 方法以“欺骗”外部调用者,系统将对象的 isa 指针指向这个新诞生的子类,实质上这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。

此外,派生类还重写了 dealloc 方法来释放资源。

说明:KVC(键值编码)是不通过存取方法,而通过属性名称字符串间接访问属性的机制,没有用到isa swizzling机制。

5、NSProxy

OC是单继承的,但是可以利用NSProxy实现一下“伪多继承”,具体参考NSProxy——少见却神奇的类

项目中,主要是利用NSProxy做消息转发的代理类,如弱引用代理类,可以打破循环引用。

@interface FLWeakProxy : NSProxy

+ (instancetype)weakProxyForObject:(id)targetObject;

@end

@interface FLWeakProxy ()

@property (nonatomic, weak) id target;

@end

@implementation FLWeakProxy

#pragma mark Life Cycle

//类没有定义默认的init方法.

+ (instancetype)weakProxyForObject:(id)targetObject{

    FLWeakProxy *weakProxy = [FLWeakProxy alloc];

    weakProxy.target = targetObject;

    return weakProxy;

}

#pragma mark Forwarding Messages

- (id)forwardingTargetForSelector:(SEL)selector{

// Keep it lightweight: access the ivar directly

return _target;

}

- (void)forwardInvocation:(NSInvocation *)invocation{

    void *nullPointer = NULL;

    [invocation setReturnValue:&nullPointer];

}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector{

    return [NSObject instanceMethodSignatureForSelector:@selector(init)];

}

@end

说明: NSProxy非常适合做消息转发的代理类,能自动转发中定义的接口和NSObject的Category中定义的方法,如果使用NSObject来做,不能自动转发NSObject的Category中定义、respondsToSelector:、isKindOfClass:这两个方法。

上一篇下一篇

猜你喜欢

热点阅读