前言:Objective C语言把能在编译期做的事情就推迟到运行期再决定。这就意味着,Objective C不仅需要一个编译器,而且需要一个运行期环境。这个运行期环境就是Runtime。
1.类和对象
1.类的数据结构
//objc.h文件
typedef struct objc_class *Class;
//runtime.h文件
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY; //指向meta-class(元类)
#if!__OBJC2__
Class super_class OBJC2_UNAVAILABLE; //父类,如果是根类,则为NULL
const char*name OBJC2_UNAVAILABLE;//类名
long version OBJC2_UNAVAILABLE;//类的版本信息,默认为0
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;
//用于缓存最近使用的方法。一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。
//在实际使用中,这个对象只有一部分方法是常用的,很多方法很少用或者根本用不上。这种情况下,如果每次消息来时都是methodLists中遍历一遍,性能势必很差。这时,cache就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才到methodLists中查找方法。
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;//协议链表
#endif
} OBJC2_UNAVAILABLE;
cache的结构:
struct objc_cache {
//指定分配的缓存bucket的总数unsignedintmask/* total = mask + 1 */ OBJC2_UNAVAILABLE;
//指定实际占用的缓存bucket的总数unsignedint occupied OBJC2_UNAVAILABLE;
//指向Method数据结构指针的数组Method buckets[1] OBJC2_UNAVAILABLE;
};
2.对象
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};/// A pointer to an instance of a class.
typedef struct objc_object * id;
objc_object结构体也只是一个指向其类的isa指针;这样,当我们向一个Objective-C对象发送消息时,运行时库会根据实例对象的isa指针找到这个实例对象所属的类;runtime库会在类的方法列表及父类的方法列表中去寻找与消息对应的selector指向的方法,找到后运行这个方法。
2.方法查找过程
Objective-C是动态语言,每个方法在运行时会被动态转为消息发送一个对象的方法 [obj test],编译器转成消息发送objc_msgSend(obj, test),Runtime时执行的流程是这样的:
1.首先,通过obj的isa指针找到它的class;
2.查找是否存在对应的方法缓存,如果存在直接返回调用
为了优化性能,方法的缓存使用了散列表的方式
3.未找到缓存,到类本身或顺着类结构向上查找方法实现,返回的method_t *类型也被命名为Method,如果在这个步骤中找到了方法的实现,那么将它加入到方法缓存中以便下次调用能快速找到(objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类)
3.未找到任何方法实现,触发消息转发机制进行最后补救
3.消息转发机制:
具体过程如下:
Monkey*monkey = [[Monkeyalloc]init];
((void(*)(id,SEL))objc_msgSend)(monkey,@selector(fly));
1、动态方法解析,先调用resolveInstanceMethod方法,如果你使用class_addMethod方法可完成消息转发:
+(BOOL)resolveInstanceMethod:(SEL)sel{
class_addMethod(self, sel, class_getMethodImplementation(self, sel_registerName("jump")), "v");
return [super resolveInstanceMethod:sel];
}
2、备援接收者 如果依然没有找到就调用forwardingTargetForSelector,如果返回一个正确的对象也可以完成转发:
-(id)forwardingTargetForSelector:(SEL)aSelector{
return [[Bird alloc]init];
}
3、完整的消息转发,当运行时系统检测到第二步中用户未返回能处理相应选择子的对象时,那么来到这一步就要启动完整的消息转发机制了。该方法可以改变消息调用目标,运行时系统根据所改变的调用目标,向调用目标方法列表中查询对应方法的实现并实现跳转,这种方式和第二步的操作非常相似。当然你也可以修改方法的选择子,亦或者向所调用方法中追加一个参数等来跳转到相关方法的实现。
最后,如果消息转发的第三步还未能处理该未知选择子的话,那么最终会调用NSObject类的如下方法用以异常的抛出,表明该选择子最终未能处理。
- (void)doesNotRecognizeSelector:(SEL)aSelector;
注意:第三步首先要使用forwardInvocation必须先要进行方法签名,系统将方法签名封装成NSinvocation
-(NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector{
//获取方法签名进入下一步,进行消息转发
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
}
过程图解如下:
4.常用的名词:
SEL:
typedef struct objc_selector *SEL; SEL是指向一个C String的指针,SEL只是方法编号。
[myObject addObject:yourObject]将翻译为objc_msgSend(myObject, 12, yourObject)(假设 addObject 的 selector 为 12);objec_msgSend()函数将会使用 myObjec 的 isa 指针来找到 myObject 的类空间结构并 在类空间结构中查找 selector 12 所对应的方法的IMP
SEL selector = NSStringFromSelector(@selector(fristview);
IMP imp = [self methodForSelector:selector];
void (*func)(id,SEL) = (void *)imp;
func(self,selector);
IMP:
IMP-指向实际执行函数体的函数指针
为什么不直接获得函数指针,而要从SEL这个编号走一圈再回到函数指针呢?
有了SEl的这个中间过程,可以对一个编号和什么方法映射做些操作,也就是说一个SEL可以指向不同的函数指针,这样就完成可以一个方法名在不同时候对应不同的函数体;;另外可以将SEL作为参数传递给不同的类执行.也就是说某些业务我们 只知道方法名但是要根据不同情况让不同的类执行的时候,SEL可以帮助我们.
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id (*IMP)(id, SEL, ...);
#endif
_cmd:
SEL 类型的一个变量,Objective C的函数的前两个隐藏参数为self 和 _cmd
method:
指向Objective C中的方法的指针
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
Ivar:
属性(property),成员变量(ivar)的关系:
property = ivar + getter + setter;
objective C中的实例变量
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
}
5.常用的方法和面试题:
runtime怎么添加属性、方法:
class_addIvar
class_addMethod
class_addProperty
class_addProtocol
class_replaceProperty
runtime如何通过selector找到对应的IMP地址?(分别考虑类方法和实例方法)
注意:实例对象和类对象的区别
Person *p = [[Person alloc] init];
Class c1 = [p class];
Class c2 = [Person class];
NSLog(@”%d”, c1 == c2); //输出 1 因此类对象获取Class得到的是其本身
每一个类对象中都一个对象方法列表(对象方法缓存)
类方法列表是存放在类对象中isa指针指向的元类对象中(类方法缓存)
方法列表中每个方法结构体中记录着方法的名称,方法实现,以及参数类型,其实selector本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现.
当我们发送一个消息给一个NSObject对象时,这条消息会在对象的类对象方法列表里查找
当我们发送一个消息给一个类时,这条消息会在类的Meta Class对象的方法列表里查找
使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?
无论在MRC下还是ARC下均不需要,它们会在被 NSObject -dealloc 调用的object_dispose()方法中释放
补充:对象的内存销毁时间表,分四个步骤
1、调用 -release :引用计数变为零
* 对象正在被销毁,生命周期即将结束.
* 不能再有新的 __weak 弱引用,否则将指向 nil.
* 调用 [self dealloc]
2、 父类调用 -dealloc
* 继承关系中最直接继承的父类再调用 -dealloc
* 如果是 MRC 代码 则会手动释放实例变量们(iVars)
* 继承关系中每一层的父类 都再调用 -dealloc
3、NSObject 调 -dealloc
* 只做一件事:调用 Objective-C runtime 中object_dispose() 方法
4. 调用 object_dispose()
* 为 C++ 的实例变量们(iVars)调用 destructors
* 为 ARC 状态下的 实例变量们(iVars) 调用 -release
* 解除所有使用 runtime Associate方法关联的对象
* 解除所有 __weak 引用
* 调用 free()
什么是method swizzling(俗称黑魔法)
简单说就是进行方法交换
在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的
每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系,selector的本质其实就是方法名,IMP有点类似函数指针,指向具体的Method实现,通过selector就可以找到对应的IMP
交换方法的几种实现方式:
利用 method_exchangeImplementations 交换两个方法的实现
利用 class_replaceMethod 替换方法的实现
利用 method_setImplementation 来直接设置某个方法的IMP
Method imageNameMethod = class_getClassMethod(self,@selector(imageNamed:));
Method xmg_imageNameMethod = class_getClassMethod(self, @selector(xmg_imageNamed:));
method_exchangeImplementations(imageNameMethod, xmg_imageNameMethod);
runtime 常见作用:
动态交换两个方法的实现
动态添加属性
动态添加方法
实现字典转模型的自动转换
发送消息或者防止数组越界或者字典传入空值
拦截并替换方法
实现 NSCoding 的自动归档和解档
其它用法:
需求:加载一张图片直接用[UIImage imageNamed:@”image”];是无法知道到底有没有加载成功。给系统的imageNamed添加额外功能(是否加载图片成功)。
实现步骤:
1.给系统的方法添加分类
2.自己实现一个带有扩展功能的方法
3.交换方法,只需要交换一次。
- (void)viewDidLoad {
[super viewDidLoad];
// 方案二:交换 imageNamed 和 ln_imageNamed 的实现,就能调用 imageNamed,间接调用 ln_imageNamed 的实现。
UIImage *image = [UIImage imageNamed:@"123"];
}
#import
@implementation UIImage (Image)
/**
load方法: 把类加载进内存的时候调用,只会调用一次
方法应先交换,再去调用
*/
+ (void)load {
// 1.获取 imageNamed方法地址
// class_getClassMethod(获取某个类的方法)
Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
// 2.获取 ln_imageNamed方法地址
Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:));
// 3.交换方法地址,相当于交换实现方式;「method_exchangeImplementations 交换两个方法的实现」
method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod);
}
// 加载图片 且 带判断是否加载成功
+ (UIImage *)ln_imageNamed:(NSString *)name {
UIImage *image = [UIImage ln_imageNamed:name];
if (image) {
NSLog(@"runtime添加额外功能--加载成功");
} else {
NSLog(@"runtime添加额外功能--加载失败");
}
return image;
}
/**
不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super
所以第二步,我们要 自己实现一个带有扩展功能的方法.
+ (UIImage *)imageNamed:(NSString *)name {
}
*/
runtime 给分类动态添加属性:
注解:系统 NSObject 添加一个分类,我们知道在分类中是不能够添加成员属性的,虽然我们用了@property,但是仅仅会自动生成get和set方法的声明,并没有带下划线的属性和方法实现生成。但是我们可以通过runtime就可以做到给它方法的实现。
@interface NSObject (Property)
// @property分类:只会生成get,set方法声明,不会生成实现,也不会生成下划线成员属性
@property NSString *name;
@property NSString *height;
@end
@implementation NSObject (Property)
- (void)setName:(NSString *)name {
// objc_setAssociatedObject(将某个值跟某个对象关联起来,将某个值存储到某个对象中)
// object:给哪个对象添加属性
// key:属性名称
// value:属性值
// policy:保存策略
objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)name {
return objc_getAssociatedObject(self, @"name");
}
// 调用
NSObject *objc = [[NSObject alloc] init];
objc.name = @"123";
NSLog(@"runtime动态添加属性name==%@",objc.name);