15、iOSの基礎となる分析 - KVO


KVC、KVOことが多いのiOSで使用されているが、多くの場合、面接で聞かれます。今日は何KVOを探索します。

KVOは何ですか?

KVO:キー観測メカニズム、プロパティを観察する方法を提供変わり
、KVOはKeyValueObservingを立って、アップルが提供するイベント通知メカニズムです。これは、オブジェクトが別のオブジェクトの特性の変化を監視することができ、イベントは、変更の時点で受信されます。KVOの実装メカニズムので、そのプロパティは、通常はNSObjectのオブジェクトから継承された役割を、果たしますデフォルトKVOによってサポートされています。
そしてKVOのiOS NSNotificationCenterはしているオブザーバモデル 1の実装。

違いは、観察と観察者との間の相対的な関係ということです。

  • KVOはの一つであります
  • NSNotificationCenter、多くの

オブジェクトであることにKVO非侵襲的モニタリングを実装するために、その内部のコードを変更せずに、監視しました。
KVOはまた、オブジェクトのコレクションの変化を監視することができ、個々の属性の変化を監視することができます。KVCのmutableArrayValueForKeyによって:内部プロキシオブジェクトの変更は、コールバックメソッドのKVOがリッスンするとき、プロキシオブジェクトのメソッドを取得します。NSArrayのは、オブジェクトとNSSetのコレクションが含まれています。

まず、KVOの基本的な使い方

使用KVOは、3つのステップ(KVOトリロジー)に分け

  1. 登録オブザーバー
  2. コールバックを実装
  3. 削除オブザーバー
#import "LJLKVOViewController.h"
#import <objc/runtime.h>
#import "LJLKVOPerson.h"
#import "LJLKVODownloader.h"

static void * personNameContext = &personNameContext;//观察person.name 的 context
static void * personKvoArrayContext = &personKvoArrayContext;//观察person.kvoArray 的 context

@interface LJLKVOViewController ()
@property(nonatomic, strong)LJLKVOPerson * person;
@end

1.1、登録した観察者が
    オブザーバーを登録するには、次の方法のオブジェクトの呼び出しを観察しました。

self.person = [[LJLKVOPerson alloc] init];
//注册观察者
[self.person addObserver:self
              forKeyPath:@"name"
                 options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                context:personNameContext];

文脈効果:迅速キー観測を検索します。
同じ名前のコンテキストを区別する必要性に、区別することは容易ではないときに、2つのキーパスが認められた場合。
しかし、また、あなたはどのような授業を観察する性質を知ることができ、このような形を通じて、そうすることは、より安全で効率的、ネスティングがコールバック多層で決定される低減します。関数は、そのタグに似ています。
idがnilを満たしている場合、この場所はそう、NULL可能void *型NULLを満たされているので、あなたは、NULLで埋めることができない場合。Xcodeのは、コンパイル時にnilをNULLに回すために私たちを助けるため、また、nilを埋めることができます。

1.2、コールバック実装
の登録を、観察者が通知を受信するために、次のコールバックを実装する必要があります。

-(void)observeValueForKeyPath:(NSString *)keyPath
                     ofObject:(id)object
                       change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                      context:(void *)context
{
//这里就可以通过注册的时候的context来快速判断出观察的哪个类的哪个属性变化触发的回调
    if (context == personNameContext) {
        NSLog(@"name change : %@",change);
    }else{
        NSLog(@"change - %@",change);
    }

//change - {
//        kind = 1; 这个就是观察的类型,1是set
//        new = "0.100000";
/*
     观察类型枚举 (被观察者变更的类型)
     NSKeyValueChangeSetting = 1,       设置 例如观察的是NSString 修改的时候就是这个类型
     NSKeyValueChangeInsertion = 2,     插入 例如观察的是NSMutableArray 添加数据的时候
     NSKeyValueChangeRemoval = 3,       删除
     NSKeyValueChangeReplacement = 4,   替换
*/
}

1.3、オブザーバを削除
    もはやオブザーバを削除してください、観察を続けていない、または例外が発生することがあります。いくつかの隠されたクラッシュを避けるために良い習慣を開発します。
    そして、あなたはそれを削除し、再登録する再び来ていない場合。これは、複数の呼び出しが発生した場合です。視聴者が単一の実施形態を使用している場合、観察対象でもあります削除されていない解放されません、ポインタフィールドがあるだろう、崩壊します。

[self.student removeObserver:self forKeyPath:@"name"];

より完全な3つのステップが、以下のように、当然のことながら、これは必ずしも彼らのニーズに応じて、単一の実施形態オブザーバで書かれている必要はありません。

#import "LGPerson.h"
{
//需要设置为公共的,否者不能在外部访问。要在外部赋值这个实例变量必须加 @public 公有.KVO不能监听实例变量
    @public
    NSString *nikcName;
}
@interface LGStudent : LGPerson
+ (instancetype)shareInstance;
@end

//------------------------------------------------
#import "LGStudent.h"
@implementation LGStudent
static LGStudent* _instance = nil;
+ (instancetype)shareInstance{
    static dispatch_once_t onceToken ;
    dispatch_once(&onceToken, ^{
        _instance = [[super allocWithZone:NULL] init] ;
    }) ;
    return _instance ;
}
@end

//------------------------------------------------
#import "LJLKVOViewController.h"
#import "LGStudent.h"

@interface LJLKVOViewController ()
@property (nonatomic, strong) LGStudent *student;
@end

@implementation LJLKVOViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.student = [LGStudent shareInstance];
//1、注册观察者
    [self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}
//触摸事件修改student.name 触发监听回调
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.student.name = @"hello word";
//    外部访问实例变量 需要设置为公共
    self.student->nikcName = @"ljl";
}
#pragma mark - KVO回调
//2、实现回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    
    NSLog(@"LJLKVOViewController :%@",change);
}

//3、移除观察者
- (void)dealloc{
    [self.student removeObserver:self forKeyPath:@"name"];
}
@end

1.4型の属性のセット

    観測のコレクションは、KVCに基づいていなければならないとき。KVCによって直接アクセスする方が便利。
    LJLKVOPerson.hは、変数、配列のプロパティを追加します

@property(nonatomic, strong) NSMutableArray * kvoArray;

KVOをサインアップし、kvoArrayを変更

//    一定要初始化,否者 kvoArray为nil addObject:的时候就崩溃了
    self.person.kvoArray = [NSMutableArray array];
    [self.person addObserver:self
                  forKeyPath:@"kvoArray"
                     options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                     context:NULL];

    [self.person.kvoArray addObject:@"123"];
//    上面这样这样写是无法触发KVO 回调的,因为addObject:方法是不走setter 的。 而KVO监听是setter方法.需要将上述向数组添加元素的方法修改一下
    [[self.person mutableArrayValueForKey:@"kvoArray"] addObject:@"123"];
//    这样做之后 self.person.kvoArray 指向一个新的数组对象,相当于:
//    NSMutableArray * tmp = [NSMutableArray arrayWithArray:self.person.kvoArray];
//    [tmp addObject:@"123"];
//    self.person.kvoArray = tmp;
//    所以能触发KVO回调

1.5は、複数の関連する属性を観察
    :ダウンロードをシミュレートするために使用、3つの属性のTotalBytesの、completedBytes、および進捗状況の進捗状況の割合があり、そのようLJLKVODownloaderクラスなどを
    UI層に我々は唯一の進捗状況を懸念しているが、進捗状況は、この影響を与える他の二つの性質の対象となりLJLKVODownloaderは、2つのメソッドを書き換える必要なとき:

#import <Foundation/Foundation.h>

@interface LJLKVODownloader : NSObject
@property(nonatomic, assign) unsigned long long totalBytes;//总字节
@property(nonatomic, assign) unsigned long long completedBytes;//完成字节
@property(nonatomic, copy) NSString * progress;//进度
@end
#import "LJLKVODownloader.h"

@implementation LJLKVODownloader
//返回属性的一组键路径,这些属性的值会影响键控属性的值
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    NSSet * keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"progress"]) {
        NSArray * dependKeys = @[@"totalBytes",@"completedBytes"];
//        通过从数组中添加对象来设置
        keyPaths = [keyPaths setByAddingObjectsFromArray:dependKeys];
    }
    return keyPaths;
}

- (NSString *)progress{
    if (self.totalBytes == 0 || self.completedBytes == 0) {
        return @"0%";
    }
    double progress = (double)self.completedBytes/(double)self.totalBytes*100;
    if (progress > 100) {
        progress = 100;
    }
    return [NSString stringWithFormat:@"%d%%",(int)ceil(progress)];
}
@end
    LJLKVODownloader * downloader = [[LJLKVODownloader alloc] init];
    downloader.totalBytes = 205;
    [downloader addObserver:self
                 forKeyPath:@"progress"
                    options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                    context:NULL];
    downloader.completedBytes = 64;
    downloader.completedBytes = 168;

モニターのコールバックを結果は以下の通りであります

2020-03-08 00:34:53.547235+0800 filedome[31182:820584] change : {
    kind = 1;
    new = "32%";
    old = "0%";
}
2020-03-08 00:34:53.547581+0800 filedome[31182:820584] change : {
    kind = 1;
    new = "82%";
    old = "32%";
}

 

第二に、自動または手動スイッチを観察

    KVOはそれを理解する前に、我々はKVCを理解する必要があります。
    KVCが見つからない場合、戻りYES、メンバ変数を探しに行く場合にクラスメソッド+ accessInstanceVariablesDirectly(ダイレクト・アクセス・インスタンス変数)を呼び出し、セッターまたはゲッターに呼び出される
    KVOする同様のメカニズムであり、KVOにおける3つのインターフェースが存在しますインタフェース:

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;(自动通知观察者)

+ automaticallyNotifiesObserversForKey:デフォルトのリターンはYES、ミドルクラスが動的に変更が呼ばれた前と後に、プロパティに投機をセッターを書き換える作成
 :-willChangeValueForKey
 ビューアを知らせるために目的を達成するために同様の方法:と-didChangeValueForKey。
 サブクラスのオーバーライド+ automaticallyNotifiesObserversForKey場合:とNOへの復帰は、自動KVO通知メカニズムをトリガすることはできませんが、我々は、手動で-willChangeValueForKeyを呼び出すことができますし、-didChangeValueForKey:KVOのコールバックをトリガします。

それが開いているか閉じて自動自動観測であるかどうかを//手動観察は、コールバックを観察する
コールバックがdidChangeValueForKeyトリガーされる//:
//自動観測は、デフォルトでは、システムは、私たちを助けるとメソッドをしただろう追加されています。あなたは彼の手で再びそれを記述する場合、それはやったトリガコールバックメソッド時間がかかり、あること、二回トリガされます

#import "LJLKVOPerson.h"
@implementation LJLKVOPerson

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    return YES;//自动观察
}

- (void)setName:(NSString *)name{
//    手动观察。不管是打开自动观察还是关闭自动观察都会回调
// 触发回调的是 didChangeValueForKey 
// 自动观察 也就是默认情况下,系统是帮我们添加了will 和 did 方法。如果在自己手动写一遍的话就会触发两次,也就是走一次did 方法就会触发一次回调
    [self willChangeValueForKey:@"name"];
    _name = name;
    [self didChangeValueForKey:@"name"];//这个方法里面触发回调
}
@end

 

第三に、KVOの原則の実現

 KVO公式文書が自動的にKVO観測は、ISA-スウィズルキーを達成するための技術を使用することであると言います

  1. 動的に生成されたサブクラス - NSKVONotifying_xxx
  2. 動的セッターメソッドを追加
  3. 動的にクラスメソッドを追加
  4. 動的にdeallocメソッドを追加
  5. 手動の観察を開きます
  6. 私たちの元のクラスへのメッセージnewValueに
  7. メッセージ - 応答のコールバックメソッド

    2.2原理

検証:

 self.person = [[LJLKVOPerson alloc] init];
//1、下一行下断点
 [self.person addObserver:self
               forKeyPath:@"name"
                  options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                  context:personNameContext];
//2、下一行下断点
 self.person.name = @"liujilou";

トップは、その後に、llbdデバッグをマークしたコードでブレークポイント

//    LLBD调试
//    1位置断点
(lldb) po object_getClassName(self.person)
"LJLKVOPerson"

//    走到2位置断点
(lldb) po object_getClassName(self.person)
"NSKVONotifying_LJLKVOPerson"

IIを確認します。

    LJLKVOPerson * person = [[LJLKVOPerson alloc] init];
    [self printClasses:[LJLKVOPerson class]];
    [person addObserver:self
             forKeyPath:@"name"
                options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
                context:NULL];
    [self printClasses:[LJLKVOPerson class]];
#pragma mark - 遍历类以及子类
- (void)printClasses:(Class)cls{
    
    // 注册类的总数
    int count = objc_getClassList(NULL, 0);
    // 创建一个数组, 其中包含给定对象
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
    // 获取所有已注册的类
    Class* classes = (Class*)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    for (int i = 0; i<count; i++) {
//        判断cls 是否等于classes[i] 的父类
        if (cls == class_getSuperclass(classes[i])) {
//            将cls 的所有子类添加进来
            [mArray addObject:classes[i]];
        }
    }
    free(classes);
    NSLog(@"classes = %@", mArray);
}
2020-03-07 22:31:31.788378+0800 filedome[29720:765937] classes = (
LJLKVOPerson
                                                                  )
2020-03-07 22:31:31.799907+0800 filedome[29720:765937] classes = (
LJLKVOPerson,
"NSKVONotifying_LJLKVOPerson"
                                                                  )
    

上記により、二つの検証は、我々が知っていることができます。

  1. 実際、中産階級を生成:NSKVONotifying_LJLKVOPerson
  2. NSKVONotifying_LJLKVOPersonの継承とLJLKVOPerson
  3. 伊佐self.personだけでなく、ミドルクラスへのオブジェクトポイント。

サブクラスダイナミックな研究、サブクラスを勉強し始めた:ISAスーパーcache_tビット - 方法 - 変数

さんは背中を検証するプロセスを続けましょう:

    self.person = [[LJLKVOPerson alloc] init];
    [self printClasses:[LJLKVOPerson class]];
    [self printClassAllMethod:[LJLKVOPerson class]];
    [self.person addObserver:self
             forKeyPath:@"name"
                options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
                context:NULL];
    [self printClasses:[LJLKVOPerson class]];
    [self printClassAllMethod:NSClassFromString(@"NSKVONotifying_LJLKVOPerson")];
#pragma mark - 遍历方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
    NSLog(@"*********************");
    unsigned int count = 0;
//  需要导入  #import <objc/runtime.h>
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i<count; i++) {
        Method method = methodList[i];
        SEL sel = method_getName(method);
        IMP imp = class_getMethodImplementation(cls, sel);
        NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
    }
    free(methodList);
}
    2020-03-07 22:59:18.260869+0800 filedome[30061:779679] classes = (
    LJLKVOPerson
                                                                      )
    2020-03-07 22:59:18.261029+0800 filedome[30061:779679] *********************
    2020-03-07 22:59:18.261129+0800 filedome[30061:779679] kvoArray-0x10b30e410
    2020-03-07 22:59:18.261252+0800 filedome[30061:779679] setKvoArray:-0x10b30e430
    2020-03-07 22:59:18.261348+0800 filedome[30061:779679] .cxx_destruct-0x10b30e470
    2020-03-07 22:59:18.261437+0800 filedome[30061:779679] name-0x10b30e3e0
    2020-03-07 22:59:18.261522+0800 filedome[30061:779679] setName:-0x10b30e340
    2020-03-07 22:59:18.272516+0800 filedome[30061:779679] classes = (
     LJLKVOPerson,
     "NSKVONotifying_LJLKVOPerson"
                                                                      )
    2020-03-07 22:59:18.272853+0800 filedome[30061:779679] *********************
    2020-03-07 22:59:18.273394+0800 filedome[30061:779679] setName:-0x10bfb0b5e
    2020-03-07 22:59:18.273505+0800 filedome[30061:779679] class-0x10bfaf592
    2020-03-07 22:59:18.273672+0800 filedome[30061:779679] dealloc-0x10bfaf336
    2020-03-07 22:59:18.273793+0800 filedome[30061:779679] _isKVOA-0x10bfaf32e

上記印刷物からNSKVONotifying_LJLKVOPersonが親LJLKVOPersonを書き換え知っています

  1. setName(セッター)
  2. クラス
  3. dealloc
  4. _isKVOA

これらのメソッド。この場所は、独自のいくつかの実際の方法をプリントアウトすることができます。自分の親を印刷する方法がないではない、それは書き換えられ、これらの方法をプリントアウトして証明することができます

リソースを解放するためにdeallocメソッドを書き換えます。

このプライベートメソッドはクラスKVOは、クラスを主張するためのメカニズムであることを示すために使用される書き換え_isKVOA。


    セッターが観察された

        視聴者のクラスインスタンスが最初に登録されている場合、システムは次の操作を行います
    NSKVONotifying_xxx:からこのクラスの継承の動的に生成された中間体1を
    中間クラスに元のオブジェクトのポイントを変更するISA(ISA-、2 )スウィズリング
    。3、サブクラスオーバーライド級元のクラス、サブクラスに依然として返すメソッドとしない
    4、メソッドオーバーライド-dealloc
    対応する属性セッターキーパス書き換え5、
    メソッドを追加6、-_isKVOA

このクラスまたはメソッド戻りのNSKVONotifying_xxx(LJLKVOPerson)LJLKVOPerson()。私たちの動作やLJLKVOPerson()に知らせます

中産階級を解放するために、コールを解放するために必要なのdealloc


戻ってくるかどうかを観察ISA削除しますか?

そして、もはやポイントNSKVONotifying_xxxポイントバックxxxのISA観察を削除

検証:

- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"name"];
}

前と後にブレークポイントし、コンソールを削除知って印刷することができます。

//在dealloc 移除观察者之前打断点
po object_getClassName(self.person)
"NSKVONotifying_LJLKVOPerson"

//移除观察者之后打断点,然后 LLBD
po object_getClassName(self.person)
"LJLKVOPerson"
//可以发现isa 又指向了 LJLKVOPerson

 

5:破壊されたかどうかを確認するために、子を除去した後の動的な中産階級?

次の使用を容易にするために、破壊しません。あなたが破壊されたすべての時間を削除する場合は、遅すぎるを作成し登録。

検証:

書き込み登録して、連続してLJLKVOViewControllerのViewControllerの観察、および削除時に観察、その後のViewControllerページに戻ります。

ViewControllerに次のコードを追加します。トラバーサルサブクラスLJLKVOPersonクラスがNSKVONotifying_LJLKVOPersonかどうかを確認するためにオブザーバーを除去した後。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self printClasses:[LJLKVOPerson class]];
}
#pragma mark - 遍历类以及子类
- (void)printClasses:(Class)cls{
    
    // 注册类的总数
    int count = objc_getClassList(NULL, 0);
    // 创建一个数组, 其中包含给定对象
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
    // 获取所有已注册的类
    Class* classes = (Class*)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    for (int i = 0; i<count; i++) {
        if (cls == class_getSuperclass(classes[i])) {
            [mArray addObject:classes[i]];
        }
    }
    free(classes);
    NSLog(@"LJLKVOViewController:classes = %@", mArray);
}
2020-03-08 01:40:23.120880+0800 002---KVO原理探讨[31915:843163] LJLKVOViewController:classes = (
    LJLKVOPerson,
    "NSKVONotifying_LJLKVOPerson",
    LGStudent
)

オブザーバを削除した後、ダイナミックサブクラスが破壊されることはありません、見ることができます。

簡単に言うと要約したものです。

第1の登録オブザーバの場合クラスのインスタンス、システムは次の操作を行います
    元のクラスから1、動的に生成された中間クラスの継承:NSKVONotifying_xxx
    2を、中間クラスNSKVONotifying_xxx(ISA-スウィズリング)に、元のオブジェクト点を変更ISA
    3。サブクラスオーバーライドクラスメソッド、依然として元のクラス、サブクラスではなくに戻り
    オーバーライドdeallocメソッド、4
    セッター5、対応する属性キーパスオーバーライド(特性を観察することが可能であるが、観察されないインスタンス変数)
    6は、_加えisKVOA方法


     KVO原則
     1:動的にサブカテゴリーを生成する:NSKVONotifying_xxx
     2:セッターは、(それが観察された特性ではなく、観測されたインスタンス変数であり得る)が観察される
     3:ダイナミックサブクラスは、多くの方法のsetNickName(セッター)、クラスのdealloc、_isKVOAをオーバーライド
     4。 :ISA時に戻ってきて、観測ポイントを削除
     ダイナミックサブクラスが破壊しない:5

 

KVOの長所と短所

利点:

  1. 2つのオブジェクト間の同期を達成するための簡単な方法を提供することが可能
  2. 私たちは、応答にそのオブジェクトの内部状態変化を非オブジェクトを作成することができますし、内部オブジェクトの実装を変更する必要はありません。最新値は、プロパティとその前の値のビューを提供することができます。
  3. 性質を観察するための鍵のパスで、それはまた、ネストされたオブジェクトを観察することができます
  4. 観察されることを可能観察する追加コードので、観察対象物の完全な抽象化、

短所:

  1. それがあるので、観察されたsetterメソッドはプロパティのみが観測されたインスタンス変数にはできません観察することができます
  2. コードを観察するために私たちをリードするプロパティの復興には使用できなくなりました
  3. リリースオブザーバーオブザーバを削除、またはいくつかの隠されたエラーかどうかをする必要がある場合
公開された83元の記事 ウォン称賛12 ビュー180 000 +

おすすめ

転載: blog.csdn.net/shengdaVolleyball/article/details/104725189