Skip to content

Objective-C

面试点

分类,关联对象,扩展,代理,通知,KVO,KVC,属性关键字

分类(Category)

1. 做了哪些事?

  • 声明私有方法,放在.m 中
  • 分解体积庞大的类文件
  • 把 framework 的私有方法公开

2. 特点

  • 运行时决议,添加的内容并没有附加到宿主类文件中,运行时通过 runtime 添加。
  • 可以为系统类添加分类。比如扩展 NSString, UIView 的方法。

3. 分类可以添加哪些内容?

  • 实例方法
  • 类方法
  • 协议
  • 属性

4. 结构

  • name: 分类的名称
  • cls: 宿主类
  • instanceMethods: 实例方法列表
  • classMethods: 类方法列表
  • protocols: 协议列表
  • instanceProperties: 实例属性列表

5. 分类的加载调用栈

  • _objc_init

    • map_2_images
      • map_images_nolock
        • read_images
          • remethodizeClass
  • 分类添加的方法可以覆盖原类方法。宿主的同名方法还在,需要特殊处理调用。

  • 同名分类方法谁能生效取决于编译顺序,最后编译的优先生效。
  • 名字相同的分类会引起编译报错。

关联对象

在Objective-C中,关联对象(Associated Objects)允许我们在运行时向类的实例动态添加属性,而不需要修改类的定义。这是通过Objective-C运行时的几个函数实现的。

1. 给分类添加 “成员变量”?

能,不能在分类的声明或者定义的时候直接添加成员变量,可以通过关联对象的方式为分类添加成员变量,达到目的。

#import <objc/runtime.h>

// 设置关联对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);

// 获取关联对象
id objc_getAssociatedObject(id object, const void *key);

// 移除所有关联对象
void objc_removeAssociatedObjects(id object);

关联策略

objc_setAssociatedObject 的最后一个参数是 objc_AssociationPolicy,它定义了如何管理关联对象的内存。常见的策略包括:

  • OBJC_ASSOCIATION_ASSIGN:弱引用,不保留对象。
  • OBJC_ASSOCIATION_RETAIN_NONATOMIC:强引用,非原子操作。
  • OBJC_ASSOCIATION_COPY_NONATOMIC:复制对象,非原子操作。
  • OBJC_ASSOCIATION_RETAIN:强引用,原子操作。
  • OBJC_ASSOCIATION_COPY:复制对象,原子操作。

示例:

假设我们有一个Person类,并希望在不修改类定义的情况下为其添加一个address属性:

#import <objc/runtime.h>

@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end

@implementation Person
@end

@interface Person (Address)
@property (nonatomic, strong) NSString *address;
@end

@implementation Person (Address)
static const char *kAddressKey = "kAddressKey";

- (void)setAddress:(NSString *)address {
    objc_setAssociatedObject(self, kAddressKey, address, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)address {
    return objc_getAssociatedObject(self, kAddressKey);
}
@end

2. 关联对象的本质

关联对象由 AssociationsManager 管理并在 AssociationsHashMap 存储。

所有对象的关联内容都在同一个全局容器中

清除关联对象的值, setValue = nil

ObjcAssociation

  • OBJC_ASSOCIATION_COPY_NONATOMIC
  • @"Hello"

ObjcAssociation 对象封装了 id 类型的 object (@"Hello"),以及 Policy (OBJC_ASSOCIATION_COPY_NONATOMIC)

ObjectAssociationMap

  • @selector(text)
  • ObjcAssociation

上一步进一步封装,key(@selector(text)), ObjcAssociation 关联起来存储在 ObjectAssociationMap 结构中。

AssociationHashMap

  • DISGUISE(obj)
  • ObjectAssociationMap

上一步的封装会通过 当前被关联对象的指针值(DISGUISE(obj)), 和 ObjectAssociationMap 进行关联存储在全局的 AssociationHashMap 结构中。

数据结构

理解关联对象的底层实现,可以借助以下几个概念:

  1. ObjcAssociation:这是表示单个关联对象的结构,包含键(key)、值(value)和关联策略(policy)。
  2. ObjectAssociationMap:这是每个对象关联对象的哈希表,键通常是SEL(选择器)或其他唯一标识符,值是ObjcAssociation
  3. AssociationHashMap:全局哈希表,键是对象指针(disguised pointer),值是ObjectAssociationMap

伪代码示例

为了理解,可以通过以下伪代码描述如何存储和获取关联对象:

// 设置关联对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
    // 获取全局的AssociationHashMap
    AssociationHashMap *globalMap = getGlobalAssociationHashMap();

    // 获取对象的ObjectAssociationMap
    ObjectAssociationMap *assocMap = globalMap[DISGUISE(object)];
    if (!assocMap) {
        assocMap = createNewObjectAssociationMap();
        globalMap[DISGUISE(object)] = assocMap;
    }

    // 创建新的ObjcAssociation并添加到ObjectAssociationMap
    ObjcAssociation assoc = { .key = key, .value = value, .policy = policy };
    assocMap[key] = assoc;
}

// 获取关联对象
id objc_getAssociatedObject(id object, const void *key) {
    // 获取全局的AssociationHashMap
    AssociationHashMap *globalMap = getGlobalAssociationHashMap();

    // 获取对象的ObjectAssociationMap
    ObjectAssociationMap *assocMap = globalMap[DISGUISE(object)];
    if (!assocMap) return nil;

    // 获取关联的ObjcAssociation
    ObjcAssociation assoc = assocMap[key];
    return assoc.value;
}

解释你提到的具体内容

  1. ObjcAssociation:这是关联对象的结构,包含key(键)、value(值)和policy(关联策略)。

  2. OBJC_ASSOCIATION_COPY_NONATOMIC:这是一个关联策略,表示关联对象在设置时会被复制,且复制操作是非原子的。

  3. ObjectAssociationMap:每个对象的关联对象集合,键是唯一标识符(例如选择器@selector(text)),值是ObjcAssociation

  4. AssociationHashMap:全局哈希表,键是对象的指针(经过伪装处理,DISGUISE(object)),值是ObjectAssociationMap

关键概念解释

  • DISGUISE(object):这是一个指针伪装操作,通常通过位操作将指针转换为一个独特的标识符,以便在全局哈希表中使用。具体实现细节可以因运行时的实现而异。

通过以上解释,你可以更深入地理解Objective-C中关联对象的工作原理以及如何利用它们为现有类动态添加属性。这种机制在增强类的功能和实现代码的灵活性方面非常有用。

扩展 Extension

1. 一般用扩展做什么用?

  • 声明私有属性

  • 声明私有方法

  • 声明私有成员变量

2. 扩展特点(与分类区别)?

  • 编译时决议

  • 只以声明的形式存在,多数情况下寄生于宿主类的.m 中

  • 不能为系统类添加扩展。

代理(Delegate)

1. 概念

  • 准确的说是一种软件设计模式。

  • iOS 当中以 @protocol 形式体现。

  • 传递方式一对一。

2. 工作流程

  • 协议: 定义方法和属性。
  • 代理方:按照协议实现方法。可能返回一个处理结果给委托方。
  • 委托方:要求代理方需要实现的接口。调用代理方遵从的协议方法。

协议中可以定义方法&属性,代理方需要实现 @required 标记的方法。@optional 为可选实现。

3. 可能遇到问题?

  • 一般声明为 weak 以规避循环引用。 代理方 strong 委托方,委托方 weak 代理方。

4. 示例讲解

当然,使用 UITableView 是讲解协议(Protocol)、代理方(Delegate)、委托方(Delegator)的一个经典例子。让我们通过具体代码来解释这些概念。

基本概念:

  1. 协议(Protocol):协议定义了一组方法,这些方法由代理方实现。协议本身并不实现这些方法,只是声明它们。
  2. 代理方(Delegate):代理方是实现协议方法的对象。它处理协议方法定义的具体任务。
  3. 委托方(Delegator):委托方是拥有代理属性的对象。它在需要的时候调用代理方实现的方法。

UITableView 中,UITableView 本身是委托方,而 UITableViewDelegateUITableViewDataSource 是协议。通常,我们在视图控制器(ViewController)中实现这些协议,视图控制器就是代理方。

  • 定义协议(由系统定义)
@protocol UITableViewDelegate <NSObject>
// 声明一些方法
@optional
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
@end

@protocol UITableViewDataSource <NSObject>
// 声明一些方法
@required
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
@end
  • 声明委托属性(由系统在 UITableView 中声明)
@interface UITableView : UIScrollView
@property (nonatomic, weak, nullable) id<UITableViewDataSource> dataSource;
@property (nonatomic, weak, nullable) id<UITableViewDelegate> delegate;
@end
  • 代理方实现协议方法

在视图控制器中,我们实现 UITableViewDelegateUITableViewDataSource 协议方法。

@interface ViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>
@property (weak, nonatomic) IBOutlet UITableView *tableView;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 设置委托方
    self.tableView.delegate = self;
    self.tableView.dataSource = self;
}

// UITableViewDataSource 方法
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return 10; // 示例数据
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
    cell.textLabel.text = [NSString stringWithFormat:@"Row %ld", (long)indexPath.row];
    return cell;
}

// UITableViewDelegate 方法
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    NSLog(@"Selected row %ld", (long)indexPath.row);
}

@end

解释

  1. 协议(Protocol)

    • UITableViewDelegateUITableViewDataSource 协议定义了一系列方法,用于配置表视图和处理用户交互。
    • 这些方法由 UITableView 在适当的时候调用,但具体的实现由代理方提供。
  2. 委托方(Delegator)

    • UITableView 是委托方。它有两个属性 delegatedataSource,分别遵循 UITableViewDelegateUITableViewDataSource 协议。
    • 当表视图需要知道有多少行、如何显示单元格、用户点击了哪一行时,它会调用这些协议方法。
  3. 代理方(Delegate)

    • ViewController 实现了 UITableViewDelegateUITableViewDataSource 协议,成为代理方。
    • ViewController 中,我们提供了表视图所需的方法实现,例如配置单元格和处理行选择事件。
    • UITableView 需要数据或响应用户交互时,它会调用 ViewController 中实现的相应方法。

总结

  • 协议:定义了一组方法,由代理方实现。
  • 委托方:拥有一个指向代理方的弱引用,并在适当的时候调用代理方的方法。UITableView 是委托方。
  • 代理方:实现协议方法并提供具体的行为。ViewController 是代理方。

通过这种方式,UITableViewViewController 之间的耦合度降低,表视图可以更灵活地处理不同的数据和用户交互逻辑。

通知 Nsnotification

1. 特点

  • 使用观察者模式来实现的用于跨层传递消息的机制。

  • 传递方式一对多。发送者经由通知中心广播给所有观察者。

2.如何实现通知机制?

内部应该维护一个Notification_Map 表, 包含notificationName, Observers_List。将相同notificationNam所有观察者添加到这个Observers_List 中,有 value 变更,逐一通知这些观察者。

KVO

1. 概念

  • KVO 是 key-value observing 的缩写。
  • KVO 是 观察者设计模式的一种实现。
  • Apple 使用了 isa(isa-swizzling) 混写来实现 KVO。

流程: 注册观察者时(调用 addObserver: forKeyPath: options: context)这个方法时,系统会自动创建一个 NSKVONotifying_A 类,然后将原有类的 isa 指针指向新创建的这个类,重写这个类的 setter 方法。新创建的这个类是原有类的子类,为了重写 setter 方法。

[self.player addObserver:self
                  forKeyPath:kCurrentItemKey
                     options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
                     context:nil];
willchangevalueforkey:@"keypath"
didchangevalueforkey:@"keypath"

2. 示例 1

1. 动态生成子类

当你为某个对象的属性添加观察者时,KVO 会在运行时动态生成该对象的一个子类。这是通过 Objective-C 运行时库的 objc_allocateClassPair 函数来完成的。

2. 重写 setter 方法

在生成的子类中,KVO 会重写被观察属性的 setter 方法。在重写的 setter 方法中,KVO 会插入一些通知代码,以便在属性值变化时通知观察者。这通常包括以下几个步骤:

  • 在属性值改变之前调用 willChangeValueForKey: 方法。
  • 调用原始的 setter 方法来改变属性值。
  • 在属性值改变之后调用 didChangeValueForKey: 方法。
3. 调用通知方法

willChangeValueForKey:didChangeValueForKey: 是 KVO 通知机制的核心部分。这些方法会通知观察者属性即将变化或已经变化。

4. 维护观察者列表

KVO 还维护一个内部结构,用于跟踪哪些对象在观察哪些属性。当属性值改变时,KVO 会遍历这个列表并通知所有注册的观察者。

5. 详细步骤

动态生成子类和重写 setter 方法

假设你有一个类 Person 和一个 name 属性:

@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end

当你向 Person 对象添加观察者时:

Person *person = [[Person alloc] init];
[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];

KVO 会做以下几件事:

  1. 动态生成一个 Person 类的子类。假设 Person 类的实际类名是 Person,KVO 生成的子类可能是 NSKVONotifying_Person

  2. 重写 name 属性的 setter 方法。原始的 setName: 方法会被重写如下:

- (void)setName:(NSString *)name {
    [self willChangeValueForKey:@"name"];
    [super setName:name];
    [self didChangeValueForKey:@"name"];
}
  1. personisa 指针指向新的子类。这样,当你调用 person.name = @"John"; 时,实际调用的是 NSKVONotifying_Person 类的 setName: 方法。

willChangeValueForKey: 和 didChangeValueForKey:

willChangeValueForKey:didChangeValueForKey: 方法会通知观察者属性即将发生变化和已经发生变化。这两个方法会调用 NSObject 类的 willChangeValueForKey:didChangeValueForKey: 方法:

- (void)willChangeValueForKey:(NSString *)key {
    [super willChangeValueForKey:key];
    // 通知观察者
}

- (void)didChangeValueForKey:(NSString *)key {
    [super didChangeValueForKey:key];
    // 通知观察者
}

观察者列表和通知机制

KVO 维护一个内部的数据结构来跟踪哪些对象在观察哪些属性。当属性值变化时,KVO 会遍历这个列表并通知所有注册的观察者。这通常是通过调用观察者对象的 observeValueForKeyPath:ofObject:change:context: 方法来实现的。

KVO 的实现细节和陷阱

  1. 动态子类化的隐患: 由于 KVO 动态生成子类并修改 isa 指针,直接访问 isa 指针或依赖于特定的类层次结构可能会导致问题。

  2. 手动 KVO 通知: 在某些情况下,你可能需要手动调用 willChangeValueForKey:didChangeValueForKey: 来实现自定义的 KVO 行为。

  3. 内存管理: 确保在对象销毁前移除所有观察者,否则会导致崩溃。这通常通过 dealloc 方法中移除观察者来实现。

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

总结

KVO 的底层实现涉及到运行时动态生成子类、重写 setter 方法和维护观察者列表。这些机制依赖于 Objective-C 的动态性,使得 KVO 在属性变化时能够灵活地通知观察者。虽然 KVO 是一个强大的工具,但由于其内部实现的复杂性,使用时需要小心,特别是在涉及内存管理和对象生命周期时。

3. 示例 2

NS_ASSUME_NONNULL_BEGIN

@interface MObject : NSObject
@property(nonatomic, assign) NSInteger value;

- (void)increase;

@end

NS_ASSUME_NONNULL_END

----------------------------------------------------------------
#import "MObject.h"

@implementation MObject

- (instancetype)init
{
    self = [super init];
    if (self) {
        _value = 0;
    }
    return self;
}

- (void)increase {
    _value += 1;
}

@end


NS_ASSUME_NONNULL_BEGIN

@interface MObserver : NSObject

@end

NS_ASSUME_NONNULL_END

----------------------------------------------------------------

#import "MObserver.h"
#import "MObject.h"

@implementation MObserver


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"value"] && [object isKindOfClass:MObject.class]) {

        NSLog(@"value 发生了改变 - %@", [change valueForKey:NSKeyValueChangeNewKey]);

    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

@end


MObject *obj = [[MObject alloc] init];
MObserver *observer = [[MObserver alloc] init];
[obj addObserver:observer forKeyPath:@"value" options:NSKeyValueObservingOptionNew context:nil];

// 通过 setter 方法 设置 value
// setter 已经被字类重写了,插入了 change 相关代码
obj.value = 2;

NSLog(@"value=%@",@(obj.value));

问题 1: 通过 kvc 设置 value,kvo 能否生效 ?

可以生效,kvc 会调用属性的 setter 方法,setter已经被字类重写了。 可以重写 value 的 setter 方法,打断点验证。

[obj setValue:@10 forKey:@"value"];

问题 2: 通过成员变量直接赋值,能否激活 KVO?

_value += 1;

----------

[self willChangeValueForKey:@"value"];
[super setName:name];
[self didChangeValueForKey:@"value"];

默认不会生效,需要手动插入代码才能激活 kvo 生效。

KVC

1. 概念

KVC 是 Objective-C 提供的一种机制,它允许通过字符串键来间接访问对象的属性。KVC 提供了灵活的数据访问方式,在很多 Cocoa 和 Cocoa Touch 框架中都得到了广泛的应用。

  • KVC 是 key-value coding 的缩写

  • 相关方法

-(id)valueForKey:(NSString *)key
-(void)setValue:(id)value forKey:(NSString *)key

使用KVC有违面相对象的编程思想,你在外界对私有成员进行了修改。

2. valueForKey 调用流程

Accessor Method

  • <getKey>
  • <key>
  • <isKey>

Instance var

  • _key
  • _isKey
  • key
  • isKey

  • 查找同名的 getter 方法
    • 首先查找与键同名的 getter 方法,比如 get<Key><key>is<Key>
  • 查找 accessInstanceVariablesDirectly
    • 如果没有找到合适的 getter 方法,KVC 会检查类方法 accessInstanceVariablesDirectly 是否返回 YES。默认情况下,这个方法返回 YES
  • 查找实例变量
    • 如果 accessInstanceVariablesDirectly 返回 YES,KVC 会查找与键同名的实例变量(包括 _key_isKeykeyisKey)。
  • 调用 valueForUndefinedKey:
    • 如果仍然没有找到对应的实例变量,会调用 valueForUndefinedKey: 方法,该方法默认抛出一个异常。

示例:

#import <Foundation/Foundation.h>

@interface Person : NSObject {
    @private
    NSString *_nickname;
}

@property (nonatomic, strong) NSString *name;
@property (nonatomic) NSInteger age;

- (NSString *)getNickname;  // 示例1
- (NSString *)nickname;     // 示例2
- (BOOL)isNickname;         // 示例3

@end

@implementation Person

- (instancetype)init {
    self = [super init];
    if (self) {
        _nickname = @"Default Nickname";
    }
    return self;
}

// 示例1: getNickname 方法
- (NSString *)getNickname {
    return @"getNickname: Vapor";
}

// 示例2: nickname 方法
- (NSString *)nickname {
    return @"nickname: Hello";
}

// 示例3: isNickname 方法
- (BOOL)isNickname {
    return YES;
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];

        // 使用 KVC 获取 nickname 属性值
        NSString *nickname = [person valueForKey:@"nickname"];
        NSLog(@"Nickname: %@", nickname); // 根据命名规则输出结果

        // 示例1: 调用 getNickname 方法
        NSLog(@"getNickname: %@", [person getNickname]);

        // 示例2: 调用 nickname 方法
        NSLog(@"nickname: %@", [person nickname]);

        // 示例3: 调用 isNickname 方法
        BOOL isNickname = [person isNickname];
        NSLog(@"isNickname: %@", isNickname ? @"YES" : @"NO");
    }
    return 0;
}

解析:

  1. 查找 getter 方法

    • 当你调用 [person valueForKey:@"nickname"] 时,KVC 首先查找是否存在 getNickname 方法。如果存在,则调用该方法并返回结果。
    • 如果没有找到 getNickname 方法,KVC 会查找 nickname 方法。如果存在,则调用该方法并返回结果。
    • 如果没有找到 getNicknamenickname 方法,KVC 会查找 isNickname 方法。如果存在,则调用该方法并返回结果。
  2. 查找实例变量

    • 如果上面的三种方法都不存在,KVC 会查找是否存在 _nickname 实例变量。如果存在,则返回该变量的值。
  3. 调用 valueForUndefinedKey:

    • 如果上述方法和实例变量都不存在,KVC 会调用 valueForUndefinedKey: 方法。

解释:

  • 存在 getNickname 方法:由于我们定义了 - (NSString *)getNickname 方法,当调用 [person valueForKey:@"nickname"] 时,KVC 会优先调用这个方法并返回 "getNickname: Vapor"

  • 存在 nickname 方法:如果我们没有定义 - (NSString *)getNickname 方法,但定义了 - (NSString *)nickname 方法,则 KVC 会调用 nickname 方法并返回 "nickname: Hello"

  • 存在 isNickname 方法:如果没有 getNicknamenickname 方法,但定义了 - (BOOL)isNickname 方法,则 KVC 会调用 isNickname 方法并返回 YES

  • 存在 _nickname 实例变量:如果没有上面三种方法,KVC 会查找 _nickname 实例变量,并返回其实例变量值 "Default Nickname"

3. setValueForKey 调用流程

  1. 查找同名的 setter 方法
    • 首先查找与键同名的 setter 方法,比如 set<Key>:
  2. 查找 accessInstanceVariablesDirectly
    • 如果没有找到合适的 setter 方法,KVC 会检查类方法 accessInstanceVariablesDirectly 是否返回 YES
  3. 查找实例变量
    • 如果 accessInstanceVariablesDirectly 返回 YES,KVC 会查找与键同名的实例变量(包括 _key_isKeykeyisKey),并直接设置其值。
  4. 调用 setValue:forUndefinedKey:
    • 如果仍然没有找到对应的实例变量,会调用 setValue:forUndefinedKey: 方法,该方法默认抛出一个异常。

4. 示例代码

定义一个类并使用 KVC

#import <Foundation/Foundation.h>

@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic) NSInteger age;
@end

@implementation Person
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];

        // 使用 KVC 设置属性值
        [person setValue:@"John" forKey:@"name"];
        [person setValue:@30 forKey:@"age"];

        // 使用 KVC 获取属性值
        NSString *name = [person valueForKey:@"name"];
        NSInteger age = [[person valueForKey:@"age"] integerValue];

        NSLog(@"Name: %@", name); // 输出: Name: John
        NSLog(@"Age: %ld", (long)age); // 输出: Age: 30
    }
    return 0;
}

相似键的处理

假设我们有一个类 Person,具有属性 name 和实例变量 _name

#import <Foundation/Foundation.h>

@interface Person : NSObject {
    @private
    NSString *_nickname;
}

@property (nonatomic, strong) NSString *name;
@property (nonatomic) NSInteger age;
@end

@implementation Person
- (instancetype)init {
    self = [super init];
    if (self) {
        _nickname = @"Default Nickname";
    }
    return self;
}

// 重写 valueForUndefinedKey: 方法
- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"Undefined key: %@", key);
    return nil;
}

// 重写 setValue:forUndefinedKey: 方法
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"Undefined key: %@", key);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];

        // 使用 KVC 设置和获取属性值
        [person setValue:@"John" forKey:@"name"];
        [person setValue:@30 forKey:@"age"];

        NSString *name = [person valueForKey:@"name"];
        NSInteger age = [[person valueForKey:@"age"] integerValue];

        NSLog(@"Name: %@", name); // 输出: Name: John
        NSLog(@"Age: %ld", (long)age); // 输出: Age: 30

        // 使用 KVC 访问私有变量
        NSString *nickname = [person valueForKey:@"_nickname"];
        NSLog(@"Nickname: %@", nickname); // 输出: Nickname: Default Nickname

        // 访问未定义的键
        [person setValue:@"Unknown" forKey:@"undefinedKey"]; // 输出: Undefined key: undefinedKey
        NSString *undefined = [person valueForKey:@"undefinedKey"]; // 输出: Undefined key: undefinedKey
    }
    return 0;
}

5. KVC 的优缺点

优点

  • 灵活性:可以在运行时动态访问和修改对象属性。
  • 减少代码量:通过字符串键访问属性,减少了大量的 getter 和 setter 方法调用。
  • 数据绑定:便于在视图和模型之间进行数据绑定。

缺点

  • 类型安全问题:由于通过字符串键访问属性,编译时无法检查键的有效性,容易出现运行时错误。
  • 性能问题:由于 KVC 依赖于 Objective-C 运行时机制,访问属性的性能可能不如直接调用 getter 和 setter 方法。
  • 可维护性:字符串键的使用降低了代码的可读性和可维护性。

总结

KVC 是一个强大的机制,允许通过字符串键来访问和修改对象的属性。它提供了灵活的数据访问方式,但也带来了类型安全和性能方面的挑战。了解 KVC 的工作原理和使用方法,可以帮助开发者在合适的场景下充分利用这一机制。

属性关键字

1. 读写权限

  • readonly
  • readwrite 默认

2. 原子性

  • atomic 保证赋值和获取是线程安全的
  • nonatomic

3. 引用计数

  • retain/strong
  • assign/unsafe_unretained
  • weak
  • copy
assign
  1. assign 修饰基本数据类型,int, bool 等
  2. assign 修饰对象类型时,不改变其引用计数
  3. 会产生悬垂指针,指针还是指向之前的地址,访问的话就 crash 了。
weak
  1. 不改变被修饰对象的引用计数
  2. 所指对象在被释放之后会自动置为 nil
copy
  • 浅拷贝

浅拷贝就是对内存地址的复制,让目标对象指针和源对象指向同一片内存空间。

浅拷贝会增加被拷贝对象的引用计数。

没有发生新的内存分配

  • 深拷贝

深拷贝让目标对象指针和源对象指针指向两片内容相同的内存空间。

深拷贝不会增加被拷贝对象的引用计数。

发生新的内存分配

  • 深拷贝 vs 浅拷贝

是否开辟了新的内存空间

是否影响了引用计数

  • 总结

可变对象的 copy 和 mutableCopy 都是深拷贝

不可变对象的 copy 是浅拷贝,mutableCopy 是深拷贝

copy 方法返回的都是不可变对象

// 理解一下, 可变的 copy 和 mutableCopy 都是深拷贝,深拷贝就是开辟内存了
// 不可变的,copy 是浅拷贝,指针复制,增加引用计数,mutableCopy 是深拷贝,开辟内存了
// copy 之后都是不可变,mutableCopy 都是可变
NSString *a = @"hello";
NSString *b = [a copy]; // 不可变的 copy 是浅拷贝,指针复制
NSMutableString *c = [a mutableCopy]; // 深拷贝
[c appendString:@"word"];

// a=0x10259c1d8, b=0x10259c1d8, c=0x600000c04fc0
NSLog(@"a=%p, b=%p, c=%p",a,b,c);


// 理解 可变的 copy 和 mutableCopy 都是深拷贝,拷贝之后能不能变取决于使用的方法。
NSMutableString * d = [NSMutableString stringWithString:@"vapor"];

NSString *e = [d copy];

NSMutableString *f = [d mutableCopy];
[f appendString:@"swift"];

NSLog(@"d=%p, e=%p, f=%p",d,e,f);
d=0x600000c59fe0, e=0x89c476c148b40460, f=0x600000c5b1b0

有何问题?

// 要想到点上,首先是声明的属性,属性系统会根据关键字在 setter 的时候 如何赋值。所以你考虑赋值时穿件来的类型 + copy 之后变成什么,然后你的对象是什么,能不能操作这个对象的相关方法。
// 不要想可变的 copy 和 mutableCopy 这样的方法。比如理解成 self.array = [x copy],那就歪了。
@property(copy) NSMutableArray *array?

如果赋值过来的是 NSMutableArray, copy 之后是 NSArray

如果赋值过来的是 NSArray, copy 之后是 NSArray

但声明的是可变对象,调用方法的时候会 crash


在 Objective-C 中,使用 @property 声明属性时,copyNSMutableArray 的组合可能会导致意料之外的行为。让我们详细解释一下:

copyNSMutableArray 的问题

copy 的作用

当你使用 copy 修饰符时,属性在设置新值时会进行“拷贝操作”。对于不可变对象(如 NSStringNSArray),copy 会创建一个新的不可变副本。然而,对于可变对象(如 NSMutableArray),copy 会将对象拷贝成其不可变版本(如 NSArray),这会导致类型的不匹配。

代码示例

@property (copy) NSMutableArray *array;

在这个声明中,array 属性使用了 copy 修饰符,这意味着任何赋值给 arrayNSMutableArray 实例都会被拷贝成一个不可变的 NSArray 实例。因此,当你尝试对这个 array 属性进行可变操作时,会发生错误。

问题演示

NSMutableArray *mutableArray = [NSMutableArray arrayWithObjects:@1, @2, @3, nil];
self.array = mutableArray;

[self.array addObject:@4]; // 这行代码会导致运行时崩溃

由于 self.array 实际上是一个 NSArray,调用 addObject: 会导致崩溃。

正确的做法

使用 strong 修饰符

如果你需要属性保持可变性,应使用 strong 修饰符:

@property (strong) NSMutableArray *array;

这样,属性会保持为 NSMutableArray,且不会被拷贝成不可变类型。

如果需要不可变副本

如果你需要一个不可变副本,可以在需要时手动进行拷贝:

NSArray *immutableArray = [self.array copy];

完整示例

以下是修正后的代码:

@interface MyClass : NSObject

@property (strong) NSMutableArray *array;

@end

@implementation MyClass

- (void)someMethod {
    NSMutableArray *mutableArray = [NSMutableArray arrayWithObjects:@1, @2, @3, nil];
    self.array = mutableArray;

    // 现在可以安全地对 array 进行可变操作
    [self.array addObject:@4];

    // 如果需要不可变副本,可以手动拷贝
    NSArray *immutableArray = [self.array copy];
    NSLog(@"%@", immutableArray);
}

@end

总结

在 Objective-C 中,@property (copy) 修饰符用于创建属性的不可变副本,对于可变对象如 NSMutableArray,它会将其拷贝成不可变版本(即 NSArray)。为了保持属性的可变性,应使用 strong 修饰符。

面试题

  1. MRC 下如何重写retain 修饰变量的 setter 方法?
@property(nonatomic, retain) id obj;

// 判断相等不做会有异常,可能过度释放,如果真想等。
- (void)setObj:(id)obj {
    if (_obj != obj) {
        [_obj release];
        _obj = [obj retain];
    }
}
  1. 简述分类实现原理

运行时决议,不同分类含有同名方法谁最终生效,取决于谁最后编译,最后编译的会生效,如果分类方法和宿主类方法同名,则会覆盖宿主方法,宿主方法仍然存在,查找是会找靠前的方法,通过一些手段可以调用宿主类的原有方法。

  1. KVO 实现原理

  2. 能否为分类添加成员变量