iOS一道复合型面试题与底层原理

0. 引言

咱们经常吐槽面试的难度,甚至出现了 “面试造火箭,开发拧螺丝” 说法。做为客户端开发人员,面试直接让你现场手撸一个红黑树,难度是很大的,除非你专门准备过。面试

但常见的考点咱们是须要知道的。有时考点可能被包装了一下,可能无法一下就看出来,但看破考点以后就会有恍然大悟的感受。由于本质仍是同样的,都是新瓶装旧酒。就像原来的理科考试题,包装一个新的场景,让你解决这个场景下的一个问题,但理论知识都是学过的。shell

好了,下面废话很少说,进入咱们的问题。api

1. 面试题

1.1 题目

咱们从热身开始,慢慢深刻:markdown

  • 面试题1框架

    现有一个继承于NSObject的实例对象,须要在不直接修改方法实现的状况下,改变一个方法的行为,你会怎么作?ide

    不直接修改方法实现,指的是不直接修改.m文件中方法的内部实现函数

    这一道题比较简单,其实问的就是 RuntimeMethod Swizzling 。可能答出来以后,还会问几个 Method Swizzling 相关的深刻问题。下面难度升级。ui

  • 面试题2atom

    问题1,若是使用 Method Swizzling 技术,至关于修改了类对象中方法选择器和IMP实现的对应关系。这将致使继承自这个类的全部子类和实例对象都影响,如何控制受影响的范围,或者说如何让方法的行为改变只对这个实例对象生效?spa

    这个题难度上升了,可是不是有一种脱离生产的感受,为了面试你而出的一道题?

    咱们对这个问题包装一下,让它看起来更接地气,同时问题也再升级一点。

  • 面试题3

    现有一个视图,咱们须要扩大一下它的响应范围。若是使用 Method Swizzling 技术,受影响的范围会比较大。固然,也能够选择继承一个子类来实现。但若是如今实例已经建立了,仍是一样的需求,你会如何实现?

    如今问题开始接近生产了。通常来讲,修改响应范围涉及到 响应链和事件传递 的知识点。

    • 若是能够继承,固然能够选择复写两个方法来解决。
      • - hitTest:withEvent:
      • - pointInside:withEvent:

    如今限制了继承并建立子类实例 的方案,只能选择其余办法。

    • 若是回答 Method Swizzling 技术,又涉及到影响范围问题,可能须要加开关、加扩大响应范围记录的变量等,则又涉及到 关联对象 相关的问题。

    如今一样也限制了 Method Swizzling 方案,还有什么办法呢?

    答案仍是 Runtime 技术。但这个会涉及到2个 Runtime 考点:消息发送与转发 以及 isa-swizzling

    • 消息发送与转发:主要是 objc_msgSend 以后的方法查找流程。若是继续深刻问,会到 消息转发 相关的考点。
    • isa-swizzling :常见于 KVO 原理考点,但其实说到 isa-swizzling 确定会伴随着 消息发送与转发 问题。由于修改了isa的指向,执行 objc_msgSend 时的查找流程会发生变化。

其实,从第1问到第3问,问的核心都是 isa-swizzling ,但经过层层包装可能涉及到 多个知识点 ,变成一道复合型面试题。

1.2 示例

咱们来写一个例子:

@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
复制代码

如今要在建立了person实例后,修改lastName的返回值,将其固定返回 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.建立一个子类
    NSString *oldName = NSStringFromClass([person class]);
    NSString *newName = [NSString stringWithFormat:@"Subclass_%@", oldName];
    Class customClass = objc_allocateClassPair([person class], newName.UTF8String, 0);
    objc_registerClassPair(customClass);
    // 2.重写get方法
    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.修改修改isa指针(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
// 输出
person full name: Tom Google
person full name: Tom Apple
person2 full name: Jerry Google
复制代码

从输出结果能够看到,咱们使用 isa-swizzlingperson对象lastName的行为改变了,而person2对象没有受到影响。

咱们通常知道 isa-swizzlingKVO 的底层原理,但不能只知道拿来作 KVO

我想经过这个面试题,介绍一种如何在平常开发中使用 isa-swizzling 的思路。

下面是 KVO 原理,若是你很是自信已经熟悉这部份内容,能够不看了~

若是以为这个面试题对你有所帮助,给我点个赞吧~ 👍🏻

2. 由浅入深探索KVO

咱们再回到应用这个原理的 KVO 上。

2.1 KVO应用

给你们再出一道简单的关于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
复制代码

如何在修改firstNamelastName时,执行通知fullName变化了。若是你的思路是,在firstNamelastName的set方法中手动调用 willChangeValueForKey:didChangeValueForKey: ,那么强烈建议阅读此部分。

2.1.1 自动通知

// 调用set方法
[account setName:@"Savings"];

// 使用KVC forKey或forKeyPath
[account setValue:@"Savings" forKey:@"name"];
[document setValue:@"Savings" forKeyPath:@"account.name"];

// 使用 mutableArrayValueForKey: 检索关系代理对象
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
复制代码

示例

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

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    // 非集合
    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"];
    // 集合
    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
// 输出
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"
)
复制代码

2.1.2 手动通知

手动通知提供了更自由的方式去决定什么时间,什么方式去通知观察者。想要使用手动通知必须实现 automaticallyNotifiesObserversForKey: (或者 automaticallyNotifiesObserversOf<Key> )方法。在一个类中同时使用自动和手动通知是可行的。对于想要手动通知的属性,能够根据它的keyPath返回NO,而其对于其余位置的keyPath,要返回父类的这个方法。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"name"]) {
        return NO;
    } else {
        return [super automaticallyNotifiesObserversForKey:key];
    }
}
// 或者
+ (BOOL)automaticallyNotifiesObserversOfName {
    return NO;
}
复制代码
一对一关系
- (void)setOpeningBalance:(double)theBalance {
     if (theBalance != _openingBalance) {
        [self willChangeValueForKey:@"openingBalance"];
        _openingBalance = theBalance;
        [self didChangeValueForKey:@"openingBalance"];
     }
}
复制代码

若是一个操做会致使多个属性改变,须要嵌套通知:

- (void)setOpeningBalance:(double)theBalance {
     [self willChangeValueForKey:@"openingBalance"];
     [self willChangeValueForKey:@"itemChanged"];
     _openingBalance = theBalance;
     _itemChanged = _itemChanged + 1;
     [self didChangeValueForKey:@"itemChanged"];
     [self didChangeValueForKey:@"openingBalance"];
}
复制代码
一对多的关系

必须注意不只仅是这个key改变了,还有它改变的类型以及索引。

- (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"];
}
复制代码

2.1.3 键之间的依赖

在不少种状况下一个属性的值依赖于在其余对象中的属性。若是一个依赖属性的值改变了,这个属性也须要被通知到。

一对一关系
@interface Person : NSObject
@property (nonatomic, strong, nullable) NSString *firstName;
@property (nonatomic, strong, nullable) NSString *lastName;
@property (nonatomic, strong, readonly) NSString *fullName;
@end
复制代码

能够重写 keyPathsForValuesAffectingValueForKey: 方法。也能够经过实现 keyPathsForValuesAffecting<Key> 方法来达到前面一样的效果,这里的 <Key> 就是属性名,不过第一个字母要大写。

@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;
}
// 或者
+ (NSSet *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
@end
复制代码
一对多关系

keyPathsForValuesAffectingValueForKey: 方法不能支持一对多关系。

举个例子,好比你有一个Department对象,和不少个Employee对象。而Employee有一个salary属性。你可能但愿Department对象有一个totalSalary的属性,依赖于全部的Employeesalary

注册Department成为全部Employee的观察者。当Employee被添加或者被移除时进行计算。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (context == totalSalaryContext) {
        [self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
    }
    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;
}
复制代码

2.2 实现细节

2.2.1 isa-swizzling

KVO的实现用了一种叫 isa-swizzling 的技术。

当一个对象的一个属性注册了观察者后,被观察对象的isa指针的就指向了一个系统为咱们生成的中间类,而不是咱们本身建立的类。在这个类中,系统为咱们重写了被观察属性的setter方法。

经过 object_getClass(id obj) 方法能够得到实例对象真实的类(isa指针的指向)。

@interface Person : NSObject
@property (nonatomic, strong, nullable) NSString *name;
@end
@implementation Person
@end
  
@interface ViewController ()
@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
// 输出
before kvo --- p2: Person
after  kvo --- p2: NSKVONotifying_Person
new name: Jerry
复制代码

咱们在p2实例对象被键值观察的先后打印其isa指针(实际使用的类)。

从结果中咱们能够看到isa指针指向了一个中间类NSKVONotifying_Person

苹果的KVO中间类的命名规则是在类名前添加NSKVONotifying_,若是咱们的类叫SonKVO以后的中间类为NSKVONotifying_Son

2.2.2 IMP

咱们再看一下KVO先后的函数方法的地址是否同样。

- (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";
}
// 输出
before kvo --- p1: 0x10ccee670 p2: 0x10ccee670
after  kvo --- p1: 0x10ccee670 p2: 0x7fff258e454b
复制代码

咱们看到监听之间两个实例对象的 setName: 方法的函数地址相同,KVO以后p2实例对象的 setName: 方法地址变了。

咱们能够查看一下这个方法地址:

(lldb) image lookup -a 0x7fff258e454b
      Address: Foundation[0x000000000006954b] (Foundation.__TEXT.__text + 422667)
      Summary: Foundation`_NSSetObjectValueAndNotify
复制代码

这个是Foundation框架中的一个私有方法 _NSSetObjectValueAndNotify

Foundation __NSSetObjectValueAndNotify

能够看到 _NSSetObjectValueAndNotify 仍是调用了 willChangeValueForKey:didChangeValueForKey: 来进行手动通知的。

2.3 自定义KVO

下面咱们根据KVO的实现细节,仿写一个 很是简化版 的KVO。

NSString *ObserverKey = @"SetterMethodKey";
// 根据方法名获取Key
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;
}
// 实现一个setter和通知函数
void _MySetObjectValueAndNotify(id self, SEL selector, NSString *name) {
    // 1.调用父类的方法
    struct objc_super superClass = {
        self,
        class_getSuperclass([self class])
    };
    objc_msgSendSuper(&superClass, selector, name);
    // 2.通知观察者
    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.建立一个子类
    NSString *oldName = NSStringFromClass([self class]);
    NSString *newName = [NSString stringWithFormat:@"CustomKVO_%@", oldName];
    Class customClass = objc_allocateClassPair([self class], newName.UTF8String, 0);
    objc_registerClassPair(customClass);
    // 2.修改修改isa指针
    object_setClass(self, customClass);
    // 3.重写set方法
    NSString *selectorName = [NSString stringWithFormat:@"set%@:", keyPath.capitalizedString];
    SEL sel = NSSelectorFromString(selectorName);
    class_addMethod(customClass, sel, (IMP)_MySetObjectValueAndNotify, "v@:@");
    // 4.绑定观察者
    objc_setAssociatedObject(self, &ObserverKey, observer, OBJC_ASSOCIATION_ASSIGN);
}
@end
复制代码

重要

使用objc_msgSendSuper时,可能编译器会报错:

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

解决办法:在Build Setting修改Enable Strict Checking of objc_msgSend CallsNo

- (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";
}
// 输出
before kvo --- p2: Person
before kvo --- p1: 0x103514460 p2: 0x103514460
after  kvo --- p2: CustomKVO_Person
after  kvo --- p1: 0x103514460 p2: 0x103513f90
new name: Jerry
复制代码

若是以为本文对你有所帮助,给我点个赞吧~ 👍🏻

相关文章
相关标签/搜索