OC语言特性
Category
Category的特点:运行时决议,可以用来分解大类,可以给系统类添加方法,可以添加方法、协议、属性。不能添加成员变量,也不会自动生成setter和getter方法。
那么分类为什么具备这些特点?首先了解下分类的结构体:
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;//属性列表
};
同时由于iOS系统通过dyld动态加载runtime库以后,在调用_read_images方法是才会处理category相关内容,所以决定了category是运行时决议的。然后我们通过runtime中的category_t结构体可以看出分类是可以添加方法、协议和属性,但是不会添加成员变量,同时也不会生成方法setter和getter方法。
通过分析runtime中的attachCategories
方法,我们得知了runtime为我们生成的方法列表为一个二维数组[[method_t,method_t],[method_t,method_t,method_t],[method_t,method_t]]
,并且是倒序的把分类的方法添加到数组中,这也决定了最后被编译的同名方法,会被访问。
Category中的load方法和initialize方法
load
: 分类中的load方法是在程序刚加载,runtime处理类的时候被调用的,load
是runtime直接通过函数地址被调用,并没有使用objc_msgSend
,所以不存在消息传递的过程,就不会像普通分类方法出现“覆盖”的现象,runtime的源码设计确保类的load
优先于分类的load
。如果类之间没有继承关系,那么就和xcode
中的编译顺序相关,且也会保证先调用类的父类的load
方法。
initialize
: initialize
方法是在类被第一调用的时候被调用,以懒加载的形式存在,确保节省系统资源,即使多次调用alloc也不会重复调用。但是存在一个问题就是,当一个类存在多个分类的时候,那么主类的initialize
可能会被调用多次。
下面是load和initialize的一些对比:
load | initialize | |
---|---|---|
调用时机 | 被添加到 runtime 时 | 收到第一条消息前,可能永远不调用 |
调用顺序 | 父类->子类->分类 | 父类->子类 |
调用次数 | 1次 | 多次 |
是否需要显式调用父类实现 | 否 | 否 |
是否沿用父类的实现 | 否 | 是 |
分类中的实现 | 类和分类都执行 | 覆盖类中的方法,只执行分类的实现 |
load
的详细调用顺序
- 先调用类的
load
方法
- 按照编译顺序去调用类的
load
方法 - 调用类
load
之前先调用父类的load
方法
- 然后是分类的
load
方法
- 按照编译顺序调用,先编译先调用(注意:和普通的分类方法顺序相反,普通的分类方法由于是后编译的方法放在method_list前面,所以会被先调用))
initialize
的详细调用顺序
Extension
扩展:编译时决议。只能写在.m文件中,可以声明一些私有属性、私有成员变量和私有方法(意义不大)。
关联对象
关联对象有两个比较重要的方法
id objc_getAssociatedObject(id object,const void *key)
和objc_setAssociatedObject(id object,const void *key,id value,objc_AssociatedPolicy policy)
,调用objc_setAssociatedObject
后会将被关联的对象和值存储在一个全局的容器
中,结构类似如下:
"0x11123124":{//对象
"@selector(key)":{
"value":"value1",
"policy":"copy"
}
}
KVO
当ClassA调用addObserver:forKeyPath:options:context后,runtime会给我们创建一个ClassA的子类
NSKVONotifying_A
,然后重写对应属性的setter方法,然后将isa指针指向NSKVONotifying_A
。例如:

KVC
两个比较关键的方法是
-(id)getValueForKey:(NSString *)key
和-(void)setValue:(id)value forKey:(NSString*)key
-(void)setValue:(id)value forKey:(NSString*)key
的原理:

-(id)getValueForKey:(NSString *)key
的原理:

属性
- atomic:保证赋值和获取线程安全,不保证操作线程安全。
- noatomic:非线程安全
- retain/strong:修饰对象
- assign:修饰基础数据类型,不改变引用计数器,修饰的对象被释放后,指针仍然指向原地址,会存在野指针。
- weak:修饰对象类型,不改变引用计数器,修饰的对象被释放后会指向nil
copy
源对象类型 | 拷贝方式 | 目标对象类型 | 深/浅 |
---|---|---|---|
可变 | copy | 不可变 | 深 |
可变 | mutableCopy | 可变 | 深 |
不可变 | copy | 不可变 | 浅 |
不可变 | mutableCopy | 可变 | 深 |
我们逐一分析一下,前提是copy出来的对象肯定是不可变,mutableCopy的对象肯定是可变对象。
表格第一、二、四行:假设可变对象被copy出比可变对象是浅拷贝,那么两个对象指向同一个内存,我们对源对象进行修改,必然也会影响目标对象,会造成值的使用上的混乱,所以必然copy后是深copy,才能保证值得准确性。第二、四行也是同样的道理,都是为了避免修改可变对象的时候影响到另一个对象。
表格第三行:不可变对象copy出来的不可变对象,为了节省内存的使用所以是浅拷贝,因为他们的值是不变的,使用同一块内容,可以节省内存空间