美文网首页
KVO,copy/mutableCopy, runloop

KVO,copy/mutableCopy, runloop

作者: 玉米须须 | 来源:发表于2019-01-26 21:17 被阅读0次

    KVO实现原理

    1.KVO是基于runtime机制实现的
    2.当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的setter 方法。派生类在被重写的setter方法内实现真正的通知机制
    3.如果原类为Person,那么生成的派生类名为NSKVONotifying_Person
    4.每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察,那么系统会偷偷将isa指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是派生类的setter方法
    5.键值观察通知依赖于NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey:;在一个被观察属性发生改变之前, willChangeValueForKey:一定会被调用,这就 会记录旧的值。而当改变发生后,didChangeValueForKey:会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。
    KVO深入原理:

    1.Apple 使用了 isa 混写(isa-swizzling)来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为:?NSKVONotifying_A的新类,该类继承自对象A的本类,且KVO为NSKVONotifying_A重写观察属性的setter?方法,setter?方法会负责在调用原?setter?方法之前和之后,通知所有观察对象属性值的更改情况。

    2.NSKVONotifying_A类剖析:在这个过程,被观察对象的 isa 指针从指向原来的A类,被KVO机制修改为指向系统新创建的子类 NSKVONotifying_A类,来实现当前类属性值改变的监听;

    3.所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对KVO的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为“NSKVONotifying_A”的类(),就会发现系统运行到注册KVO的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为NSKVONotifying_A的中间类,并指向这个中间类了。

    4.(isa 指针的作用:每个对象都有isa 指针,指向该对象的类,它告诉 Runtime 系统这个对象的类是什么。所以对象注册为观察者时,isa指针指向新子类,那么这个被观察的对象就神奇地变成新子类的对象(或实例)了。)?因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。

    5.子类setter方法剖析:KVO的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:和 didChangevlueForKey:,在存取数值的前后分别调用2个方法: 被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath?的属性值即将变更;当改变发生后, didChangeValueForKey: 被调用,通知系统该 keyPath?的属性值已经变更;之后,?observeValueForKey:ofObject:change:context: 也会被调用。且重写观察属性的setter?方法这种继承方式的注入是在运行时而不是编译时实现的。
    观察以下代码的输出结构

    @implementation Son : Father
    - (id)init {
        self = [super init];
        if (self) {
            NSLog(@"%@", NSStringFromClass([self class]));
            NSLog(@"%@", NSStringFromClass([super class]));
        }
        return self;
    }
    @end
    

    这个写法会出什么问题: @property (copy) NSMutableArray *array;

    @property (nonatomic, copy) NSMutableArray *mutableArray;
    
    NSMutableArray *array = [NSMutableArray arrayWithObjects:@1,@2,nil];
    self.mutableArray = array;
    [self.mutableArray removeObjectAtIndex:0];
    

    解析:代码会发生崩溃, -[__NSArrayI removeObjectAtIndex:]: unrecognized selector sent to instance 0x7fcd1bc30460

    两个问题:1、添加,删除,修改数组内的元素的时候,程序会因为找不到对应的方法而崩溃.因为 copy 就是复制一个不可变 NSArray 的对象;2、使用了 atomic 属性会严重影响性能 ;

    在iOS开发中,你会发现,几乎所有属性都声明为 nonatomic。

    一般情况下并不要求属性必须是“原子的”,因为这并不能保证“线程安全” ( thread safety),若要实现“线程安全”的操作,还需采用更为深层的锁定机制才行。例如,一个线程在连续多次读取某属性值的过程中有别的线程在同时改写该值,那么即便将属性声明为 atomic,也还是会读到不同的属性值。

    因此,开发iOS程序时一般都会使用 nonatomic 属性。但是在开发 Mac OS X 程序时,使用 atomic 属性通常都不会有性能瓶颈。

    runtime 如何实现 weak 属性

    要实现 weak 属性,首先要搞清楚 weak 属性的特点:
    weak 此特质表明该属性定义了一种“非拥有关系” (nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同 assign 类似, 然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。
    那么 runtime 如何实现 weak 变量的自动置nil?
    runtime 对注册的类, 会进行布局,对于 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil.

    以下描述weak变量的过程很清晰

    我们可以设计一个函数(伪代码)来表示上述机制:
    objc_storeWeak(&a, b)函数:
    objc_storeWeak函数把第二个参数--赋值对象(b)的内存地址作为键值key,将第一个参数--weak修饰的属性变量(a)的内存地址(&a)作为value,注册到 weak 表中。如果第二个参数(b)为0(nil),那么把变量(a)的内存地址(&a)从weak表中删除,

    你可以把objc_storeWeak(&a, b)理解为:objc_storeWeak(value, key),并且当key变nil,将value置nil。

    在b非nil时,a和b指向同一个内存地址,在b变nil时,a变nil。此时向a发送消息不会崩溃:在Objective-C中向nil发送消息是安全的。

    而如果a是由 assign 修饰的,则: 在 b 非 nil 时,a 和 b 指向同一个内存地址,在 b 变 nil 时,a 还是指向该内存地址,变野指针。此时向 a 发送消息极易崩溃。

    用@property声明的NSString(或NSArray,NSDictionary)经常使用copy关键字,为什么?如果改用strong关键字,可能造成什么问题?因为父类指针可以指向子类对象,使用 copy 的目的是为了让本对象的属性不受外界影响,使用 copy 无论给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本.

    如果我们使用是 strong ,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性.
    copy 此特质所表达的所属关系与 strong 类似。然而设置方法并不保留新值,而是将其“拷贝” (copy)。 当属性类型为 NSString 时,经常用此特质来保护其封装性,因为传递给设置方法的新值有可能指向一个 NSMutableString 类的实例。这个类是 NSString 的子类,表示一种可修改其值的字符串,此时若是不拷贝字符串,那么设置完属性之后,字符串的值就可能会在对象不知情的情况下遭人更改。所以,这时就要拷贝一份“不可变” (immutable)的字符串,确保对象中的字符串值不会无意间变动。只要实现属性所用的对象是“可变的” (mutable),就应该在设置新属性值时拷贝一份。
    举例说明:

    定义一个以 strong 修饰的 array:

    @property (nonatomic ,readwrite, strong) NSArray *array;
    

    然后进行下面的操作:

       NSArray *array = @[ @1, @2, @3, @4 ];
       NSMutableArray *mutableArray = [NSMutableArray arrayWithArray:array];
    
       self.array = mutableArray;
       [mutableArray removeAllObjects];;
       NSLog(@"%@",self.array);
    
       [mutableArray addObjectsFromArray:array];
       self.array = [mutableArray copy];
       [mutableArray removeAllObjects];;
       NSLog(@"%@",self.array);
    

    打印结果如下所示:

    2015-09-27 19:10:32.523 CYLArrayCopyDmo[10681:713670] (
    )
    2015-09-27 19:10:32.524 CYLArrayCopyDmo[10681:713670] (
       1,
       2,
       3,
       4
    )
    

    (详见仓库内附录的 Demo。)

    为了理解这种做法,首先要知道,两种情况:

    1. 对非集合类对象的 copy 与 mutableCopy 操作;
    2. 对集合类对象的 copy 与 mutableCopy 操作。

    1. 对非集合类对象的copy操作:

    在非集合类对象中:对 immutable 对象进行 copy 操作,是指针复制,mutableCopy 操作时内容复制;对 mutable 对象进行 copy 和 mutableCopy 都是内容复制。用代码简单表示如下:

    • [immutableObject copy] // 浅复制
    • [immutableObject mutableCopy] //深复制
    • [mutableObject copy] //深复制
    • [mutableObject mutableCopy] //深复制

    比如以下代码:

    NSMutableString *string = [NSMutableString stringWithString:@"origin"];//copy
    NSString *stringCopy = [string copy];
    

    查看内存,会发现 string、stringCopy 内存地址都不一样,说明此时都是做内容拷贝、深拷贝。即使你进行如下操作:

    [string appendString:@"origion!"]
    

    stringCopy 的值也不会因此改变,但是如果不使用 copy,stringCopy 的值就会被改变。 集合类对象以此类推。 所以,

    用 @property 声明 NSString、NSArray、NSDictionary 经常使用 copy 关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary,他们之间可能进行赋值操作,为确保对象中的字符串值不会无意间变动,应该在设置新属性值时拷贝一份。

    2、集合类对象的copy与mutableCopy

    集合类对象是指 NSArray、NSDictionary、NSSet ... 之类的对象。下面先看集合类immutable对象使用 copy 和 mutableCopy 的一个例子:

    NSArray *array = @[@[@"a", @"b"], @[@"c", @"d"]];
    NSArray *copyArray = [array copy];
    NSMutableArray *mCopyArray = [array mutableCopy];
    

    查看内容,可以看到 copyArray 和 array 的地址是一样的,而 mCopyArray 和 array 的地址是不同的。说明 copy 操作进行了指针拷贝,mutableCopy 进行了内容拷贝。但需要强调的是:此处的内容拷贝,仅仅是拷贝 array 这个对象,array 集合内部的元素仍然是指针拷贝。这和上面的非集合 immutable 对象的拷贝还是挺相似的,那么mutable对象的拷贝会不会类似呢?我们继续往下,看 mutable 对象拷贝的例子:

    NSMutableArray *array = [NSMutableArray arrayWithObjects:[NSMutableString stringWithString:@"a"],@"b",@"c",nil];
    NSArray *copyArray = [array copy];
    NSMutableArray *mCopyArray = [array mutableCopy];
    

    查看内存,如我们所料,copyArray、mCopyArray和 array 的内存地址都不一样,说明 copyArray、mCopyArray 都对 array 进行了内容拷贝。同样,我们可以得出结论:

    在集合类对象中,对 immutable 对象进行 copy,是指针复制, mutableCopy 是内容复制;对 mutable 对象进行 copy 和 mutableCopy 都是内容复制。但是:集合对象的内容复制仅限于对象本身,对象元素仍然是指针复制。用代码简单表示如下:

    [immutableObject copy] // 浅复制
    [immutableObject mutableCopy] //单层深复制
    [mutableObject copy] //单层深复制
    [mutableObject mutableCopy] //单层深复制
    

    这个代码结论和非集合类的非常相似。

    1.runloop是来做什么的?runloop和线程有什么关系?主线程默认开启了runloop么?子线程呢?

    runloop:字面意思就是跑圈,其实也就是一个循环跑圈,用来处理线程里面的事件和消息。
    runloop和线程的关系:每个线程如果想继续运行,不被释放,就必须有一个runloop来不停的跑圈,以来处理线程里面的各个事件和消息。
    主线程默认是开启一个runloop。也就是这个runloop才能保证我们程序正常的运行。子线程是默认没有开始runloop的

    2.runloop的mode是用来做什么的?有几种mode?

    介绍一下分类,能用分类做什么?内部是如何实现的?它为什么会覆盖掉原来的方法?

    category:我们可以给类或者系统类添加实例方法方法。我们添加的实例方法,会被动态的添加到类结构里面的methodList列表里面。

    临界区:指的是一块对公共资源进行访问的代码,并非一种机制或是算法。

    自旋锁:是用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。

    互斥锁(Mutex):是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成。

    读写锁:是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”、多读者-单写者锁) 用于解决多线程对公共资源读写问题。读操作可并发重入,写操作是互斥的。 读写锁通常用互斥锁、条件变量、信号量实现。

    信号量(semaphore):是一种更高级的同步机制,互斥锁可以说是semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。

    条件锁:就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行

    为什么block中不能修改普通变量的值?

    由于无法直接获得原变量,技术上无法实现修改,所以编译器直接禁止了。

    __block和__weak修饰符的区别

    __block不管是ARC还是MRC模式下都可以使用,可以修饰对象,还可以修饰基本数据类型。
    __weak只能在ARC模式下使用,也只能修饰对象(NSString),不能修饰基本数据类型(int)。
    __block对象可以在block中被重新赋值,__weak不可以。
    Block不允许修改外部变量的值,这里所说的外部变量的值,指的是栈中指针的内存地址。__block 所起到的作用就是只要观察到该变量被 block 所持有,就将“外部变量”在栈中的内存地址放到了堆中。进而在block内部也可以修改外部变量的值。
    lock不允许修改外部变量的值。Apple这样设计,应该是考虑到了block的特殊性,block也属于“函数”的范畴,变量进入block,实际就是已经改变了作用域。在几个作用域之间进行切换时,如果不加上这样的限制,变量的可维护性将大大降低。又比如我想在block内声明了一个与外部同名的变量,此时是允许呢还是不允许呢?只有加上了这样的限制,这样的情景才能实现。于是栈区变成了红灯区,堆区变成了绿灯区。

    使用KVC进行字典转模型

    可以封装一个BaseModel 实现上面两个方法,创建如下转模型方法

    // 字典转模型
    -(id)initWithDic:(NSDictionary *)modelDic{
        self = [super init];
        if(self){
            [self setValuesForKeysWithDictionary:modelDic];
        }
        return self;
    }
    // 将所有的数据转换为字符串
    -(void)setValue:(id)value forKey:(NSString *)key{
    
        if([value isKindOfClass:[NSNull class]]){
            value=nil;
        }else if([value isKindOfClass:[NSArray class]]){
        }else{
            value = [NSString stringWithFormat:@"%@",value];
        }
        [super setValue:value forKey:key];
    }
    
    // 对特殊字符 id 进行处理
    -(void)setValue:(id)value forUndefinedKey:(NSString *)key{
        NSLog(@"Undefined Key: %@", key);
    }
    

    KVO和NSnotification的区别

    被观察者发出 addObserver:forKeyPath:options:context: 方法来添加观察者。然后只要被观察者的keyPath值变化(注意:单纯改变其值不会调用此方法,只有通过getters和setters来改变值才会触发KVO),就会在观察者里调用方法observeValueForKeyPath:ofObject:change:context:因此观察者需要实现方法 observeValueForKeyPath:ofObject:change:context: 来对KVO发出的通知做出响应。这 些代码都只需在观察者里进行实现,被观察者不用添加任何代码,所以谁要监听谁注册,然后对响应进行处理即可,使得观察者与被观察者完全解耦,运用很灵活很 简便;但是KVO只能检测类中的属性,并且属性名都是通过NSString来查找,编译器不会帮你检错和补全,纯手敲所以比较容易出错。
    这里的通知不是由被观察者发出,而是由NSNotificationCenter来统一发出,而不同通知通过唯一的通知标识名notificationName来区分,标识名由发送通知的类来起。首先被观察者自己在必要的方法A里,通过方法postNotificationName:object:来发出通知notificationName这样发送通知者这边的工作就完成了,每次A被调用,就会发送一次通知notificationName。然后谁要监听A的变化,就通过[NSNotificationCenter defaultCenter]的方法addObserver:selector:name:object:为观察者注册监听name为notificationName的通知然后每次发出name为notificationName的通知时,注册监听后的观察者就会调用其自己定义的方法notificationSelector来进行响应。NSNotification的特点呢,就是需要被观察者先主动发出通知,然后观察者注册监听后再来进行响应,比KVO多了发送通知的一步,但是其优点是监听不局限于属性的变化,还可以对多种多样的状态变化进行监听,监听范围广,使用也更灵活

    如果一个button有一部分超出父控件的范围了,这部分无法响应点击,如果想让它响应点击应该怎么做

    当按钮超出Tab bar的view后,那么其实按钮超出的部分是无法被点击的。那么先来说说解决办法

    1.我们父视图view的- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event的方法

     - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
        //if内的条件应该为,当触摸点point超出蓝色部分,但在黄色部分时
        if (.....){
         return YES;
        }
        return NO;
    }
    

    这和iOS的事件分发机制 hit-Testing有关,简单的说,hit-Testing的作用就是找出你每次触摸屏幕,点到的究竟是哪个view。

    比如以下这个图
    A add B,B add C
    当我去点击View-C的时候,hit-Testing实际上是这样检测的
    1.首先,视图会先从View-A开始检查,发现触摸点在View-A,所以检查View-A的子视图View-B。
    2.发现触摸点在View-B内,好棒!看看View-B内的子视图View-C。
    3.发现触摸点在View-C内,但View-C没有子视图了,所以View-C是此次触摸事件的hit-TestView了。

    那么UIView中其实提供了两个方法来确定hit-TestView
    1.- (UIView )hitTest:(CGPoint)point withEvent:(UIEvent )event;
    2.- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;//这个就是我们上面重写的方法

    注意其实在每次递归去调用hitTest:(CGPoint)point withEvent:(UIEvent *)event之前,都会调用pointInside:withEvent:来确定该触摸点是否在该View内。

    所以当我们重写pointInside:(CGPoint)point withEvent:(UIEvent *)event后,其实我们的点击后调用hitTest来递归的找hit-TestView的区域从这样:
    这样当我们愉快的点击上半凸起的区域时,hit-Testing便回去检查蓝色视图内的子视图,即黄色区域。从而来完成此次触摸事件。

    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
    {
        UIView * view = [super hitTest:point withEvent:event];
        if (view == nil) {
            // 转换坐标系
            CGPoint newPoint = [commentImageView convertPoint:point fromView:self];
            // 判断触摸点是否在button上
            if (CGRectContainsPoint(commentImageView.bounds, newPoint)) {
                view = commentImageView;
            }
        }
        return view;
    }
    

    所以针对超出父视图的button的点击,首先要让父视图的hitTest方法返回不为nil,也就是如果点击范围在button范围内,就让父视图的

    hitTest方法返回响应的按钮对象,这个过程中需要对点击事件的坐标进行处理,可以通过调用button的pointInside来确定点击的点是否在

    按钮上,以此来判断父视图是该返回按钮对象还是nil!

    具体操作过程如下

    • (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{ // 将点击事件在父视图的坐标转为参照按钮的坐标,这样方便确定点击是否在按钮上 CGPoint btnPoint = [self.tstBtn convertPoint:point fromView:self]; if ([self.tstBtn pointInside:btnPoint withEvent:event]) { return self.tstBtn; }else{ return [super hitTest:point withEvent:event]; }}
      这样,在确定事件发生的点在button上之后,就可以通过正常的响应者链来确定事件的传递了!

    通过这种方法,还可以实现许多效果,比如屏蔽一些操作,或者拦截操作等!

    而针对响应传递链可以实现一个操作改变一系列的状态!

    比如在ViewController中的touchesBegan中调用[super touchesBegan]方法,可以在ViewController响应的同时,UIWindow也进行响应!

    需要注意的是这里的super并非指ViewController的父类!而是指它的下一个响应者,即nextResponser!

    相关文章

      网友评论

          本文标题:KVO,copy/mutableCopy, runloop

          本文链接:https://www.haomeiwen.com/subject/eugioftx.html