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
- read_images
- map_images_nolock
- map_2_images
-
分类添加的方法可以覆盖原类方法。宿主的同名方法还在,需要特殊处理调用。
- 同名分类方法谁能生效取决于编译顺序,最后编译的优先生效。
- 名字相同的分类会引起编译报错。
关联对象¶
在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 结构中。
数据结构
理解关联对象的底层实现,可以借助以下几个概念:
- ObjcAssociation:这是表示单个关联对象的结构,包含键(key)、值(value)和关联策略(policy)。
- ObjectAssociationMap:这是每个对象关联对象的哈希表,键通常是
SEL
(选择器)或其他唯一标识符,值是ObjcAssociation
。 - 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;
}
解释你提到的具体内容
-
ObjcAssociation:这是关联对象的结构,包含
key
(键)、value
(值)和policy
(关联策略)。 -
OBJC_ASSOCIATION_COPY_NONATOMIC:这是一个关联策略,表示关联对象在设置时会被复制,且复制操作是非原子的。
-
ObjectAssociationMap:每个对象的关联对象集合,键是唯一标识符(例如选择器
@selector(text)
),值是ObjcAssociation
。 -
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)的一个经典例子。让我们通过具体代码来解释这些概念。
基本概念:
- 协议(Protocol):协议定义了一组方法,这些方法由代理方实现。协议本身并不实现这些方法,只是声明它们。
- 代理方(Delegate):代理方是实现协议方法的对象。它处理协议方法定义的具体任务。
- 委托方(Delegator):委托方是拥有代理属性的对象。它在需要的时候调用代理方实现的方法。
在 UITableView
中,UITableView
本身是委托方,而 UITableViewDelegate
和 UITableViewDataSource
是协议。通常,我们在视图控制器(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
- 代理方实现协议方法
在视图控制器中,我们实现 UITableViewDelegate
和 UITableViewDataSource
协议方法。
@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
解释
-
协议(Protocol):
UITableViewDelegate
和UITableViewDataSource
协议定义了一系列方法,用于配置表视图和处理用户交互。- 这些方法由
UITableView
在适当的时候调用,但具体的实现由代理方提供。
-
委托方(Delegator):
UITableView
是委托方。它有两个属性delegate
和dataSource
,分别遵循UITableViewDelegate
和UITableViewDataSource
协议。- 当表视图需要知道有多少行、如何显示单元格、用户点击了哪一行时,它会调用这些协议方法。
-
代理方(Delegate):
ViewController
实现了UITableViewDelegate
和UITableViewDataSource
协议,成为代理方。- 在
ViewController
中,我们提供了表视图所需的方法实现,例如配置单元格和处理行选择事件。 - 当
UITableView
需要数据或响应用户交互时,它会调用ViewController
中实现的相应方法。
总结
- 协议:定义了一组方法,由代理方实现。
- 委托方:拥有一个指向代理方的弱引用,并在适当的时候调用代理方的方法。
UITableView
是委托方。 - 代理方:实现协议方法并提供具体的行为。
ViewController
是代理方。
通过这种方式,UITableView
和 ViewController
之间的耦合度降低,表视图可以更灵活地处理不同的数据和用户交互逻辑。
通知 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 会做以下几件事:
-
动态生成一个
Person
类的子类。假设Person
类的实际类名是Person
,KVO 生成的子类可能是NSKVONotifying_Person
。 -
重写
name
属性的 setter 方法。原始的setName:
方法会被重写如下:
- (void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];
}
- 将
person
的isa
指针指向新的子类。这样,当你调用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 的实现细节和陷阱
-
动态子类化的隐患: 由于 KVO 动态生成子类并修改
isa
指针,直接访问isa
指针或依赖于特定的类层次结构可能会导致问题。 -
手动 KVO 通知: 在某些情况下,你可能需要手动调用
willChangeValueForKey:
和didChangeValueForKey:
来实现自定义的 KVO 行为。 -
内存管理: 确保在对象销毁前移除所有观察者,否则会导致崩溃。这通常通过
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>
。
- 首先查找与键同名的 getter 方法,比如
- 查找
accessInstanceVariablesDirectly
:- 如果没有找到合适的 getter 方法,KVC 会检查类方法
accessInstanceVariablesDirectly
是否返回YES
。默认情况下,这个方法返回YES
。
- 如果没有找到合适的 getter 方法,KVC 会检查类方法
- 查找实例变量:
- 如果
accessInstanceVariablesDirectly
返回YES
,KVC 会查找与键同名的实例变量(包括_key
、_isKey
、key
和isKey
)。
- 如果
- 调用
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;
}
解析:
-
查找 getter 方法:
- 当你调用
[person valueForKey:@"nickname"]
时,KVC 首先查找是否存在getNickname
方法。如果存在,则调用该方法并返回结果。 - 如果没有找到
getNickname
方法,KVC 会查找nickname
方法。如果存在,则调用该方法并返回结果。 - 如果没有找到
getNickname
和nickname
方法,KVC 会查找isNickname
方法。如果存在,则调用该方法并返回结果。
- 当你调用
-
查找实例变量:
- 如果上面的三种方法都不存在,KVC 会查找是否存在
_nickname
实例变量。如果存在,则返回该变量的值。
- 如果上面的三种方法都不存在,KVC 会查找是否存在
-
调用
valueForUndefinedKey:
:- 如果上述方法和实例变量都不存在,KVC 会调用
valueForUndefinedKey:
方法。
- 如果上述方法和实例变量都不存在,KVC 会调用
解释:
-
存在
getNickname
方法:由于我们定义了- (NSString *)getNickname
方法,当调用[person valueForKey:@"nickname"]
时,KVC 会优先调用这个方法并返回"getNickname: Vapor"
。 -
存在
nickname
方法:如果我们没有定义- (NSString *)getNickname
方法,但定义了- (NSString *)nickname
方法,则 KVC 会调用nickname
方法并返回"nickname: Hello"
。 -
存在
isNickname
方法:如果没有getNickname
和nickname
方法,但定义了- (BOOL)isNickname
方法,则 KVC 会调用isNickname
方法并返回YES
。 -
存在
_nickname
实例变量:如果没有上面三种方法,KVC 会查找_nickname
实例变量,并返回其实例变量值"Default Nickname"
。
3. setValueForKey 调用流程¶
- 查找同名的 setter 方法:
- 首先查找与键同名的 setter 方法,比如
set<Key>:
。
- 首先查找与键同名的 setter 方法,比如
- 查找
accessInstanceVariablesDirectly
:- 如果没有找到合适的 setter 方法,KVC 会检查类方法
accessInstanceVariablesDirectly
是否返回YES
。
- 如果没有找到合适的 setter 方法,KVC 会检查类方法
- 查找实例变量:
- 如果
accessInstanceVariablesDirectly
返回YES
,KVC 会查找与键同名的实例变量(包括_key
、_isKey
、key
和isKey
),并直接设置其值。
- 如果
- 调用
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¶
- assign 修饰基本数据类型,int, bool 等
- assign 修饰对象类型时,不改变其引用计数
- 会产生悬垂指针,指针还是指向之前的地址,访问的话就 crash 了。
weak¶
- 不改变被修饰对象的引用计数
- 所指对象在被释放之后会自动置为 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
声明属性时,copy
和 NSMutableArray
的组合可能会导致意料之外的行为。让我们详细解释一下:
copy
和 NSMutableArray
的问题
copy
的作用
当你使用 copy
修饰符时,属性在设置新值时会进行“拷贝操作”。对于不可变对象(如 NSString
和 NSArray
),copy
会创建一个新的不可变副本。然而,对于可变对象(如 NSMutableArray
),copy
会将对象拷贝成其不可变版本(如 NSArray
),这会导致类型的不匹配。
代码示例
@property (copy) NSMutableArray *array;
在这个声明中,array
属性使用了 copy
修饰符,这意味着任何赋值给 array
的 NSMutableArray
实例都会被拷贝成一个不可变的 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
修饰符。
面试题¶
- MRC 下如何重写retain 修饰变量的 setter 方法?
@property(nonatomic, retain) id obj;
// 判断相等不做会有异常,可能过度释放,如果真想等。
- (void)setObj:(id)obj {
if (_obj != obj) {
[_obj release];
_obj = [obj retain];
}
}
- 简述分类实现原理
运行时决议,不同分类含有同名方法谁最终生效,取决于谁最后编译,最后编译的会生效,如果分类方法和宿主类方法同名,则会覆盖宿主方法,宿主方法仍然存在,查找是会找靠前的方法,通过一些手段可以调用宿主类的原有方法。
-
KVO 实现原理
-
能否为分类添加成员变量