本文主要内容
一.分类Category
二.关联对象
三.扩展Extension
四.代理Delegate
五.通知NSNotification
六.KVO
七.KVC
八.属性关键字
一.分类Category
1、分类可以做哪些事?
- 声明私有方法:方法只放在.m中
- 分解体积庞大的类文件
- 把Framework的私有方法公开
2、分类的特点
- 分类在运行时决议:运行时才通过runtime把分类添加的内容真实的添加到宿主类上
- 可以为系统类添加分类
3、分类中可以添加的内容
- 实例方法
- 类方法
- 协议
- 属性:
只是声明了对应的get和set方法,并没有在分类中添加实例变量
4、分类底层原理
- 分类的结构体
分类的结构体为category_t
,实际上就是创建的分类文件,第一个成员属性为name
,即分类的名称,第二个为class
的成员变量,表示分类所属的宿主类,接下来为实例方法的结构体、类方法结构体、协议、实例属性列表,如上分类结构体可以验证我们可以为分类添加哪些内容,并且不能添加实例变量。
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta) {
if (isMeta) return nil; // classProperties;
else return instanceProperties;
}
};
- 加载调用栈
- 源码分析
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;
runtimeLock.assertWriting();
/**
我们只分析分类当中实例方法添加的逻辑
因此在这里假设 isMeta = NO
*/
isMeta = cls->isMetaClass();
// Re-methodizing: check for more categories
// 获取cls中未完成整合的所有分类
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
if (PrintConnecting) {
_objc_inform("CLASS: attaching categories to class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
/**
我们只分析分类当中实例方法添加的逻辑
因此在这里假设 isMeta = NO
*/
bool isMeta = cls->isMetaClass();
/**
二维数组
[ [method_t,method_t,...], [method_t], [method_t,method_t,method_t],... ]
*/
// fixme rearrange to remove these intermediate allocations
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count; // 宿主类分类的总数
bool fromBundle = NO;
while (i--) { // 这里是倒序遍历,最先访问最后编译的分类
// 获取一个分类
auto& entry = cats->list[i];
// 获取该分类的方法列表
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
// 最后编译的分类最先添加到分类数组中
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
// 属性列表添加规则:同方法列表添加规则
property_list_t *proplist = entry.cat->propertiesForMeta(isMeta);
if (proplist) {
proplists[propcount++] = proplist;
}
// 协议列表添加规则:同方法列表添加规则
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
// 获取宿主类当中的rw数据,其中包含宿主类的方法列表信息
auto rw = cls->data();
// 主要是针对分类中有关于内存管理相关方法情况下的一些特殊处理
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
/*
rw代表类
methods代表类的方法列表
attachLists 方法的含义是:将含有mcount个元素的mlists拼接到rw的methods上
*/
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
rw->properties.attachLists(proplists, propcount);
free(proplists);
rw->protocols.attachLists(protolists, protocount);
free(protolists);
}
通过解读分类源码总结如下:
- 分类添加的方法可以“覆盖”原类方法
- 同名分类方法谁能生效取决于编译顺序
- 名字相同的分类会引起编译报错
注意:分类有多个的情况下,每个分类中有一个同名的分类方法,最终哪个会生效?最后编译的分类中的方法会最终生效。
二.关联对象
1、能否给分类添加“成员变量”?
我们不能在分类的声明或者定义实现时直接为分类添加成员变量,但是可以通过关联对象
的技术为分类添加成员变量,达到分类可以添加成员变量的效果。
2、关联对象的本质及相关函数
关联对象由AssociationsManager
管理并在AssociationsHashMap
存储。所有对象的关联内容都在同一个全局容器
中。
id objc_getAssociatedObject(id object, const void *key)
id objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
void objc_removeAssociatedObjects(id object)
3、关联对象源码分析
id
objc_getAssociatedObject(id object, const void *key)
{
return objc_getAssociatedObject_non_gc(object, key);
}
void
objc_setAssociatedObject(id object, const void *key, id value,
objc_AssociationPolicy policy)
{
objc_setAssociatedObject_non_gc(object, key, value, policy);
}
#endif
void objc_removeAssociatedObjects(id object)
{
#if SUPPORT_GC
if (UseGC) {
auto_zone_erase_associative_refs(gc_zone, object);
} else
#endif
{
if (object && object->hasAssociatedObjects()) {
_object_remove_assocations(object);
}
}
}
三.扩展Extension
1、一般用扩展做什么?
- 声明私有属性
- 声明私有方法
- 声明私有成员变量
2、扩展的特点
- 编译时决议
- 只以声明的形式存在,多数情况下寄生于宿主类的.m中
- 不能为系统类添加扩展
四.代理Delegate
1、代理的特点
- 准确的说是一种软件设计模式
- iOS当中以@protocol的形式体现
- 传递方式一对一
2、代理工作流程
3、使用代理过程中的注意点
- 一般声明为weak以规避循环引用
五.通知NSNotification
1、通知的特点
- 是使用
观察者模式
来实现的,用于跨层传递消息的机制 - 传递方式为一对多
代理和通知的区别:
1.代理是用代理模式实现的,通知是由观察者模式实现的;
2.代理传递方式是一对一,通知是一对多的传递方式。
2、通知的传递方式(一对多)
发送者经由通知中心广播给多个观察者!
问题:如何实现通知机制?或假如让你实现系统的通知机制流程,你怎样去实现?
在通知中心系统类内部可能会维护一个Notification_Map表(或字典),在该表(字典)当中,key是notificationName,也就是addObserver时传递的监听的名称,value是Observers_List,同一个notificationName可能添加多个Observer,Observer对应的value应该是一个数组列表
,列表中的每一个成员首先应该包含通知接收的观察者,还应该包含该观察者需要调用的方法,即观察者收到通知后的回调方法
。
六.KVO
1、KVO特点
- KVO是Key-value observing的缩写
- KVO是Objective-C对观察者设计模式的又一实现
- Apple使用了isa混写(isa-swizzling)来实现KVO
当注册一个对象的观察者时,实际上是调用了系统的addObserver:forKeyPath:
方法,调用此方法后,观察者观察对象A中的某一个成员变量或者属性时,系统会在运行时动态创建一个NSKVONotifying_A
类,然后会将原来指向A类的isa指针指向该类,把isa指针指向修改实际上就是isa混写技术的标志。
isa混写技术在KVO当中是如何体现的?
当调用addObserver:forKeyPath:
方法后系统会在运行时动态创建一个NSKVONotifying_A类
(如类A为HObject,则新类为NSKVONotifying_HObject),同时将原来类A
的isa指针
指向新创建的类。NSKVONotifying_A类
实际上是原来类A
的一个子类
。之所以继承类A
是为了重写类A
中的setter
方法,子类通过对setter
方法的重写达到可以通知所有观察对象的目的。
2、NSKVONotifying_A类
中setter方法重写
- (void)setValue:(id)obj {
[self willChangeValueForKey:@"keyPath"];
// 调用父类实现,也即原类的实现
[super setValue:obj];
[self didChangeValueForKey:@"keyPath"];
}
问题1:通过KVC设置value能否生效?
KVC的setValue:forKey:方法可以调用对应对象的setter方法上,setter方法已经被动态运行时动态创建的子类重写,所以可以使KVO达到生效。
// 通过kvc设置value:可以生效
[obj setValue: @2 forKey:@"value"];
问题2:通过成员变量直接赋值value能否生效?
无法生效,没有触发系统KVO。系统KVO相当于在代码中添加willChangeValueForKey
、didChangeValueForKey
,所以可以手动KVO触发KVO生效
,即在代码中添加上述两个调用即可实现KVO。
问题3:手动KVO
在对变量直接成员赋值时,在之前和之后分别添加willChangeValueFor Key
和didChangeValueForKey
就可以实现手动KVO,didChangeValueForKey
在系统内部会触发KVO回调,即observeValueForKeyPath
回调方法的调用。
总结
- 使用setter方法改变值KVO才会生效
- 使用setValue:forKey:改变值KVO才会生效
- 成员变量直接修改需手动添加KVO才户生效
六.KVC
1、KVC特点
- KVC是Key-value coding的缩写
- (id)valueForKey:(NSString *)key
- (id)setValue:(id)value ForKey:(NSString *)key
2、KVC系统实现流程
六.属性关键字
- 读写权限
- 原子性
- 引用计数
1、读写权限
-
readonly
-
readwrite
2、原子性
-
atomic
:atomic修饰的属性可以保证赋值和获取(不包括操作和访问)保证线程安全
,如对数组元素的添加和移除不保证线程安全,只对数组的赋值和获取保证线程安全。 -
nonatomic
3、引用计数
-
retain/strong:都用来修饰
对象
,retain在MRC中使用,strong在ARC中使用。 -
assign/unsafe_unretained:assign既可以修饰基本数据类型也可修饰对象,unsafe_unretained在MRC中使用。
-
weak
-
copy
问题1:assign和weak关键字之间的异同
- assign可以修饰基本数据类型,如int、BOOL等;
- assign修饰对象类型时,不改变其引用计数;
- assign会产生悬垂指针,即assign修饰的对象在被释放后,assign指针仍指向原对象地址,会导致内存泄漏或程序异常;
- weak不改变被修饰对象的引用计数;
- weak所指对象在被释放后会自动置为nil;
4、copy
- 浅拷贝:是对
内存地址
的复制,让目标对象指针和源对象指向同一片内存空间
1.浅拷贝会增加被拷贝对象的引用计数
2.浅拷贝没有新的内存分配
- 深拷贝:让目标对象指针和源对象指针指向
两片内容相同的内存空间
1.深拷贝不会增加被拷贝对象的引用计数
2.深拷贝产生了新的内存分配
- 浅拷贝和深拷贝的区别
1.是否开辟了新的内存空间;2.是否影响了引用计数
copy操作总结:
1.可变对象的copy和mutableCopy都是深拷贝;
2.不可变对象的copy是浅拷贝,mutableCopy是深拷贝;
3.copy方法返回的都是不可变对象,mutableCopy方法返回的都是可变对象。
问题2:如下声明是否可以
@property(copy) NSMutableArray *array
copy方法返回的都是不可变对象
,所以
- 如果赋值过来的是NSMutableArray,copy之后是NSArray;
- 如果赋值过来的是NSArray,copy之后是NSArray;
即结果都是NSArray,由于原来的属性被声明为
NSMutableArray
,就不可避免的会有调用方调用array的添加、移除等方法,而array被copy之后是NSArray类型,就会产生 程序异常或crash。
问题3:MRC下如何重写retain修饰变量的setter方法?
- (void)setObj:(id)obj {
// 必须要进行不等的判断,否则在释放_obj的同时就会将obj也释放
if(_obj != obj) {
[_obj release];
_obj = [obj retain];
}
}
问题4:请简述分类实现原理
- 分类是在运行时决议的;
- 不同分类中含有同名方法,谁最终生效去决议最后参与编译的分类当中的方法最终生效;
- 分类中添加的方法如果和宿主类中的方法同名,分类方法会“覆盖”宿主类中的方法,覆盖只是因为消息传递过程中优先查找数组靠前的元素,所以说宿主类中的方法依然存在,仍可用特殊方式调用。
问题5:KVO的实现原理是怎样的
- 系统关于观察者模式的实现
- 运用isa混写技术动态运行时为某个类添加一个子类,重写setter方法,同时把原有类的isa指针指向新创建的子类。
问题5:能否为分类添加成员变量
我们不能在分类的声明或者定义实现时直接为分类添加成员变量,但是可以通过关联对象
的技术为分类添加成员变量,达到分类可以添加成员变量的效果。
有任何问题,欢迎👏各位评论指出!觉得博主写的还不错的麻烦点个赞喽👍