一.NSObjet -C
initialize
runtime发送initialize()消息在一个线程安全的方法,其他线程想发送消息到该类,需等initialize()完成
父类的方法可能被调用多次如果子类没有实现initialize() (测 试:若一级,二级子类都没有实现,父类调用了三次),如果你想保护自己不被多次运行
+ (void)initialize {
if (self == [ClassName self]) {
// ... do the initialization ...
}
}
在initialize()方法中使用锁可能导致死锁,不能依赖initialize()实现复杂的初始化
initialize在每个类中会调用一次,如果像是实现独立 的初始化方法,实现 + laod()
initialize()总是在单线程中运行,可以将这样实现单例
static id sharedManager = nil;
+ (void)initialize {
if (self == [SomeManager class]) {
sharedManager = [[self alloc] init];
}
}
+ (id)sharedManager {
return sharedManager;
}
Load
在自定义的load方法中可以传递消息到其他不相关的类,不过可能其他的类可能还没有运行load()方法,
注意
:在swift中自定义load,桥接到oc时load方法不会自动调用
Init
allocWithZone:已被启用 使用alloc方法初始化一个类的实例的时候,默认是调用了 allocWithZone 的方法。于是覆盖allocWithZone方法的原因已经 很明 显了:为了保持单例类实例的唯一性,需要覆盖所有会生成 新的实例 的方法,
如果有人初始化这个单例类的时候不走[[Class alloc] init] , 直接 allocWithZone, 那么这个单例就不再是单例 了,所以必须把这个方法也堵上
Copy
通过NSCoding协议的copyWithZone:
方法返回对象,如果没有实现copyWithZone:
将会发生异常, NSObject并不支持NSCoding协议,子类必须要遵循NSCoding协议并实现copyWithZone:方法,
copyWithZone:的子方法必须调用super来合并实现,除非这个子类直接继承自NSObject
一个copy对象暗中retain对象,并负责释放它
Dealloc
如果一个被释放的对象的空间没有被使用,发送后续信息给这个对象可能产生错误, 可以重写这个方法处理实例变量以外的资源
在方法中不要调用父类的实现方法,您应该尽量避免使用dealloc来管理有限资源的生命周期,例如文件描述符。 不用主动发送dealloc,而是通过runtime接受 如果不适用ARC,则应该因super作为最后一条指令
isSubclassOfClass:
类方法:判断是否是子类或者自身类
instancesRespondToSelector
类方法,判断实例对象是否响应某个方法,如果对象将消息转发给其他对象,仍然能够接受消息而不会报错,但是本方法还是会返回NO
instancesRespondToSelector与respondsToSelector的区别:
1.前者只能用于类,后者可用于类或对象;
2.前者只适用于判断实例方法是否存在,后者适用于实例方法和类方法;(用类调用可检查类方法,实例调用可检查实例方法)
消息转发:
如果你给一个对象发送它不认识的消息时,系统会抛出一个错误,但在错误抛出之前,运行时会给改对象发送forwardInvocation:消 息,同时传递一个NSInvocation对象作为该消息的参数,NSInvocation对象包封装原始消息和对应的参数。
你可以实现 forwardInvocation:方法来对不能处理的消息做一些默认的处理, 以避免程序崩溃,但正如该函数的名字一样,这个函数主要是用来将消息转发给其它对象。
每一个对象都从NSObject类继承了 forwardInvocation:方法,但在NSObject中,该方法只是简单的调用doesNotRecognizeSelector:,通过重写该方法你就可以利用 forwardInvocation:将消息转发给其它对象。
转发有点像模仿继承,一个对象通过转发响应一个消息,看起来像
是继承了其它类实现的方法。继承将不同的功能集合到一个对象;而 转发是将不同的功能分配给不同的对象,它将一个问题分解成小的对象,然后通过一种对消息发送者透明的方式把这些对象关联起来。
同时在调用forwardInvocation:之前,需要先调用
methodSignatureForSelector:
获取指定selector的方法签名:
@interface ForwardClass : NSObject
-(void)doSomethingElse;
@end
@implementation ForwardClass
-(void)doSomethingElse
{
NSLog(@"doSomethingElse was called on %@", [self class]);
}
@end
@interface SomeClass : NSObject
{
id forwardClass;
}
-(void)doSomething;
@end
@implementation SomeClass
-(id)init
{
if (self = [super init]) {
forwardClass = [ForwardClass new];
}
return self;
}
-(void)doSomething
{
NSLog(@"doSomething was called on %@", [self class]);
}
-(void)forwardInvocation:(NSInvocation *)invocation
{
if (! forwardClass) {
[self doesNotRecognizeSelector: [invocation selector]];
}
[invocation invokeWithTarget: forwardClass];
}
-(NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
NSMethodSignature *signature = [super methodSignatureForSelector:selector];
if (! signature) {
//生成方法签名
signature = [forwardClass methodSignatureForSelector:selector];
}
return signature;
}
@end
现在给SomeClass对象发送doSomethingElse消息后程序不会crash了:
id someClass = [SomeClass new];
[someClass performSelector:@selector(doSomething)];
//ForwardTest[1291:56187] doSomething was called on SomeClass
[someClass performSelector:@selector(doSomethingElse)];
//ForwardTest[1291:56187] doSomethingElse was called on ForwardClass
消息转发有很多的用途,比如:
1)创建一个对象负责把消息转发给一个由其它对象组成的响应链,代 理对象会在这个有其它对象组成的集合里寻找能够处理该消息的对 象;
2) 把一个对象包在一个logger对象里,用来拦截或者纪录一些有
趣的消息调用;
3)比如声明类的属性为dynamic,使用自定义的方法
来截取和取代由系统自动生成的getter和setter方法。
conformsToProtocol:
此方法仅根据头文件中的正式声明来确定一致性,如上所示。它不检查在协议中声明的方法是否实际实现
BOOL result11 = [sview conformsToProtocol:@protocol(SomeProtocol)];
methodForSelector:
返回接受者方法实现的地址.参数必须是有效非Null的,如果不确定,先用respondsToSelector检查方法是否存在,如果接收方是实例,那么aSelector应该引用实例方法;如果接收者是一个类,它应该引用类方法。
+ (void)test1 {
}
- (NSNumber *)test2 {
return @1;
}
类调用类方法:(IMP) imp2 = 0x000000010efea820 (TestDamo`+[View test1] at View.m:45)
类调用实例方法:(IMP) imp22 = 0x000000010bf0c5c0 (libobjc.A.dylib`_objc_msgForward)
instanceMethodForSelector:
只能返回实例方法,类方法应调用methodForSelector:
instanceMethodSignatureForSelector:
返回一个NSMethodSignature对象,该对象包含给定选择器标识的实例方法的描述。,如果方法不存在返回nil
<NSMethodSignature: 0x600000275480>
number of arguments = 2
frame size = 224
is special struct return? NO
return value: -------- -------- -------- --------
type encoding (@) '@'
flags {isObject}
modifiers {}
frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}
argument 0: -------- -------- -------- --------
type encoding (@) '@'
flags {isObject}
modifiers {}
frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}
argument 1: -------- -------- -------- --------
type encoding (:) ':'
flags {}
modifiers {}
frame {offset = 8, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}
methodSignatureForSelector:
效果同methodForSelector:,该方法用于协议的实现。这种方法也用于在需要创建NSInvocation对象的情况下,例如在消息转发期间。如果您的对象维护一个委托或能够处理它不直接实现的消息,则应该重写该方法以返回适当的方法签名。
autoContentAccessingProxy
返回接受者的代理对象,接受者必须遵循协议NSDiscardableContent,必须实现方法,若不遵循协议返回nil,若不实现方法接受到发送的[View beginContentAccess]崩溃
一个NSDiscardableContent对象的生命周期依赖于一个“计数器”变量。一个NSDiscardableContent对象是一个可清除的内存块,可以跟踪它是否正在被其他对象使用。当该内存被读取或仍然需要时,其计数器变量将大于或等于1。当它没有被使用,并且可以被丢弃时,计数器变量将等于0。
当计数器等于0时,如果在那个时间点内存很紧,那么内存块可能会被丢弃。为了丢弃内容,在对象上调用discardContentIfPossible(),如果计数器变量为0,将释放相关的内存。
默认情况下,NSDiscardableContent对象被初始化,其计数器等于1,以确保内存管理系统不会立即丢弃它们。从这一点开始,您必须跟踪计数器变量的状态。调用beginContentAccess()方法将计数器变量增加1(测试主动调用时没反应),从而确保对象不会被丢弃。当您不再需要对象时,通过调用endContentAccess()来递减计数器。
基础框架包括NSPurgeableData类,它提供了该协议的默认实现。
NSDiscardableContent
当一个类的对象有子组件可以在不使用时被丢弃时,您将实现该协
议,从而使应用程序占用更小的内存空间。
@interface View : UIView <NSDiscardableContent>
@end
@implementation View {
- (BOOL)beginContentAccess {
NSLog(@"%s",__FUNCTION__);
printf("beginContentAccess retain count = %ld\n",CFGetRetainCount((__bridge CFTypeRef)(self)));
return YES; //会使引用计数加1
}
- (void)endContentAccess {
printf("endContentAccess retain count = %ld\n",CFGetRetainCount((__bridge CFTypeRef)(self)));
}
- (void)discardContentIfPossible {
printf("discardContentIfPossible retain count = %ld\n",CFGetRetainCount((__bridge CFTypeRef)(self)));
NSLog(@"%s",__FUNCTION__);
}
- (BOOL)isContentDiscarded {
printf("isContentDiscarded retain count = %ld\n",CFGetRetainCount((__bridge CFTypeRef)(self)));
NSLog(@"%s",__FUNCTION__);
return YES;
}
}
- (void)viewDidLoad {
View *view2 = [[View alloc] init];
_weakView = view2;
printf("\nretain count = %ld\n",CFGetRetainCount((__bridge CFTypeRef)(view2)));
//retain count = 1
id result12 = view2.autoContentAccessingProxy;
//2018-02-12 11:46:22.166757+0800 TestDamo[2224:165761] - [View beginContentAccess]
//beginContentAccess retain count = 1
printf("retain count = %ld\n",CFGetRetainCount((__bridge CFTypeRef)(view2)));
//retain count = 2
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@"weakView = %@",_weakView);
//2018-02-12 11:46:32.536811+0800 TestDamo[2224:165761] weakView = <View: 0x7fb7add05000; frame = (0 0; 0 0); layer = <CALayer: 0x6000002252c0>>
//endContentAccess retain count = 1
}
performSelector:withObject:afterDelay:
方法运行在当前线程,模式: NSDefaultRunLoopMode
当计时器触发时,线程试图从消息循环中出列消息并执行,只有当线程正在运行并且处于默认模式才会成功,否则计时器会等待消息循环回到默认模式
delay:若参数为0,并表示立刻执行,仍然在线程的运行循环上排队,并尽快执行。
如果你想在其他模式下运行方法,可以使用 performSelector:withObject:afterDelay:inModes:
如果你不确定当前的线程是否是主线程
performSelectorOnMainThread:withObject:waitUntilDone:
or
performSelectorOnMainThread:withObject:waitUnitlDone:modes:
取消已经出列的消息
cancelPreviousPerformRequestsWithTarget:
or
canclePerviousPerformRequestWithTarget:selector:object:
该方法使用当前上下文的runloop进行注册,并依赖于在常规基础上运行的runloop来正确执行。您可能会调用此方法的一个常见上下文,最后注册的runloop不是自动运行的,而是由调度队列调用的。如果您在调度队列上运行时需要这种功能,您应该使用dispatch_after和相关的方法来获得您想要的行为。
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
wait:
它指定当前线程是否阻塞,直到在主线程上的接收器上执行指定的方法。指定YES以阻塞此线程;否则,指定NO让该方法立即返回。
如果当前线程也是主线程,并且您为该参数指定了YES,则消息将立即交付并处理。否则执行将排队等待下一次通过运行循环运行
模式:NSRunLoopCommonModes
同一线程对该方法的多个调用会导致相应的选择器被排队,并按相同的顺序执行调用。(测试:不论wait是YES or NO,按顺序执行)
您不能使用此方法来取消队列中的消息。如果你想要的选项取消当前线程的消息,你必须使用performSelector:withObject:afterDelay:或performSelector:withObject:afterDelay:inModes:方法。
cancelPreviousPerformRequestsWithTarget:
取消执行请求之前注册performSelector:withObject:afterDelay:实例方法。
所有执行相同目标aTarget的请求都被取消。此方法只在当前运行循环中删除执行请求,而不是所有运行循环。
cancelPreviousPerformRequestsWithTarget:selector:object:
object:参数相等是用isEqual来确定的:因此,值不必是最初传递的相同对象。传递nil以匹配最初作为参数传递的nil请求。
forwardingTargetForSelector:
用法见instanceResponseToSelector
如果一个类重写或继承了这个方法,返回一个非空非self的结果没,那么这个结果就是新的消息接受者(如果返回的是self,就会陷入一个死循环)
如果你在一个非根类中,如果你在不需要对方法进行特殊处理,那就调用super方法
这个方法在forwardInvocation:之前执行
(同时实现两个方法,只执行该方法),这个方法比普通转发更快.如果转发的目标是捕获NSInvocation,或者在转发过程中操纵参数或返回值,这是没有用的。(可以用属性接收?,修改参数无作用)
- (void)forwardInvocation:(NSInvocation *)invocation
{
SEL aSelector = [invocation selector];
if ([friend respondsToSelector:aSelector])
[invocation invokeWithTarget:friend];
else
[super forwardInvocation:invocation];
}
resolveInstanceMethod:
为给定的实例方法提供默认的实现
#import "SubView.h"
#import <objc/runtime.h>
@implementation SubView
void dynamicMethodIMP (id self,SEL _cmd) {
NSLog(@"This is dynamic method");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class],sel,(IMP) dynamicMethodIMP,"v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
//调用
SSubView *sview = [[SSubView alloc] init];
[sview performSelector:@selector(resolveThisMethodDynamically)];
方法执行在OC转发机制之前,如果respondsToSelector:
or instanceRespondsToSelector:
被调用,该方法会被调用,给方法添加实现。
UI控件初始化,会调用此方法的顺序,NSObject无;
(SEL) sel = "displayLayer:"
(SEL) sel = "_layoutSublayersOfLayer:"
(SEL) sel = "animationDidStart:"
(SEL) sel = "animationDidStop:finished:"
doesNotRecognizeSelector:
如果一个类接受到这个方法的消息而他无法响应和转发,运行时系统会调用此方法,并且会返回一个NSInvalidArgumentException,生成一个错误消息
此方法通常只有运行时系统发送消息,但它可以在项目中使用,以防止方法被继承.
例如subview 调用responseToSelector:
时,返回YES,但当调用performSelector:
时,程序崩溃;
如果你重写了这个方法,你必须在方法实现的后面调用super 或者是提起一个NSInvalidArgumentException,简而言之,一定会抛出异常
@interface View()
@end
@implementation View {
- (void)preventInherited {
NSLog(@"%@ preventInherited ",self);
[self doesNotRecognizeSelector:_cmd];
}
@end
awakeAfterUsingCoder:
这个方法很少用到,在
NSObject (NSCoderMethods)
中定义,由NSCoder
在decode过程中调用(于-initWithCoder:
之后),所以说就算从文件里decode出来的对象也会走这个方法。
方法后面有NS_REPLACES_RECEIVER
这个宏:
在clang的文档中可以找到对这个编译器属性的介绍
One use of this attribute is declare your own init-like methods that do not follow the standard Cocoa naming conventions.
所以这个宏主要为了给编译器标识出这个方法可以像
self = [super init]
一样使用,并作出合理的内存管理。
So,这个方法提供了一个机会,可以将decode出来的对象替换成另一个对象
动态桥接流程
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
self = [super awakeAfterUsingCoder:aDecoder];
// 0. 判断是否要进行替换
// 1. 根据self.class从xib创建真正的view
// 2. 将placeholder的属性、autolayout等替换到真正的view上
// 3. return 真正的view
}
流程不难理解,就是有2个小难点:
步骤1从xib创建真正的view时也会调用这个方法,会造成递归,如何判断
迁移AutoLayoutConstrains
详见suunnyxx的技术博客(http://blog.sunnyxx.com/2014/07/01/ios_ib_bridge/)
//在从xib(sb)中脱线需要替换的XXView,在XXView中替换成Xib中的图
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
if (self.subviews.count == 0) {
return [self replacecdPlacholder:(UIView *)self];
}
return self;
}
- (UIView *)replacecdPlacholder:(UIView *)placeholderView {
NSArray *array = [[UINib nibWithNibName:NSStringFromClass([self class]) bundle:nil] instantiateWithOwner:nil options:nil];
UIView *realView = nil;
for (UIView *view in array) {
if ([view isKindOfClass:[self class]]) {
realView = view;
realView.tag = placeholderView.tag;
realView.frame = placeholderView.frame;
realView.bounds = placeholderView.bounds;
realView.hidden = placeholderView.hidden;
realView.clipsToBounds = placeholderView.clipsToBounds;
realView.autoresizingMask = placeholderView.autoresizingMask;
realView.userInteractionEnabled = placeholderView.userInteractionEnabled;
realView.translatesAutoresizingMaskIntoConstraints = placeholderView.translatesAutoresizingMaskIntoConstraints;
if (placeholderView.constraints.count > 0) {
for (NSLayoutConstraint *constraint in placeholderView.constraints) {
NSLayoutConstraint *newConstraint = nil;
if (!constraint.secondItem) {
newConstraint = [NSLayoutConstraint constraintWithItem:constraint.firstItem
attribute:constraint.firstAttribute relatedBy:constraint.relation toItem:nil attribute:constraint.secondAttribute multiplier:constraint.multiplier constant:constraint.constant];
} else if ([constraint.firstItem isEqualToString:constraint.secondItem]) {
newConstraint = [NSLayoutConstraint constraintWithItem:constraint.firstItem attribute:constraint.firstAttribute relatedBy:constraint.relation toItem:constraint.secondItem attribute:constraint.secondAttribute multiplier:constraint.multiplier constant:constraint.constant];
}
if (newConstraint) {
newConstraint.shouldBeArchived = constraint.shouldBeArchived;
newConstraint.priority = constraint.priority;
if ([UIDevice currentDevice].systemVersion.floatValue >= 7.0) {
newConstraint.identifier = constraint.identifier;
}
[realView addConstraint:newConstraint];
}
}
}
}
}
return realView;
}
classForCoder
该方法由NSCoder调用。NSObject的实现返回接收方的类。类集群的私有子类在被归档时替换其公共超类的名称。(资料少未搜集到)
classFallbacksForKeyedArchiver
NSKeyedArchiver调用此方法并将结果存储在归档文件中。如果一个对象的实际类在未归档时不存在,那么NSKeyedUnarchiver就会遍历类的存储列表,并使用第一个作为替代类的类来对对象进行解码。该方法的默认实现返回一个空数组。
如果您在应用程序中引入了一个新的类,以提供一些向后兼容性,您可以使用这个方法,以防止在没有该类的系统上读取存档。有时可能会有另一个类可能工作近以及这个新类的替代品,和存档键和归档状态为新类可以精心挑选的对象(或兼容性写入),这样在必要时可以作为替代类从未归档。
classFallbacksForKeyedArchiver
在新建的类model中实现NSCoding协议,并实现该方法,保证存档时,兼容了以前没有Model类,但是有相似的类TestModel(可能部分属性相同),也可以通过[NSKeyedUnarchiver unarchiveObjectWithFile:arrayFilePath];
解析数据
+ (NSArray<NSString *> *)classFallbacksForKeyedArchiver {
return @[@"TestModel"];
}
observationInfo
记录该对象添加的观察者的信息及keyPath
使用场景一:防止多次添加观察者或者是删除引发的错误
#import "NSObject+DamoKVO.h"
#import <objc/runtime.h>
@implementation NSObject (DamoKVO)
+ (void)load {
[self swizzingMethod];
}
- (void)addDamo:(NSObject *)observe
keyPath:(NSString *)keyPath
opthions:(NSKeyValueObservingOptions)options
context:(void *)context {
if (![self observerKeyPath:keyPath observer:observe]) {
[self addDamo:observe keyPath:keyPath opthions:options context:context];
}
}
- (void)removeDamo:(NSObject *)observer keyPath:(NSString *)keyPath {
if ([self observerKeyPath:keyPath observer:observer]) {
[self removeDamo:observer keyPath:keyPath];
}
}
// 进行检索获取Key
- (BOOL)observerKeyPath:(NSString *)key observer:(id )observer
{
id info = self.observationInfo;
//所有的观察者
NSArray *array = [info valueForKey:@"_observances"];
for (id objc in array) {
id Properties = [objc valueForKeyPath:@"_property"];
id newObserver = [objc valueForKeyPath:@"_observer"];
NSString *keyPath = [Properties valueForKeyPath:@"_keyPath"];
if ([key isEqualToString:keyPath] && [newObserver isEqual:observer]) {
return YES;
}
}
return NO;
}
+ (void)swizzingMethod {
SEL systemRemoveSel = @selector(removeObserver:forKeyPath:);
SEL myRemoveSel = @selector(removeDamo:keyPath:);
SEL systemAddSel = @selector(addObserver:forKeyPath:options:context:);
SEL myAddSel = @selector(addDamo:keyPath:opthions:context:);
Method systemRemoveMethod = class_getClassMethod([self class], systemRemoveSel);
Method myRemoveMethod = class_getClassMethod([self class], myRemoveSel);
Method systemAddMethod = class_getClassMethod([self class], systemAddSel);
Method myAddMethod = class_getClassMethod([self class], myAddSel);
method_exchangeImplementations(systemRemoveMethod, myRemoveMethod);
method_exchangeImplementations(systemAddMethod, myAddMethod);
}
@end
UIAccessibility 非正式协议
详情见:https://blog.csdn.net/heyc861221/article/details/52138698
accessInstanceVariablesDirectly
默认返回YES。子类可以覆盖它返回NO,在这种情况下,键值编码方法不会访问实例变量。
+ (BOOL)accessInstanceVariablesDirectly;
//默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//KVC提供属性值正确性�验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。
- (nullable id)valueForUndefinedKey:(NSString *)key;
//如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//和上一个方法一样,但这个方法是设值。
- (void)setNilValueForKey:(NSString *)key;
//如果你在SetValue方法时面给Value传nil,则会调用这个方法
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
当调用setValue:属性值 forKey:@”name“的代码时,底层的执行机制如下:[详情]:(https://www.jianshu.com/p/45cbd324ea65)
成员变量调用顺序
setKey
-->accessInstanceVariablesDirectly
-->_isKey
-->key
-->isKey
程序优先调用set<Key>:
属性值方法,代码通过setter方法完成设置。注意,这里的<key>
是指成员变量名,首字母大小写要符合KVC的命名规则,下同
如果没有找到setName:
方法,KVC机制会检查+ (BOOL)accessInstanceVariablesDirectly
方法有没有返回YES,默认该方法会返回YES,如果你重写了该方法让其返回NO的话,那么在这一步KVC会执行setValue:forUndefinedKey:
方法,不过一般开发者不会这么做。所以KVC机制会搜索该类里面有没有名为_<key>
的成员变量,无论该变量是在类接口处定义,还是在类实现处定义,也无论用了什么样的访问修饰符,只在存在以_<key>
命名的变量,KVC都可以对该成员变量赋值。
如果该类即没有set<key>:
方法,也没有_<key>
成员变量,KVC机制会搜索_is<Key>
的成员变量。
和上面一样,如果该类即没有set<Key>:
方法,也没有_<key>
和_is<Key>
成员变量,KVC机制再会继续搜索<key>
和is<Key>
的成员变量。再给它们赋值。
如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的setValue:forUndefinedKey:
方法,默认是抛出异常。
如果开发者想让这个类禁用KVC里,那么重写+ (BOOL)accessInstanceVariablesDirectly
方法让其返回NO即可,这样的话如果KVC没有找到set<Key>:
属性名时,会直接用setValue:forUndefinedKey:
方法。
@interface KVOModel()
@end
@implementation KVOModel {
NSString* isName;
NSString* name;
NSString* _name;
NSString* _isName;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"%s,key = %@",__FUNCTION__,key);
}
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"%s,key = %@",__FUNCTION__,key);
return nil;
}
+ (BOOL)accessInstanceVariablesDirectly {
NSLog(@"%s",__FUNCTION__);
return YES;
}
KVOModel *model = [[KVOModel alloc] init];
[model setValue:@"damo" forKey:@"name"];
// _name = damo,name = (null),_isName = (null),isName = (null)
//注释_name: name = (null),_isName = damo,isName = (null)
//再注释_isName:name = damo,isName = (null)
//再注释name:isName = damo
KVOModel *model = [[KVOModel alloc] init];
[model setValue:@"damo" forKey:@"name"];
NSString *name = [model valueForKey:@"name"];
NSString *_name = [model valueForKey:@"_name"];
NSString *isName = [model valueForKey:@"isName"];
NSString *_isName = [model valueForKey:@"_isName"];
NSLog(@"name = %@,_name = %@,isName = %@,_isName = %@",name,_name,isName,_isName);
//name = damo,_name = damo,isName = (null),_isName = (null)
//1.注释_name:name = damo,_name = (null),isName = damo,_isName = damo
//再注释_isName:name = damo,_name = (null),isName = (null),_isName = (null)
//再注释name:name = damo,_name = (null),isName = damo,_isName = (null)
//2.注释name:name = damo,_name = damo,isName = (null),_isName = (null)
当调用valueForKey:@”name“的代码时,KVC对key的搜索方式不同于setValue:属性值 forKey:@”name“,其搜索方式如下:
首先按get<Key>,<key>,is<Key>的顺序方法查找getter方法,找到的话会直接调用。如果是BOOL或者Int等值类型, 会将其包装成一个NSNumber对象。
如果上面的getter没有找到,KVC则会查找countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes格式的方法。如果countOf<Key>方法和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray所�有方法的代理集合(它是NSKeyValueArray,是NSArray的子类),调用这个代理集合的方法,或者说给这个代理集合发送属于NSArray的方法,就会以countOf<Key>,objectIn<Key>AtIndex�或<Key>AtIndexes这几个方法组合的形式调用。还有一个可选的get<Key>:range:方法。所以你想重新定义KVC的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC的标准命名方法,包括方法签名。
如果上面的方法没有找到,那么会同时查找countOf<Key>,enumeratorOf<Key>,memberOf<Key>格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法的代理集合,和上面一样,给这个代理集合发NSSet的消息,就会以countOf<Key>,enumeratorOf<Key>,memberOf<Key>组合的形式调用。
如果还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),那么和先前的设值一样,会按_<key>,_is<Key>,<key>,is<Key>的顺序搜索成员变量名,这里不推荐这么做,因为这样直接访问实例变量破坏了封装性,使代码更脆弱。如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly返回NO的话,那么会直接调用valueForUndefinedKey:
还没有找到的话,调用valueForUndefinedKey:
#import "Model.h"
- (NSUInteger)countOfNumbers{
NSLog(@"%s",__FUNCTION__);
return 7;
}
-(id)objectInNumbersAtIndex:(NSUInteger)index{ //当key使用numbers时,KVC会找到这两个方法。
NSLog(@"%s",__FUNCTION__);
return @(index * 3);
}
KVOModel *model = [[KVOModel alloc] init];
NSArray *arr = @[@1];
[model setValue:arr forKey:@"array"];
id obj = [model valueForKey:@"numbers"];
//<NSKeyValueArray 0x600000232260>(
//0,
//3,
//6,
//9,
//12,
//15,
//18
//)
NSLog(@"%@",NSStringFromClass([obj class]));
NSLog(@"0:%@ 1:%@ 2:%@ 3:%@",obj[0],obj[1],obj[2],obj[3]);
2018-04-09 13:49:34.310283+0800 TestDamo[21058:22309559] +[KVOModel accessInstanceVariablesDirectly]
2018-04-09 13:49:34.310460+0800 TestDamo[21058:22309559] key = numbers
2018-04-09 13:49:34.310697+0800 TestDamo[21058:22309559] NSKeyValueArray
2018-04-09 13:49:34.310806+0800 TestDamo[21058:22309559] -[KVOModel objectInNumbersAtIndex:]
2018-04-09 13:49:34.310900+0800 TestDamo[21058:22309559] -[KVOModel objectInNumbersAtIndex:]
2018-04-09 13:49:34.310991+0800 TestDamo[21058:22309559] -[KVOModel objectInNumbersAtIndex:]
2018-04-09 13:49:34.311090+0800 TestDamo[21058:22309559] -[KVOModel objectInNumbersAtIndex:]
2018-04-09 13:49:34.311175+0800 TestDamo[21058:22309559] 0:0 1:3 2:6 3:9
- 用KVC实现高阶消息传递
当对容器类使用KVC时,valueForKey:将会被传递给容器中的每一个对象,而不是容器本身进行操作。结果会被添加进返回的容器中,这样,开发者可以很方便的操作集合来返回另一个集合。
NSArray* arrStr = @[@"english",@"franch",@"chinese"];
NSArray* arrCapStr = [arrStr valueForKey:@"capitalizedString"];
for (NSString* str in arrCapStr) {
NSLog(@"%@",str);
}
NSArray* arrCapStrLength = [arrStr valueForKeyPath:@"capitalizedString.length"];
for (NSNumber* length in arrCapStrLength) {
NSLog(@"%ld",(long)length.integerValue);
}
打印结果
2016-04-20 16:29:14.239 KVCDemo[1356:118667] English
2016-04-20 16:29:14.240 KVCDemo[1356:118667] Franch
2016-04-20 16:29:14.240 KVCDemo[1356:118667] Chinese
2016-04-20 16:29:14.240 KVCDemo[1356:118667] 7
2016-04-20 16:29:14.241 KVCDemo[1356:118667] 6
2016-04-20 16:29:14.241 KVCDemo[1356:118667] 7
- 用KVC中的函数操作集合
KVC同时还提供了很复杂的函数,主要有下面这些
①简单集合运算符
简单集合运算符共有@avg, @count , @max , @min ,@sum5种,都表示啥不用我说了吧, 目前还不支持自定义。
@interface Book : NSObject
@property (nonatomic,copy) NSString* name;
@property (nonatomic,assign) CGFloat price;
@end
@implementation Book
@end
Book *book1 = [Book new];
book1.name = @"The Great Gastby";
book1.price = 22;
Book *book2 = [Book new];
book2.name = @"Time History";
book2.price = 12;
Book *book3 = [Book new];
book3.name = @"Wrong Hole";
book3.price = 111;
Book *book4 = [Book new];
book4.name = @"Wrong Hole";
book4.price = 111;
NSArray* arrBooks = @[book1,book2,book3,book4];
NSNumber* sum = [arrBooks valueForKeyPath:@"@sum.price"];
NSLog(@"sum:%f",sum.floatValue);
NSNumber* avg = [arrBooks valueForKeyPath:@"@avg.price"];
NSLog(@"avg:%f",avg.floatValue);
NSNumber* count = [arrBooks valueForKeyPath:@"@count"];
NSLog(@"count:%f",count.floatValue);
NSNumber* min = [arrBooks valueForKeyPath:@"@min.price"];
NSLog(@"min:%f",min.floatValue);
NSNumber* max = [arrBooks valueForKeyPath:@"@max.price"];
NSLog(@"max:%f",max.floatValue);
打印结果
2016-04-20 16:45:54.696 KVCDemo[1484:127089] sum:256.000000
2016-04-20 16:45:54.697 KVCDemo[1484:127089] avg:64.000000
2016-04-20 16:45:54.697 KVCDemo[1484:127089] count:4.000000
2016-04-20 16:45:54.697 KVCDemo[1484:127089] min:12.000000
2016-04-20 16:45:54.697 KVCDemo[1484:127089] max:111.000000
②对象运算符
比集合运算符稍微复杂,能以数组的方式返回指定的内容,一共有两种:
@distinctUnionOfObjects
@unionOfObjects
它们的返回值都是NSArray,区别是前者返回的元素都是唯一的,是去重以后的结果;后者返回的元素是全集。
用法如下:
NSLog(@"distinctUnionOfObjects");
NSArray* arrDistinct = [arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];
for (NSNumber *price in arrDistinct) {
NSLog(@"%f",price.floatValue);
}
NSArray *array = @[@"12-11",@"12-13",@"12-11"];
NSArray *resultArr = [array valueForKeyPath:@"@distinctUnionOfObjects.self"];
NSLog(@"unionOfObjects");
NSArray* arrUnion = [arrBooks valueForKeyPath:@"@unionOfObjects.price"];
for (NSNumber *price in arrUnion) {
NSLog(@"%f",price.floatValue);
}
2016-04-20 16:47:34.490 KVCDemo[1522:128840] distinctUnionOfObjects
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 111.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 12.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 22.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] unionOfObjects
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 22.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 12.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 111.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 111.000000
KVO
大致原理描述:
KVO 是基于 runtime 运行时来实现的,当你观察了某个对象的属性,内部会生成一个该对象所属类的子类,然后从写被观察属性的setter方法,当然在重写的方法中会调用父类的setter方法从而不会影响框架使用者的逻辑,之后会将该对象的isa指针指向新创建的这个类,最后会重写-(Class)class;方法,让使用者通过[obj class]查看当前对象所属类的时候会返回其父类,达到移花接木的目的。
KVOModel *model = [[KVOModel alloc] init];
[model addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
NSLog(@"model -addKVO:%@",object_getClass(model));
//model -addKVO:NSKVONotifying_KVOModel
NSLog(@"setterName-addKVO: %p \n", [model methodForSelector:@selector(setName:)]);
//setterName-addKVO: 0x10d4a0efe
[model removeObserver:self forKeyPath:@"name"];
NSLog(@"model -removeKVO:%@",object_getClass(model));
//model -removeKVO:KVOModel
NSLog(@"setterName-removeKVO: %p \n", [model methodForSelector:@selector(setName:)]);
//setterName-removeKVO: 0x10cdaa0e0
想让类的某个属性支持KVO机制的话,这个类必须满足一下3点:
- 这个类必须使得该属性支持KVC。
- 这个类必须保证能够将改变通知发出。
- 当有依赖关系的时候,注册合适的依赖键。
注:KVO是基于KVC的,直接使用addObject不会接受到通知,通过mutableArrayValueForKey:这个方法对集合对象进行的相关操作(增加,删除,替换元素)就会触发KVO通知,这个方法会返回一个中间代理对象,这个中间代理对象的类会指向一个中间类,你在这个代理对象上进行的操作最终应在原始对象上造成同样的效果。
第二个条件:手动发送通知
//ViewController.m
Model *model = [[Model alloc] init];
model.name = @"a";
[model addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld |NSKeyValueObservingOptionNew context:NULL];
[model addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld |NSKeyValueObservingOptionNew context:NULL];
[model addObserver:self forKeyPath:@"array" options:NSKeyValueObservingOptionOld |NSKeyValueObservingOptionNew context:NULL];
model.name = @"b";
NSMutableArray *array = [model mutableArrayValueForKey:@"array"];
[array addObject:@"c"];
//Model.m
- (void)setName:(NSString *)name {
//手动发送通知,如果没有关闭自动通知会收到两遍
[self willChangeValueForKey:@"name"];
[self willChangeValueForKey:@"age"];
//to-many也可以使用该方法,或是`mutableArrayValueForKey:`.在通知中会接受到NSindexSet
[self willChange:NSKeyValueChangeReplacement valuesAtIndexes:[[NSIndexSet alloc] initWithIndex:0] forKey:@"array"];
_name = name;
//注:如果age使用self调用,会收到两次通知,因为没有禁止,而_调用灭有使用setter或者是setValue:forKey:
_age = @"15";
[self didChange:NSKeyValueChangeReplacement valuesAtIndexes:[[NSIndexSet alloc] initWithIndex:0] forKey:@"array"];
[self didChangeValueForKey:@"name"];
[self didChangeValueForKey:@"age"];
}
//每个属性都会生成一个方法
+ (BOOL)automaticallyNotifiesObserversOfName {
return NO;
}
//或者使用下面方法关闭自动通知
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
BOOL automatic = NO;
if ([key isEqualToString:@"name"]) {
automatic = NO;
} else {
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
第三个条件:当有依赖关系的时候,注册合适的依赖键。
一个监听者监听了info,当firstName或者age改变时,这个监听者应该被通知。
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"info"]) {
NSArray *affectingKeys = @[@"name", @"age"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
另一种实现同样结果的方法是实现一个遵循命名方式为keyPathsForValuesAffecting<Key>的类方法,<Key>是依赖于其他值的属性名(首字母大写),用上面代码的例子来重新实现一下:
+ (NSSet<NSString *> *)keyPathsForValuesAffectingInfo {
return [NSSet setWithArray:@[@"name",@"age"]];
}
但是在To-many Relationships中(比如数组属性),上面的方法就不管用了,比如,假如你有一个Department类,它有一个针对Employee类的to-many关系(即拥有一个装有Employee类对象的数组),Employee类有salary属性。你希望Department类有一个totalSalary属性来计算所有员工的薪水,也就是在这个关系中Department的totalSalary依赖于所有Employee的salary属性。这种情况你不能通过实现keyPathsForValuesAffectingTotalSalary方法并返回employees.salary。
有两种解决方法:
你可以用KVO将parent(比如Department)作为所有children(比如Employee)相关属性的观察者。你必须在把child添加或删除到parent时也把parent作为child的观察者添加或删除。在observeValueForKeyPath:ofObject:change:context:方法中我们可以针对被依赖项的变更来更新依赖项的值:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == totalSalaryContext) {
[self updateTotalSalary];
}
else
// deal with other observations and/or invoke super...
}
- (void)updateTotalSalary {
[self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
}
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
if (totalSalary != newTotalSalary) {
[self willChangeValueForKey:@"totalSalary"];
_totalSalary = newTotalSalary;
[self didChangeValueForKey:@"totalSalary"];
}
}
- (NSNumber *)totalSalary {
return _totalSalary;
}
使用iOS中观察者模式的另一种实现方式:通知 (NSNotification) ,
prepareForInterfaceBuilder
当Xib使用IB_DESIGNABLE属性实例化一个类时,它调用此方法来让结果对象知道它是在设计时创建的。您可以在可设计的类中实现此方法,并使用它来配置它们的设计时外观。例如,您可以使用该方法来配置一个带有默认字符串的自定义文本控件。该系统不调用此方法;只有Xib调用它。
Xib等待,直到在调用此方法之前创建并初始化图中的所有对象。因此,如果对象的运行时配置依赖于子视图或父视图,那么这些对象应该在调用该方法时存在。
这个方法的实现必须在某个点调用super,以便父类可以执行自己的自定义设置。
IB_DESIGNABLE
@interface View : UIView
@property (nonatomic, strong)IBInspectable UIColor *bgColor;
@end
@implementation View
- (void)prepareForInterfaceBuilder {
[super prepareForInterfaceBuilder];
self.layer.cornerRadius = 30;
self.layer.masksToBounds = true;
}
@end
设置后只能在xib中看到效果,运行后还是没有设置圆角,需要在awakeFromNib中进行设置
网友评论