开局一张图
围绕着OC语言相关问题,大致涉及到图片中所有的内容。
分类(Category)
你用分类都做了哪些事情?
- 声明私有方法
- 分解体积庞大的类文件
- 把Framework的私有方法公开
声明私有方法这个,我感觉就是将原来的写在原类中的私有方法,在分类中进行声明。
比如定义一个分类,只有头文件放到对应宿主.m里,满足私有方法的声明和使用,不暴露具体实现。
分类的特点:
- 运行时决议
指的是在运行时,通过runtime将分类里面的内容添加到宿主类里面 - 可以为系统类添加分类
分类中都可以添加哪些内容?
- 实例方法
- 类方法
- 协议
- 属性(只声明了get和set方法,并没有实现get和set方法,也没有生成实例变量)
分类的结构:
具体代码不再具体分析,可参考iOS分类Category的本质
本文只摘抄部分上文中没有的内容,或者比较重要的内容。
这里只分析分类中的实例方法。
在编译阶段:
编译器将category里面的内容,编译成category_t类型的结构。
在运行阶段:
- runtime将每一个分类中的实例方法组成一个数组,然后,将这些数组再组合到一个大数组中,
- 然后从最后一个元素(数组类型)开始,添加到分类的instance_methods数组中,
- 再将拼接好的二维数组instance_methods放入宿主类的class_rw_t的methods当中。
举个例子:
假如某个类YZPerson有三个分类:YZPerson+eat.m、YZPerson+run.m、YZPerson+walk.m
首先,将每个分类里面的实例方法组合成一个数组,然后,再将三个分类组合成一个大数组。
这样,分类中的实例方法列表,就是一个二维数组。
假如,上面三个分类,第一个分类里面有2个实例方法,第二个分类里面有1个实例方法,第三个分类里面有3个实例方法,那么,该实例方法在分类存储形式如下图中的二维数组类似。
以上是将分类合并成一个大数组,其顺序是按照编译顺序(最先编译的在数组最前面)
然后,再通过一个while循环,从数组的最后一个元素(也是数组)开始,一个一个添加到分类的instance_methods里面。
然后,将分类的instance_methods这个二维数组,拼接到rw的methods上,也就是拼接到原类的methods方法列表中。
并且从attachLists方法里面可以看出,是将分类实例方法列表插入到宿主类的方法列表的最前面。
这样,最后编译的分类里面,方法最先被调用。
属性、协议的添加方法与上面实例方法添加方法类似。
- 分类添加的方法可以“覆盖”原类方法(当然不是真的覆盖)
- 两个分类,里面有一个同名方法,最后编译的分类里面的方法被调用。
- 名字相同的分类会引起编译报错
关联对象
问:能否为分类添加成员变量?
不能
问:能否为分类添加属性?
能
id objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
void objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)
void objc_removeAssociatedObjects(id _Nonnull object)
问:为分类添加的属性被添加到了哪里?
没有被添加到原宿主类中,而是被添加到。。。
属性、实例变量、成员变量的区别?
属性,property,是指的右@property建立的
例如:@property (copy, nonatomic) NSString *postId;
@property负责三个事情:
- set,get方法的声明
- set,get方法的实现
- 生成_postId的实例变量
成员变量指的是
@interface
{
int age;
NSObject *obj1;
}
@end
大括号中间的内容
实例变量ivar(instance variables
),指的是{}中,是class类对象的一类,也就是有指针的,非基本数据类型。例如NSObject *obj1;
也就是说,成员变量 = 实例变量 + 基本数据类型变量
需要注意⚠️的是:如果既有成员变量,又有属性,则先写成员变量,再写属性
也有资料说,成员变量 = 实例变量
属性property
实例变量ivar
成员变量的英文单词是?
扩展(Extension)
问:一般用扩展做什么?
- 声明私有属性
- 声明私有方法
- 声明私有成员变量
扩展的特点:
- 编译时决议
- 只以声明的形式存在,多数情况下存在于宿主类的.m文件中
- 不能为系统类添加扩展
代理(Delegate)
代理分三个模块:协议、委托、代理
协议,就是@property
委托,就是协议调用协议方法的地方,指定代理
代理,就是实现协议里面具体方法的地方
是一种软件设计模式
传递方式一对一
代理也可以添加属性
MJ说过,delegate不是代理设计模式,NSProxy是代理设计模式
于海说delegate是代理设计模式
那么,delegate是不是代理设计模式呢?
问:如何实现代理的一对多?
即,同一个代理,被多个对象监听
方法一:
将代理设置成数组、集合,而不是单单的一个weak属性值
类似:
@property (nonatomic, strong)NSMutableArray <id<TestSubViewDelegate>>* __nullable delegates;
需要注意的是,在消耗的时候,要手动销毁delegates
- (void)destory {
[self.delegates removeAllObjects];
self.delegates = nil;
}
不过,最好使用NSMapTable NSHashTable NSPointerArray
这些数据类型
因为,这几个数据类型,里面存放的元素都是weak指针引用的,销毁的时候不需要手动销毁
iOS开发-NSMapTable NSHashTable NSPointerArray的使用
方法二:
利用消息转发机制
iOS:利用消息转发机制实现多播委托
通知(NSNotification)
通知是使用观察者模式来实现的用于跨层传递消息的机制
传递方式:一对多
问:如何实现通知机制?
在通知中心,有一个Notification_Map字典,里面存储着以通知名称为key,观察者组成的数组作为value
KVO
KVO是Key-value observing的缩写
KVO是Objective-C对观察者设计模式的一种实现
Apple使用了isa混写(isa-swizzling)来实现KVO
isa混写技术:
指的就是将isa指针动态重新指向新的类
setter方法重写的方法:
问:通过kvc设置value,kvo能否生效?
可以
KVC改变属性值,会进入属性值的setter方法,从而触发KVO
问:通过成员变量直接赋值value,kvo能否生效?
不可以
但,可以通过手动修改setter方法,触发KVO
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];
使用setter方法改变值,KVO会生效
使用setValue:forKey:改变值,KVO会生效
成员变量直接修改需手动添加某些代码,KVO才会生效
KVC
KVC是Key-value coding的缩写。
里面有两个重要的方法:
- (id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
问:KVC是否有违面向对象思想
KVC只要知道某个对象的私有变量名称key,可以在外部将其value进行修改,也就是有违面向对象思想的。
valueForKey:的系统实现流程:
valueForKey:说白了就是通过一个key找到对应的value
找value可以是通过getter方法找,也可以通过直接找对应的实例变量
也就是,一个是方法,一个是实例变量,两种方法
图中
Accessor Method is exist?
就是通过getter方法找,有的话直接调用
Instance Var is exist?
就是有没有对应的实例变量,有的话调用
问:Accessor Method如何找方法呢?
通过getKey、key(正好是getter方法)、isKey三个方法,如果有则YES
这三个都是方法
问:+(BOOL)accessInstanceVariablesDirectly的作用?
系统给我们提供了一个方法:
+(BOOL)accessInstanceVariablesDirectly
,默认是返回YES
在返回YES的基础上,才会调用Instance Var is exist,判断是否存在相应的实例变量或者相似的实例变量
如果我们重写该方法,使其返回NO,那么直接报没有找到对应的值处理;不再进行实例变量的判断。
问:Instance Var is exist?判断规则是什么?
通过查找是否存在:_key、_isKey、key、isKey四个实例变量,如果有则YES
这四个都是实例变量
问:如果valueForKey:没有找到对应的value,会怎样?
如果valueForKey:没有找到对应的value,会调用valueForUndefinedKey:
从而造成崩溃
YZPerson *person1 = [[YZPerson alloc] init];
NSLog(@"person1.age = %@", [person1 valueForKey:@"age"]);
setValue:forKey:的系统实现流程:
里面的具体方法与流程与valueForKey:类似,不再进行具体分析。
属性关键字
问:属性关键字可以分为哪几类?
读写权限:readonly、readwrite(默认)
原子性:atomic(默认)、nonatomic
引用计数
setter\getter
atomic可以保证获取和赋值是线程安全的
但是,并不能保证在使用过程中是线程安全的
引用计数:
retain\strong
assign\unsafe_unretain
weak
copy
问:assign和weak的区别有哪些?
assign
可以修饰基本数据类型,如int、BOOL等
可以修饰对象类型,但不改变其引用计数器
当使用assign修饰对象,在对象释放的时候,会产生悬垂指针
weak
不可以修饰基本数据类型
修饰对象类型,但不改变其引用计数器
当使用weak修饰对象,在对象释放的时候,会自动置为nil
问:关于空指针、野指针、悬垂指针的区别
空指针:指针指向的地址为空的指针
野指针:指针创建时未初始化。指针变量刚被创建时不会自动成为NULL指针,它会随机指向一个内存地址。
垂悬指针:指针所指向的对象已经被释放或者回收了,但是指向该对象的指针没有作任何的修改,仍旧指向已经回收的内存地址。
更多学习关于悬垂指针
按照上面的说法,我们之前很多说的野指针错误,严格说来并不是野指针错误,而是悬垂指针。
问:浅拷贝、深拷贝
浅拷贝并没有新建一块内存
浅拷贝对原来的对象引用计数+1
深拷贝会新建一块内存,与原来的内存里面内容一样
深拷贝对原来的对象引用计数不变
如果赋值过来是NSMutableArray,copy后其实是NSArray,如果对其进行添加或者删除操作,会造成程序崩溃
问:为何会做 _obj != obj 的判断?不做可以吗?
如果不做,则先进行[_obj release];也就是原来的对象可能已经被释放掉了。再进行[obj retain]操作,会造成程序异常。