全方位剖析iOS高级技术问题(二)之Object-C语言相关问题

509 阅读10分钟

本文主要内容

一.分类Category
二.关联对象
三.扩展Extension
四.代理Delegate
五.通知NSNotification
六.KVO
七.KVC
八.属性关键字

截屏2022-08-10 11.35.47.png

一.分类Category

1、分类可以做哪些事?

  • 声明私有方法:方法只放在.m中
  • 分解体积庞大的类文件
  • 把Framework的私有方法公开

2、分类的特点

  • 分类在运行时决议:运行时才通过runtime把分类添加的内容真实的添加到宿主类上
  • 可以为系统类添加分类

3、分类中可以添加的内容

  • 实例方法
  • 类方法
  • 协议
  • 属性:只是声明了对应的get和set方法,并没有在分类中添加实例变量

4、分类底层原理

  • 分类的结构体
    分类的结构体为category_t,实际上就是创建的分类文件,第一个成员属性为name,即分类的名称,第二个为class的成员变量,表示分类所属的宿主类,接下来为实例方法的结构体、类方法结构体、协议、实例属性列表,如上分类结构体可以验证我们可以为分类添加哪些内容,并且不能添加实例变量。
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;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta) {
        if (isMeta) return nil; // classProperties;
        else return instanceProperties;
    }
};
  • 加载调用栈

截屏2022-08-08 19.25.14.png

  • 源码分析
static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;
    
    runtimeLock.assertWriting();

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

    isMeta = cls->isMetaClass();

    // Re-methodizing: check for more categories

    // 获取cls中未完成整合的所有分类
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
        if (PrintConnecting) {
            _objc_inform("CLASS: attaching categories to class '%s' %s", 
                         cls->nameForLogging(), isMeta ? "(meta)" : "");
        }

        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}
static void 

attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

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

    bool isMeta = cls->isMetaClass();

    /**
    二维数组
     [ [method_t,method_t,...], [method_t], [method_t,method_t,method_t],... ]
     */

    // fixme rearrange to remove these intermediate allocations
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    protocol_list_t **protolists = (protocol_list_t **)
        malloc(cats->count * sizeof(*protolists));

    // Count backwards through cats to get newest categories first
    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    int i = cats->count; // 宿主类分类的总数
    bool fromBundle = NO;

    while (i--) { // 这里是倒序遍历,最先访问最后编译的分类
        // 获取一个分类
        auto& entry = cats->list[i];

        // 获取该分类的方法列表
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            // 最后编译的分类最先添加到分类数组中
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        // 属性列表添加规则:同方法列表添加规则
        property_list_t *proplist = entry.cat->propertiesForMeta(isMeta);
        if (proplist) {
            proplists[propcount++] = proplist;
        }

        // 协议列表添加规则:同方法列表添加规则
        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }
    
    // 获取宿主类当中的rw数据,其中包含宿主类的方法列表信息
    auto rw = cls->data();

    // 主要是针对分类中有关于内存管理相关方法情况下的一些特殊处理
    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    
    /*
     rw代表类
     methods代表类的方法列表
     attachLists 方法的含义是:将含有mcount个元素的mlists拼接到rw的methods上
    */
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);
   
    rw->properties.attachLists(proplists, propcount);
    free(proplists);

    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}

通过解读分类源码总结如下:

  • 分类添加的方法可以“覆盖”原类方法
  • 同名分类方法谁能生效取决于编译顺序
  • 名字相同的分类会引起编译报错

注意:分类有多个的情况下,每个分类中有一个同名的分类方法,最终哪个会生效?最后编译的分类中的方法会最终生效。

二.关联对象

1、能否给分类添加“成员变量”?

我们不能在分类的声明或者定义实现时直接为分类添加成员变量,但是可以通过关联对象的技术为分类添加成员变量,达到分类可以添加成员变量的效果。

2、关联对象的本质及相关函数

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

截屏2022-08-09 14.41.21.png

id objc_getAssociatedObject(id object, const void *key)

id objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

void objc_removeAssociatedObjects(id object) 

3、关联对象源码分析

id 
objc_getAssociatedObject(id object, const void *key) 
{
    return objc_getAssociatedObject_non_gc(object, key);
}

void 
objc_setAssociatedObject(id object, const void *key, id value, 

                         objc_AssociationPolicy policy) 
{
    objc_setAssociatedObject_non_gc(object, key, value, policy);
}

#endif

void objc_removeAssociatedObjects(id object) 
{
#if SUPPORT_GC
    if (UseGC) {
        auto_zone_erase_associative_refs(gc_zone, object);
    } else 
#endif
    {
        if (object && object->hasAssociatedObjects()) {
            _object_remove_assocations(object);
        }
    }
}

三.扩展Extension

1、一般用扩展做什么?

  • 声明私有属性
  • 声明私有方法
  • 声明私有成员变量

2、扩展的特点

  • 编译时决议
  • 只以声明的形式存在,多数情况下寄生于宿主类的.m中
  • 不能为系统类添加扩展

四.代理Delegate

1、代理的特点

  • 准确的说是一种软件设计模式
  • iOS当中以@protocol的形式体现
  • 传递方式一对一

2、代理工作流程

截屏2022-08-09 15.05.24.png

3、使用代理过程中的注意点

  • 一般声明为weak以规避循环引用

截屏2022-08-09 15.07.08.png

五.通知NSNotification

1、通知的特点

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

代理和通知的区别:
1.代理是用代理模式实现的,通知是由观察者模式实现的;
2.代理传递方式是一对一,通知是一对多的传递方式。

2、通知的传递方式(一对多)

发送者经由通知中心广播给多个观察者! 截屏2022-08-09 15.12.10.png

问题:如何实现通知机制?或假如让你实现系统的通知机制流程,你怎样去实现?
在通知中心系统类内部可能会维护一个Notification_Map表(或字典),在该表(字典)当中,key是notificationName,也就是addObserver时传递的监听的名称,value是Observers_List,同一个notificationName可能添加多个Observer,Observer对应的value应该是一个数组列表,列表中的每一个成员首先应该包含通知接收的观察者,还应该包含该观察者需要调用的方法,即观察者收到通知后的回调方法截屏2022-08-09 15.17.29.png

六.KVO

1、KVO特点

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

当注册一个对象的观察者时,实际上是调用了系统的addObserver:forKeyPath:方法,调用此方法后,观察者观察对象A中的某一个成员变量或者属性时,系统会在运行时动态创建一个NSKVONotifying_A类,然后会将原来指向A类的isa指针指向该类,把isa指针指向修改实际上就是isa混写技术的标志。

isa混写技术在KVO当中是如何体现的?
当调用addObserver:forKeyPath:方法后系统会在运行时动态创建一个NSKVONotifying_A类(如类A为HObject,则新类为NSKVONotifying_HObject),同时将原来类Aisa指针指向新创建的类。NSKVONotifying_A类实际上是原来类A的一个子类。之所以继承类A是为了重写类A中的setter方法,子类通过对setter方法的重写达到可以通知所有观察对象的目的。

截屏2022-08-09 15.47.54.png

2、NSKVONotifying_A类中setter方法重写

- (void)setValue:(id)obj {

    [self willChangeValueForKey:@"keyPath"];

    // 调用父类实现,也即原类的实现
    [super setValue:obj];
    [self didChangeValueForKey:@"keyPath"];
}

问题1:通过KVC设置value能否生效?
KVC的setValue:forKey:方法可以调用对应对象的setter方法上,setter方法已经被动态运行时动态创建的子类重写,所以可以使KVO达到生效。

// 通过kvc设置value:可以生效
[obj setValue: @2 forKey:@"value"];

问题2:通过成员变量直接赋值value能否生效? 无法生效,没有触发系统KVO。系统KVO相当于在代码中添加willChangeValueForKeydidChangeValueForKey,所以可以手动KVO触发KVO生效,即在代码中添加上述两个调用即可实现KVO。

问题3:手动KVO 在对变量直接成员赋值时,在之前和之后分别添加willChangeValueFor KeydidChangeValueForKey就可以实现手动KVO,didChangeValueForKey在系统内部会触发KVO回调,即observeValueForKeyPath回调方法的调用。

总结

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

六.KVC

1、KVC特点

  • KVC是Key-value coding的缩写
- (id)valueForKey:(NSString *)key

- (id)setValue:(id)value ForKey:(NSString *)key

2、KVC系统实现流程

截屏2022-08-10 09.52.58.png

截屏2022-08-10 09.56.52.png

六.属性关键字

  • 读写权限
  • 原子性
  • 引用计数

1、读写权限

  • readonly

  • readwrite

2、原子性

  • atomic:atomic修饰的属性可以保证赋值和获取(不包括操作和访问)保证线程安全,如对数组元素的添加和移除不保证线程安全,只对数组的赋值和获取保证线程安全。

  • nonatomic

3、引用计数

  • retain/strong:都用来修饰对象,retain在MRC中使用,strong在ARC中使用。

  • assign/unsafe_unretained:assign既可以修饰基本数据类型也可修饰对象,unsafe_unretained在MRC中使用。

  • weak

  • copy

问题1:assign和weak关键字之间的异同

  • assign可以修饰基本数据类型,如int、BOOL等;
  • assign修饰对象类型时,不改变其引用计数;
  • assign会产生悬垂指针,即assign修饰的对象在被释放后,assign指针仍指向原对象地址,会导致内存泄漏或程序异常;
  • weak不改变被修饰对象的引用计数;
  • weak所指对象在被释放后会自动置为nil;

4、copy

  • 浅拷贝:是对内存地址的复制,让目标对象指针和源对象指向同一片内存空间

截屏2022-08-10 10.19.30.png

1.浅拷贝会增加被拷贝对象的引用计数
2.浅拷贝没有新的内存分配

  • 深拷贝:让目标对象指针和源对象指针指向两片内容相同的内存空间

截屏2022-08-10 10.22.50.png

1.深拷贝不会增加被拷贝对象的引用计数
2.深拷贝产生了新的内存分配

  • 浅拷贝和深拷贝的区别
    1.是否开辟了新的内存空间;2.是否影响了引用计数

copy操作总结:
1.可变对象的copy和mutableCopy都是深拷贝;
2.不可变对象的copy是浅拷贝,mutableCopy是深拷贝;
3.copy方法返回的都是不可变对象,mutableCopy方法返回的都是可变对象。

截屏2022-08-10 10.28.07.png

问题2:如下声明是否可以

@property(copy) NSMutableArray *array

copy方法返回的都是不可变对象,所以

  • 如果赋值过来的是NSMutableArray,copy之后是NSArray;
  • 如果赋值过来的是NSArray,copy之后是NSArray; 即结果都是NSArray,由于原来的属性被声明为NSMutableArray,就不可避免的会有调用方调用array的添加、移除等方法,而array被copy之后是NSArray类型,就会产生 程序异常或crash。

问题3:MRC下如何重写retain修饰变量的setter方法?

- (void)setObj:(id)obj {
    // 必须要进行不等的判断,否则在释放_obj的同时就会将obj也释放
    if(_obj != obj) {
        [_obj release];
        _obj = [obj retain];
    }
}

问题4:请简述分类实现原理

  • 分类是在运行时决议的;
  • 不同分类中含有同名方法,谁最终生效去决议最后参与编译的分类当中的方法最终生效;
  • 分类中添加的方法如果和宿主类中的方法同名,分类方法会“覆盖”宿主类中的方法,覆盖只是因为消息传递过程中优先查找数组靠前的元素,所以说宿主类中的方法依然存在,仍可用特殊方式调用。

问题5:KVO的实现原理是怎样的

  • 系统关于观察者模式的实现
  • 运用isa混写技术动态运行时为某个类添加一个子类,重写setter方法,同时把原有类的isa指针指向新创建的子类。

问题5:能否为分类添加成员变量
我们不能在分类的声明或者定义实现时直接为分类添加成员变量,但是可以通过关联对象的技术为分类添加成员变量,达到分类可以添加成员变量的效果。

有任何问题,欢迎👏各位评论指出!觉得博主写的还不错的麻烦点个赞喽👍