Tìm hiểu về KVC và KVO trong Objective-C và Swift

KVC (Key-Value Coding) và KVO (Key-Value Observing) là hai cơ chế mạnh mẽ trong Objective-C, cung cấp cách tiếp cận gián tiếp để truy cập và quan sát các thuộc tính của đối tượng. Trong Swift, KVO có những hạn chế và yêu cầu riêng, đồng thời có các giải pháp thay thế như Property Observers.

1. KVC (Key-Value Coding)

KVC là một cơ chế cho phép truy cập thuộc tính của đối tượng thông qua một chuỗi khóa (key) thay vì gọi trực tiếp phương thức setter/getter. Điều này mang lại sự linh hoạt, đặc biệt khi làm việc với các thuộc tính riêng tư hoặc khi thực hiện các thao tác như chuyển đổi dữ liệu từ Dictionary sang Model.

1.1. Gán giá trị cho thuộc tính thông qua Key Path

Bạn có thể sử dụng KVC để gán giá trị cho các thuộc tính, kể cả những thuộc tính không được định nghĩa rõ ràng trong phạm vi công khai của lớp.

Ví dụ với thuộc tính công khai:

// Animal.h
@interface Animal : NSObject
@property (nonatomic, strong) NSString *type;
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end

// Sử dụng Dot Syntax
Animal *animal = [[Animal alloc] init];
animal.type = @"dog";
animal.name = @"小白";
animal.age = 2;

// Sử dụng KVC
[animal setValue:@"cat" forKey:@"type"];
[animal setValue:@"小花猫" forKey:@"name"];
[animal setValue:@3 forKey:@"age"];

KVC cũng có thể truy cập các thuộc tính hoặc biến thành viên riêng tư:

// Animal.m
@implementation Animal {
    NSInteger _weight; // Biến thành viên riêng tư
}
- (void)logWeight {
    NSLog(@"Weight = %ld", _weight);
}
@end

// Gán giá trị cho biến thành viên riêng tư _weight
[animal setValue:@15 forKey:@"weight"]; // KVC có thể tìm thấy _weight
[animal logWeight]; // Output: Weight = 15

KVC tìm kiếm thuộc tính theo thứ tự ưu tiên: tên khóa khớp trực tiếp, sau đó là tên khóa bắt đầu bằng dấu gạch dưới (_).

1.2. Sử dụng Key Path để truy cập thuộc tính lồng nhau

Phương thức setValue:forKeyPath: cho phép truy cập và gán giá trị cho các thuộc tính lồng nhau, điều mà setValue:forKey: không làm được.

// Giả sử có lớp People với thuộc tính weight
// Animal có thuộc tính people kiểu People*

// Gán giá trị cho thuộc tính weight của đối tượng people bên trong animal
[animal setValue:@70 forKeyPath:@"people.weight"];
NSLog(@"People's weight = %@", [animal valueForKeyPath:@"people.weight"]);

forKeyPath: có khả năng mạnh mẽ hơn forKey: và nên được ưu tiên sử dụng.

1.3. Lấy giá trị thuộc tính

Bạn có thể sử dụng valueForKey: và valueForKeyPath: để lấy giá trị thuộc tính.

NSLog(@"Name: %@", [animal valueForKey:@"name"]);
NSLog(@"People's weight: %@", [animal valueForKeyPath:@"people.weight"]);

1.4. Dictionary to Model

KVC rất hữu ích cho việc chuyển đổi dữ liệu từ Dictionary sang đối tượng Model.

// Animal.h
- (instancetype)initWithDict:(NSDictionary *)dict;

// Animal.m
- (instancetype)initWithDict:(NSDictionary *)dict {
    if (self = [super init]) {
        [self setValuesForKeysWithDictionary:dict];
    }
    return self;
}

// Sử dụng
NSDictionary *animalDict = @{@"type":@"dog", @"name":@"小黑", @"age":@"3"};
Animal *animal2 = [[Animal alloc] initWithDict:animalDict];

Lưu ý khi sử dụng Dictionary to Model với KVC:

  • Key trong Dictionary phải tương ứng với tên thuộc tính trong Model, nếu không sẽ gây lỗi NSUnknownKeyException. Bạn có thể xử lý lỗi này bằng cách cài đặt phương thức setValue:forUndefinedKey:.
  • KVC không trực tiếp hỗ trợ chuyển đổi lồng nhau (model chứa model khác).

1.5. Các ứng dụng khác của KVC

  • Truy cập và sửa đổi thuộc tính nội bộ của các control hệ thống (ví dụ: placeholder của UITextField).
  • Thực hiện các phép toán trên tập hợp (Collection Operations) bằng cách sử dụng các toán tử như @sum, @count, @avg, @max, @min.
  • Thực hiện các phép toán trên đối tượng tập hợp (Object Operations) như @unionOfObjects, @distinctUnionOfObjects.
// Ví dụ: Tính tổng giá sách
@interface Book : NSObject
@property (nonatomic, assign) CGFloat price;
@end

NSArray* arrBooks = @[book1, book2, book3, book4];
NSNumber* totalPrice = [arrBooks valueForKeyPath:@"@sum.price"];

// Ví dụ: Lấy danh sách giá sách duy nhất
NSArray* distinctPrices = [arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];

2. KVO (Key-Value Observing)

KVO là một cơ chế dựa trên mô hình Observer, cho phép một đối tượng (observer) lắng nghe và phản ứng khi một thuộc tính cụ thể của đối tượng khác (subject) thay đổi. KVO được xây dựng dựa trên KVC.

2.1. Sử dụng KVO cơ bản

  1. Đăng ký quan sát: Sử dụng addObserver:forKeyPath:options:context:.
  2. Xử lý thay đổi: Triển khai phương thức observeValueForKeyPath:ofObject:change:context: trong đối tượng observer.
  3. Hủy đăng ký quan sát: Sử dụng removeObserver:forKeyPath:, thường trong phương thức dealloc hoặc viewWillDisappear để tránh crash.
// Trong đối tượng Subject (ví dụ: Animal)
// Đăng ký quan sát thuộc tính "name"
[animal2 addObserver:self
           forKeyPath:@"name"
              options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
              context:nil];

// Trong đối tượng Observer (ví dụ: ViewController)
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"Change details: %@", change);
        // Xử lý thay đổi giá trị của name
    }
}

// Trong dealloc của Observer
- (void)dealloc {
    [animal2 removeObserver:self forKeyPath:@"name"];
}

2.2. Tự động thông báo và Thủ công thông báo

  • Tự động: Theo mặc định, KVO tự động thông báo khi thuộc tính thay đổi.
  • Thủ công: Bạn có thể tắt thông báo tự động bằng cách cài đặt + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key hoặc + (BOOL)automaticallyNotifiesObserversOfPropertyName. Sau đó, bạn phải tự gọi willChangeValueForKey: và didChangeValueForKey: trong phương thức setter để kích hoạt KVO.
// Tắt tự động thông báo cho thuộc tính 'name'
+ (BOOL)automaticallyNotifiesObserversOfName {
    return NO;
}

// Thủ công thông báo trong setter
- (void)setName:(NSString *)name {
    if (_name != name) {
        [self willChangeValueForKey:@"name"]; // Thông báo trước khi thay đổi
        _name = name;
        [self didChangeValueForKey:@"name"];  // Thông báo sau khi thay đổi
    }
}

2.3. Thuộc tính phụ thuộc (Dependent Properties)

Bạn có thể khai báo rằng một thuộc tính phụ thuộc vào các thuộc tính khác.

+ (NSSet<NSString *> *)keyPathsForValuesAffectingName {
    return [NSSet setWithObjects:@"type", nil]; // Nếu 'type' thay đổi, KVO sẽ thông báo cho 'name'
}

2.4. Quan sát thay đổi trên đối tượng Collection

KVO không trực tiếp quan sát sự thay đổi của NSArray hoặc NSSet. Bạn cần đóng gói chúng vào một đối tượng Model và quan sát thuộc tính của Model đó. Sử dụng mutableArrayValueForKey: để có được một đối tượng proxy có thể thêm/bớt phần tử, từ đó kích hoạt KVO.

2.5. Các tình huống áp dụng KVO

  • Theo dõi thay đổi contentOffset của UIScrollView (cho Pull-to-refresh, Infinite scroll).
  • Cập nhật giao diện người dùng (UI) dựa trên thay đổi của Model.
  • Tạo hiệu ứng chuyển màu cho thanh điều hướng (Navigation Bar).
  • Xử lý các thay đổi trên đối tượng Collection.

3. So sánh KVC, KVO, Delegate và Notification

  • KVC vs KVO: KVC là cơ chế truy cập thuộc tính gián tiếp, KVO là cơ chế quan sát sự thay đổi của thuộc tính (dựa trên KVC).
  • KVO/Notification vs Delegate: Cả ba đều là cơ chế giao tiếp giữa các đối tượng. Delegate thường là quan hệ một-một, chặt chẽ và yêu cầu định nghĩa rõ ràng. KVO và Notification có thể là một-nhiều và xử lý việc gửi/nhận thông báo một cách hệ thống hơn.
  • KVO vs Notification: Notification yêu cầu bước gửi thông báo rõ ràng thông qua NotificationCenter. KVO ẩn đi quá trình thông báo và tập trung vào việc quan sát sự thay đổi thuộc tính. Notification linh hoạt hơn, có thể lắng nghe nhiều loại sự kiện hệ thống (như bàn phím, chuyển đổi ứng dụng), không chỉ giới hạn ở thay đổi thuộc tính.

4. Nguyên lý hoạt động của KVO

Khi một thuộc tính của đối tượng được quan sát bằng KVO:

  1. Hệ thống Runtime của Objective-C sẽ tạo ra một lớp con mới (ví dụ: NSKVONotifying_OriginalClass) kế thừa từ lớp gốc.
  2. Lớp con này sẽ ghi đè phương thức setter của thuộc tính được quan sát.
  3. Trong setter ghi đè, nó sẽ gọi các phương thức willChangeValueForKey: và didChangeValueForKey: trước và sau khi gọi setter gốc.
  4. Con trỏ isa của đối tượng ban đầu sẽ được thay đổi để trỏ đến lớp con mới này, khiến mọi tương tác với đối tượng (như gọi setter) đều đi qua lớp con và kích hoạt cơ chế thông báo KVO.
  5. Lớp con cũng ghi đè phương thức class để trả về lớp gốc, che giấu sự tồn tại của lớp con đối với lập trình viên.

5. KVO trong Swift

Trong Swift, KVO không được hỗ trợ mặc định cho tất cả các thuộc tính như trong Objective-C.

5.1. Điều kiện để sử dụng KVO trong Swift

  • Lớp chứa thuộc tính cần quan sát và lớp quan sát viên phải kế thừa từ NSObject.
  • Thuộc tính cần quan sát phải được đánh dấu bằng @objc dynamic.
class Person: NSObject {
    @objc dynamic var age: Int = 0 // Có thể quan sát bằng KVO
    var name: String = ""          // Không thể quan sát bằng KVO
}

// Đăng ký quan sát tương tự Objective-C
let person = Person()
person.addObserver(self, forKeyPath: "age", options: [.new, .old], context: nil)

// Xử lý trong observeValue
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    guard keyPath == "age" else { return super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) }
    // Xử lý thay đổi age
}

// Huỷ quan sát
deinit {
    person.removeObserver(self, forKeyPath: "age")
}

5.2. Giải pháp thay thế: Property Observers (willSet, didSet)

Swift cung cấp Property Observers (willSet và didSet) như một cách tự nhiên và hiệu quả hơn để theo dõi thay đổi thuộc tính mà không yêu cầu kế thừa từ NSObject hay sử dụng @objc dynamic.

struct Point {
    var x: Int = 0 {
        willSet { print("x sắp thay đổi thành \(newValue)") }
        didSet { print("x đã thay đổi từ \(oldValue) thành \(x)") }
    }
}

Hạn chế: Property Observers chỉ hoạt động trong cùng một scope (không thể quan sát thay đổi thuộc tính của đối tượng khác).

5.3. Lưu ý

  • Sử dụng @objc dynamic có thể ảnh hưởng đến hiệu năng do buộc phải sử dụng dynamic dispatch.
  • KVO chỉ áp dụng cho các lớp (class), không áp dụng cho cấu trúc (struct) hoặc enum.

Thẻ: KVC KVO objective-c Swift runtime

Đăng vào ngày 2 tháng 11 lúc 04:46