1. OC对象的本质形式 (一个NSObject对象占用多少内存)
-
OC本质底层都是C,C++代码混合实现的--编译汇编代码--机器代码
-
对象和类结构是基于C和C++中的结构体struct实现的。探究NSObject的本质,OC代码转换为C和C++混合代码。xcode用的编译器前端是clang。
-
因为1个NSObject对象对应1个结构体内只有1个isa指针,指针在iOS64位系统内占8个字节,因此一个NSObject对象在内存里是占用1个指针的大小。类内部的方法和方法的实现存储空间并不在对象内,obj指针就是isa地址其实就是结构体地址就是NSObject对象的地址。
-
只要继承自NSObject对象,结构中肯定会有一个isa指针。
2. 实例对象 class类对象 元类对象(类信息存放在什么地方)
-
instance实例对象:alloc出来的。存放成员变量的值,isa。
-
class类对象 :class类型,[实例对像 class],object_getClass(object1),每一个类在内存中有且只有一个class对象,存放对象方法信息,属性信息,成员变量信息(名字等),协议信息,superClass指针,isa等。
-
metaclass元类对象 :也是class类型,每一个类在内存中有且只有一个元类对象,在内存中和类对象结构一样的,但是用途不一样。object_getClass(类对象),object_getClass([NSObject class]);存放有用信息和class类对象不一样,static类型成员变量,属性可能是空的,存放类方法,superClass指针,isa等。
3. isa和superClass底层指针指向
- instance实例对象:isa指向自己的class类对象
- class类对象:isa指向自己的metaClass元类对象。class类对象中的superClass指针指向父类的类对象
- metaClass元类对象:所有元类的isa最终都指向基类( 如NSObject)的metaClass。metaClass元类中的superClass指针指向父类的metaClass元类对象。
- Tips: 基类的metaClass的superClass指针指向基类的class类对象. (这里之后应该再说一下消息转发机制的)
4. OC消息传递
- 熟悉runtime的都知道,OC的方法调用其实应该叫消息传递,消息传递是动态绑定的机制来决定需要调用的方法;
[person age];
会被翻译为objc_msgSend(person, @selector(age));
。objc_msgSend
查找方法时,会先从Person缓存中查找,找到直接返回 (缓存是存在类中的,每个类都有一份方法缓存struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
)。- 找不到,再去 Class 的方法列表中找。在 objc-runtime-new.mm 文件中有一个函数
lookUpImpOrForward
,这个函数的作用就是无缓存时去查找方法的实现。lookUpImpOrForward
并不是objc_msgSend
直接调用的,而是通过_class_lookupMethodAndLoadCache3
方法。
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls){ return lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/);}复制代码
lookUpImpOrForward属于源代码层级的了,想要具体了解可以直接
具体查找过程
- 在当前类中查找实现:先调用了
cache_getImp
从某个类的cache 属性中获取对应的实现,如果查找到实现,跳转到done。 - 如果没找到缓存。通过当前类instance实例对象的isa找到类对象(class),接着在当前类的class对象内找到方法列表
methodLists
找到对应的Method,最后找到method中的IMP,执行具体实现并添加到缓存。 - 如果在当前类方法列表中没有找到,通过superClass指针找到当前类的父类,在父类中寻找实现。这一操作与上一步基本是一样的,先查找缓存,没找到接着搜索方法列表,添加到缓存。与当前类的区别是,在父类中找到了
_objc_msgForward_impcache
实现会交给当前类处理。
没找到对应的selector?(方法决议+消息转发)
当前类中和父类中都没有找到对应方法处理,系统会提供三次补救机会
- 方法决议(method resolve):动态加载,系统就会调用receiver的
+ (BOOL)resolveInstanceMethod:(SEL)sel {}
(实例方法) 和+ (BOOL)resolveClassMethod:(SEL)sel {}
(类方法)
void myMethod(id self, SEL _cmd,NSString *nub) {NSLog(@"ifelseagexx%@",nub);};+ (BOOL)resolveInstanceMethod:(SEL)sel{ NSLog(@"Method Resolution"); if (sel == @selector(age)) { // 方法没有被实现 class_addMethod([self class], sel, imp_implementationWithBlock(^() { // 实现方法的代码写在这里 }), "v@:"); return YES; } return [super resolveInstanceMethod:sel];}复制代码
我们只需要在resolveInstanceMethod:
方法中,利用class_addMethod
方法,将未实现的 age
绑定到(IMP)myMethod
上。这样就能完成转发,最后返回YES。如果实现了这个方法,系统就会重新启动一次消息发送。
- 在缓存、当前类、父类以及 resolveInstanceMethod: 都没有解决实现查找的问题时,执行第二次:
- (id)forwardingTargetForSelector:(SEL)aSelector {}复制代码
确定是哪个对象处理(找到该对象的方法名与消息中的选择器的方法名一致的方法并调用)这个消息。使用场景一般是将 A 类的某个方法,转发到 B 类的实现中去。
forwardingTargetForSelector:
如果实现这个方法时,返回值为nil或者self即代表不处理消息。执行第三次:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {};- (void)forwardInvocation:(NSInvocation *)anInvocation {};复制代码
第一个要求返回一个方法签名,第二个方法转发具体的实现。二者相互依赖,只有返回了正确的方法签名,才会执行第二个方法。
这次的转发作用和第二次的比较类似,都是将 A 类的某个方法,转发到 B 类的实现中去。不同的是,第三次的转发相对于第二次更加灵活,forwardingTargetForSelector:
只能固定的转发到一个对象;forwardInvocation:
可以让我们转发到多个对象中去。 tips:如果传递走到最后都没有处理,系统就会崩溃并报错:unrecognized selector sent to instance 0x7fea0ac2b0a0
5. KVO的本质
- 当一个对象使用了KVO监听,系统会修改这个对象的isa指针,改为指向一个全新的通过runtime动态创建的子类
NSKVONotifying_class
。 - 子类拥有自己的set方法实现:
原来的seter方法
willChangeValueForKey:
setClass:
didChangeValueForkey:
,(这个方法内部又会调用监听器方法observeValueForKeyPath:ofObject:change:context:
)
- 手动触发KVO
在
addObserver:selector:name:object:
后手动调用willChangeValueForKey:
和didChangeValueForkey:
,可以实现不改变属性值手动触发监听KVO方法。注意:必须will和did一起成对调用,猜测可能didChangeValueForkey
在触发监听方法的时候会检测will方法有没有被调用,进而成功触发observeValueForKeyPath:ofObject:change:context:
。
- 动态创建的子类
NSKVONotifying_class
中其实除了会重写原类的setClass:
方法(不会重写get方法)外,通过class_copyMethodList()
可以发现,class()
dealloc
_isKVOA
会新出现在NSKVONotifying_class
类对象的方法里。因为子类重写了KVO的class方法,[object class]
获取的类对象还是原类对象,object_getClass(object)
获取到的是NSKVONotifying_class
类对象。
6. KVC的本质
KVC的全称是Key-Value Coding,俗称“键值编码”,可以通过一个Key来访问某个属性
- 常见的API:
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- kvc赋值属于runtime层级的直接赋值,通过KVC修改属性也会触发KVO。
- 首先会按照,
setKey:``,_setKey:
顺序寻找方法。如果有找到方法存在会传递参数直接调用这个方法。 - 如果没有找到方法,会查看
accessInstanceVariablesDirectly
方法的返回值。如果返回值为NO,不允许直接返回成员变量,调用setValue:forUndefineKey:
并抛出异常。如果允许会去访问成员变量,如果找到了成员变量会直接赋值(依然会触发KVO,内部做了willChangeValueForKey:
和didChangeValueForkey:
),如果没找到依然会抛出异常错误。
- 首先会按照,