iOS一道複合型面試題與底層原理

0. 引言

咱們經常吐槽面試的難度,甚至出現了 「面試造火箭,開發擰螺絲」 說法。做爲客戶端開發人員,面試直接讓你現場手擼一個紅黑樹,難度是很大的,除非你專門準備過。面試

但常見的考點咱們是須要知道的。有時考點可能被包裝了一下,可能無法一下就看出來,但看破考點以後就會有恍然大悟的感受。由於本質仍是同樣的,都是新瓶裝舊酒。就像原來的理科考試題,包裝一個新的場景,讓你解決這個場景下的一個問題,但理論知識都是學過的。shell

好了,下面廢話很少說,進入咱們的問題。api

1. 面試題

1.1 題目

咱們從熱身開始,慢慢深刻:框架

  • 面試題1ide

    現有一個繼承於NSObject的實例對象,須要在不直接修改方法實現的狀況下,改變一個方法的行爲,你會怎麼作?函數

    不直接修改方法實現,指的是不直接修改.m文件中方法的內部實現flex

    這一道題比較簡單,其實問的就是 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
複製代碼

若是以爲本文對你有所幫助,給我點個贊吧~ 👍🏻

相關文章
相關標籤/搜索