Introductions to the 0.

We often make fun of the difficulty of interviewing, and there is even a saying “interview builds the rocket, develops the screw”. As a client developer, the interview asks you to hand out a red-black tree on the spot, which is very difficult unless you have prepared for it.

But the common test points we need to know. Sometimes the test points may be packaged, may not be able to see, but after seeing the test points will have the sense of enlightenment. Because it’s the same thing, old wine in new bottles. Just like the original science exam questions, package a new scene and ask you to solve a problem in this scene, but the theoretical knowledge is learned.

All right, without further ado, let’s move on to our question.

1. The interview questions

1.1 the topic

Let’s start with the warm-up and work our way through:

  • The interview questions 1

    You have an instance object that inherits from NSObject, and you need to change the behavior of a method without directly modifying its implementation. What do you do?

    By not modifying the method implementation directly, I mean not modifying the internal implementation of the method in the.m file directly

    Method Swizzling Runtime Method Swizzling Runtime You’ll probably ask a few more in-depth questions about Method Swizzling after that. The next difficulty level.

  • The interview questions 2

    Problem 1: If you use Method Swizzling, you change the relationship between Method selectors and IMP implementations in class objects. This causes all subclasses and instance objects that inherit from this class to be affected. How do you control the scope of the affected object, or how do you make method behavior changes only apply to this instance object?

    The difficulty of this question has risen, but isn’t there a kind of feeling out of production, to interview you for a question?

    Let’s wrap this up a little bit, make it seem more grounded, and escalate it a little bit.

  • The interview question 3

    We have a view, and we need to expand its response scope. If Method Swizzling technology is used, the affected range will be relatively large. Of course, you can also choose to inherit a subclass. But now that the instance has been created, how would you implement the same requirement?

    Now the problem is starting to get closer to production. In general, modifying the response scope involves the knowledge of the response chain and event delivery.

    • If you can inherit, of course, you can choose to copy two methods to solve.
      • – hitTest:withEvent:
      • – pointInside:withEvent:

    The option of inheriting and creating instances of subclasses is now limited to the alternative.

    • If the answer to Method Swizzling technology involves the scope of influence, it may need to add switches and variables recorded to expand the response range, etc., which also involves the problems related to the associated objects.

    Now we’re also limiting the Method Swizzling scheme, so what else can we do?

    The answer is again Runtime technology. But this involves two Runtime points: message sending and forwarding and ISa-swizzling.

    • Message sending and forwarding: this is mainly the method lookup process after objc_msgSend. If you ask further, you will come to the topic of message forwarding.
    • isa-swizzling: commonKVOPrinciple test point, but actually speaking ofisa-swizzlingWill be accompanied bySending and forwarding messagesThe problem. Because it’s modifiedisaPoint to, executeobjc_msgSendThe search process will change.

In fact, from the first asked the third question, the core of the question is Isa-Swizzling, but through layers of packaging may involve many knowledge points, into a compound interview question.

1.2 the sample

Let’s write an example:

@interface Person : NSObject
@property (nonatomic.strong.nullable) NSString *firstName;
@property (nonatomic.strong.nullable) NSString *lastName;
@end

@implementation Person
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *person = [[Person alloc] init];
    person.firstName = @"Tom";
    person.lastName = @"Google";
    
    NSLog(@"person full name: %@ %@", person.firstName, person.lastName);
}
@end
Copy the code

Now, after the Person instance is created, change the return value of lastName to fix it back to Apple.

@interface Person : NSObject
@property (nonatomic.strong.nullable) NSString *firstName;
@property (nonatomic.strong.nullable) NSString *lastName;
@end

@implementation Person
@end

NSString *demo_getLastName(id self, SEL selector)
{
    return @"Apple";
}

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *person = [[Person alloc] init];
    person.firstName = @"Tom";
    person.lastName = @"Google";
    
    NSLog(@"person full name: %@ %@", person.firstName, person.lastName);
    
    // 1. Create a subclass
    NSString *oldName = NSStringFromClass([person class]);
    NSString *newName = [NSString stringWithFormat:@"Subclass_%@", oldName];
    Class customClass = objc_allocateClassPair([person class], newName.UTF8String, 0);
    objc_registerClassPair(customClass);
    // 2. Override the get method
    SEL sel = @selector(lastName);
    Method method = class_getInstanceMethod([person class], sel);
    const char *type = method_getTypeEncoding(method);
    class_addMethod(customClass, sel, (IMP)demo_getLastName, type);
    // 3. Modify the isa pointer (ISA swizzling)
    object_setClass(person, customClass);
    
    NSLog(@"person full name: %@ %@", person.firstName, person.lastName);
    
    Person *person2 = [[Person alloc] init];
    person2.firstName = @"Jerry";
    person2.lastName = @"Google";
    NSLog(@"person2 full name: %@ %@", person2.firstName, person2.lastName);
}
@end
/ / output
person full name: Tom Google
person full name: Tom Apple
person2 full name: Jerry Google
Copy the code

As you can see from the output, we changed the behavior of the Person object lastName using ISa-Swizzling, while the Person2 object was unaffected.

We generally know that ISa-Swizzling is the underlying principle of KVO, but we can not only know how to make KVO.

Through this interview question, I would like to introduce an idea of how to use ISa-Swizzling in daily development.

Here’s the KVO principle, if you’re confident you’re already familiar with it, don’t bother

If you find this interview question helpful, give me a thumbs up ~ 👍🏻

2. Explore KVO from shallow to deep

Let’s go back to KVO, which applies this principle.

2.1 KVO application

Let me give you a simple question about the daily application of KVO.

@interface Person : NSObject
@property (nonatomic.strong.nullable) NSString *firstName;
@property (nonatomic.strong.nullable) NSString *lastName;
@property (nonatomic.strong.readonly) NSString *fullName;
@end

@implementation Person
- (NSString *)fullName {
    return [NSString stringWithFormat:@ % @ % @ "".self.firstName, self.lastName];
}
@end
Copy the code

How to execute notification fullName changed when changing firstName or lastName. This section is highly recommended if your idea is to manually call willChangeValueForKey: and didChangeValueForKey: in the set method of firstName or lastName.

2.1.1 Automatic Notification

// Call set
[account setName:@"Savings"];

// Use KVC forKey or forKeyPath
[account setValue:@"Savings" forKey:@"name"];
[document setValue:@"Savings" forKeyPath:@"account.name"];

// Use mutableArrayValueForKey to retrieve the relational proxy object
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
Copy the code

The sample

@interface ViewController(a)
@property (nonatomic.strong) Person *person;
@property (nonatomic.strong) NSMutableArray<Person *> *people;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    / / the collection
    self.person = [[Person alloc] init];
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    self.person.name = @"Tom";
    [self.person setValue:@"Jerry" forKey:@"name"];
    [self setValue:@"Tom" forKeyPath:@"person.name"];
    / / collection
    self.people = [NSMutableArray array];
    Person *person0 = [[Person alloc] init];
    person0.name = @"Tom";
    [self.people addObject:person0];
    Person *person1 = [[Person alloc] init];
    person1.name = @"Jerry";
    [self.people addObject:person1];
    NSString *key = @"people";
    [self addObserver:self forKeyPath:key options:NSKeyValueObservingOptionNew context:nil];
    Person *person2 = [[Person alloc] init];
    person2.name = @"Frank";
    NSMutableArray *people = [self mutableArrayValueForKey:key];
    [people addObject:person2];
    NSLog(@"People: \n%@".self.people);
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"new name: %@", change[NSKeyValueChangeNewKey]);
    } else if ([keyPath isEqualToString:@"people"]) {
        NSLog(@"new array: %@", change[NSKeyValueChangeNewKey]);
        NSArray<Person *> *people = change[NSKeyValueChangeNewKey];
        NSLog(@"new person: %@", people.firstObject.name); }}@end
/ / output
new name: Tom
new name: Jerry
new name: Tom
new array: (
    "<Person: 0x60000276cc20>"
)
new person: Frank
People: 
(
    "Person name: Tom"."Person name: Jerry"."Person name: Frank"
)
Copy the code

2.1.2 Manual Notification

Manual notifications provide a freer way to decide when and how to notify observers. Want to use the manual notice must implement automaticallyNotifiesObserversForKey: (or automaticallyNotifiesObserversOf < Key >) method. It is possible to use both automatic and manual notifications in one class. For a property that you want to manually inform, you can return NO based on its keyPath, and for keyPath elsewhere, you return the parent method.

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"name"]) {
        return NO;
    } else {
        return [superautomaticallyNotifiesObserversForKey:key]; }}/ / or
+ (BOOL)automaticallyNotifiesObserversOfName {
    return NO;
}
Copy the code
One-to-one relationship
- (void)setOpeningBalance:(double)theBalance {
     if(theBalance ! = _openingBalance) { [self willChangeValueForKey:@"openingBalance"];
        _openingBalance = theBalance;
        [self didChangeValueForKey:@"openingBalance"]; }}Copy the code

If an operation results in multiple property changes, nested notifications are required:

- (void)setOpeningBalance:(double)theBalance {
     [self willChangeValueForKey:@"openingBalance"];
     [self willChangeValueForKey:@"itemChanged"];
     _openingBalance = theBalance;
     _itemChanged = _itemChanged + 1;
     [self didChangeValueForKey:@"itemChanged"];
     [self didChangeValueForKey:@"openingBalance"];
}
Copy the code
A one-to-many relationship

It is important to note that not only does the key change, but also the type and index it changes.

- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
     [self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"transactions"];
     // Remove the transaction objects at the specified indexes.
     [self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"transactions"];
}
Copy the code

2.1.3 Dependencies between keys

In many cases, the value of an attribute depends on attributes in other objects. If the value of a dependent property changes, that property also needs to be notified.

One-to-one relationship
@interface Person : NSObject
@property (nonatomic.strong.nullable) NSString *firstName;
@property (nonatomic.strong.nullable) NSString *lastName;
@property (nonatomic.strong.readonly) NSString *fullName;
@end
Copy the code

Can override keyPathsForValuesAffectingValueForKey: method. Can also by implementing keyPathsForValuesAffecting < Key > methods to achieve the same effect in front of the, here < Key > is the attribute name, but the first letter is capitalized.

@implementation Person
- (NSString *)fullName {
    return [NSString stringWithFormat:@ % @ % @ "".self.firstName, self.lastName];
}

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName".@"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}
/ / or
+ (NSSet *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"lastName".@"firstName".nil];
}
@end
Copy the code
One-to-many relationship

KeyPathsForValuesAffectingValueForKey: method does not support a one-to-many relationship.

For example, let’s say you have a Department object and a number of Employee objects. Employee has a salary attribute. You might want the Department object to have a totalSalary attribute that depends on the salary of all employees.

Register the Department as the observer for all employees. The calculation is performed when Employee is added or removed.

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (context == totalSalaryContext) {
        [self setTotalSalary:[self valueForKeyPath:@"[email protected]"]];
    }
    else
    // deal with other observations and/or invoke super...
}
 
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
    if(totalSalary ! = newTotalSalary) { [self willChangeValueForKey:@"totalSalary"];
        _totalSalary = newTotalSalary;
        [self didChangeValueForKey:@"totalSalary"]; }} - (NSNumber *)totalSalary {
    return _totalSalary;
}
Copy the code

2.2 Implementation Details

2.2.1 isa – swizzling

KVO is implemented using a technique called ISa-Swizzling.

When a property of an object registers an observer, the isa pointer to the observed object points to an intermediate class that the system generated for us, rather than the class we created ourselves. In this class, the system overrides the setter method for the observed property for us.

The object_getClass(id obj) method is used to obtain the actual class of the instance object (to which the ISA pointer points).

@interface Person : NSObject
@property (nonatomic.strong.nullable) NSString *name;
@end
@implementation Person
@end
  
@interface ViewController(a)
@property (nonatomic.strong) Person *p1;
@property (nonatomic.strong) Person *p2;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.p1 = [[Person alloc] init];
    self.p2 = [[Person alloc] init];  
    self.p1.name = @"Tom";
    
  	NSLog(@"before kvo --- p2: %s", object_getClassName(self.p2));
    [self.p2 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    NSLog(@"after kvo --- p2: %s", object_getClassName(self.p2));
    
    self.p2.name = @"Jerry";
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"new name: %@", change[NSKeyValueChangeNewKey]); }}@end
/ / output
before kvo --- p2: Person
after  kvo --- p2: NSKVONotifying_Person
new name: Jerry
Copy the code

We print the ISA pointer (the actual class used) to the P2 instance object before and after it is observed by key value.

From the result, we can see that the ISA pointer points to an intermediate class, NSKVONotifying_Person.

Apple’s KVO middle class is named NSKVONotifying_ before the class name. If our class is called SonKVO, the middle class will be NSKVONotifying_Son.

2.2.2 IMP

Let’s see if the addresses of the KVO function methods are the same.

- (void)viewDidLoad {
    [super viewDidLoad];
    self.p1 = [[Person alloc] init];
    self.p2 = [[Person alloc] init];
    self.p1.name = @"Tom";
    
    NSLog(@"before kvo --- p1: %p p2: %p"[self.p1 methodForSelector:@selector(setName:)], [self.p2 methodForSelector:@selector(setName:)]);
    [self.p2 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    NSLog(@" after kvo --- p1: %p p2: %p"[self.p1 methodForSelector:@selector(setName:)], [self.p2 methodForSelector:@selector(setName:)]);
    
    self.p2.name = @"Jerry";
}
/ / output
before kvo --- p1: 0x10ccee670 p2: 0x10ccee670
after  kvo --- p1: 0x10ccee670 p2: 0x7fff258e454b
Copy the code

After KVO, the setName: method address of p2 instance object was changed.

We can look at the method address:

(lldb) image lookup -a 0x7fff258e454b
      Address: Foundation[0x000000000006954b] (Foundation.__TEXT.__text + 422667)
      Summary: Foundation`_NSSetObjectValueAndNotify
Copy the code

This is a private method _NSSetObjectValueAndNotify of Foundation framework.

Can see _NSSetObjectValueAndNotify or call the willChangeValueForKey: and didChangeValueForKey: for manual notifications.

2.3 Customizing KVO

Below we write a very simplified version of KVO according to the implementation details of KVO.

NSString *ObserverKey = @"SetterMethodKey";
// Get the Key from the method name
NSString *getKeyForSetter(NSString *setter) {
    NSRange range = NSMakeRange(3.setter.length - 4);
    NSString *key = [setter substringWithRange:range];
    NSString *letter = [[key substringToIndex:1] lowercaseString];
    key = [key stringByReplacingCharactersInRange:NSMakeRange(0.1) withString:letter];
    return key;
}
// Implement a setter and notification function
void _MySetObjectValueAndNotify(id self, SEL selector, NSString *name) {
    // 1. Call the parent method
    struct objc_super superClass = {
        self,
        class_getSuperclass([self class])}; objc_msgSendSuper(&superClass, selector, name);// 2. Notify the observer
    NSObject *observer = objc_getAssociatedObject(self, &ObserverKey);
    NSString *selectorName = NSStringFromSelector(selector);
    NSString *key = getKeyForSetter(selectorName);
    objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:), key, self, @ {NSKeyValueChangeNewKey: name}, nil);
}

@implementation Person
- (void)snx_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {
    // 1. Create a subclass
    NSString *oldName = NSStringFromClass([self class]);
    NSString *newName = [NSString stringWithFormat:@"CustomKVO_%@", oldName];
    Class customClass = objc_allocateClassPair([self class], newName.UTF8String, 0);
    objc_registerClassPair(customClass);
    // 2. Modify Modify the ISA pointer
    object_setClass(self, customClass);
    // 3. Override the set method
    NSString *selectorName = [NSString stringWithFormat:@"set%@:", keyPath.capitalizedString];
    SEL sel = NSSelectorFromString(selectorName);
    class_addMethod(customClass, sel, (IMP)_MySetObjectValueAndNotify, "v@:@");
    // 4. Bind observer
    objc_setAssociatedObject(self, &ObserverKey, observer, OBJC_ASSOCIATION_ASSIGN);
}
@end
Copy the code

important

If objc_msgSendSuper is used, the compiler may report an error:

Too many arguments to function call, expected 0, have 3

Solution: Change Enable Strict Checking of objc_msgSend Calls to No in Build Setting.

- (void)viewDidLoad {
    [super viewDidLoad];
    self.p1 = [[Person alloc] init];
    self.p2 = [[Person alloc] init];
    self.p1.name = @"Tom";
    
    NSLog(@"before kvo --- p2: %s", object_getClassName(self.p2));
    NSLog(@"before kvo --- p1: %p p2: %p"[self.p1 methodForSelector:@selector(setName:)], [self.p2 methodForSelector:@selector(setName:)]);
// [self.p2 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    [self.p2 snx_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    NSLog(@"after kvo --- p2: %s", object_getClassName(self.p2));
    NSLog(@"after kvo --- p1: %p p2: %p"[self.p1 methodForSelector:@selector(setName:)], [self.p2 methodForSelector:@selector(setName:)]);
    
    self.p2.name = @"Jerry";
}
/ / output
before kvo --- p2: Person
before kvo --- p1: 0x103514460 p2: 0x103514460
after  kvo --- p2: CustomKVO_Person
after  kvo --- p1: 0x103514460 p2: 0x103513f90
new name: Jerry
Copy the code

If you found this article helpful, give me a thumbs up ~ 👍🏻