属性
属性与成员变量之间的关系
- 属性对成员变量扩充了存储方法
- 属性默认会生成带下划线的成员变量
- 声明了成员变量不会生成属性
成员变量地址可以根据实例的内存地址偏移寻址。而属性的读写都需要函数调用,相对更慢。self对应类的成员变量的首地址
类别
利用OC的动态运行时为现有类添加方法,您可以在先有类中添加属性,但是不能添加成员变量,但是我们的编译器会把属性自动生成存储方法和带下划线的成员变量,所以我们的声明的属性必须是@dynamic类型的,及需要重写存储方法。
#import "Person.h"
@interface Person (Ex)
@property(nonatomic, strong) NSString *name;
- (void)setName:(NSString *)name; //***
- (NSString *)name;//***
- (void)method_one;
@end
分类方法实现中可以访问原来类中声明的成员变量。
类别的优缺点
- 不破坏原有的类的基础之上扩充方法。
- 实现代码之间的解偶
缺点
- 无法在类别中添加新的实例变量,类别中没有空间容纳实例变量
- 命名冲突
类别无法添加实例变量的原因,从Runtime角度,我们发现的确没有空间容纳实力变量,为什么可以添加属性,原因是属性的本质是实例变量+ getter方法 + setter方法;所以我们重写了set/get方法
同名方法调用的优先级为 分类 > 本类 > 父类
/*
* Category Template //类别模版
*/
typedef struct objc_category *Category;
struct objc_category {
char *category_name;
char *class_name;
struct objc_method_list *instance_methods;
struct objc_method_list *class_methods;
struct objc_protocol_list *protocols;
};
扩展
没有名字的类别
类别中创建的方法和属性都是私有的,只有这个类对象可以使用
优点
- 可以添加实例变量
- 可以更改读写权限(但是更改的权限的存储方法只能是私有的)
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic,readonly) NSString *name; //只有getter方法,是公有的
@end
#import "Person.h"
@interface Person ()
@property (nonatomic, readwrite) NSString *name; //更改name的权限,但是setter方法是私有的不能被外部访问
@end
@implementation Person
@end
类别和扩展的区别
就category和extension的区别来看,我们可以推导出一个明显的事实,extension可以添加实例变量,而category是无法添加实例变量的(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)extension在编译期决议,它就是类的一部分,但是category则完全不一样,它是在运行期决议的。extension在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,它、extension伴随类的产生而产生,亦随之一起消亡。
- extension一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加extension,所以你无法为系统的类比如NSString添加extension,除非创建子类再添加extension。而category不需要有类的源码,我们可以给系统提供的类添加category。
- extension可以添加实例变量,而category不可以。
- extension和category都可以添加属性,但是category的属性不能生成成员变量和getter、setter方法的实现。
Extension
在编译器决议,是类的一部分,在编译器和头文件的@interface和实现文件里的@implement一起形成了一个完整的类。
伴随着类的产生而产生,也随着类的消失而消失。
Extension一般用来隐藏类的私有消息,你必须有一个类的源码才能添加一个类的Extension,所以对于系统一些类,如NSString,就无法添加类扩展
Category
是运行期决议的
类扩展可以添加实例变量,分类不能添加实例变量
原因:因为在运行期,对象的内存布局已经确定,如果添加实例变量会破坏类的内部布局,这对编译性语言是灾难性的。
非正式协议
创建一个NSObject的类别称为"非正式协议"
正式协议
正式协议中可以方法,同时协议也是可以继承的
@protocal MySuperDuberProtocol <MyParentProtocol>
@optional //可选
@required //必须要实现
@end
委托
委托就是某个对象指定另一个对象处理某些特定事物的设计模式
代理主要由三部分组成:
- 协议:用来指定代理双方可以做什么,必须做什么。
- 代理:根据指定的协议,完成委托方需要实现的功能。
- 委托:根据指定的协议,指定代理去完成什么功能。
这里用一张图来阐述一下三方之间的关系:
代理使用原理
在iOS中代理的本质就是代理对象内存的传递和操作,我们在委托类设置代理对象后,实际上只是用一个id类型的指针将代理对象进行了一个弱引用。委托方让代理方执行操作,实际上是在委托类中向这个id类型指针指向的对象发送消息,而这个id类型指针指向的对象,就是代理对象。
代理内存管理
为什么我们设置代理属性都使用weak呢?
我们定义的指针默认都是__strong类型的,而属性本质上也是一个成员变量和set、get方法构成的,strong类型的指针会造成强引用,必定会影响一个对象的生命周期,这也就会形成循环引用。
Block
原理:
//最基础的结构体实现
void (^Blk)(void) = ^(void) {
};
Blk();
clang -rewrite-objc main.m后得到的结果
/*
// __block_impl 是 block 实现的结构体
struct __block_impl
{
void *isa; //说明block是一个对象来实现的
int Flags; //按位承载 block 的附加信息;
int Reserved; //保留变量
void *FuncPtr; //函数指针,指向Block需要执行的函数
};
*/
// __main_block_impl_0 是 block 实现的结构体,也是 block 实现的入口
struct __main_block_impl_0 {
struct __block_impl impl; //实现的结构体变量及__block_impl
struct __main_block_desc_0* Desc; //描述结构体变量
//结构体的构造函数,初始化结构体变量impl、Desc
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//__main_block_func_0最终需要执行的函数代码块
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
}
// __main_block_desc_0 是 block 的描述信息结构体
static struct __main_block_desc_0 {
size_t reserved; //结构体信息保留字段
size_t Block_size; //结构体大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)}; //定义一个结构体变量,初始化结构体,计算结构体大小
int main(int argc, const char * argv[]) {
void (*Blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)Blk)->FuncPtr)((__block_impl *)Blk);
}
isa 指向实例对象,表明 block 本身也是一个 Objective-C 对象。block 的三种类型:_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock,即当代码执行时,isa 有三种值
- impl.isa = &_NSConcreteStackBlock;
- impl.isa = &_NSConcreteMallocBlock;
- impl.isa = &_NSConcreteGlobalBlock;
- NSConcreteGlobalBlock 全局的静态 block,不会访问任何外部变量。
- _NSConcreteStackBlock 保存在栈中的 block,当函数返回时会被销毁。
- _NSConcreteMallocBlock 保存在堆中的 block,当引用计数为 0 时会被销毁。
block 实现的执行流程
main() >> 调用__main_block_impl_0构造函数初始化结构体__main__block_impl_0(__main_block_func_0, __main_block_desc_0_DATA) >> 得到的__main_block_impl_0类型变量赋值给Blk >> 执行Blk->FuncPtr()函数 >> END
带参数的Block
int intValue = 1;
void (^Blk)(void) = ^(void) {
NSLog(@"%d",intValue);
};
Blk();
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int intValue;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _intValue, int flags=0) : intValue(_intValue) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int intValue = __cself->intValue; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_r__t4y1n4fj5xlgntt308jvn7c80000gn_T_main_be769b_mi_0,intValue);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
int intValue = 1;
void (*Blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, intValue));
((void (*)(__block_impl *))((__block_impl *)Blk)->FuncPtr)((__block_impl *)Blk);
}
原来 block 通过参数值传递获取到 intValue 变量,通过函数
__main_block_impl_0 (void *fp, struct __main_block_desc_0 *desc, int _intValue, int flags=0) : intValue(_intValue)
保存到 __main_block_impl_0 结构体的同名变量 intValue,通过代码 int intValue = __cself->intValue; 取出 intValue,打印出来。
构造函数 __main_block_impl_0 冒号后的表达式 intValue(_intValue) 的意思是,用 _intValue 初始化结构体成员变量 intValue。
有四种情况下应该使用初始化表达式来初始化成员:
- 初始化const成员
- 初始化引用成员
- 当调用基类的构造函数,而它拥有一组参数时
- 当调用成员类的构造函数,而它拥有一组参数时
KVC、KVO
KVC/KVO是观察者模式的一种实现,在Cocoa中是以被万物之源NSObject类实现的NSKeyValueCoding/NSKeyValueObserving非正式协议的形式被定义为基础框架的一部分。从协议的角度来说,KVC/KVO本质上是定义了一套让我们去遵守和实现的方法.当然,KVC/KVO实现的根本是Objective-C的动态性和runtime
KVC定义了一种按名称(字符串)访问对象的机制,而不是访问器
KVC的实现细节
-(void)setValue:(id)value forKey:(NSString *)key;
- 首先搜索set方法,有就直接赋值
- 如果上面的 setter 方法没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly
- 返回 NO,则执行setValue:forUNdefinedKey:
- 返回 YES,则按<key>,<isKey>,<key>,<isKey>的顺序搜索成员
- 还没有找到的话,就调用setValue:forUndefinedKey:
// 允许直接访问实例变量,默认返回YES。如果某个类重写了这个方法,且返回NO,则KVC不可以访问该类。
+ (BOOL)accessInstanceVariablesDirectly
// 如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常
- (id)valueForUndefinedKey:(NSString *)key;
// 如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常
- (id)valueForUndefinedKey:(NSString *)key;
-(id)valueForKey:(NSString *)key;
首先查找 getter 方法,找到直接调用。如果是 bool、int、float 等基本数据类型,会做 NSNumber 的转换。
-
如果没查到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly
- 返回 NO,则执行valueForUndefinedKey:
- 返回 YES,则按<key>,<isKey>,<key>,<isKey>的顺序搜索成员
还没有找到的话,调用valueForUndefinedKey:
KVC 与点语法比较
用 KVC 访问属性和用点语法访问属性的区别:
用点语法编译器会做预编译检查,访问不存在的属性编译器会报错,但是用 KVC 方式编译器无法做检查,如果有错误只能运行的时候才能发现(crash)。
相比点语法用 KVC 方式 KVC 的效率会稍低一点,但是灵活,可以在程序运行时决定访问哪些属性。
用 KVC 可以访问对象的私有成员变量。
KVO 实现原理
当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。 派生类在被重写的 setter 方法实现真正的通知机制,就如前面手动实现键值观察那样。这么做是基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。 同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。
派生类 NSKVONotifying_Person 剖析:
在这个过程,被观察对象的 isa 指针从指向原来的 Person 类,被 KVO 机制修改为指向系统新创建的子类 NSKVONotifying_Person 类,来实现当前类属性值改变的监听。
所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对 KVO 的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为 NSKVONotifying_Person 的类(),就会发现系统运行到注册 KVO 的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为 NSKVONotifying_Person 的中间类,并指向这个中间类了。
因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。这也是 KVO 回调机制,为什么都俗称 KVO 技术为黑魔法的原因之一吧:内部神秘、外观简洁。
子类 setter 方法剖析:
KVO 在调用存取方法之前总是调用 willChangeValueForKey:,通知系统该 keyPath 的属性值即将变更。 当改变发生后,didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更。 之后,observeValueForKey:ofObject:change:context: 也会被调用。
重写观察属性的 setter 方法这种方式是在运行时而不是编译时实现的。 KVO 为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:
- (void)setName:(NSString *)newName
{
[self willChangeValueForKey:@"name"]; // KVO在调用存取方法之前总调用
[super setValue:newName forKey:@"name"]; // 调用父类的存取方法
[self didChangeValueForKey:@"name"]; // KVO在调用存取方法之后总调用
}
总结: KVO 的本质就是监听对象的属性进行赋值的时候有没有调用 setter 方法
系统会动态创建一个继承于 Person 的 NSKVONotifying_Person
person 的 isa 指针指向的类 Person 变成 NSKVONotifying_Person,所以接下来的 person.age = newAge 的时候,他调用的不是 Person 的 setter 方法,而是 NSKVONotifying_Person(子类)的 setter 方法
重写NSKVONotifying_Person的setter方法:[super setName:newName]
#import "ViewController.h"
#import "Student.h"
@interface ViewController ()
{
Student *_student;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_student = [[Student alloc] init];
_student.stuName = @"oldName_hu";
// 1.给student对象的添加观察者,观察其stuName属性
[_student addObserver:self forKeyPath:@"stuName" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
// 此时,stuName发生了变化
_student.stuName = @"newName_wang";
}
// stuName发生变化后,观察者(self)立马得到通知。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
// 最好判断目标对象object和属性路径keyPath
if(object == _student && [keyPath isEqualToString:@"stuName"])
{
NSLog(@"----old:%@----new:%@",change[@"old"],change[@"new"]);
}else
{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)dealloc
{
// 移除观察者
[_student removeObserver:self forKeyPath:@"stuName"];
}
@end
@interface Target : NSObject
{
int age;
}
// for manual KVO - age
- (int) age;
- (void) setAge:(int)theAge;
@end
@implementation Target
- (id) init
{
self = [super init];
if (nil != self)
{
age = 10;
}
return self;
}
// for manual KVO - age
- (int) age
{
return age;
}
- (void) setAge:(int)theAge
{
[self willChangeValueForKey:@"age"];
age = theAge;
[self didChangeValueForKey:@"age"];
}
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
@end
///
/**
* 添加观察者
*
* @param observer 观察者
* @param keyPath 被观察的属性名称
* @param options 观察属性的新值、旧值等的一些配置(枚举值,可以根据需要设置,例如这里可以使用两项)
* @param context 上下文,可以为nil。
*/
[person addObserver: self
forKeyPath: @"age"
options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context: nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
Person *per = object;
NSLog(@"keyPath: %@ object: %ld",keyPath, (long)per.age);
}