OC基础特性

基础特性

一、分类(Category)

问题1:你用分类都做了哪些事情?

  • 声明私有方法
  • 分解体积庞大的类文件
  • 把Framework的私有方法公开

1.1、特点

  • 运行时决议
    编写完成的分类文件,并没有把分类内容添加到宿主类中。
    也就说宿主中还没有分类中的方法,而是在运行时,通过runtime将分类的方法添加到宿主当中。
    这也是分类的最大特点,也是分类和扩展的最大区别。

  • 可以为系统类添加分类
    扩展不能为系统添加扩展

1.2:分类中都可以添加哪些内容?

  • 实例方法
  • 类方法
  • 协议
  • 属性
    在分类中定义一个属性,实际是只是生成了setter和getter方法,并没有在分类添加实例变量。想要添加实例变量,可以通过关联对象的方法。

1.3、分类数据结构

Category 是表示一个指向分类的结构体的指针,其定义如下:
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;
 //实例属性列表
  struct property_list_t *instanceProperties;
}

1.4、加载调用栈(了解)

加载调用栈

备注:

  • _objc_init:runtime初始化方法。
  • images:指的是镜像

1.5、源码分析

这里以添加实例方法举例:
remethodizeClass开始入手分析(只列出主要代码):

1.5.1

我们只分析分类当中实例方法添加的逻辑
因此在这里我们假设 isMeta = NO

bool isMeta = cls->isMetaClass();
1.5.2

生成一个新的二维数组,用来存放分类中的方法

method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
1.5.3

遍历分类数组,采用倒叙遍历

//宿主分类的总数
int i = cats->count;
    while (i--) {
        //获取一个分类
        auto& entry = cats->list[i];
        //获取该分类的方法列表
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            //最后编译的分类,方法最先添加到分类数组中
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }
    }
1.5.4

获取宿主类当中的rw数据,其中包含宿主类的方法列表信息

auto rw = cls->data();
1.5.5

分类方法拼接到rw的methods上
参数/变量含义:
rw代表类。
methods代表类的方法列表。
attachLists方法:将含有mcount个元素的mlists拼接到rw的methods上。

rw->methods.attachLists(mlists, mcount);
1.5.6

attachLists方法解析
假设列表中原有元素总数为2(oldCount = 2)。
假设将要添加的分类元素总数为3(addedCount = 3)。
[ [method_t , method_t], [method_t],
[method_t , method_t , method_t] ]

这里只分析有数组的情况

//列表中原有元素总数。假设oldCount = 2
uint32_t oldCount = array()->count;
//拼接之后的元素总数
uint32_t newCount = oldCount + addedCount;
//根据新总数重新分配内存
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
//重新设置元素总数
array()->count = newCount;
/*
内存移动
[ [ ], [ ], [ ], [原有的第一个元素], [原有的第二个元素]]
*/
memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
/*
内存拷贝
[
    A ---> [addedLists中的第一个元素],
    B ---> [addedLists中的第二个元素],
    C ---> [addedLists中的第三个元素],
    [原有的第一个元素],
    [原有的第二个元素]
]
这也是分类方法会"覆盖"宿主类的方法的原因
*/
memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));

问题2:一个类的多个分类包含同名方法,最后哪个会生效?

解释:
谁最后编译,谁就生效。

1.6、分类总结

  • 分类添加的方法可以"覆盖"原类方法
    效果上是"覆盖",实际上原类方法还是存在的。
  • 同名分类方法谁能生效取决于编译顺序
  • 名字相同的分类会引起编译报错

二、关联对象

问题3:能否给分类添加成员变量?

解释:
可以添加。
在分类的声明时,不能添加成员变量;但是可以通过关联对象的方法添加。

问题4:给分类添加的成员变量是否添加到宿主中?

解释:
没有添加到宿主中。被存放在一个全局容器中,并且为不同类添加的关联对象都放在同一个全局容器中。

2.1、获取属性值

objc_getAssociatedObject(id object, const void *key);
  • object
    目标对象。即获取哪个对象的关联属性
  • key
    根据对应的key查找对象的属性

2.2、添加并设置属性值

objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
  • object
    目标对象。给自己添加属性,就用self。
  • key
    属性名。
  • value
    关联值。
  • policy
    策略。属性以什么形式保存。
    主要形式有以下几种:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,  // 指定一个弱引用相关联的对象
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相关对象的强引用,非原子性
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,  // 指定相关的对象被复制,非原子性
    OBJC_ASSOCIATION_RETAIN = 01401,  // 指定相关对象的强引用,原子性
    OBJC_ASSOCIATION_COPY = 01403     // 指定相关的对象被复制,原子性   
};

2.3、移除所有的关联对象

objc_removeAssociatedObjects(id object);

2.4、关联对象的本质

  • 关联对象由AssociationsManager管理;并在AssociationsHashMap存储。
  • 所有对象的关联内容都在同一个全局容器中
关联对象本质
json对比

这里包含三部分数据结构:

  • AssociationsManager
    管理对象,里面有一个AssociationsHashMap对象。
  • AssociationsHashMap
    可以理解为全局容器。
    由多个obj:ObjectAssociationMap组成。
    一个实例对象就对应一个ObjectAssociationMap。
  • ObjectAssociationMap
    由多个属性名key:ObjcAssociation组成。
  • ObjcAssociation
    由value和policy组成。

由上面可以知道,关联对象没有存放到原来的对象里面,而是自己维护了一个全局map来存放的

问题5:怎样清除一个关联对象?

解释:
通过objc_setAssociatedObject,将其中value传值为nil,就可以了。

三、扩展(Extension)

  • 声明私有属性
  • 声明私有方法(只是为了方便阅读,无太大作用)
  • 声明私有变量

问题6:属性和变量的区别?

属性 = 变量 + set方法 + get方法

问题7:分类和扩展的区别?

  • 扩展是编译时决议,分类是运行时决议
  • 扩展是以声明的形式存在,不是独立的存在,多数寄存于宿主类.m文件中;而分类是单独存在的。
  • 不能为系统类添加扩展;但是可以为系统的类添加分类。

四、代理

  • 是一种软件设计模式。
  • iOS中以@protocal形式体现。
  • 传递方式是一对一

问题8:代理的工作流程是什么?

代理工作流程

问题9:为什么代理声明的时候,要使用weak?

因为代理方一般强引用委托方,为了防止循环引用,委托方必须对代理方进行弱引用。


代理

五、通知

  • 使用观察者模式来实现的,用于跨层传递消息的机制。
  • 传递方式为一对多

问题10:通知和代理的区别?

  • 代理是由代理模式实现的;通知是由观察者模式实现的。
  • 代理传递是一对一,通知是一对多

问题11:如何实现通知机制?

通知原理

在通知中心,内部维护一个Map表,它的key是通知名称,value是观察者表List观察者表List内部包含多个observer对象;observer对象包含通知接收的观察者,观察者调用的方法等等。

六、KVO

6.1、概念

  • KVO是Key-value observing的缩写。
  • KVO是OC对观察者设计模式的又一实现。
  • Apple使用了isa混写(isa-swizzling)来实现KVO。

6.2、KVO调用发生过程图

过程图
  • A类对象调用系统方法addObserver:forKeyPath:options:context:
  • 在运行时会创建一个NSKVONotifying_A类,这个NSKVONotifying_A类A类的子类
  • 此时,A类的isa指针会指向NSKVONotifying_A
  • 然后在NSKVONotifying_A中会重写setter方法
  • 在重写的setter方法中,先调用willChangeValueForKey: ➡️调用父类方法,给父类赋值➡️给子类属性赋值➡️调用didChangeValueForKey:➡️最终会调用observeValueForKeyPath通知了所有的观察者。

问题12:isa混写技术在KVO中是怎么体现的?

见6.2.KVO调用发生过程图

6.3、重写setter方法的具体实现

6.3.1、两个重要方法
重写方法
6.3.2、具体实现
具体实现

备注:

  • 一定会调用父类方法实现,否则数值不统一。
  • didChangeValueForKey调用后会触发observeValueForKeyPath方法。

问题13:通过KVC设置value能否生效?

可以生效

问题14:为什么通过KVC设置value能生效?

因为KVC内部原理表明,这样设置value会调用obj对象的setter方法

问题15:通过成员变量直接赋值value能够生效?

  • 不能。
  • 可以手动触发KVO,在给变量赋值的代码前后加入willChangeValueForKey:didChangeValueForKey:
  • 代码如下:


    手动KVO

6.4、KVO总结

  • 使用setter方法改变值KVO才生效。
  • 使用setValue:forKey:改变值KVO才会生效。
  • 成员变量直接修改需手动添加KVO才会生效。

七、KVC

问题16:什么是KVC

  • 键值编码技术、Key-value coding缩写
  • (id)valueForKey: (NSString *)key
  • (void)setValue:(id)value forKey:(NSString *)key

问题17:KVC会不会破坏面向对象的编程思想?

会破坏。
因为上面的key是没有限制的,即使是私有变量,也可以通过KVC设置私有变量的value。

7.1、valueForKey系统实现流程

7.1.1、流程图
valueForKey
  • 先查找对应的get方法是否存在。
    如果存在,则调用对应方法,结束流程。
    如果不存在,则查找对应的实例变量是否存在。
  • 查找实例变量是否存在。
    系统提供了一个开关函数accessInstanceVariablesDirectly,默认是YES,允许查询相同相似实例变量
    如果实例变量存在,则获取当前值返回。
  • 如果实例变量不存在,则会调用valueForUndefinedKey方法,抛出一个NSUndefinedKeyException异常。
7.1.2、流程图 get方法是否存在的判断规则

  • 如果有以上命名的访问器方法,则默认当前get方法是存在的
7.1.3、流程图 实例方法是否存在的判断规则
  • _key
  • _isKey
  • key
  • isKey
    如果找到上述同名或者类似的变量,则默认当前的成员变量是存在的。

7.2、setValue:forKey:系统实现流程

7.2.1、流程图

setValue:forKey:
  • 先查找对应的set方法是否存在。
    如果存在,则调用对应方法,结束流程。
    如果不存在,则查找对应的实例变量是否存在。
  • 其余步骤同valueForKey相同。

八、属性关键字

8.1、属性关键字分类:

1、读写权限

  • readonly
  • readwrite(默认关键字)

2、 原子性

  • atomic(系统默认)
    可以赋值和获取是线程安全的。但是操作对象是无法保证线程安全的。
    比如:一个数组Array。如果对Array进行赋值或获取是线程安全的,如果对Array进行操作(添加/移除对象),则无法保证线程安全
  • nonatomic

3、 引用计数

  • retain/strong
    retain:MRC
    strong:ARC
  • assign/unsafe_unretained
    unsafe_unretained:ARC基本不使用
  • weak

8.2、assign特点

  • 修饰基本数据类型,如int、Bool等。
  • 修饰对象类型时,不改变其引用计数。
  • 会产生悬垂指针。
    assign指针指向的对象被释放后,assgin指针仍然指向原对象内存地址。

8.2、weak特点

  • 不改变被修饰对象的引用计数。
  • 所指对象在被释放后会自动置为nil。

问题18:assgin与weak区别

  • 修饰数据类型方面
    assign既可以修饰基本数据类型,也可以修饰对象。
    weak只可以修饰对象。
  • 指针
    assgin可以产生悬垂指针。
    weak指针自动指向nil。
    二者的共同点:都不影响引用计数。

8.3、copy

问题19:见下图

copy面试题
  • 如果赋值过来的是NSMutableArray,copy之后是NSArray。
  • 如果赋值过来的是NSArray,copy之后是NSArray。
    因为最终array是NSArray,不可变对象。
8.3.1、深拷贝和浅拷贝
  • 是否开辟新的内存空间
    深拷贝会,浅拷贝不会

  • 是否影响引用计数
    深拷贝不会,浅拷贝会


    总结
  • 可变对象的copy和mutableCopy都是深拷贝。

  • 不可变对象的copy是浅拷贝,mutableCopy都是深拷贝。

  • copy方法返回的都是不可变对象。

九、笔试题总结

9.1、MRC下如何重写retain修饰变量的setter方法?

MRC

9.2、请简述分类的实现原理

1、分类的实现原理是由运行时决议的。
2、不同分类的含有同名分类方法,谁最后编译,谁就生效。
3、如果分类中的方法恰好与目标类中的方法同名,分类会覆盖同名的目标类方法。

9.3、KVO的实现原理是怎样的?

1、KVO是系统关于观察者模式的一种体现。
2、KVO运用了isa混写技术;在动态运行时,为某一个类动态添加一个子类,重写了它的setter方法;将原有类的isa指针指向新创建的子类。

9.4、能否为分类添加成员变量?

不能。因为它的数据结构中,没有成员变量。
可以通过关联对象


问题1:什么是分类?分类的原理和实现机制是什么?

问题5:KVO实现机制?

问题6:KVC实现机制?

问题7:属性关键字?

你可能感兴趣的:(OC基础特性)