简介
objective-c(简写objc)
属于动态语言,不像C
语言一样静态编译期,就确定了调用方法的指针,而objc
所谓的方法调用只是消息的发送,如下:
[recevier message];
//转换为
objc_msgSend(recevier, selector);
//如果存在参数如下:
objc_msgSend(recevier, selector, arg1, arg2, ...);
因此具体的调用的函数指针是在运行期确定的,并且在此期间还可以动态修改最终调用的函数指针位置,如isa-swizzling
及method-swizzling
技术,或者若未存在此方法,可以动态的添加方法;
另外,objc
源码是开源的,且几乎全部使用C
语言实现(有些使用了汇编实现),可以从苹果开源官方网站获取此代码。
与runtime交互
objc
从三种不同的层级上与 Runtime 系统进行交互,分别是通过 Objective-C 源代码,通过 Foundation 框架的NSObject
类定义的方法,通过对 runtime 函数的直接调用。
Objective-C源代码
大部分都是编写objc
代码,其他都交由runtime
系统来后台执行具体的操作,如[recevier message]
转为调用objc_msgSend
方法来执行;
NSObject类
Cocoa
中大多数类都继承自NSObject
类(NSProxy
类是个例外,它是一个抽象超类,来充当其他对象或尚不存在的对象的替代者),自然也继承了其方法,如description
方法可重载实现定义类描述;还提供了一些运行时获取类信息并检查一些特性:如class
获取类对象; isMemberOfClass
检查类对象是否在给定类的实例,isKindOfClass
检查类实例是否为给定类或者类的继承类实例;respondsToSelector:
检查对象能否响应指定的消息;conformsToProtocol:
检查对象是否实现了指定协议类的方法;methodForSelector:
则返回指定方法实现的地址。
Runtime函数
runtime
系统是一个由一系列数据结构和函数组成,具有公共接口的动态共享库。其构成了NSObject
类的基础,大部分还是使用更上层的接口编程,一般会用在hook
接口或者与其他语言桥接等场景;
消息
objc
方法调用是以消息发送的形式传递的并获取到相应的函数指针,从而实现函数的直接调用,具体使用的objc
方法为:
void objc_msgSend(receiver, selector, arg1, ....);
其中发送调用时隐含了receiver=self, selector=_cmd
,这两个参数是由编译器编译时自动添加上的;
而方法中的
super
关键词接收到消息时,编译器会创建一个objc_super
的结构体,如下:struct objc_super { id receiver; Class class; };
这个结构体指明了消息应该被传递给特定超类的定义。但
receiver
仍然是self
本身,这点需要注意,因为当我们想通过[super class]
获取超类时,编译器只是将指向self
的id
指针和class
的SEL传递给了objc_msgSendSuper
函数,因为只有在NSObject
类才能找到class
方法,然后class
方法调用object_getClass()
,接着调用objc_msgSend(objc_super->receiver, @selector(class))
,传入的第一个参数是指向self
的id
指针,与调用[self class]
相同,所以我们得到的永远都是self
的类型。划重点以上表明:
在同一对象中调用
[self class]
和[super class]
返回的都是self
的Class类isa指针
;@implementation Son : Father - (id)init { self = [super init]; if (self) { NSLog(@"%@", NSStringFromClass([self class])); NSLog(@"%@", NSStringFromClass([super class])); } return self; } @end
方法的调用流程
检测这个
selector
是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会retain
,release
这些函数了。检测这个 target 是不是
nil
对象。ObjC 的特性是允许对一个nil
对象执行任何一个方法不会 Crash,因为会被忽略掉。如果上面两个都过了,那就通过获取
self
实例对象的isa
获取类结构体(包含父类super_class
、cache
缓存及方法列表);优先从
cache
缓存中查找,若查找到就跳转到对应的IMP
函数指针去执行;若
cache
缓存中未找到,就去methodLists
方法列表中查找;如果方法列表中未找到,就去
super_class
父类结构体中查找,一直找到NSObject
类为止;如果还找不到就要开始进入消息转发流程了,后面会提到。
获取方法地址
直接获取到方法地址(IMP
函数指针)可有效避免runtime
消息发送流程,对于大量同函数调用的情况,可提升调用效率,但不太常见;
NSObject
提供了通过id
及selector
来获取对应IMP
函数指针(包括实例对象或者类对象的函数指针)的方法:
- (IMP)methodForSelector:(SEL)aSelector;
该方法不属于objc
的本身的特性,而为cocoa runtime
的特性,查看源码实际为NSObject
的类方法,通过class_getMethodImplementation
方法获取,源码如下:
+ (IMP)instanceMethodForSelector:(SEL)sel {
if (!sel) [self doesNotRecognizeSelector:sel];
return class_getMethodImplementation(self, sel);
}
具体的IMP
函数指针调用,需要转换为具体的函数指针类型,包括返回值类型,参数列表及其类型,如下:
IMP imp = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
//无返回值
void (*setter)(id, SEL, BOOL) = (void *)imp;
//存在返回值,则指定返回值类型即可
id (*setter)(id, SEL, BOOL) = (void *)imp;
//or
int (*setter)(id, SEL, BOOL) = (void *)imp;
消息转发
对于[objc message]
消息发送调用形式,若找不到该方法,在编译期就会报错;若是通过[objc performSelector:@selectro(message)]
形式,需要在运行期才能确定是否能响应该消息,若无法响应就会报找不到该实例方法,进而引发崩溃;
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[KVCTest test1]: unrecognized selector sent to instance 0x600000c4e8e0'
*** First throw call stack:
xxxx
因此通常会通过respondsToSelector:
方法来判定是否能响应此消息,才触发消息的发送;
if ([self respondsToSelector:@selector(method)]) {
[self performSelector:@selector(method)];
}
消息转发步骤如下:
- 动态方法解析
- 备用接收者
-
完整转发
动态方法解析
对象在缓存及方法列表中未找到相应的方法后,runtime
首先会调用+ (BOOL)resolveInstanceMethod:(SEL)sel
(实例对象)或者+ (BOOL)resolveClassMethod:(SEL)sel
(类对象),若在此方法通过class_addMethod
方法添加方法并且返回YES
(或者在父类往上继承体系添加也可),则会调用添加的方法,具体的方法使用如下:
+ (BOOL)resolveInstanceMethod:(SEL)sel;
+ (BOOL)resolveClassMethod:(SEL)sel;
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
其中class_addMethod
涉及到类型编码(Type Encodings),其指定的是imp(id, SEL, ...)
方法返回值+输入参数组合类型,为字符数组(每个对应一个字符);types
变量第一个输入参数一定是id
对象类型,则类型为@
;第二个参数为SEL
方法选择器类型,则为:
;对于其他类型,具体见官网,不过runtime
也提供了相应的获取类型编码的方法:
const char * method_getTypeEncoding(Method m);
使用如下:
@property(nonatomic, copy) NSString *propertyName;
@property(nonatomic, copy, class) NSString *className;
@dynamic propertyName;
@dynamic className;
+ (NSString *)name {
return @"class test";
}
- (NSString *)description {
return @"test";
}
//动态方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(propertyName)) {
// const char *typeCode = method_getTypeEncoding(class_getInstanceMethod([self class], @selector(description)));
const char *typeCode = "@@:";
class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(description)), typeCode);
return YES;
} else if (sel == @selector(test1)) {
class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(test)), "i@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(className)) {
class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(name)), "@@:");
return YES;
}
return [class_getSuperclass(self) resolveClassMethod:sel];
}
@dynamic
关键字在类的实现文件中修饰一个属性,表明系统不用自动生成setter/getter
方法,由自己去动态实现;
注意:[self class]
object_getClass(self)
object_getClass([self class])
的区别,先上源码:
+ (id)self {
return (id)self;
}
- (id)self {
return self;
}
+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}
+ (Class)superclass {
return self->superclass;
}
- (Class)superclass {
return [self class]->superclass;
}
若self
为实例对象时,[self class]
返回的类对象,等价于object_getClass(self)
,object_getClass([self class])
返回的是元类;
若self
为类对象时,[self class]
返回的是self
自身即类对象,且object_getClass(self)
与object_getClass([self class])
等价,且都返回的是元类对象,不同于类对象;
动态方法解析,一般用于@dynamic
属性,且需要指定已经实现了的处理方法,如上description
及name
;
备用接收者
如果动态方法无法解析,runtime
会继续调用如下方法:
- (id)forwardingTargetForSelector:(SEL)aSelector
方法解析:
如果对象实现(或者继承)此方法,并且返回非
nil
,则消息会转发至新的接收者;返回的对象不能为self
,会导致无限循环;如果在非根类对象实现此方法且未返回任何内容(即未指定新的接收者),应该调用
[super forwardingTargetForSelector]
将其转发给父类,以此往上继承调用;该方法无法对消息进行处理,如操作消息的参数和返回值;
该方法适合将消息转发给能处理该消息的接收者,如实现多继承;
使用如下:
+ (id)forwardingTargetForSelector:(SEL)aSelector {
if(aSelector == @selector(xxx)) {
return NSClassFromString(@"Class name");
}
return [super forwardingTargetForSelector:aSelector];
}
完整消息转发
当动态方法不能解析且未转发给其他接收者时,runtime
就启动完整消息转发机制:通过调用如下方法来转发给其他接收者,并且可以修改该消息;
- (void)forwardInvocation:(NSInvocation *)invocation;
方法解析如下:
NSObject
对象实现了该方法,但只是调用doesNotRecognizeSelector:
方法,若继承对象未实现该方法(前提是未动态解析方法或转发备用接收者),就会抛出异常;该消息的唯一参数为
NSInvocation
类型的对象,该对象封装了原始的消息及消息参数;该参数来源于methodSignatureForSelector:
方法返回的方法签名,该方法必须被重写,否则会抛出异常;
forwardInvocation:
方法就像一个不能识别的消息的分发中心,将这些消息转发给不同接收对象。或者它也可以象一个运输站将所有的消息都发送给同一个接收对象。它可以将一个消息翻译成另外一个消息,或者简单的”吃掉“某些消息,因此没有响应也没有错误。forwardInvocation:
方法也可以对不同的消息提供同样的响应,这一切都取决于方法的具体实现。该方法所提供是将不同的对象链接到消息链的能力。
使用如下:
- (void)forwardInvocation:(NSInvocation *)invocation
{
SEL aSelector = [invocation selector];
if ([friend respondsToSelector:aSelector])
[invocation invokeWithTarget:friend];
else
[super forwardInvocation:invocation];
}
转发与多继承
objective-c
只支持单继承,但是通过消息转发机制可以实现“多继承”的效果;如上图所示,Warrior
和Diplomat
没有继承关系,但是Warrior
将negotiate
消息转发给了Diplomat
后,就好似Diplomat
是Warrior
的超类一样;
注意:
forwardInvocation:
方法只有在消息接收对象中无法正常响应消息时才会被调用。 所以,如果我们希望一个对象将negotiate
消息转发给其它对象,则这个对象不能有negotiate
方法。否则,forwardInvocation:
将不可能会被调用。
转发与继承
不过消息转发虽然类似于继承,但NSObject的一些方法还是能区分两者。如respondsToSelector:
和isKindOfClass:
只能用于继承体系,而不能用于转发链。便如果我们想让这种消息转发看起来像是继承,则可以重写这些方法,如以下代码所示:
- (BOOL)respondsToSelector:(SEL)aSelector
{
if ( [super respondsToSelector:aSelector])
return YES;
else {
/* Here, test whether the aSelector message can *
* be forwarded to another object and whether that *
* object can respond to it. Return YES if it can. */
}
return NO;
}
方法调配 Method Swizzling
Method Swizzling
方法调配技术是苹果的“黑魔法”,可以不用继承或者重写方法,就可以修改类的方法实现;
常用的方法如下:
Method class_getInstanceMethod(Class cls, SEL name);
void method_exchangeImplementations(Method m1, Method m2);
具体使用如下:
#import <objc/runtime.h>
@implementation UIViewController (Tracking)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
// When swizzling a class method, use the following:
// Class class = object_getClass((id)self);
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xxx_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
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
针对使用的答疑如下:
-
为啥使用
load
方法+load
方法是类初始加载时(应用启动就会加载)调用且只调用一次,load方法按照父类到子类,类自身到Category的顺序被调用,且若都实现了该方法,都会被调用;不同于+initialize
方法,该方法是在第一次调用类方法或者实例方法是调用,有可能被调用多次(若子类未实现此方法或者子类调用[super initialize]
都会导致被调用多次);注意:在方法中不能调用
[super load]
,因此+load
方法在父类、子类是被分别调用的,且存在顺序;如果调用,若父类已经交换IMP,导致又被交换回来,进而失效; -
是否需要
dispatch_once
+load
方法系统会自动调用且只调用一次,但不保证被手动调用,为防止被多次调用并发问题,建议添加dispatch_once
来保证唯一性; -
为啥调用
class_addMethod
,直接调用method_exchangeImplementations
交换方法不就行了class_getInstanceMethod
返回的可能是父类的实现,即子类未实现被交换的方法,导致父类的实现指向了子类交换的方法,进而导致父类调用被交换的方法(交换后实际调用的是子类交换的方法)引发崩溃;对于该方法调用不同场景的影响,可见Runtime Method Swizzling 实战
-
xxx_viewWillAppear
方法中调用[self xxx_viewWillAppear:animated]
是否会造成死循环不会,
[self xxx_viewWillAppear:animated]
实际调用的被交换的方法viewWillAppear:
;
对于不同类方法交换的场景,可见Method Swizzling的各种姿势;
Method Swizzling
方法调配使用的场景如下:
实现AOP面向切面编程;
实现埋点统计,如用户行为统计等,见iOS动态性(二)可复用而且高度解耦的用户统计埋点实现
实现异常保护,如避免NSArray数组越界;
AOP 面向切面编程
AOP(Aspect Oriented Program)
面向切面编程,如下是百度百科及维基百科的解释:
面向切面的程序设计(Aspect-oriented programming,AOP,又译作面向方面的程序设计、剖面导向程序设计)是计算机科学中的一种程序设计思想,旨在将横切关注点与业务主体进行进一步分离,以提高程序代码的模块化程度。通过在现有代码基础上增加额外的通知(Advice)机制,能够对被声明为“切点(Pointcut)”的代码块进行统一管理与装饰,如“对所有方法名以‘set*’开头的方法添加后台日志”。该思想使得开发人员能够将与代码核心业务逻辑关系不那么密切的功能(如日志功能)添加至程序中,同时又不降低业务代码的可读性。面向切面的程序设计思想也是面向切面软件开发的基础。
百度百科:
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
主要目的:将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
理解:
横切关注点不同于主业务逻辑代码,应该是主业务逻辑代码中的非业务逻辑代码通用部分,如主业务逻辑中添加行为统计,“行为统计”就是横切关注点,将其抽离出来;主业务逻辑(可能是多个业务模块)需要横切关注点时可统一插入到主业务逻辑中,而不影响主业务逻辑。
典型案例就是日志记录,因为日志功能往往横跨系统中的每个业务模块,即“横切”所有有日志需求的类及方法体。
针对ios
的面向切面编程就是Method Swizzling
黑魔法,可以在不改变原有代码(或者函数)逻辑上,添加非业务逻辑,如添加行为日志记录及上报;
在 Objective-C 的实现结构中 Runtime 的动态派发机制保证了这么语言的灵活性,而在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是AOP(面向切面编程)。
Method Swizzling
如上所述,不做阐述,不过有人通过该技术实现了优秀的AOP
库,如Aspects
;
Aspects
Aspects 就是一个不错的 AOP 库,封装了 Runtime , Method Swizzling 这些黑色技巧,只提供两个简单的API:
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
使用举例如下:
- viewWillAppear`中添加日志
[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) {
NSLog(@"View Controller %@ will appear animated: %tu", aspectInfo.instance, animated);
} error:NULL];
-
调试查看点击状态
[_singleTapGesture aspect_hookSelector:@selector(setState:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) { NSLog(@"%@: %@", aspectInfo.instance, aspectInfo.arguments); } error:NULL];
-
为系统类添加处理程序,如
UIVieweController
,官方demo:@implementation UIViewController (DismissActionHook) // Will add a dismiss action once the controller gets dismissed. - (void)pspdf_addWillDismissAction:(void (^)(void))action { PSPDFAssert(action != NULL); [self aspect_hookSelector:@selector(viewWillDisappear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) { if ([aspectInfo.instance isBeingDismissed]) { action(); } } error:NULL]; } @end
isa swizzling
isa swizzling
是KVO(键值观察)
机制的实现技术,其通过修改object
对象的isa
指针指向生成的中间代理类NSKVONotifying_xxx(官方的类名称)
,NSKVONotifying_xxx
的super_class
指针指向原有的观察类对象object class
;
NSKVONotifying_xxx
生成的中间类,重写被观察的对象四个方法:class
,setter
,dealloc
,_isKVOA
;
重写setter
重写class
方法目的是让被观察者对象调用[object class]
时返回的原有的类实例;
官方文档上对于KVO的实现的最后,给出了需要我们注意的一点是,永远不要用用isa来判断一个类的继承关系,而是应该用class方法来判断类的实例。
重写setter
重写setter
方法目的是能监听到被观察者调用属性设置方法,如setXxxx(Xxxx为属性名称)
或者调用setValude:forKey:
时,能添加通知消息方法:
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
在didChangValueForKey:
中调用观察者必须重写的方法:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
因此,若- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
方法生效,即观察者能通过上述方法受到属性被改变的通知,则需要满足如下:
-
自动通知,
NSObject
实现了自动通知的方法;- 存在
setter
访问器方法,并且通过setter
方法或者self.xxx
间接调用setter
方法,则中间类会添加will/didChangeValueForKey:
触发事件通知; - 不存在
setter
访问器方法,需要通过setValude:forKey:
方法来修改属性,中间类会添加will/didChangeValueForKey:
触发事件通知; - 对于集合类,如NSMutalArray,需要通过
mutableArrayValueForKey
来获取中间代理类,触发通知,否则直接通过addObject:
无法收到通知; - 对于存在依赖关系的属性,具体可查看官方文档;
- 存在
手动通知:手动通知提供了更自由的方式去决定什么时间,什么方式去通知观察者。这可以帮助你最少限度触发不必要的通知,或者一组改变值发出一个通知,想要使用手动通知必须实现
automaticallyNotifies-ObserversForKey:
方法;并且手动调用will/didChangeValueForKey:
来触发通知;
重写dealloc
用来销毁新生成的NSKVONotifying_
类;
重写_isKVOA方法
这个私有方法估计可能是用来标示该类是一个 KVO 机制声称的类。
YYModel
具体的实现机制可参考YYModel
对于setter
封装:
/// 获取监控的属性
objc_property_t getKVOProperty(Class cls, NSString *keyPath) {
if (!keyPath || !cls) {
return NULL;
}
objc_property_t res = NULL;
unsigned int count = 0;
const char *property_name = keyPath.UTF8String;
objc_property_t *properties = class_copyPropertyList(cls, &count);
for (unsigned int idx = 0; idx < count; idx++) {
objc_property_t property = properties[idx];
if (strcmp(property_name, property_getName(property)) == 0) {
res = property;
break;
}
}
free(properties);
return res;
}
/// 检测属性是否存在setter方法
BOOL ifPropertyHasSetter(objc_property_t property) {
BOOL res = NO;
unsigned int attrCount;
objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);
for (unsigned int idx = 0; idx < attrCount; idx++) {
if (attrs[idx].name[0] == 'S') {
res = YES;
}
}
free(attrs);
return res;
}
/// 获取属性的数据类型
YYEncodingType getPropertyType(objc_property_t) {
unsigned int attrCount;
YYEncodingType type = YYEncodingTypeUnknown;
objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);
for (unsigned int idx = 0; idx < attrCount; idx++) {
if (attrs[idx].name[0] == 'T') {
type = YYEncodingGetType(attrs[idx].value);
}
}
free(attrs);
return type;
}
/// 根据setter名称获取属性名
NSString *getPropertyNameFromSelector(SEL selector) {
NSString *selName = [NSStringFromSelector(selector) substringFromIndex: 3];
NSString *firstAlpha = [[selName substringToIndex: 1] lowercaseString];
return [selName stringByReplacingCharactersInRange: NSMakeRange(0, 1) withString: firstAlpha];
}
/// 根据属性名获取setter名称
SEL getSetterFromKeyPath(NSString *keyPath) {
NSString *firstAlpha = [[keyPath substringToIndex: 1] uppercaseString];
NSString *selName = [NSString stringWithFormat: @"set%@", [keyPath stringByReplacingCharactersInRange: NSMakeRange(0, 1) withString: firstAlpha]];
return NSSelectorFromString(selName);
}
/// 设置bool属性的kvo setter
static void setBoolVal(id self, SEL _cmd, BOOL val) {
NSString *name = getPropertyNameFromSelector(_cmd);
void (*objc_msgSendKVO)(void *, SEL, NSString *) = (void *)objc_msgSend;
void (*objc_msgSendSuperKVO)(void *, SEL, BOOL) = (void *)objc_msgSendSuper;
objc_msgSendKVO(self, @selector(willChangeValueForKey:), val);
objc_msgSendSuperKVO(self, _cmd, val);
objc_msgSendKVO(self, @selector(didChangeValueForKey:), val);
}
/// KVO实现
static void addObserver(id observedObj, id observer, NSString *keyPath) {
objc_property_t observedProperty = getKVOProperty([observedObj class], keyPath);
if (!ifPropertyHasSetter(observedProperty)) {
return;
}
NSString *kvoClassName = [@"SLObserved_" stringByAppendString: NSStringFromClass([observedObj class])];
Class kvoClass = NSClassFromString(kvoClassName);
if (!kvoClass)) {
kvoClass = objc_allocateClassPair([observedObj class], kvoClassName.UTF8String, NULL);
Class(^classBlock)(id) = ^Class(id self) {
return class_getSuperclass([self class]);
};
class_addMethod(kvoClass, @selector(class), imp_implementationWithBlock(classBlock), method_getTypeEncoding(class_getMethodImplementation([observedObj class], @selector(class))));
objc_registerClassPair(kvoClass);
}
YYEncodingType type = getPropertyType(observedProperty);
SEL setter = getSetterFromKeyPath(observedProperty);
switch (type) {
case YYEncodingTypeBool: {
class_addMethod(kvoClass, setter, (IMP)setBoolVal, method_getTypeEncoding(class_getMethodImplementation([observedObj class], setter)));
} break;
......
}
}
其中实现点包含了TypeCode键值编码
,runtime
中创建类、消息发送、获取属性列表、添加类对象方法等知识点;
YYModel 源码
神经病院 Objective-C Runtime 出院第三天——如何正确使用 Runtime
Key-Value Observing Programming Guide
概念及数据结构
id
/// A pointer to an instance of a class.
typedef struct objc_object *id;
//objc-private.h
struct objc_object {
private:
isa_t isa;
public:
// ISA() assumes this is NOT a tagged pointer object
Class ISA();
// getIsa() allows this to be a tagged pointer object
Class getIsa();
... 此处省略其他方法声明
}
//objc.h
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
指向类实例的指针;
objc_object
结构体包含一个isa
指针,类型为isa_t
联合体。根据isa
就可以顺藤摸瓜找到对象所属的类。isa
这里还涉及到 tagged pointer 等概念。因为isa_t
使用union
实现,所以可能表示多种形态,既可以当成是指针,也可以存储标志位。有关isa_t
联合体的更多内容可以查看 Objective-C 引用计数原理。PS:
isa
指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用class
方法来确定实例对象的类。因为KVO的实现机理就是将被观察对象的isa
指针指向一个中间类而不是真实的类,这是一种叫做 isa-swizzling 的技术,详见官方文档
SEL
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
称为方法选择器,是一个方法selector
的指针,用于类查找方法列表中的对应的IMP
;
在objc
源码中未找到objc_selector
结构体的定义,objc
在编译时会根据方法的名字及参数列表,生成一个整型标识,这个标识就是SEL
;本质上SEL
是一个指向方法的指针(准确的说,只是一个根据方法名hash
化了的KEY
值,能唯一代表一个方法,不同类查找的方法列表不同,因此不会导致类方法指向同一个IMP
),为了加快方法的查询速度;
不同类中相同方法名及参数名的SEL
相同,即使参数的类型不同,因此在一个类中方法名及参数名相同但参数类型不同的方法编译错误,objc
使用了大量带参数类型的方法名称,导致objc
方法名都很长;
工程中的所有的
SEL
组成一个Set
集合,Set的特点就是唯一,因此SEL是唯一的。因此,如果我们想到这个方法集合中查找某个方法时,只需要去找到这个方法对应的SEL就行了,SEL实际上就是根据方法名hash
化了的一个字符串,而对于字符串的比较仅仅需要比较他们的地址就可以了,可以说速度上无语伦比!!但是,有一个问题,就是数量增多会增大hash冲突而导致的性能下降(或是没有冲突,因为也可能用的是perfect hash
)。但是不管使用什么样的方法加速,如果能够将总量减少(多个方法可能对应同一个SEL
),那将是最犀利的方法。那么,我们就不难理解,为什么SEL
仅仅是函数名了。
可在运行时添加新的selector
,也可以通过如下三种方法获取方法的SEL
:
-
sel_registerName
函数 - Objective-C编译器提供的
@selector()
-
NSSelectorFromString()
方法
IMP
IMP
在objc.h
中的定义是:
typedef void (*IMP)(void /* id, SEL, ... */ );
它就是一个函数指针,这是由编译器生成的。第一个参数为self
的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则指向元类的指针);第二个SEL
为方法选择器;接下来就是参数列表;
Class
Class
其实是一个指向 objc_class
结构体的指针:
typedef struct objc_class *Class;
//objc/objc-private.h定义
struct objc_class : objc_object {
// Class ISA; //add, 指向类对象的指针
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
... 省略其他方法
}
// objc/runtime.h定义
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY; //注意此处,类自身存在指向类对象的指针,称为元类
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父类
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; // 方法缓存
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表
#endif
} OBJC2_UNAVAILABLE;
objc_class
继承自objc_object
,因此类本身也是类对象,为了处理类本身及类对象的关系,runtime
创建了元类(Meta-Class),类对象所属的类型叫做元类,用来标识类对象具备的元数据。类方法(可以理解为类对象的实例方法,区别于类本身的实例方法)就定义在类对象中,每个类仅有一个类对象,每个类对象仅有一个与之相关的元类,所有的元类最终指向根元类(即为NSObject
),最终根元类的isa指针指向自己,见下图,如[NSObject alloc]消息发送是,会在类对象中查询能够响应消息的方法。
同时,可以看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议;
图中类实例的
isa
指向类本身,类本身的isa
指向元类,元类的isa
指向根元类Root class
,根元类的isa
指向自身;其他的super_class
指向父类的类本身及元类,最终父类指向nil
;实例对象
typedef struct objc_object *id;
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
实例对象是我们对类对象alloc或者new操作时所创建的,在这个过程中会拷贝实例所属类的成员变量并初始化,其中isa指针也会被初始化,让对象可以访问类及类的继承体系,但并不拷贝类定义的方法。调用实例方法时,系统会根据实例的isa
指针去类的方法列表及父类的方法列表中寻找与消息对应的selector指向的方法。
实例对象即id
类型,其是一个objc_object结构类型的指针。该类型的对象可以转换为任何一种对象,类似于C语言中void *指针类型的作用;
类对象
类对象即类本身,即实例对象的isa
指向的地址。类对象存储着类的成员变量、缓存及实例方法列表,但不存储类方法;
元类对象
元类对象即类对象的isa
指向的地址,存储着类方法,其isa
指向根元类(Metal-Class,NSObject),根元类的isa
指向自身;
cache
接收者收到消息,优先去cache
缓存中查找,如果没有就通过isa
指针去类本身的实例方法列表中methodLists
查找,提升查找速度;
该字段指向struct objc_cache
的结构体,具体如下:
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method _Nullable buckets[1] OBJC2_UNAVAILABLE;
};
-
mask
:一个整数,指定分配的缓存bucket
的总数。在方法查找过程中,Objective-C runtime使用这个字段来确定开始线性查找数组的索引位置。指向方法selector
的指针与该字段做一个AND
位操作(index = (mask & selector))
。这可以作为一个简单的hash
散列算法。
-
occupied
:一个整数,指定实际占用的缓存bucket
的总数。buckets
:指向Method
数据结构指针的数组。这个数组可能包含不超过mask+1
个元素。需要注意的是,指针可能是NULL,表示这个缓存bucket
没有被占用,另外被占用的bucket
可能是不连续的。这个数组可能会随着时间而增长。
Method
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
struct objc_method_list {
struct objc_method_list * _Nullable obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}
对于struct objc_method
结构体描述如下:
- 方法名类型为
SEL
,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同; - 方法类型
types
是个char
指针,其实存储着方法的参数类型和返回值类型; -
imp
指向了方法的实现,本质上是一个函数指针;
相关的runtime
调动方法如下:
// 调用指定方法的实现
id method_invoke ( id receiver, Method m, ... );
// 调用返回一个数据结构的方法的实现
void method_invoke_stret ( id receiver, Method m, ... );
// 获取方法名
SEL method_getName ( Method m );
// 返回方法的实现
IMP method_getImplementation ( Method m );
// 获取描述方法参数和返回值类型的字符串
const char * method_getTypeEncoding ( Method m );
// 获取方法的返回值类型的字符串
char * method_copyReturnType ( Method m );
// 获取方法的指定位置参数的类型字符串
char * method_copyArgumentType ( Method m, unsigned int index );
// 通过引用返回方法的返回值类型字符串
void method_getReturnType ( Method m, char *dst, size_t dst_len );
// 返回方法的参数的个数
unsigned int method_getNumberOfArguments ( Method m );
// 通过引用返回方法指定位置参数的类型字符串
void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len );
// 返回指定方法的方法描述结构体
struct objc_method_description * method_getDescription ( Method m );
// 设置方法的实现
IMP method_setImplementation ( Method m, IMP imp );
// 交换两个方法的实现
void method_exchangeImplementations ( Method m1, Method m2 );
Ivar
typedef struct objc_ivar *Ivar;
struct objc_ivar {
char * _Nullable ivar_name OBJC2_UNAVAILABLE;
char * _Nullable ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
struct objc_ivar_list {
int ivar_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
}
Ivar
是类中实例变量的类型,可以根据实例来查找类中的名字,也称“反射”;
成员变量的方法调用如下:
// 获取成员变量名
const char * ivar_getName ( Ivar v );
// 获取成员变量类型编码
const char * ivar_getTypeEncoding ( Ivar v );
// 获取成员变量的偏移量
ptrdiff_t ivar_getOffset ( Ivar v );
ivar_getOffset函数,对于类型
id或其它对象类型的实例变量,可以调用
object_getIvar和
object_setIvar`来直接访问成员变量,而不使用偏移量;
类成员变量支持权限控制:
- @protected是受保护的,只能在本类及其子类中访问,在{}声明的变量默认是@protect;
- @private是私有的,只能在本类访问;
- @public公开的,可以被在任何地方访问;
objc_property_t**
objc_property_t
是表示Objective-C声明的属性的类型,其实际是指向objc_property
结构体的指针,其定义如下:
typedef struct objc_property *objc_property_t;
属性相关的方法如下:
// 获取属性名
const char * property_getName ( objc_property_t property );
// 获取属性特性描述字符串
const char * property_getAttributes ( objc_property_t property );
// 获取属性中指定的特性
char * property_copyAttributeValue ( objc_property_t property, const char *attributeName );
// 获取属性的特性列表
objc_property_attribute_t * property_copyAttributeList ( objc_property_t property, unsigned int *outCount );
-
property_copyAttributeValue
函数,返回的char *
在使用完后需要调用free()
释放。 -
property_copyAttributeList
函数,返回值在使用完后需要调用free()
释放。
objc_property_attribute_t**
objc_property_attribute_t
定义了属性的特性(attribute
),它是一个结构体,定义如下:
typedef struct {
const char *name; // 特性名
const char *value; // 特性值
} objc_property_attribute_t;
protocol协议
struct objc_protocol_list {
struct objc_protocol_list * _Nullable next;
long count;
__unsafe_unretained Protocol * _Nullable list[1];
};
struct protocol_t : objc_object {
const char *mangledName;
struct protocol_list_t *protocols;
method_list_t *instanceMethods;
method_list_t *classMethods;
method_list_t *optionalInstanceMethods;
method_list_t *optionalClassMethods;
property_list_t *instanceProperties;
uint32_t size; // sizeof(protocol_t)
uint32_t flags;
// Fields below this point are not always present on disk.
const char **_extendedMethodTypes;
const char *_demangledName;
property_list_t *_classProperties;
//省略一些封装的便捷 get 方法
....
};
@interface Protocol : NSObject
@end
Protocol
就是继承自NSObject
的对象,其id
结构体类型为struct protocol_t
;
runtime
提供了一系列关于协议的方法,如下:
// 返回指定的协议
Protocol * objc_getProtocol ( const char *name );
// 获取运行时所知道的所有协议的数组
Protocol ** objc_copyProtocolList ( unsigned int *outCount );
// 创建新的协议实例
Protocol * objc_allocateProtocol ( const char *name );
// 在运行时中注册新创建的协议
void objc_registerProtocol ( Protocol *proto );
// 为协议添加方法
void protocol_addMethodDescription ( Protocol *proto, SEL name, const char *types, BOOL isRequiredMethod, BOOL isInstanceMethod );
// 添加一个已注册的协议到协议中
void protocol_addProtocol ( Protocol *proto, Protocol *addition );
// 为协议添加属性
void protocol_addProperty ( Protocol *proto, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount, BOOL isRequiredProperty, BOOL isInstanceProperty );
// 返回协议名
const char * protocol_getName ( Protocol *p );
// 测试两个协议是否相等
BOOL protocol_isEqual ( Protocol *proto, Protocol *other );
// 获取协议中指定条件的方法的方法描述数组
struct objc_method_description * protocol_copyMethodDescriptionList ( Protocol *p, BOOL isRequiredMethod, BOOL isInstanceMethod, unsigned int *outCount );
// 获取协议中指定方法的方法描述
struct objc_method_description protocol_getMethodDescription ( Protocol *p, SEL aSel, BOOL isRequiredMethod, BOOL isInstanceMethod );
// 获取协议中的属性列表
objc_property_t * protocol_copyPropertyList ( Protocol *proto, unsigned int *outCount );
// 获取协议的指定属性
objc_property_t protocol_getProperty ( Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty );
// 获取协议采用的协议
Protocol ** protocol_copyProtocolList ( Protocol *proto, unsigned int *outCount );
// 查看协议是否采用了另一个协议
BOOL protocol_conformsToProtocol ( Protocol *proto, Protocol *other );
-
objc_getProtocol
函数,需要注意的是如果仅仅是声明了一个协议,而未在任何类中实现这个协议,则该函数返回的是nil。 -
objc_copyProtocolList
函数,获取到的数组需要使用free来释放 -
objc_allocateProtocol
函数,如果同名的协议已经存在,则返回nil -
objc_registerProtocol
函数,创建一个新的协议后,必须调用该函数以在运行时中注册新的协议。协议注册后便可以使用,但不能再做修改,即注册完后不能再向协议添加方法或协议
需要强调的是,协议一旦注册后就不可再修改,即无法再通过调用protocol_addMethodDescription
、 protocol_addProtocol
和protocol_addProperty
往协议中添加方法等。
category类别
/// An opaque type that represents a category.
typedef struct objc_category *Category;
struct objc_category {
char * _Nonnull category_name OBJC2_UNAVAILABLE;
char * _Nonnull class_name OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable instance_methods OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable class_methods OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
}
这个结构体主要包含了分类定义的实例方法与类方法,其中instance_methods
列表是objc_class
中方法列表的一个子集,而class_methods
列表是元类方法列表的一个子集。
Runtime
并没有在<runtime.h>
头文件中提供针对分类的操作函数。因为这些分类中的信息都包含在objc_class
中,我们可以通过针对objc_class
的操作函数来获取分类的信息;
可以通过类别增加类的方法,但不能通过类别增加实例变量,不过objc提供了解决方案--关联对象(Associated Object);
关联对象
关联对象
类似字典,通过key
关键词类设定类中的成员变量并关联相关的对象,并通过key
来获取关联的对象,不过需要手动指定内存策略,来告知runtime
如何管理关联的对象,具体的内存策略如下:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
* The association is not made atomically. */
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.
* The association is not made atomically. */
OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.
* The association is made atomically. */
OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.
* The association is made atomically. */
};
具体的<runtime.h>
定义的方法如下:
//若value为nil,则移除指定的存在的关联对象
void objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
id _Nullable value, objc_AssociationPolicy policy);
id _Nullable objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key);
//移除所有的关联对象
void objc_removeAssociatedObjects(id _Nonnull object);
typedef void (*objc_hook_setAssociatedObject)(id _Nonnull object, const void * _Nonnull key,
id _Nullable value, objc_AssociationPolicy policy);
void objc_setHook_setAssociatedObject(objc_hook_setAssociatedObject _Nonnull newValue,
objc_hook_setAssociatedObject _Nullable * _Nonnull outOldValue)
关联对象使用起来并不复杂,它让我们可以动态地增强类现有的功能,如关联UIAlertView
点击按钮关联响应的block
,在按钮点击代理实现中直接获取关联的按钮block
执行;
参考资料
Objective-C Runtime Programming Guide
Objective-C Runtime 运行时之一:类与对象
Objective-C Runtime 运行时之二:成员变量与属性
Objective-C Runtime 运行时之三:方法与消息
Objective-C Runtime 运行时之四:Method Swizzling