OC语言相关

232 阅读4分钟

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的详细调用顺序

  1. 先调用类的load方法
  • 按照编译顺序去调用类的load方法
  • 调用类load之前先调用父类的load方法
  1. 然后是分类的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出来的不可变对象,为了节省内存的使用所以是浅拷贝,因为他们的值是不变的,使用同一块内容,可以节省内存空间