Objective-C语言
- 分类(什么是分类?分类的实现机制、原理是怎样的?我们应该怎样为一个分类提供实例变量呢?--涉及到第2点关联对象的考察)
- 关联对象(Runtime分析和查看 分类和关联对象的实现机制、原理)
- 扩展、代理怎么使用(分类和扩展的区别在哪里?)
- NSNotification(通知的实现机制和原理)
- KVO、KVC( KVO、KVC的实现机制、原理、包括涉及到了哪些设计思想)
- 属性关键字(声明属性时,会用到一些关键字的实际问题)
KVO的相关面试问题
什么是KVO,KVO的实现机制是怎样的?
- KVO是NSObject的一个分类
- 是OC对于观察者设计模式的一种实现(每次当被观察对象的某个属性值发生改变时,注册的观察者便能获得通知)
- Apple使用isa混写技术(isa-swizzling)来实现KVO,本质是重写了setter方法
isa混写技术在KVO当中是怎样实现的呢?
- A-->当调用了addObserver: forKeyPath: 方法
- 系统会在运行时,为这个类动态的创建了一个子类(NSKVONotifying_A 继承关系),改写isa指向(将原来类A的isa指针指向NSKVONotifying_A这个类),同时重写setter方法,来实现KVO机制
通过下图可以看到,在调用[addObserver forKeyPath]之前,类还是MObject,在调用方法之后,类就变成了NSKVONotifying_MObject,其中isa指针的指向也发生了修改
KVO原理是重写setter方法,NSKVONotifying_A类实际是类A的子类,之所以继承是为了重写类A的setter方法, NSKVONotifying_A通过对setter方法的重写,达到了可以通知所有观察者的目的
为什么更改setter就可以实现KVO的监听呢
NSKVONotifying_A重写了原来类的setter方法,具体实现是下面两行代码
- (void)setValue:(id)obj
{
[self willChangeValueForKey:@"keyPath"];
//调用父类实现,也即原类的实现
[super setValue:obj];
[self didChangeValueForKey:@"keyPath"];
}
- 首先调用willChangeValueForKey方法
- 然后调用父类实现,也就是原来类的setter实现
- 然后调用didChangeValueForKey,这个方法会触发observeValueForKeyPath这个KVO回调,来通知观察者value发生了变化
1 通过kvc设置value能否生效?2. 通过成员变量直接赋值value能否生效?
1.kvc [obj setValue:@2 forKey:@"value"] 可以调到对应对象的set方法上面,set方法已经被被动态运行时创建的子类重写了,那么就可以使KVO拿到生效这样的一个结果。
2.手动kvo,我们在通过成员变量直接赋值的时候,在他之前和之后分别添加上willChangeValueForKey和didChangeValueForKey就可以实现手动kvo。然后didChangeValueForKey在系统内部实现当中,会触发我们的kvo回调observeValueForKeyPath。
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
//实现KVO
MObject *obj = [[MObject alloc] init];
MObserver *observer = [[MObserver alloc] init];
//调用kvo方法监听obj的value属性的变化
[obj addObserver:observer forKeyPath:@"value" options:NSKeyValueObservingOptionNew context:NULL];
//通过setter方法修改value
obj.value = 1;
// 1 通过kvc设置value能否生效?
[obj setValue:@2 forKey:@"value"];
// 2. 通过成员变量直接赋值value能否生效?
[obj increase];
return YES;
}
- (void)increase
{//手动kvo
//直接为成员变量赋值 模拟系统方法的setter实现
[self willChangeValueForKey:@"value"];
_value += 1;
//模拟系统方法的setter实现
[self didChangeValueForKey:@"value"];
}
KVO总结:
- 使用setter方法改变值,KVO回调可以生效
- 使用setValue: forKey: 改变值,KVO回调可以生效
- 成员变量直接赋值KVO无法生效,必须手动添加KVO两个方法,KVO回调才会生效
KVC的相关面试问题
什么是KVC?(我们通过键值编码技术,是否会破坏面向对象的编程方法,过着说有违背于面向对象编程思想呢?会破坏。)
KVC是一种键值编码机制(key-value),通过NSKeyValueCoding协议来间接访问成员变量
它会破坏面向对象编程思想,上面的key是没有任何限制的,当我们知道一个类内部的某个私有成员变量名称,就可以通过key进行设置和访问
KVC怎么用?
可以随意修改一个对象的属性或者成员变量,因为key没有限制,私有也可以
赋值: 设置某一对象当中和key同名或者相似名字的实例变量的值
- (void)setValue:(id)value forKey:(NSString *)key;
- (void)setValue:(id)value forKeyPath:(NSString *)key; 通过点可层层赋值
取值: 获取和key同名或者相似名字的实例变量的值
- (id)valueForKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)key;
KVC系统内部实现流程:
`赋值过程`
先找相关方法set<Key> _set<Key> setIs<Key>
若没有相关方法,判断是否可以直接方法成员变量accessInstanceVariablesDirectly == YES ?
YES:继续找相关变量 _<Key> _is<Key> <Key> is<Key>赋值,若都不存在,则调forUndefinedKey,抛出异常
NO:执行setValue: forUndefinedKey 系统抛出异常:未定义的key
取值过程
先找相关方法get<Key> <Key> <countOfKey> && objectInKeyAtIndex
若没有相关方法,判断是否可以直接方法成员变量accessInstanceVariablesDirectly == YES ?
YES: 继续找相关变量 _<Key> _is<Key> <Key> is<Key>赋值,若都不存在,则调forUndefinedKey,抛出异常
NO: 执行setValue: forUndefinedKey 系统抛出异常:未定义的key
其他
赋值为空: setNilValueForKey
key值不存在: setValue: forUndefinedKey
validateValue方法的工作原理: 属性值正确性验证,用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因
属性关键字
#属性关键字可以分为三类#
//带星标的为默认
1.读写权限: *readwrite readonly
2.原子性: *atomic nonatomic
3.引用计数
*strong(ARC) / return(MRC) #这两个关键字都是用来修饰对象
*assign / unsafe_unretained(MRC) #assign既可以修饰基本数据类型,也可以修饰对象类型
weak
copy
//atomic修饰的属性: 可以保证对该成员变量的赋值和获取数据是线程安全的
ps:
@property(nonatomic, retain) UITextField *userName;
@property(atomic, retain) UITextField *userName;
如果我们用atomic修饰了一个数组,那么对这个数组进行赋值和获取,是可以保证线程安全的
但如果我们对数组进行操作,添加或移除对象,那么是不在atomic的负责范围内,所以是没有办法保证线程安全的
后两行是一样的,不写的话默认就是atomic。
atomic 和 nonatomic 的区别在于,系统自动生成的 getter/setter 方法不一样。如果你自己写 getter/setter,那 atomic/nonatomic/retain/assign/copy 这些关键字只起提示作用,写不写都一样。
对于atomic的属性,系统生成的 getter/setter 会保证 get、set 操作的完整性,不受其他线程影响。比如,线程 A 的 getter 方法运行到一半,线程 B 调用了 setter:那么线程 A 的 getter 还是能得到一个完好无损的对象。
而nonatomic就没有这个保证了。所以,nonatomic的速度要比atomic快。
不过atomic可并不能保证线程安全。如果线程 A 调了 getter,与此同时线程 B 、线程 C 都调了 setter——那最后线程 A get 到的值,3种都有可能:可能是 B、C set 之前原始的值,也可能是 B set 的值,也可能是 C set 的值。同时,最终这个属性的值,可能是 B set 的值,也有可能是 C set 的值。
保证数据完整性——这个多线程编程的最大挑战之一——往往还需要借助其他手段。
assign
- 修饰基本数据类型,如int, BOOL等
- 修饰对象类型时,不改变其引用计数
- 会产生悬垂指针: assign所修饰的对象,在被释放之后,assign指针仍指向原对象内存地址,如果这时通过assign指针,继续访问原对象,会导致内存泄露或程序异常
weak
- 不改变被修饰对象的引用计数,一般使用weak是用来解决循环引用问题
- 所修饰的对象在被释放之后会自动置为nil
weak和assign的区别
- weak可以修饰对象,assign既可以修饰对象,也可以修饰基本数据类型
- weak所修饰的对象,在被废弃之后,weak指针置为nil. assign所修饰的对象,在被释放之后,assign指针仍指向原对象内存地址
weak和assign的联系
在修饰对象时,weak和assign都不改变被修饰对象的引用计数
weak所修饰的对象,在被废弃之后,为什么置为nil(内存管理)
copy
#NSMutableArray用copy修饰会出现什么问题?#
@property (copy) NSMutableArray *array;
若上面的NSMutableArray类型的array被copy修饰,会导致程序异常
1.因为如果赋值过来的是NSMutableArray对象A,会对可变对象A进行copy操作,拷贝结果是不可变的,那么copy后就是NSArray
2.如果赋值过来的是NSArray对象B,会对不可变对象B进行copy操作,拷贝结果仍是不可变的,那么copy之后仍是NSArray
所以不论赋值过来的是什么对象,只要对NSMutableArray进行copy操作,结果一定是不可变的
但因为原来属性声明的是NSMutableArray,就会调用添加或者移除方法
但因为拷贝后的结果是不可变对象,所以一旦调用这些方法就会程序崩溃(crash)
浅拷贝:是指针拷贝,会增加被拷贝对象的引用计数,是对内存地址的复制,让目标对象指针和源对象指向同一片内存空间
指针A指向某个对象的内存,对象在内存当中用黑框表示,若此时对对象A进行浅拷贝,就会有个B指针指向和A同一块的内存地址
所以说,浅拷贝是对内存地址的复制,但是并没有对内存空间复制,A和B两个指针是指向了同一片内存空间
深拷贝:是内存拷贝,是让目标对象指针和源对象指针分别指向两片内容相同的内存空间
指针A指向某个对象的内存,对象在内存当中用黑框表示,若此时对对象A进行深拷贝,就会产生新的内存分配,然后会有一个新的指针B,指向这块新的内存
两片内存空间只是内容相同,但不是同一块内存空间
深拷贝的特点
- 不会增加被拷贝对象的引用计数,因为没有新指针指向原来的内存空间
- 产生了新的内存分配,出现了两块内存
深拷贝和浅拷贝的区别
-
是否开辟了新的内存空间
深拷贝开辟了新的内存空间,浅拷贝没有开辟 -
是否影响了引用计数
深拷贝没有影响,浅拷贝增加了引用计
copy关键字的使用
- 可变对象(mutable)copy和mutableCopy都是深拷贝
- 不可变对象(immutable)的copy是浅拷贝,mutableCopy是深拷贝
-
copy方法返回的都是不可变对象,若被拷贝对象是可变对象,返回的也是不可变对象