Object-C语言相关特性

接下来主要讲一下OC语言的相关特性
分别从以下几个方面进行讲解:

  • 分类
  • 扩展
  • 关联对象
  • 扩展
  • 代理
  • 通知
  • KVO
  • KVC

一、分类

Category是Objective-C 2.0之后添加的语言特性,分类、类别其实都是指的Category。Category的主要作用是为已经存在的类添加方法。
Objective-C 中的 Category 就是对装饰模式的一种具体实现。它的主要作用是在不改变原有类的前提下,动态地给这个类添加一些方法。

分类的作用

  • 可以吧Framework的方法公开化。
  • 分解体积庞大的类文件。
  • 声明并添加一些方法(包括实例方法和类方法)。
  • 能够添加协议。
  • 可以通过属性关联的形式给分类添加成员变量(本身分类是无法声明成员变量的)。
  • 可以模拟多继承。(OC中是没有多继承这个概念的)

分类的特点

  • 运行时决议的。这体现的OC动态语言的特性。
  • 可以为系统添加分类。

分类的结构

我们都知道不管是对象还是类,存储在内存都是结构体的形式,比如

id->objc_object,  //实例对象
Class->objc_class。//类

同样我们的分类对应的结构体如下:

    Category 是表示一个指向分类的结构体的指针,其定义如下:
    typedef struct objc_category *Category;
    struct objc_category {
      char *category_name                          OBJC2_UNAVAILABLE; // 分类名
      char *class_name                             OBJC2_UNAVAILABLE; // 分类所属的类名
      struct objc_method_list *instance_methods    OBJC2_UNAVAILABLE; // 实例方法列表
      struct objc_method_list *class_methods       OBJC2_UNAVAILABLE; // 类方法列表
      struct objc_protocol_list *protocols         OBJC2_UNAVAILABLE; // 分类所实现的协议列表
    }

通过分类的结果我们可以总结如下:
1.分类是用于给原有类添加方法的,因为分类的结构体指针中,没有属性列表,只有方法列表。原则上讲它只能添加方法, 不能添加属性(成员变量),实际上可以通过其它方式添加属性 ;
2.分类中的可以写@property, 但不会生成setter/getter方法, 也不会生成实现以及私有的成员变量,会编译通过,但是引用变量会报错;
3.如果分类中有和原有类同名的方法, 会优先调用分类中的方法, 就是说会忽略原有类的方法,同名方法调用的优先级为 分类 > 本类 > 父类;
4.如果多个分类中都有和原有类中同名的方法, 那么调用该方法的时候执行谁由编译器决定;编译器会执行最后一个参与编译的分类中的方法。
5.运行时决议
6.同名分类方法生效取决于编译顺序

创建分类

通过Xcode创建分类

然后给分类添加一个方法

成名方法

然后我们通过Clang找到编译后的文件,看看具体编译时期都做了什么。

iOS clang编译.m

打开终端
cd到想要编译的文件目录
xcrun(xcoderun) iphoneos(特定平台) -arch arm64(架构,x86是mac环境) -o(输出)
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
生成main.cpp
就可以愉快的看源码了

结构

可以看出分类存放在了DATA数据段当中,并声称了对应的实例方法

实例方法

Categroy加载调用栈

_objc_init(runtime初始化) —> map_2_images(镜像相关处理 )—>map_images _nolock(镜像相关处理)—> _read_images(读取镜像)—> remethodizeClass

上面是对加载分类的一个简单流程说明,分类的实现逻辑都在remethodizeClass中,因此我们最主要的是看remethodizeClass实现.因此我们最主要的是看remethodizeClass实现.
但是要了解remethodizeClass 我们先从程序入口开始。

我们知道,程序的入口点是从main函数:我们写的代码,它执行的第一步,均是从main开始的,但是,在程序进入main函数之前,内核已经为我们的程序加载和运行做了很多事情。

我们设置符号断点_objc_init,可以看到如下调用栈信息,这些函数都是在main之前调用的

添加符号断点
加载调用栈

_objc_init是Object-C runtime的入口函数,在这里主要是读取Mach-0文件OC对应的segment section,并根据其中的数据代码信息,完成为OC的内存布局,以及初始化runtime相关的数据结构。
我们可以看到,_objc_init是被_dyld_start所调用的,_dyld_start是dyld的bootstrap方法,最终调用到了_objc_init.
当程序启动时,系统内核首先会加载dyld,dyld是苹果的动态加载器,他是用来加载image(注意这里的image不是图片,而是Mach-O格式的二进制文件),并且把所依赖的库加载到内存空间中,其中就包括libobjc库,这些都是main之前完成的。
category写入target_class的方法列表,则是在_dyld_objc_notify_mapped,即将Match-O相关sections都加载到内存之后发生的。我们的类、协议和分类,都是在_read_images方法中读取出来的。

接下来就总结一下分类的加载:

  • 首先调用getObjc2CategoryList读取__objc_catlist_section下所记录的所有分类。并存储到category_t * 数组中。
  • 依次读取数组中的category_t * 数组中。
  • 对每个cat,先调用rempClass(cat->cls),并返回一个objc_class*对象cls。这一步目的在于找到分类对应的类对象cls。
  • 找到类对象cls,接下来就是对cls进行修改操作了。
  • 不管是对cls还是cls的元类进行操作,都是调用的方法addUnattachedCategoryForClass。使class对其category进行了映射。
  • 做好映射后,会调用remethodizeClass方法来修改class的method_list结构,这才是runtime实现category的关键所在。

remethodizeClass

/***********************************************************************
* remethodizeClass
* Attach outstanding categories to an existing class.
* Fixes up cls's method list, protocol list, and property list.
* Updates method caches for cls and its subclasses.
* Locking: runtimeLock must be held by the caller
 将未完成整合的分类附加到现有的类中
 整理类的方法列表,协议列表,属性列表
 更新类及其子类的方法缓存
 Locking:runtimeLock必须由调用者持有
**********************************************************************/
static void remethodizeClass(Class cls)
{
    category_list *cats;//分类列表
    bool isMeta;//是否为元类
    
    runtimeLock.assertLocked();
    
    /*
    分析分类当中实例方法添加的逻辑
    所以假设isMeta = NO
    */
    isMeta = cls->isMetaClass();

    // Re-methodizing: check for more categories
    //unattachedCategoriesForClass函数获取cls中未完成整合的所有分类
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
        if (PrintConnecting) {
            _objc_inform("CLASS: attaching categories to class '%s' %s", 
                         cls->nameForLogging(), isMeta ? "(meta)" : "");
        }
        //将分类cats拼接到cls上
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}

attachCategories函数内部实现:

#pragma mark - attachCategories函数内部实现
// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order, 
// oldest categories first.
//附加分类的方法、协议、属性到类中
//假设cats中的类别都已加载并按加载顺序排序,
static void attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    /*
     分析分类当中实例方法添加的逻辑
     so,假设isMeta = NO
     */
    bool isMeta = cls->isMetaClass();

    /*
     声明3个局部变量 分别是方法列表、属性列表、协议列表 都是二维数组
     如method_list_t二维数组结构
     [[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, entry.hi);
        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);
}
     /*
      假如有3个分类,分类A中有两个方法,分类B中有一个方法,分类C中有3个方法
      那么最终mlists就得到下面的结果
      [[method_t,method_t],  [method_t], [method_t,method_t,method_t]]
       -------------------   ----------  -----------------------------
      分类A中的方法列表           B                   C
     */

从上面的内部实现可以看出:

1.Category是运行时决议,它在运行时才把分类方法添加到对应宿主类的方法列表中
2.假如在分类有多个的情况下,如果每个分类都有一个同名的分类方法,那么最终最后编译的分类方法会生效(前面的会被”覆盖“)
因为最后编译的分类方法会位于数组列表的前面位置,在消息发送或者消息函数查找的过程中,会根据选择器的名字来查找,一旦查找到了方法实现就是返回

附加列表函数具体实现:

    //附加列表函数具体实现
    /*
     addedLists传递过来的二维数组
     [[method_t,method_t],   [method_t],   [method_t,method_t,method_t]]
     --------------------    ----------    -----------------------------
     分类A中的方法列表(A)         B                     C
    
     addedCount = 3
     */
    //addedLists:要附加的分类列表  addedCount:二维数组元素个数
    void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;//判空

        if (hasArray()) {
            // many lists -> many lists
            //列表中原有元素总数  假设oldCount = 2
            uint32_t oldCount = array()->count;
            //拼接之后的元素总数  5
            uint32_t newCount = oldCount + addedCount;
           //根据新总数重新分配内存
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
           //重新设置元素总数  5
            array()->count = newCount;
            /*
             内存移动
             [[],[],[],[原有的第一个元素],[原有的第二个元素]]
             */
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            /*
             内存拷贝
             [
                A  --->  [addedLists中的第一个元素],
                B  --->  [addedLists中的第二个元素],
                C  --->  [addedLists中的第三个元素],
                [原有的第一个元素],
                [原有的第二个元素]
             ]
             */
            //memcpy函数把addedLists中的元素拷贝到lists当中
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }//后两个分支是 关于列表当中存储的分类具体采取的是list还是array_t结构的区别,不影响分析分类实现逻辑
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
        } 
        else {
            // 1 list -> many lists
            List* oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            array()->count = newCount;
            if (oldList) array()->lists[addedCount] = oldList;
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
    }

从上面的内部实现可以看出:

方法同名情况下,分类方法会”覆盖“宿主类的方法的原因:
宿主类方法仍然存在,但是分类方法位于方法列表数组前面的位置了,在消息函数查找的过程中,根据方法的名字来查找,一旦查找到方法实现就会返回,也即分类方法会被优先实现.


二、扩展

扩展的作用

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

扩展的特点

  • 编译时决议。
  • 只能以声明的形式存在,多数情况是寄生于宿主类的文件中。
  • 不能给系统的类添加扩展。

扩展和分类的区别

  • 分类是运行时决议;扩展是编译时决议;(所以扩展中声明的方法没有被实现,编译器会报警,但是分类种的方法没有被实现编译器是不会有任何警告的)
  • 分类原则上能增加属性,实例方法,类方法,协议,并且是公开的;扩展能添加方法,实例变量,默认是@private类型的,且只能作用于自身类,而不是子类或者其他地方;
  • 分类有自己的实现部分;扩展无自己的实现部分,只能依托对应类的实现部分来实现;
  • 分类可以为系统类添加分类;扩展不可以为系统类添加扩展;

扩展格式

@interface ViewController ()
//可以添加私有属性
//私有方法
@end

三、属性关联

在平时的工作中经常碰到给类别添加属性的操作,那么实现思路是怎么样的呢?
首先我们直接在分类中添加成员变量看是否能成功?

//对象类的代码
@interface ViewController : UIViewController
@property(nonatomic,copy)NSString * name;
@end

//类别的代码h
@interface ViewController (test)
@property(nonatomic,copy)NSString * intro;
-(void)test_method;

//类别的代码m
@implementation ViewController (test)
-(void)test_method
{
    NSLog(@"ddd");
}
@end

//类别使用

ViewController * v = [[ViewController alloc] init];
v.name = @"lgq";
v.intro = @"p7";
NSLog(@"%@-%@",v.name,v.title);

这个时候编译代码可以通过,但是运行的时候会如下错误

报错

提示没有实现set和get方法,我们添加一下

-(void)setIntro:(NSString *)intro
{
    
}
- (NSString *)intro
{
    return @"p7";
}

这时候可以程序可以正常打印了,但是我们如何能正确的设置成员变量的值呢?
先说一下本质问题:
本质原因:Category 结构体,并没有存储成员变量,在上面我们通过分析类别的代码可以得到答案。

解决方法一:
为了解决这个问题我们可以在分类中添加一个字典用来保存成员变量对应的值

#import "ViewController+test.h"

#define KEY [NSString stringWithFormat:@"%p",self] //以分类内存地址作为key

@implementation ViewController (test)


NSMutableDictionary * names_;
+(void)load
{
    names_ = [NSMutableDictionary dictionary];
}

-(void)setIntro:(NSString *)intro
{
    names_[KEY] = intro;
}

- (NSString *)intro
{
    return names_[KEY];
}

通过这种思路确实可以实现给类别添加属性的功能,但是也有明显的弊端。

  • 给属性赋值或者取值时会出现线程完全问题,需要加锁控制。
  • 字典什么时候释放,也存在内存泄漏的隐患。

解决方法二:

如果使用上述思路维护难度较大,这时候就用到了runtime关联对象方法,代码如下

-(void)setIntro:(NSString *)intro
{
    objc_setAssociatedObject(self, @selector(intro), intro, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)intro
{
    // 隐式参数
    // _cmd == @selector(name)
    return objc_getAssociatedObject(self, _cmd);
}

参数一:id object : 给哪个对象添加属性,这里要给自己添加属性,用self。
参数二:void * == id key : 属性名,根据key获取关联对象的属性的值,在objc_getAssociatedObject中通过次key获得属性的值并返回。
参数三:id value : 关联的值,也就是set方法传入的值给属性去保存。
参数四:objc_AssociationPolicy 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     // 指定相关的对象被复制,原子性   
};

移除所有关联对象

- (void)removeAssociatedObjects{
    // 移除所有关联对象
    objc_removeAssociatedObjects(self);
}

关联对象原理

那么关联对象的原理是什么呢?我们从源码来进一步分析一下
打开源码:找到objc_setAssociatedObject函数 (分析源码版本objc4-818.2)

objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
    _object_set_associative_reference(object, key, value, policy);
}

找到 _object_set_associative_reference

void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
    // This code used to work when nil was passed for object and key. Some code
    // probably relies on that to not crash. Check and handle it explicitly.
    // rdar://problem/44094390
    if (!object && !value) return;

    if (object->getIsa()->forbidsAssociatedObjects())
        _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));

    DisguisedPtr disguised{(objc_object *)object};
    ObjcAssociation association{policy, value};

    // retain the new value (if any) outside the lock.
    association.acquireValue();

    bool isFirstAssociation = false;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());

        if (value) {
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            if (refs_result.second) {
                /* it's the first association we make */
                isFirstAssociation = true;
            }

            /* establish or replace the association */
            auto &refs = refs_result.first->second;
            auto result = refs.try_emplace(key, std::move(association));
            if (!result.second) {
                association.swap(result.first->second);
            }
        } else {
            auto refs_it = associations.find(disguised);
            if (refs_it != associations.end()) {
                auto &refs = refs_it->second;
                auto it = refs.find(key);
                if (it != refs.end()) {
                    association.swap(it->second);
                    refs.erase(it);
                    if (refs.size() == 0) {
                        associations.erase(refs_it);

                    }
                }
            }
        }
    }

    // Call setHasAssociatedObjects outside the lock, since this
    // will call the object's _noteAssociatedObjects method if it
    // has one, and this may trigger +initialize which might do
    // arbitrary stuff, including setting more associated objects.
    if (isFirstAssociation)
        object->setHasAssociatedObjects();

    // release the old value (outside of the lock).
    association.releaseHeldValue();
}

从以上代码分析有以下关键词:
ObjcAssociation
AssociationsManager
AssociationsHashMap
ObjectAssociationMap

通过分析这四个关键词的作用我们可以发现:

一个实例对象就对应一个ObjectAssociationMap,而ObjectAssociationMap中存储着多个此实例对象的关联对象的key以及ObjcAssociation,为ObjcAssociation中存储着关联对象的value和policy策略。

由此我们可以知道关联对象并不是放在了原来的对象里面,而是自己维护了一个全局的map用来存放每一个对象及其对应关联属性表格。

objc_setAssociatedObject底层实现

接下来我们看一下取值相关 objc_getAssociatedObject

_object_get_associative_reference(id object, const void *key)
{
    ObjcAssociation association{};

    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            ObjectAssociationMap &refs = i->second;
            ObjectAssociationMap::iterator j = refs.find(key);
            if (j != refs.end()) {
                association = j->second;
                association.retainReturnedValue();
            }
        }
    }

    return association.autoreleaseReturnedValue();
}

可以看到是通过key在AssociationsHashMap 中找到对应的value从而return出去

移除函数:objc_removeAssociatedObjects函数

void objc_removeAssociatedObjects(id object) 
{
    if (object && object->hasAssociatedObjects()) {
        _object_remove_assocations(object, /*deallocating*/false);
    }
}

首先判断当前对象是否有关联属性,如果有的话通过_object_remove_assocations 函数对object进行移除操作

void
_object_remove_assocations(id object, bool deallocating)
{
    ObjectAssociationMap refs{};

    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            refs.swap(i->second);

            // If we are not deallocating, then SYSTEM_OBJECT associations are preserved.
            bool didReInsert = false;
            if (!deallocating) {
                for (auto &ref: refs) {
                    if (ref.second.policy() & OBJC_ASSOCIATION_SYSTEM_OBJECT) {
                        i->second.insert(ref);
                        didReInsert = true;
                    }
                }
            }
            if (!didReInsert)
                associations.erase(i);
        }
    }

    // Associations to be released after the normal ones.
    SmallVector laterRefs;

    // release everything (outside of the lock).
    for (auto &i: refs) {
        if (i.second.policy() & OBJC_ASSOCIATION_SYSTEM_OBJECT) {
            // If we are not deallocating, then RELEASE_LATER associations don't get released.
            if (deallocating)
                laterRefs.append(&i.second);
        } else {
            i.second.releaseHeldValue();
        }
    }
    for (auto *later: laterRefs) {
        later->releaseHeldValue();
    }
}

底层实现就是从AssociationsHashMap 中找到对应value进行移除

总结

关联对象并不是存储在被关联对象本身内存中,而是存储在全局的统一的一个AssociationsManager中,如果设置关联对象为nil,就相当于是移除关联对象。


四、代理协议

代理,又称委托代理(delegate),是iOS中常用的设计一种模式。顾名思义,它是把某个对象要做的事情委托给别的对象去做。那么别的对象就是这个对象的代理,代替它来打理要做的事。反映到程序中, 首先要明确一个对象的委托方是哪个对象,委托所做的内容是什么。在iOS程序设计中,委托通过一种@protocol的方式实现,所以又称为协议。在iOS的SDK里面,UITableView、UITextField等都有用到这种设计模式。

协议,是多个类共享的一个方法列表,在协议中所列出的方法没有响应的实现,由其它类来实现。委托是指给一个对象提供机会对另一对象中的变化做出反应或者相应另一个对象的行为。其基本思想是协同解决问题。

代理模式是一种消息传递方式,一个完整的代理模式包括:委托对象、代理对象和协议。

名词解释
  • 协议:用来指定代理双方可以做什么,必须做什么。
  • 委托对象:根据协议指定代理对象需要完成的事,即调用协议中的方法。
  • 代理对象:根据协议实现委托方需要完成的事,即实现协议中的方法。

协议是公共的定义,如果只是某个类使用,我们常做的就是写在某个类中。如果是多个类都是用同一个协议,建议创建一个Protocol文件,在这个文件中定义协议。遵循的协议可以被继承,例如我们常用的UITableView,由于继承自UIScrollView的缘故,所以也将UIScrollViewDelegate继承了过来,我们可以通过代理方法获取UITableView偏移量等状态参数。

协议只能定义公用的一套接口,类似于一个约束代理双方的作用。但不能提供具体的实现方法,实现方法需要代理对象去实现。协议可以继承其他协议,并且可以继承多个协议,在iOS中对象是不支持多继承的,而协议可以多继承。

代理的具体实现

代理方h

@protocol PersonDelegate
@required
-(void)getPersonName:(NSString *)name;
@end
@interface Person : NSObject
@property(nonatomic,weak)NSObject * delegate;
@end

代理方m

- (void)inputName:(NSString *)name
{
    name = @"lgq";
    if ([self.delegate respondsToSelector:@selector(getPersonName:)]) {
        [self.delegate getPersonName:name];
    }
}

委托方

- (id)init
{
    if (self = [super init])
    {
        [self performSelector:@selector(inputName:) withObject:self afterDelay:2.0];
    }
    return self;
}
- (void)inputName:(NSString *)name
{
    name = @"lgq";
    if ([self.delegate respondsToSelector:@selector(getPersonName:)]) {
        [self.delegate getPersonName:name];
    }
}
代理属性使用weak
  • 首先
    在我们的tableViewController中,控制器的view就是tableView,这就相当于tableViewController强引用着tableView(代理对象)。
  • 然后
    当我们设置delegate的时候,一般都是让tableViewController成为代理,这个时候代理如果也使用strong,那么tableView的delegate又强引用着tableViewController,所以导致循环引用,因此代理得用weak!
  • 为什么不用assign
    weak和assign是一种“非拥有关系”的指针,通过这两种修饰符修饰的指针变量,都不会改变被引用对象的引用计数。但是在一个对象被释放后,weak会自动将指针指向nil,而assign则不会。在iOS中,向nil发送消息时不会导致崩溃的,所以assign就会导致野指针的错误unrecognized selector sent to instance。
代理与block对比
  • 在有多个消息传递时,用delegate实现更合适,看起来也更清晰。block就不太好了,这个时候block反而不便于维护,而且看起来非常臃肿,很别扭。
    代理更加面相过程,block则更面向结果。
  • 从设计模式的角度来说,代理更佳面向过程,而block更佳面向结果。例如我们使用NSXMLParserDelegate代理进行XML解析,NSXMLParserDelegate中有很多代理方法,NSXMLParser会不间断调用这些方法将一些转换的参数传递出来,这就是NSXMLParser解析流程,这些通过代理来展现比较合适。而例如一个网络请求回来,就通过success、failure代码块来展示就比较好。
  • 从性能上来说,block的性能消耗要略大于delegate,因为block会涉及到栈区向堆区拷贝等操作,时间和空间上的消耗都大于代理。而代理只是定义了一个方法列表,在遵守协议对象的objc_protocol_list中添加一个节点,在运行时向遵守协议的对象发送消息即可。

五、通知

通知:NSNotification,是iOS开发中一种重要的设计模式,它的实质是程序内部提供的一种广播机制。把接受到的消息根据内部消息转发表,将消息转发给需要的对象。
它的主要作用就是使用观察者模式来实现的,用于跨层传递消息。

通知中心的特点:
  • 同步执行
    通知中心是单例,目的就是从任意一个发送消息到任意一个接收者,是同步执行的。
    在通知中心里就是每发送一次消息,要等消息被接收并完全执行完里面的方法,然后才返回来发送第二条消息,这就是同步,即通知中心发送消息是一条一条发送,而且是上条消息执行完才执行下一条的。

  • 一对多发送消息
    消息传递是一对多的,一个广播发送者,但是可以注册多个观察者。当发送者发送消息后,只要注册观察者的地方都会同步一一收到观察者发送的消息。

  • 降低程序耦合度
    因为通知可以用来进行跨层传递消息,那么发送者和各个监听者之间可以没有任何耦合,所以大大降低了程序之间的耦合程度。

通知过程:
  • 发送通知
    //发送通知(广播)
    [[NSNotificationCenter defaultCenter] postNotificationName:@"notication_name" object:nil];
    
    [[NSNotificationCenter defaultCenter] postNotificationName:@"notication_name" object:nil userInfo:@{@"key":@"valye",@"key2":@"valye2",}];
  • 注册观察者
    //注册观察者
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(method:) name:@"notication_name" object:nil];
    
    [[NSNotificationCenter defaultCenter] addObserverForName:@"notication_name" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
        
    }];
  • 移除观察者
    //移除所有观察者
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    //根据注册观察者名称针对性移除
    [[NSNotificationCenter defaultCenter] removeObserver:self name:@"" object:nil];
观察者的销毁:

观察者的创建和销毁要成对存在,一次添加对应一次销毁
创建的位置如   viewWillAppear 和 viewDidAppear, 销毁的位置如 viewWillDisappear 、viewDidDisappear 和 dealloc
就是在页面出现的时候注册通知,页面消失时移除通知。一定要成双成对出现,如果你只在viewWillAppear 中 addObserver没有在viewWillDisappear 中 removeObserver那么当消息发生的时候,你的方法会被调用多次。

个人还是建议在viewDidLoad中注册通知,在dealloc中移除通知。


六、KVO

KVO 全称 Key Value Observing,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于 KVO 的实现机制,只针对属性才会发生作用,一般继承自 NSObject 的对象都默认支持 KVO。既然KVO是通知机制实现的,那么KVO就是观察者模式的一种体现。

KVO 可以监听单个属性的变化,也可以监听集合对象的变化。通过 KVC 的 mutableArrayValueForKey: 等方法获得代理对象,当代理对象的内部对象发生改变时,会回调 KVO 监听的方法。集合对象包含 NSArray 和 NSSet。

KVO基本使用

使用KVO大致分为三个步骤:

  • 通过addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接收keyPath属性的变化事件
  • 在观察者中实现observeValueForKeyPath:ofObject:change:context:方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者
  • 当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除。需要注意的是,调用removeObserver需要在观察者消失之前,否则会导致Crash
注册观察者
    ViewController * v = [[ViewController alloc] init];
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        v.name = @"lgq";
    });
    
    [v addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
监听回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"%@-%@-%@",keyPath,object,change);
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
移除监听
[v removeObserver:self forKeyPath:@"name"];
观察者未实现监听方法

若观察者对象 -observeValueForKeyPath:ofObject:change:context: 未实现,将会 Crash

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: ': An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
Key path: name
Observed object: 
Change: {
    kind = 1;
    new = theonelgq;
}
Context: 0x0'
未及时移除观察者
Crash: Thread 1: EXC_BAD_ACCESS (code=1, address=0x105e0fee02c0)
调用方式
  • 自动调用
    调用KVO属性对象时,不仅可以通过点语法和set语法进行调用,还可以使用KVC方法
        //通过属性的点语法间接调用
        v.name = @"";

        // 直接调用set方法
        [v setName:@"Savings"];

        // 使用KVC的setValue:forKey:方法
        [v setValue:@"Savings" forKey:@"name"];

        // 使用KVC的setValue:forKeyPath:方法
        [v setValue:@"Savings" forKeyPath:@"account.name"];
  • 手动调用

触发手动调用的两种方式

1.直接对属性进行赋值

-(void)changeNameValue
{
    _name = @"theonelgq";
}

那么这个时候就要添加代码触发KVO

-(void)changeNameValue
{
    [self willChangeValueForKey:@"name"];
    _name = @"theonelgq";
    [self didChangeValueForKey:@"name"];
}

2.通过以下方法令automatic为NO

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"name"]) {
        automatic = NO;//对该key禁用系统自动通知,若要直接禁用该类的KVO则直接返回NO;
    }
    else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}

这个时候要重写set方法来触发KVO

- (void)setName:(NSString *)name {
    if (name != _name) {
        [self willChangeValueForKey:@"name"];
        _name = name;
        [self didChangeValueForKey:@"name"];
    }
}
实际应用

KVO主要用来做键值观察操作,想要一个值发生改变后通知另一个对象,则用KVO实现最为合适。斯坦福大学的iOS教程中有一个很经典的案例,通过KVO在Model和Controller之间进行通信。

MVC
KVO实现原理

KVO是通过isa 混写技术实现的。在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指向中间类。并且将class方法重写,返回原类的Class。所以苹果建议在开发中不应该依赖isa指针,而是通过class实例方法来获取对象类型。

首先我们打印一下添加KVO前后对象名称的变化

    ViewController * v = [[ViewController alloc] init];
    
    //刚创建实例对象后
    NSLog(@"刚创建实例对象后---%@ - %p",object_getClass(v),v);
    
    NSLog(@"ViewController添加KVO监听之前-方法实现 -%p", [v methodForSelector:@selector(setName:)]);
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
        [v changeNameValue];
        
    });
    
    [v addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    
    //给实例添加KVO之后
    NSLog(@"给实例添加KVO之后---%@ - %p",object_getClass(v),v);
    
    NSLog(@"ViewController添加KVO监听之后-方法实现 -%p", [v methodForSelector:@selector(setName:)]);


打印结果

2021-01-28 14:35:29.521790+0800 category_test[40896:2882616] 刚创建实例对象后---ViewController - 0x7ff135705e00
2021-01-28 14:35:29.522053+0800 category_test[40896:2882616] ViewController添加KVO监听之前-方法实现 -0x101bb3230
2021-01-28 14:35:29.523132+0800 category_test[40896:2882616] 给实例添加KVO之后---NSKVONotifying_ViewController - 0x7ff135705e00
2021-01-28 14:35:29.523716+0800 category_test[40896:2882616] ViewController添加KVO监听之后-方法实现 -0x7fff207b5b57

我们发现KVO创建后,添加对象的名称发生了变化,但是内存地址没有发生变化,但是setName方法的内存却发生了变化, 这样就对上面的说明解释的通了。生成了一个新的子类,这个子类继承了原类的所有属性和方法,并重写了原始类的set方法,从而添加了监听属性变化的功能。

新的子类(中间对象)

从上述测试代码的结果我们发现,ViewController 中的 isa 从开始指向ViewController类对象,变成指向了 NSKVONotifying_ViewController 类对象。

  • KVO会在运行时动态创建一个新类,将对象的isa指向新创建的类,新类是原类的子类,命名规则是NSKVONotifying_xxx的格式。

添加KVO之前类的关系


KVO之前

添加KVO之后类的关系


KVO之后
  • NSKVONotifying_Person类内部实现

1、首先调用willChangeValueForKey:方法。
2、然后调用setAge:方法真正的改变属性的值。
3、开始调用didChangeValueForKey:这个方法,调用[super didChangeValueForKey:key]时会通知监听者属性4、值已经改变,然后监听者执行自己的- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context这个方法。

内部调用流程图


KVO内部调用栈
KVO和 notification(通知)的区别?
  • KVO 和 NSNotificationCenter 都是 iOS 中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO 是一对一的,而不是一对多的。KVO 对被监听对象无侵入性,不需要修改其内部代码即可实现监听。

  • notification 的优点是监听不局限于属性的变化,还可以对多种多样的状态变化进行监听,监听范围广,例如键盘、前后台等系统通知的使用也更显灵活方便。

六、KVC

KVC(键值编码),即 Key-Value Coding,一个非正式的 Protocol,使用字符串(键)访问一个对象实例变量的机制。而不是通过调用 Setter、Getter 方法等显式的存取方式去访问。

iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。这样就可以在运行时动态地访问和修改对象的属性。而不是在编译时确定,很多高级的iOS开发技巧都是基于KVC实现的。

取值
  • 通过key
- (nullable id)valueForKey:(NSString *)key; 
  • 通过keyPath
- (nullable id)valueForKeyPath:(NSString *)keyPath; 
基于getter取值底层实现
  • 当调用valueForKey的代码时,其搜索方式如下:
    你需要先看一下这张流程图,大致知道如何运转的,之后再看文字描述,仔细了解其机制
取值流程

1、通过getter方法搜索实例,按照get, , is, _的顺序查找getter`方法。如果发现符合的方法,就调用对应的方法并拿着结果跳转到第五步。否则,就继续到下一步。
如果没有找到简单的getter方法,则搜索其匹配模式的方法countOf、objectInAtIndex:、AtIndexes:。
如果找到其中的第一个和其他两个中的一个,则就会返回一个可以响应NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子类)。或者说给这个代理集合发送属于NSArray的方法,就会以countOf,objectInAtIndex或AtIndexes这几个方法组合的形式调用。否则,继续到第三步。

2、代理对象随后将NSArray接收到的countOf、objectInAtIndex:、AtIndexes:的消息给符合KVC规则的调用方。
当代理对象和KVC调用方通过上面方法一起工作时,就会允许其行为类似于NSArray一样。
如果没有找到NSArray简单存取方法,或者NSArray存取方法组。那么会同时查找countOf、enumeratorOf、memberOf:命名的方法。

3、如果找到三个方法,则创建一个集合代理对象,该对象响应所有NSSet方法并返回。否则,继续执行第四步。

给这个代理对象发NSSet的消息,就会以countOf,enumeratorOf,memberOf组合的形式调用。

如果没有发现简单getter方法,或集合存取方法组,以及接收类方法accessInstanceVariablesDirectly是返回YES的。搜索一个名为_、_is、is的实例,根据他们的顺序。

如果发现对应的实例,则立刻获得实例可用的值并跳转到第五步,如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly返回NO的话,那么会直接调用valueForUndefinedKey:。

如果取回的是一个对象指针,则直接返回这个结果。
如果取回的是一个基础数据类型,但是这个基础数据类型是被NSNumber支持的,则存储为NSNumber并返回。
如果取回的是一个不支持NSNumber的基础数据类型,则通过NSValue进行存储并返回。
如果所有情况都失败,则调用valueForUndefinedKey:方法并抛出异常,这是默认行为。但是子类可以重写此方法。

KVC设值
  • 通过key

    直接将属性名当做key,并设置value,即可对属性进行赋值。

- (void)setValue:(nullable id)value forKey:(NSString *)key;          //通过Key来设值
  • 通过keyPath

除了对当前对象的属性进行赋值外,还可以对其更“深层”的对象进行赋值。KVC进行多级访问时,直接类似于属性调用一样用点语法进行访问即可。例如Person属性中有name属性,我就可以通过Person.name进行赋值

- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  //通过KeyPath来设值
基于setter赋值底层实现

这是setValue:forKey:的默认实现,给定输入参数value和key。试图在接收调用对象的内部,设置属性名为key的value,通过下面的步骤

你需要先看一下这张流程图,大致知道如何运转的,之后再看文字描述,仔细了解其机制

取值流程
KVC异常处理

1、key或者keyPath发生错误
当根据KVC搜索规则,没有搜索到对应的key或者keyPath,则会调用对应的异常方法。异常方法的默认实现,在异常发生时会抛出一个NSUndefinedKeyException的异常,并且应用程序Crash。

我们可以重写下面两个方法:

- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
传参为nil

通常情况下,KVC不允许你要在调用setValue:属性值 forKey:(或者keyPath)时对非对象传递一个nil的值。因为值类型是不能为nil的。如果你不小心传了,KVC会调用setNilValueForKey:方法。这个方法默认是抛出异常,所以一般而言最好还是重写这个方法。

我们可以重写这个方法:

-(void)setNilValueForKey:(NSString *)key{
    NSLog(@"不能将%@设成nil",key);
}
KVC适用场景
  • 动态的取值和设值

    利用KVC动态的取值和设值是最基本的用途了。相信每一个iOS开发者都能熟练掌握

  • 用KVC来访问和修改私有变量
    根据上面的实现原理我们知道,KVC本质上是操作方法列表以及在内存中查找实例变量。我们可以利用这个特性访问类的私有变量,例如下面在.m中定义的私有成员变量和属性,都可以通过KVC的方式访问。

这个操作对readonly的属性,@protected的成员变量,都可以正常访问。如果不想让外界访问类的成员变量,则可以将accessInstanceVariablesDirectly属性赋值为NO

  • 修改一些控件的内部属性
    这也是iOS开发中必不可少的小技巧。众所周知很多UI控件都由很多内部UI控件组合而成的,但是Apple度没有提供这访问这些控件的API,这样我们就无法正常地访问和修改这些控件的样式。而KVC在大多数情况可下可以解决这个问题。

七、属性关键字

原子性操作关键字
  • nonatomic:非原子的, atomiac 原子的 。属性默认是 atomiac , 也就是原子性的。nonatomic执行效率高。

  • atomiac:读写安全,但效率低,不是绝对的安全,比如操作数组,增加或移除,这种情况可以使用互斥锁来保证线程安全

读写
  • readwrite 读写,属性默认是 readwrite , 支持读写。readwirte属性同时具有 set 和 get 方法。

  • readonly 只读 ,属性只具有 get 方法。

    ViewController * v = [[ViewController alloc] init];
    
    [v setValue:@"12" forKey:@"age"];
    
    NSLog(@"%@",[v valueForKey:@"age"]);

readonly修饰的关键词,不管是私有的还是公开的成员变量,都可以通过KVC进行修改和访问

内存管理关键字

内存管理主要有copy、strong、weak、assign、retain、unsafe_unretained

  • copy 主要用来修饰NSString,NSArray等有可变类型的不可变对象,根据不同情况,进行深拷贝或者浅拷贝。避免NSMutableString对象赋值给NSString后,可变对象值改变,影响到NSString指向的对象

copy分深copy和浅copy
浅copy,对象指针的复制,目标对象指针和原对象指针指向同一块内存空间,引用计数增加
深copy,对象内容的复制,开辟一块新的内存空间
可变的对象的copy和mutableCopy都是深拷贝
不可变对象的copy是浅拷贝,mutable是深拷贝
copy方法返回的都是不可变对象

  • strong 表示强引用,指向并持有对象,引用计数+1

  • weak 表示弱引用,指向但是并不持有该对象,引用计数也不会加1。在 Runtime 中对该属性进行了相关操作,无需处理,可以自动销毁。weak用来修饰对象,多用于避免循环引用的地方。weak 不可以修饰基本数据类型。weak修饰的对象释放后自动职位nil,避免发生迷途指针错误

  • assign ARC用来修饰‘基本数据类型’、‘枚举’、‘结构体’ 等非OC对象类型

  • retain MRC使用,ARC不用了。引用计数+1

  • unsafe_unretained 类似于weak,但是没有runtime的特殊处理。指向的对象被释放后,指针仍然指向原来的地址。当给其指向对象发送消息是,会出现异常

为什么我们不用assign去声明对象呢?

  • 因为 assign 修饰的对象,在释放之后,指针的地址还是存在的,也就是说指针并没有被置为nil,造成野指针。访问野指针,会导致程序 crash。

为什么可以用assign修饰基本数据类型?

  • 因为基本数据类型是分配在栈上,栈的内存会由系统自己自动处理回收,不会造成野指针。

@synthesize 和 @dynamic 分别有什么作用?

@property 有两个对应的词,一个是 @synthesize,一个是 @dynamic。如果 @synthesize 和 @dynamic 都没写,那么默认的就是 @syntheszie var = _var;

  • @synthesize 的语义是如果你没有手动实现 setter 方法和 getter 方法,那么编译器会自动为你加上这两个方法。

  • @dynamic 告诉编译器:属性的 setter 与 getter 方法由用户自己实现,不自动生成。(当然对于 readonly 的属性只需提供 getter 即可)。假如一个属性被声明为 @dynamic var,然后你没有提供 @setter 方法和 @getter 方法,编译的时候没问题,但是当程序运行到 instance.var = someVar,由于缺 setter 方法会导致程序崩溃;或者当运行到 someVar = var 时,由于缺 getter 方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。

__weak
  • 主要用于解决循环引用,用__weak修饰的变量 ,当对象释放后,指针自动设置为nil,当后面继续使用该指针变量的时候不会造成crash,更不会造成强引用使该释放的对象无法释放,造成内存泄露。
__weak typeof(self) weakSelf = self;
__strong
  • 相反与__weak,主要用于当使用某个对象是,希望它没有提前被释放。强引用该对象使其无法释放。例如在block内部,希望block调用时该对象不会被提前释放造成错误。可以使用强引用。

使用场景

TestAlertView *alertView = [TestAlertView new];
alertView = ^()
{
  //当block内部需要使用本身这个局部对象时,需要用强引用方式,让alertView在传递完block后不会被释放依然可以执行setTitle操作
   __strong typeof(alertView) strongAlertView = alertView;
  [strongAlertView setTitle:@"1234"];
}
[alertView show];

__weak和__block在block中运用较多,以后我会在block章节中进行讲解。

你可能感兴趣的:(Object-C语言相关特性)