苹果官网的objc的源码可以直接下载查看,我在文档中分析使用的源码是objc4-818.2.tar
一、Class
根据苹果开源的objc的源码来探索对象和类的一些底层逻辑。
从NSObject中头文件中我们知道包含一个成员变量Class isa
。
从objc源代码中的objc.h文件下,可以看到Class的定义:
typedef struct objc_class *Class; // Class是objc_class结构体指针
// objc_object结构体的定义,包含一个Class类型的指针变量isa
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
// id类型实际是objc_object结构体类型的指针
typedef struct objc_object *id;
可以看出Class本质是objc_class结构体的指针。源码runtime.h文件中可以看到objc_class的结构体定义:
struct objc_class {
Class _Nonnull isa; // 一个objc_class类型的指针
#if !__OBJC2__
Class _Nullable super_class; // 指向父类,如果是根类,则为NULL
const char * _Nonnull name; // 类的名称,C字符串
long version; //类版本信息,默认初始化为0
long info; // 供运行期使用的位标识
long instance_size; // 该类的实例变量大小
struct objc_ivar_list * _Nullable ivars; // 存储每个实例变量的内存地址
struct objc_method_list * _Nullable * _Nullable methodLists; //方法链表,存储实例方法
struct objc_cache * _Nonnull cache; //方法缓存,缓存最近使用的方法来提高效率
struct objc_protocol_list * _Nullable protocols; //协议链表,存储该类遵循的正式协议
#endif
};
/* Use `Class` instead of `struct objc_class *` */
objc_class结构体成员详细说明:
- isa: 是一个 objc_class 类型的指针。objc_class可以当作一个objc_object来对待,看objc_object结构体也含有objc_class结构体的指针,其实在Objective-C里面区分类对象和实例对象,objc_class其实也就是类对象。在Objective-C中,类对象中的isa指向类结构被称作metaclass,metaclass存储类的static类成员变量和static类成员方法(类方法);实例对象中的isa指向类结构称作class(普通类),class结构存储类的普通成员变量和普通成员方法(实例方法)。
- super_class: 是该类的父类,如果是根类,则为NULL。
- name: C字符串,指示类的名称。在运行期间,可以通过名称来查找该类,id objc_getClass(const char aClassName)或该类的 metaclass(id objc_getMetaClass(const char aClassName))
- version:类的版本信息,默认初始化为 0。在运行期可以对其进行修改(class_setVersion)或获
取(class_getVersion)。
- info:供运行期间使用的位标识别。如下面一些掩码:
- CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含实例方法和变量;
- CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
- CLS_INITIALIZED (0x4L) 表示该类已经被运行期初始化了,这个标识位只被 objc_addClass 所设置;
- CLS_POSING (0x8L) 表示该类被 pose 成其他的类;(poseclass 在 ObjC 2.0 中被废弃了);
- CLS_MAPPED (0x10L) 为 ObjC 运行期所使用
- CLS_FLUSH_CACHE (0x20L) 为 ObjC 运行期所使用
- CLS_GROW_CACHE (0x40L) 为 ObjC 运行期所使用
- CLS_NEED_BIND (0x80L) 为 ObjC 运行期所使用
- CLS_METHOD_ARRAY (0x100L) 该标志位指示 methodlists 是指向一个 objc_method_list 还是
一个包含 objc_method_list 指针的数组;
- instance_size:该类的实例变量大小(包括从父类继承下来的实例变量)
- ivars: 指向 objc_ivar_list 的指针,存储每个实例变量的内存地址,如果该类没有实例变量则为NULL;
- methodLists: 与 info 的一些标志位有关,CLS_METHOD_ARRAY 标识位决定其指向的东西(是指
向单个 objc_method_list 还是一个 objc_method_list 指针数组),如果 info 设置了 CLS_CLASS 则 objc_method_list 存储实例方法,如果设置的是 CLS_META 则存储类方法;
- cache:指向 objc_cache 的指针,用来缓存最近使用的方法,以ᨀ高效率;
- protocols:指向 objc_protocol_list 的指针,存储该类声明要遵守的正式协议。
子类,父类,根类(这些都是普通 class)以及其对应的 metaclass 的 isa 与 super_class 之间关系:
- 类的实例对象的 isa 指向该类;该类的 isa 指向该类的 metaclass;
- 类的 super_class 指向其父类,如果该类为根类则值为 NULL;
- metaclass 的 isa 指向根 metaclass,如果该 metaclass 是根 metaclass 则指向自身;
- metaclass 的 super_class 指向父 metaclass,如果该 metaclass 是根 metaclass 则指向
该 metaclass 对应的类;
关系图如下:
class 与 metaclass的区别
class 是 instance object 的类类型。当我们向实例对象发送消息(实例方法)时,我们在该实例对象的
class 结构的 methodlists 中去查找响应的函数,如果没找到匹配的响应函数则在该 class 的父类中的
methodlists 去查找(查找链为上图的中间那一排)。如下面的代码中,向 str 实例对象发送
lowercaseString 消息,会在 NSString 类结构的 methodlists 中去查找 lowercaseString 的响应
函数。
NSString * str;
[str lowercaseString];
metaclass 是 class object 的类类型。当我们向类对象发送消息(类方法)时,我们在该类对象的
metaclass 结构的 methodlists 中去查找响应的函数,如果没有找到匹配的响应函数则在该
metaclass 的父类中的 methodlists 去查找(查找链为上图的最右边那一排)。如下面的代码中,向
NSString 类对象发送 stringWithString 消息,会在 NSString 的 metaclass 类结构的
methodlists 中去查找 stringWithString 的响应函数。
[NSString stringWithString:@"str"];
什么是元类可以参考这篇文章 What is a meta-class in Objective-C?
译文 : 什么是Objective-C中的元类
关于runtime更多底层原理的文章 Objective-C Runtime
——比较好的分析isa和Class,但是与最新源码于文档中的objc源码不太一致,主要是版本差异————
神经病院Objective-C Runtime入院第一天——isa和Class
———————————END—————————————————————————-
二、消息发送/消息转发
1、SEL,IMP
SEL:表示一个指向objc_selector指针,表示方法的名字/签名
typedef struct objc_selector *SEL;
IMP:是一个函数指针,这个被指向的函数包含一个接收消息的对象id,调用方法的SEL,以及不定参数。
typedef void (*IMP)(void /* id, SEL, ... */ );
Method:
typedef struct objc_method *Method;
objc_method结构体实现:
struct objc_method {
SEL _Nonnull method_name; //方法名称
char * _Nullable method_types; //参数类型
IMP _Nonnull method_imp ; //方法具体实现的函数指针
}
关于objc中的方法更加深入的探析,可以查看深入解析 ObjC 中方法的结构
**
2、消息发送
消息发送(Messaging)是 Runtime 通过 selector 快速查找 IMP 的过程,有了函数指针就可以执行对应的方法实现;
探究方法调用的过程,先看如下代码:
Person *p = [Person new];
[p startWork];
clang编译成底层代码之后
Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("new"));
// 调用方法
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("startWork"));
编译器会将方法调用转化为对消息函数objc_msgSend的调用,objc_msgSend具有两个参数:
- 消息接收者id
- 消息对应的方法选标SEL,同时接受消息中的任意参数
objc_msgSend做的动态绑定的工作流程
- 首先,它找到SEL对应的方法实现IMP。
- 然后,将消息接受者对象(指向消息接受者对象的指针)以及方法中指定的参数传递给方法实现IMP。
- 最后,将方法实现的返回值作为函数的返回值返回。
在方法中可以通过 self 来引用消息接收者对象,通过选标_cmd 来引用方法本身。
**
查看objc-runtime-new.mm中对方法的查找过程:
/***********************************************************************
* lookupMethodInClassAndLoadCache.
* 类似lookUpImpOrForward,但不搜索superclasses
* 如果在类中找不到该方法,则缓存并返回objc_msgForward
**********************************************************************/
IMP lookupMethodInClassAndLoadCache(Class cls, SEL sel)
{
IMP imp;
// fixme this is incomplete - no resolver, +initialize -
// but it's only used for .cxx_construct/destruct so we don't care
ASSERT(sel == SEL_cxx_construct || sel == SEL_cxx_destruct);
// 优先在缓存中查找
//
// 如果用于查询的缓存是预先优化的,
// 我们要求在缓存未命中时返回_objc_msgForward_impcache,
// 因此在运行时加锁的情况下使用isConstantOptimizedCache和调用cache_getImp()避免资源竞争。
//
// 对于动态缓存,如果缺少将返回nil
imp = cache_getImp(cls, sel, _objc_msgForward_impcache);
if (slowpath(imp == nil)) {
// 如果在缓存中没找到,则在该类的方法列表中查找
mutex_locker_t lock(runtimeLock);
if (auto meth = getMethodNoSuper_nolock(cls, sel)) {
// 在方法列表中命中,则缓存它
imp = meth->imp(false);
} else {
imp = _objc_msgForward_impcache;
}
// Note, because we do not hold the runtime lock above
// isConstantOptimizedCache might flip, so we need to double check
if (!cls->cache.isConstantOptimizedCache(true /* strict */)) {
cls->cache.insert(sel, imp, nil);
}
}
return imp;
}
从以上查找代码可以得出在查找方法的过程是:
- 首先在方法缓存中进行查找
- 如果方法缓存中没有查找到,则到该类的方法列表中进行查找,并进行缓存。
- 如果没有查找到,则返回_objc_msgForward_impcache
3、消息转发
消息转发(Message Forwarding)是在查找 IMP 失败后执行一系列转发流程的慢速通道,如果不作转发处理,则会打日志和抛出异常。
在Objective-C中方法调用中,查找selecter失败的情况下,会进行三次调用其他方法来阻止程序崩溃。
1)动态方法解析:看消息接受者是否能动态添加方法
- (BOOL)resolveInstanceMethod:(SEL)selector; //当遇到无法解读的实例方法时调用这个方法
- (BOOL)resolveClassMethod:(SEL)selector; //当遇到无法解读的类方法时调用这个方法
2)备援的接收者:先看其他有没有对象能处理这条消息
- (id)forwardingTargetForSelector:(SEL)aSelector;
3)消息重定向:将所有信息封装到 NSInvocation
对象中处理
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation: (NSInvocation*)invocation;
forwardInvocation
forwardInvocation的作用:
- 决定将消息转发给谁
- 将消息和原来的参数一块转发出去
消息转发的完整流程:
消息转发源码例子:
// 动态方法实现
void dynamicIMP(id self,SEL _cmd) {
NSLog(@">>dynamicIMP");
}
@interface Person ()
@property(nonatomic, strong) OtherPerson *otherTarget;
@end
@implementation Person
- (instancetype)init {
if (self = [super init]) {
self.otherTarget = [[OtherPerson alloc] init];
}
return self;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"--resolveInstanceMethod --%@",NSStringFromSelector(sel));
if (sel == @selector(test)) {
// 动态添加方法实现
// class_addMethod([self class], sel, (IMP)dynamicIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
+ (BOOL)resolveClassMethod:(SEL)sel {
return [super resolveClassMethod:sel];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"forwardingTargetForSelector: %@",NSStringFromSelector(aSelector));
return [super forwardingTargetForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"forwardInvocation");
// 消息转发
if ([self.otherTarget respondsToSelector:[anInvocation selector]]) {
[anInvocation invokeWithTarget:self.otherTarget];
} else {
[super forwardInvocation:anInvocation];
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"methodSignatureForSelector");
return [self.otherTarget methodSignatureForSelector:aSelector];
}
//- (void)test {
//
// NSLog(@"Person test");
//}
@end
详细的内容参考文章 Objective-C 消息发送与转发机制原理
三、KVO、KVC
1、KVO
1)KVO是什么?
KVO 并不是什么新事物,换汤不换药,它只是观察者模式在 Objective C 中的一种运用
基本思想: 一个目标对象管理所有依赖于它的观察者对象,并在它自身的状态改变时主动通知观察者对象。这个主动 通知通常是通过调用各观察者对象所提供的接口方法来实现的。观察者模式较完美地将目标对象与观察者 对象解耦。
注册监听
- (void)addObserver: forKeyPath: options: context:
注册监听了,需要实现监听方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
移除监听
- (void)removeObserver: forKeyPath:
2)KVO实现原理
要实现属性监听,赋值时需用setter方法或key-path设置
底层实现原理:
- 当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类NSKVONotifying_XXXX,在这个派生类中重写基类中任何被观察属性的 setter 方法。派生类重写了:
- getter
- setter
- class
- dealloc
- isKVOA:用来标示该类是一个 KVO 机制声称的类
- 派生类在被重写的 setter 方法实现真正的通知机制,就如前面手动实现键值观察那样。当然前提是要通过遵循KVO的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。
- 同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。
- 然后系统将这个对象的isa指针指向这个新诞生的派生类。因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制
- 派生类还重写了 dealloc 方法来释放资源
手动实现KVO
- 需要手动实现属性的 setter 方法,并在设置操作的前后分别调用
willChangeValueForKey
: 和didChangeValueForKey
方法,这两个方法用于通知系统该key 的属性值即将和已经变更了
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
- 要实现类方法
automaticallyNotifiesObserversForKey
,并在其中设置对该 key 不自动发送通知(返回 NO 即可)。这里要注意,对其它非手动实现的 key,要转交给 super 来处理。
automaticallyNotifiesObserversForKey
3)KVO实践
4)KVO常见问题
a.KVO的一种缺陷(其实不能称为缺陷,应该称为特性)是,当对同一个keypath进行两次removeObserver时会导致程序crash,这种情况常常出现在父类有一个kvo,父类在dealloc中remove了一次,子类又remove了一次的情况下。
2、KVC
1)KVC是什么?
key values coding 键值编码,间接通过字符串对应的key取出、修改其对应的属性。
2)KVC原理
当调用setValue:forKey:的底层实现逻辑:
- 优先调用set
:属性值方法,代码通过setter方法完成设置。注意,这里的 是指成员变量名 - 如果没有找到setName:方法
- KVC机制会检查+ (BOOL)accessInstanceVariablesDirectly方法有没有返回YES。默认该方法会返回YES
- 如果你重写了该方法让其返回NO的话,那么在这一步KVC会执行setValue:forUndefinedKey:方法
- KVC机制会搜索该类里面有没有名为_
的成员变量 - 如果该类即没有set
:方法,也没有_ 成员变量,KVC机制会搜索_is 的成员变量。 - 如果该类即没有set
:方法,也没有_ 和_is 成员变量,KVC机制再会继续搜索 和is 的成员变量 - 上面列出的方法或者成员变量都不存在,系统将会执行该对象的setValue:forUndefinedKey:方法,默认是抛出异常
总结:setName->_name->_isName->name->isName->抛出查找异常
当调用valueForKey:
实现过程:
- 首先按get
, ,is 的顺序方法查找getter方法,找到的话会直接调用。如果是BOOL或者Int等值类型, 会将其包装成一个NSNumber对象。 - 如果上面的getter没有找到,KVC则会查找countOf
,objectIn AtIndex或 AtIndexes格式的方法。如果countOf 方法和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子类),调用这个代理集合的方法,或者说给这个代理集合发送属于NSArray的方法,就会以countOf ,objectIn AtIndex或 AtIndexes这几个方法组合的形式调用。还有一个可选的get :range:方法。 - 如果上面的方法没有找到,那么会同时查找countOf
,enumeratorOf ,memberOf 格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法的代理集合,和上面一样,给这个代理集合发NSSet的消息,就会以countOf ,enumeratorOf ,memberOf 组合的形式调用。 - 如果还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),那么和先前的设值一样,会按_
,_is , ,is 的顺序搜索成员变量名,这里不推荐这么做,因为这样直接访问实例变量破坏了封装性,使代码更脆弱。 - 如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly返回NO的话,那么会直接调用valueForUndefinedKey:
- 还没有找到的话,调用valueForUndefinedKey:
总结:get<Key>,<key>,is<Key>
-> countOf<Key>,objectIn<Key>AtIndex,<Key>AtIndexes
-> countOf<Key>,enumeratorOf<Key>,memberOf<Key>
-> accessInstanceVariablesDirectly
,如果返回YES -> _<key>,_is<Key>,<key>,is<Key>
-> valueForUndefinedKey:
;如果返回NO -> valueForUndefinedKey:
3)KVC实践
使用场景:
- 动态取值/设值
- 用KVC访问或修改私有变量
- model和字典转换
- 修改一些控件的内部属性,例如searchBar
- 用KVC中的函数操作集合
- 简单集合运算符
- @avg
- @count
- @max
- @min
- @sum
- 对象运算符
- @distinctUnionOfObjects : 返回的元素都是唯一的,是去重以后的结果
- @unionOfObjects : 返回的元素是全集
- Array和Set操作符
- @distinctUnionOfArrays:该操作会返回一个数组,这个数组包含不同的对象,不同的对象是在从关键路径到操作器右边的被指定的属性里
- @unionOfArrays: 该操作会返回一个数组,这个数组包含的对象是在从关键路径到操作器右边的被指定的属性里和@distinctUnionOfArrays不一样,重复的对象不会被移除
- @distinctUnionOfSets: 返回不重复的set集合
四、方法交换
五、关联对象
六、Runloop
https://blog.ibireme.com/2015/05/18/runloop/
七、NSNotificationCenter
NSNotificatinonCenter
是使用观察者模式来实现的用于跨层传递消息,用来降低耦合度。NSNotificatinonCenter
用来管理通知,将观察者注册到NSNotificatinonCenter
的通知调度表中,然后发送通知时利用标识符name
和object
识别出调度表中的观察者,然后调用相应的观察者的方法,即传递消息(在Objective-C中对象调用方法,就是传递消息,消息有name或者selector,可以接受参数,而且可能有返回值),如果是基于block
创建的通知就调用NSNotification
的block
。