这篇学习文章的框架草稿其实一早打好了,但是工作上接二连三有些琐碎的事情打乱了节奏,中途也稍微又有些泄气,不过最后还是希望坚持一下
全篇图文基本自网络,属于知识总结,如有错漏欢迎指出
对于一个已经封装好的类(比如系统类、第三方库),不想/不能改动这个类,但是想在类中增加一个方法,这时候只需要给原来的类增加一个分类。
其实分类不应该理解成“把东西分成几类”,而应该理解为和“主类”相对应的“分类”,是对主类的扩展。
文件名:主类名+分类名 比如 NSString+Safe.h/NSString+Safe.m
文件中的语法:
@interface 主类类名(分类类名)
//一般来说不能定义成员变量
@end
@implementation 主类类名(分类类名)
@end
分类中可以添加实例方法、类方法、协议、属性(这里的属性是指添加getter和setter方法,并没有实例变量)
typedef 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; /* 添加的所有属性 */
} category_t;
//NSObject+SpecialName.h
@interface NSObject (SpecialName)
@property (nonatomic, copy) NSString *specialName;
@end
//NSObject+SpecialName.m
#import "NSObject+Extension.h"
#import
static const void *SpecialNameKey = &SpecialNameKey;
@implementation NSObject (SpecialName)
@dynamic specialName;
- (NSString *)specialName {
//如果属性值是非id类型,可以通过属性值先构造OC的id对象,再通过对象获取非id类型属性
return objc_getAssociatedObject(self, SpecialNameKey);
}
- (void)setSpecialName:(NSString *)specialName{
//如果属性值是非id类型,可以通过属性值先构造OC的id对象,再通过对象获取非id类型属性
objc_setAssociatedObject(self, SpecialNameKey, specialName, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
在网上的资料说的扩展大多是指分类,这里的扩展可以理解为一种匿名的分类。
扩展的作用是为某个类附加额外的属性、成员变量、声明方法(声明方法的意义不大)
扩展一般直接写在扩展类的.m文件中,也可以写成单一的.h文件,命名为"类名_类别名.h"(比如BaseViewController_Extension.h)
#import "类名.h"
@interface 类名 ()
// 在此添加私有成员变量、属性、声明方法
@end
协议Protocol是多个类之间协商的一个公共接口,提供一系列方法的声明给类使用,而代理Delegate是协议的一个典型应用机制。先看一下协议
协议是由一系列方法声明组成的,一个类只要遵守了协议,就相当于拥有了协议的所有方法声明
协议具有“继承”特性,一个协议遵守了其他协议,就用了其他协议的所有方法声明
遵守了协议的类在.m中实现协议方法,也就是说不同类对协议方法的实现可以是不同的。
类是否需要实现协议中的方法取决于方法在协议中的声明方式,默认方法为@required
@required //@required之下的方法是必须实现的,编译器会warning
- (void)eat;
@optional //@optional之下的方法是可选实现的
- (void)watch;
协议的声明方法:
@protocol Eat//@protocol Eat<其他协议>
@required
- (void)eat;
@optional
- (void)watch;
@end
NSObject在这里表示为一个基协议,所有协议都继承自基协议。
NSObject协议中声明了很多基本方法,比如description、retain、release等。
遵循协议的类的声明方法:
#import "协议名称.h"
@interface 类名 : 父类 <协议名称1, 协议名称2,…>
// 这里不要再声明协议里已经声明过的方法
@end
之后即可在.m中实现协议方法。
协议的限制对象类型作用:假设有协议MyProtocol,则声明一个id
个人理解:一个类不想/不能实现协议的方法,他就找一个遵循协议的代理类来实现,类在恰当的时机就可以调起代理类实现特定功能
图片来源:https://www.jianshu.com/p/e70bac443cf2
举例步骤:
1.创建了一个列表类协议UITableViewDelegate,视图类ListController遵循这个协议;
2.UITableView声明一个代理属性:@property (nonatomic, weak) id<代理协议> delegate;(UITableView本身应该不用遵循UITableViewDelegate协议)
3.ListController实现UITableViewDelegate的方法numberOfSectionsInTableView
4.设置代理:UITableView.delegate = ListController
5.当UITableView初始化时调用代理ListController的numberOfSectionsInTableView来获取section的数量
6.ListController执行代理方法numberOfSectionsInTableView,返回NSInteger告知UITableView结果
看到第二步,UITableView的代理属性使用weak关键词避免循环引用。
解释一下循环引用的情况:如果delegate为强引用(strong),假设UITableView其实是ListController中的一个属性(很普遍的情况),那么UITableView持有了一个ListController对象强引用,外部持有了一个ListController的引用,同时ListController持有了一个UITableView强应用,造成了UITableView和ListController的循环引用
如果delegate采用assign修饰,没有引用计数操作,但当页面销毁时这个对应的代理地址并没置为nil,从而导致成为了一个野指针,然后在调用方法时便会产生崩溃
调用代理的语法:
if ([obj.delegate respondsToSelector:@selector(doSth)]) {
[obj.delegate doSth];
}
题外话:感觉代理的特性和我平时block的部分用法有点异曲同工?
通知中心负责发布通知和监听通知,每一个应用程序有一个通知中心实例,负责不同对象之间的消息通信。
任何一个对象都可以向通知中心发布通知NSNotification,描述自己在做什么。其他感兴趣的对象Observer可以申请在某个特定通知发布时(或在某个特定的对象发布通知时)收到这个通知
通知的属性和初始化
- (NSString *)name; // 通知的名称
- (id)object; // 通知发布者(是谁要发布通知)
- (NSDictionary *)userInfo; // 一些额外的信息(通知发布者传递给通知接收者的信息内容)
//初始化一个通知(NSNotification)对象
+ (instancetype)notificationWithName:(NSString *)aName object: (id)anObject;
+ (instancetype)notificationWithName:(NSString *)aName object: (id)anObject userInfo:(NSDictionary *)aUserInfo;
- (instancetype)initWithName:(NSString *)name object:(id)object userInfo:(NSDictionary *)userInfo;
通知的发布
//通知中心的方法
- (void)postNotification:(NSNotification *)notification;
发布一个notification通知,可在notification对象中设置通知的名称、通知发布者、额外信息等
- (void)postNotificationName:(NSString *)aName object:(id)anObject;
发布一个名称为aName的通知,anObject为这个通知的发布者
- (void)postNotificationName:(NSString *)aName object:(id)anObject userInfo:(NSDictionary *)aUserInfo;
发布一个名称为aName的通知,anObject为这个通知的发布者, aUserInfo为额外信息```
通知的发送与处理时同步的,在某个地方发送一个通知时,会等到所有观察者对象执行完处理操作后,才回到发送通知的地方,继续执行后面的代码。
通知的监听
注册监听通知的监听器
-(void)addObserver:(id)observer selector:(SEL)aSelector name:(NSString )aName object:(id)anObject;
//observer:监听器,即谁要接收这个通知
//aSelector:收到通知后,回调监听器的这个方法,并且把通知对象当做参数传入*
//aName:通知的名称。如果为nil,那么无论通知的名称是什么,监听器都能收到这个通知
//anObject:通知发布者。如果为anObject和aName都为nil,监听器都收到所有的通知
- (id)addObserverForName:(NSNotificationName)name object:(id)obj queue:(NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block;
//name:通知的名称
//obj:通知发布者
//block:收到对应的通知时,会回调这个block
//queue:决定了block在哪个操作队列中执行,如果传nil,默认在当前操作队列中同步执行
第一个方法是常用的方法,让observer监听anObject发送的aName通知,接受通知后执行aSelector方法
第二个方法不太常见,创建了一个匿名对象作为观察者(返回的id
addObserverForName方法简析:
- name和obj和第一个方法的aName,anObject是相同的
- 如果queue为nil,则接受通知的线程和发送通知的线程相同
- block块会被通知中心拷贝copy一份,维护block对象知道观察者从通知中心中移除-》所以block中需要避免对象循环引用
- 如果给定通知出发了多个block,这些操作会在各自的queue中被并发执行,不能预期其顺序
对于3.所说情况网上有很多例子,可以自行百度,这里不再复述
取消注册(移除)监听器
-(void)removeObserver:(id)observer;
-(void)removeObserver:(id)observer name:(NSString *)aName object:(id)anObject;
前一个方法将observer从通知中心移除,而后一个会根据三个参数来移除相应的观察者。
注意对于用addObserverForName添加的监视器,正确的observer应该是addObserverForName返回的匿名对象,而不是注册通知所在类。
- 通知中心会维护一个观察者的unsafe_unretained引用,所以在释放对象时需要确保移除对象所有监听的通知,否则通知中心依然可以发送通知给观察者,产生不可预期的问题
- 第一个方法适合在类的dealloc方法中调用(注意出现循环引用时不会进入dealloc),确保将对象从通知中心移除;第二个方法适合在类的viewWillDisappear:中调用,这是为了避免移除父类/系统类的通知
总结来说通知的注册和移除有这样的对应
viewDidLoad->dealloc
viewWillAppear->viewWillDisappear
按照这个作者说法实际上根据系统版本可能可以不严格对应,因为
https://www.jianshu.com/p/26323f5b823d
不过还是严格对应最安全吧
键值观察是是使用“获取其他对象的特定属性变化”的通知机制。可以使用KVO来检测对象属性的变化,快速做出响应,为开发强交互、响应式应用以及实现视图和模型的双向绑定时提供大量的帮助。
使用KVO,需要注册成为某个对象属性的观察者,在合适的时间点将观察者移除,同时重写一个方法,在方法中判断是否为需要观察的属性发生变化。
这里假设Fizz类持有一个@property (nonatomic, strong) NSNumber *number 属性
//class ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.fizz = [[Fizz alloc] init];
[self.fizz addObserver:self
forKeyPath:@"number"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];//注册成为某个对象属性的观察者
self.fizz.number = @2;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
//重写一个方法,在方法中判断是否为需要观察的属性发生变化
if ([keyPath isEqualToString:@"number"]) {
NSLog(@"%@", change);
}
}
- (void)dealloc {
[self.fizz removeObserver:self forKeyPath:@"number"];//在合适的时间点将观察者移除
}
上述代码可以对应到前面所讲的使用KVO步骤。
第三个方法在大多数情况下只需要对比 keyPath
,就可以知道到底监控的是哪个对象,但是在更复杂的业务场景下,使用 context
上下文以及其它辅助手段才能够更加精准地确定被观测的对象
需要注意的情况:
//class ViewController
Fizz *fizz = [[Fizz alloc] init];
fizz.observer = self;
//clas Fizz
- (void)dealloc {
[self removeObserver:self.observer forKeyPath:@"number"];
}
本小节参考:https://draveness.me/kvocontroller 作者并不推荐使用原生KVO
KVO的实现依赖于OC的Runtime运行时机制。
为一个对象注册观察者时,一个新的类会被动态创建,这个类继承自该对象原本的类,并重写了被观察属性的setter方法。重写的setter方法会负责在调用原setter方法之前和之后通知所有观察对象属性值的更改。最后再把对象的isa指针指向这个新创建的子类,对象就可以成为新创建的子类的实例。同时系统还重写了-class方法,这样就显得对象还是原来的类没有改变。
具体实现可以参考这篇Blog:https://tech.glowing.com/cn/implement-kvo/ 这里先不深入研究了
键值编码是指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值,而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性,而不是在编译时确定。
最重要的四个方法
- (nullable id)valueForKey:(NSString *)key; //直接通过Key来取值
- (void)setValue:(nullable id)value forKey:(NSString *)key; //通过Key来设值
- (nullable id)valueForKeyPath:(NSString *)keyPath; //通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通过KeyPath来设值
具体使用这里就不细说了。
参考:https://www.jianshu.com/p/45cbd324ea65
设值(基础Setter搜索模式)
调用setValue:forKey:时,底层执行机制:(这里以setValue:@"123" forKey:@"name"为例进行说明)
1.先调用setName:@"123" 方法,通过setter方法完成设置
2.如果没有找到setName:方法,KVC机制检查accessInstanceVariablesDirectly方法返回值,默认返回YES。如果重写该方法让其返回NO,则KVC执行setValue:forUndefined:方法(即不让该类实现KVC,找不到setName:就直接到5.)
3.如果accessInstanceVariablesDirectly方法返回YES,就会搜索该类里面有没有名为_name的成员变量,无论该变量是在类接口处定义,还是在类实现处,也无论用了什么样的访问修饰符,只要存在以_name命名的变量,KVC都可以对该成员变量赋值
4.如果上述操作都没有找到对应变量,接着搜索_isName的成员变量,之后再搜索name和isName成员变量。只要找到对应变量就给他们赋值
5.如果上述方法都找不到成员变量,系统会执行该对象的setValue:forUndefinedKey:方法,默认抛出异常。
取值(基础getter搜索模式)
调用valueForKey:时,KVC的搜索不同于setValue:forKey:(这里以valueForKey:@"name"为例进行说明)
1.首先按getName,name,isName的顺序方法查找getter方法,找到则直接调用。如果是bool,int等值类型,会将其包装成一个NSNumber对象
2.如果上面的方法没有找到,KVC查找countOfName,objectInNameAtIndex或nameAtIndexes方法,如果这三个方法中的countOfName和另外两个方法中的一个被找到,那就返回一个可以响应NSArray所有方法的代理集合NSKeyValueArray(NSArray子类),调用这个代理集合的方法,就会以countOfName,objectInNameAtIndex或nameAtIndexes这几个方法组合的形式调用。还有一个getName:range:方法可选,通过添加(重写)这些方法可以重新定义KVC的一些功能
3.如果没有找到上述方法,再查找countOfName,enumeratorOfName,memberOfName方法。如果三个方法都找到,返回一个可以响应NSSet所有方法的代理集合
2.的条件是countOfName+objectInNameAtIndex或者countOfName+nameAtIndexes,把对象当作NSArray
3.的条件是countOfName+enumeratorOfName+memberOfName,把对象当作NSSet
4.如果没有找到找到,检查类方法accessInstanceVariablesDirectly,如果返回YES,按_Name,_isName,name,isName的顺序搜索成员变量名。
5.如果accessInstanceVariablesDirectly返回NO或者没有搜索到变量名,则调用valueForUndefinedKey:方法并抛出异常。
- (nullable id)valueForKeyPath:(NSString *)keyPath; //通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通过KeyPath来设值
回到这两个方法上,主要解决了一个类成员变量是复杂类型,需要多次使用valueForKey/setValue获取属性的问题。
keyPath是用小数点拼接的key,因此这两个方法的搜索机制的第一步是分割keyPath为key,然后像普通key一样搜索下去。
//TODO
参考:OC知识--成员变量(属性,实例变量)的相关知识
参考:OC - 属性关键字和所有权修饰符
参考:@property详解,@property修饰符以及各个修饰符区别
成员变量一般是指在类中声明的变量,如下
@interface Iphone : NSObject
{
// 成员变量声明
int _cpu; // cup 0
int _size; // 尺寸 0
int _color; // 颜色 0
// 其中_cpu、_size、_color 就是 Iphone 类的成员变量
}
成员变量无法从外界被直接访问,因此通过getter-setter方法为调用者提供对成员变量的访问、赋值等操作
如果给成员变量提供了getter-setter方法,就可以通过点语法来访问成员变量
点语法的本质就是调用getter-setter方法,因此不能对没有getter-setter方法的成员变量使用点语,也不能在getter-setter方法中使用本属性的点语法,因为会造成死循环
成员变量可以使用修饰符来确定作用域:@public/@protected/@private/@package。@package是框架级别的,作用域介于私有和公开之间,只要处于同一框架就相当于@public,在框架外部相当于@private
在@interface@end之间声明的成员变量如果不做特别说明默认是为protected
@property是声明属性方法(getter-setter)的语法
@interface
@property int size;
@end
|
V
- (int)size;
- (void)setSize:(int)size;
@synthesize是实现属性方法的语法
@synthesize size;
|
V
- (int)size{
}
- (void)setSize:(int)size{
}
//注意:@synthesize size; 并没有告诉setter和getter 把size赋值给谁,返回谁
@synthesize size= _size;
|
V
- (int)size{
return _size;
}
- (void)setSize:(int)size{
_size = size;
}
按照上图代码,如果成员变量_size不存在,就会自动生成一个@private的成员变量_size
另外还有一个@dynamic,告诉编译器属性的setter和getter方法由用户自己实现不自动生成。如果一个属性被声明为@dynamic但没有提供setter/getter方法,编译不会出错,但在执行get/set操作时会程序崩溃
自Xcode4.4以后,apple对@property进行了一个增强,以后不用再写@synthesize了,只用一个@property就可以同时生成setter/getter方法的声明和实现。
默认@property会将传入的属性赋值给_开头的对应成员变量,如果没有则自动生成一个私有的_开头的成员变量
如果重写了setter/getter方法,那么@property只会生成getter/setter方法
如果同时重写了setter和getter方法,那么@property不会自动生成_开头的成员变量
atomic在setter/getter方法内加锁(自动生成自旋锁代码),避免多线程下对一个属性进行setter/getter操作造成数据混乱,耗费系统资源。而且setter和getter之间并不会加锁,因此不能真正保证线程安全
readonly只生成getter方法
指定生成的setter方法名,setter = xxx:
指定生成的getter方法名,getter = xxx
//TODO