探求系列已发布文章列表,有爱好的同学能够翻阅一下:

第一篇 | iOS 特点 @property 详细探求

第二篇 | iOS 深化了解 Block 运用及原理

第三篇 | iOS 类别 Category 和扩展 Extension 及相关目标详解

第四篇 | iOS 常用锁 NSLock ,@synchronized 等的底层完成详解

第五篇 | Equality 详细探求

——- 正文开端 ——-

引言

类别 category 答应你在没有源代码情况下,依然能够向已有的类中增加办法。它的功用很强大,答应你无需子类化而扩展现有类。运用类别,还能够将类的完成分发到多个文件中。类扩展 extension 与此相似,但答应在主类 @interface 块内以外的位置为类声明额定的 API。


  • 代码示例

类别 Category:

#import "ClassName.h"
@interface ClassName (CategoryName)
// method declarations
@end

扩展 Extension:

@interface MyClass : NSObject
@property (nonatomic, copy, readonly) NSString *name;
@end
// Private extension, typically hidden in the main implementation file.
@interface MyClass ()
@property (nonatomic, copy, readwrite) NSString *name;
@end

  • 实质

您能够经过在接口文件中,以类别称号声明它们,并在完成文件中以相同称号界说它们来将办法增加到类。类别称号表明这些办法是对在别处声明的类的增加,而不是一个新类。可是,不能经过类别增加实例变量到类中。

类别增加的办法成为类类型的一部分。例如,在一个类别中增加到 NSArray类中的办法,是编译器希望 NSArray 实例在其装备表中包含的办法。然而,子类中增加到 NSArray 类中的办法并不包含在 NSArray 类型中。(这只对静态类型的目标有影响,由于静态类型是编译器知道目标类的仅有办法。)


  • 常用介绍

类别:

Category 在经历过编译后里边的内容:目标办法、类办法、协议、特点都转化为类型为 category_t 的结构体变量:

struct category_t {
    const char *name;
    classref_t cls;
    WrappedPtr<method_list_t, PtrauthStrip> instanceMethods;
    WrappedPtr<method_list_t, PtrauthStrip> classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;
    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }
    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
    protocol_list_t *protocolsForMeta(bool isMeta) {
        if (isMeta) return nullptr;
        else return protocols;
    }
};

详细 category 都能做什么,常用的大致有如下几个场景:

  1. 在不修改原有类的基础上给原有类增加办法,由于 category 的结构体指针中没有特点列表,只要办法列表。所以原则来说只能给 category 增加办法,不能增加特点,假如需求给 category 增加相似特点功用,能够经过相关目标完成,下面会有详细介绍;
  2. Category 中的办法优先于原有类同名的办法,即会优先调用 category 中的办法,疏忽原有类的办法。即 category 与原有类同名办法调用的优先级为: category > 本类 > 父类。开发中尽量不要掩盖本类的办法,假如掩盖会导致本类办法失效;
  3. 假如给 category 增加特点 @property,只会生成 setter/getter 办法的声明,并不会有详细的代码完成,详细解说可参阅历史文章:iOS 特点 @property 详细探求
  4. Category 中能够拜访原有类中 .h 中声明的成员变量;

类的扩展 Extension:

@interface Person ()
@end

类的 extension 看起来很像一个匿名的 category。一般用来声明私有办法,私有特点和私有成员变量。

extension 在编译期抉择, category 在运行期抉择。

类扩展不能像类别 category 那样拥有独立的完成部分(@implementation 部分)。也便是说,类的扩展所声明的办法必须依托原类的完成代码部分来完成。

因而,咱们不能给体系类增加类扩展。即扩展的办法只能在原类中完成。例如咱们扩展 NSString ,那么只能在 NSString的.m 中完成,但咱们拿不到 NSString.m 的源码。因而,咱们不能给 NSString 增加扩展,只能给 NSString 增加 category

界说在 .m 文件中的类扩展办法为私有的,假如需求声明私有办法,这种办法特别适宜。界说在 .h 文件(头文件)中的类扩展办法为公有的。


类别 Category 与类扩展 Extension 的差异

  1. Category 有姓名,extension 没有姓名,像是一个匿名的 category;
  2. Category 是运行时抉择,而 extension 是编译时抉择。所以 category 中的办法没有完成不会正告,而 extension 声明的办法不完成则会呈现正告;
  3. Category 原则上能够增加特点,实例办法,类办法,并且外部类是能够拜访的。extension 能增加特点、办法、实例变量,且默许是私有的;
  4. Category 有自己的完成部分,extension 没有自己的完成部分,只能依靠类本身来完成;
  5. 能够为体系类增加 category,而不能为体系类增加 extension;

关于类的 + (void)load+ (void)initialize 的差异

+ (void)load

+ (void)initialize

  • 两者的差异如下:
  1. 相同点:
  • 两个函数都是体系主动调用,因而无需手动调用(假如手动调用则与一般函数调用相似);
  • 两个函数都会山人调用各自父类对应的 + (void)load+ (void)initialize 办法,即子类调用办法之前,会优先调用其父类对应的办法;
  • 两个函数内部都运用了锁,因而两个函数都是线程安全的;
  1. 不同点:
  1. 调用机遇不同:+ (void)loadmain 函数之前履行,即 objc_init Runtime初始化时调用,且只会调用一次。 + (void)initialize 在类的办法首次被调用时履行,每个类只会调用一次,但父类可能会调用屡次;
  2. 调用办法不同:+ (void)load 是依据函数地址直接调用,+ (void)initialize 是经过音讯发送机制即 objc_msgSend(id self, SEL _cmd, ...) 调用;
  3. 子类父类调用联系不同:
  • 假如子类没有完成 + (void)load,则不会调用其父类的 + (void)load 办法。
  • 假如子类没有完成 + (void)initialize,则会调用其父类的办法,因而父类的 + (void)initialize 可能会调用屡次;
  1. 类别 category 对调用的影响不同:
  • 假如 category 中完成了 + (void)load,则会优先调用原类的的 + (void)load,再调用 category 的,即优先级为:父类 > 原类 > category
  1. 没有承继联系的不同类中的 + (void)load 的调用次序跟 Compile Sources 次序有关,即在前面的优先编译的类或许 category 先调用( 备注: 一切类的 + (void)load 优先级大于 category 的优先级);
  2. 同一个类的 category+ (void)load 的调用次序跟 Compile Sources 次序有关,即在前面的优先编译的 category 会先调用;
  3. 同一镜像中主工程的 + (void)load 办法优先调用,然后再调用静态库的 + (void)load 办法。有多个静态库时,静态库之间的履行次序与编译次序有关,即它们在 Link Binary With Libraries 中的次序;
  4. 不同镜像中,动态库的 + (void)load 办法优先调用,然后再调用主工程的 + (void)load,多个动态库的 + (void)load 办法的调用次序跟编译次序有关,即它们在 Link Binary With Libraries 中的次序;
  • 假如 category 中完成了 + (void)initialize,则原类的 + (void)initialize 将不会再调用
  1. 多个 category 中一同完成了 + (void)initialize 办法时,Compile Sources中次序最下面的一个,即最终一个被编译 Category 的 + (void)initialize 会履行;

类别 Category 中增加相关目标

Category 中增加特点 @property 在之前文章已做过简单介绍,详细可检查 iOS 特点 @property 详细探求,这儿咱们重点说一下相关目标的完成原理:

操作相关目标有三个中心办法:

  1. 设置相关目标办法:

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

    1. id _Nonnull object: 给哪个目标增加相关目标,一般是当前目标,即用 self 即可;
    1. const void * _Nonnull key: 相关目标的 key,作为相关目标的仅有标识存在,它只要是一个非空指针即可;
    1. id _Nullable value: 相关目标的值,经过相关 key 进行设值及获取值,假如需求铲除一个已存在的相关目标,将其值设置为 nil 即可;
    1. objc_AssociationPolicy policy: 相关战略,即相关目标的存储形式,其可选枚举值如下:
public enum objc_AssociationPolicy : UInt {
    case OBJC_ASSOCIATION_ASSIGN = 0 // 指定对相关目标的弱引证
    case OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1 // 指定对相关目标的强引证,非原子性
    case OBJC_ASSOCIATION_COPY_NONATOMIC = 3 // 指定仿制相关的目标,非原子性
    case OBJC_ASSOCIATION_RETAIN = 769 // 指定对相关目标的强引证,原子性
    case OBJC_ASSOCIATION_COPY = 771 // 指定仿制相关的目标,原子性
} 

依据源码,咱们能够知道 objc_setAssociatedObject 实践调用的是 _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<objc_object> 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 依据传入的 valuepolicy 创立目标,并经过 acquireValue 函数处理生成新的 _valueacquireValue 函数内部是经过对战略 policy 的判别进行相应处理,生成新值,其完成如下:

inline void acquireValue() {
    if (_value) {
        switch (_policy & 0xFF) {
        case OBJC_ASSOCIATION_SETTER_RETAIN:
            _value = objc_retain(_value);
            break;
        case OBJC_ASSOCIATION_SETTER_COPY:
            _value = ((id(*)(id, SEL))objc_msgSend)(_value, @selector(copy));
            break;
        }
    }
}

接下来咱们首要需求了解一下 AssociationsManagerAssociationsHashMap

class AssociationsManager {
    using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
    static Storage _mapStorage;
public:
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }
    AssociationsHashMap &get() {
        return _mapStorage.get();
    }
    static void init() {
        _mapStorage.init();
    }
};

由源码能够知道,AssociationsManager 是以 DisguisedPtr<objc_object> 即一个指针地址作为 key,以 ObjectAssociationMap 即一个相关表作为 value 的哈希表来运用的。其内部是运用一个大局静态变量 static Storage _mapStorage 来存储程序中一切的相关目标。

这儿重点介绍一下大局静态变量 static Storage _mapStorage 的初始化机遇。App 发动过程中,在 _objc_init 函数中会调用 void _dyld_objc_notify_register(...),详细如下:

void _objc_init(void)
{
    //...
    // 此处仅保留谈到的函数
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
    //...
}

dyld 源码中能够看到,函数 _dyld_objc_notify_register 中的三个参数为三个回调函数的指针,如下图:

iOS 全面深入理解 Category 类别,+ (void)load 与 + (void)initialize及关联对象实现原理
回调函数会在一切镜像文件初始化完成之后,回调 map_images(unsigned count, const char * const paths[], const struct mach_header * const mhdrs[]) 函数。详细调用流程如下图:

备注:图中已对无关代码进行删减,仅用来展现调用流程

iOS 全面深入理解 Category 类别,+ (void)load 与 + (void)initialize及关联对象实现原理
从上图咱们能够知道,在 App 发动过程中 AssociationsManager 中的静态变量 static Storage _mapStorage 的初始化机遇。在 App 发动之后,一切用到相关目标的当地,程序都是从这个大局静态变量 _mapStorage 中获取 AssociationsHashMap 来对相关目标进行进一步处理。

AssociationsManager 中,咱们能够看到是由一个 AssociationsManagerLock 叫做 spinlock_t 的互斥锁:

using spinlock_t = mutex_tt;

它是用来保证 AssociationsManager 中对 AssociationsHashMap 操作的线程安全。

AssociationsHashMap &get() {
    return _mapStorage.get();
}

关于 AssociationsHashMap 这个哈希表,则是由大局静态变量 _mapStorage 获取而来,因而不管任何时分操作相关目标,程序一直都是在操作这个 AssociationsHashMap 大局仅有的哈希表。

再回到上面 _object_set_associative_reference 源码中,当咱们增加一个相关目标时,AssociationsHashMap 会调用如下函数:

auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});

try_emplace 函数的源码如下:

  template <typename... Ts>
  std::pair<iterator, bool> try_emplace(const KeyT &Key, Ts &&... Args) {
    BucketT *TheBucket;
    if (LookupBucketFor(Key, TheBucket))
      return std::make_pair(
               makeIterator(TheBucket, getBucketsEnd(), true),
               false); // Already in map.
    // Otherwise, insert the new element.
    TheBucket = InsertIntoBucket(TheBucket, Key, std::forward<Ts>(Args)...);
    return std::make_pair(
             makeIterator(TheBucket, getBucketsEnd(), true),
             true);
  }

首要依据传来的 keydisguisedAssociationsHashMap 中查找对应的 ObjectAssociationMap 是否已在映射表中,假如不在则将元素插入。假如键不在,则创立一个 BucketT 即一个空的桶。在第2次调用 try_emplace 时将 ObjcAssociation (里边包含了 _policy_value )存储到这个 BucketT 空桶中。

当设置的相关 value 为空 nil 的时分会进入 if 判别的 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);
        }
    }
}

先去 AssociationsHashMap 里边查找 disguised ,假如找到则依据 key 查找到指定的相关目标,然后进行铲除 erase 操作。之后判别当前 object 的相关目标是否为0,假如为0,则将当前相关目标从大局的 AssociationsHashMap 中移除。

  1. 获取相关目标办法:

objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)

    1. id _Nonnull object: 获取哪个目标里边的相关目标;
    1. const void * _Nonnull key: 相关目标的 key ,与 objc_setAssociatedObject 中的 key 相对应,经过 key 值取出 value 即相关目标;

其内部调用的是 _object_get_associative_reference ,内部详细完成如下:

iOS 全面深入理解 Category 类别,+ (void)load 与 + (void)initialize及关联对象实现原理
假如咱们了解了设置相关目标的过程,上面的代码了解起来就比较简单了,从大局的 AssociationsHashMap 中取得 object 目标对应的 ObjectAssociationMap ,然后依据 keyObjectAssociationMap 获取对应的 ObjcAssociation ,然后依据相关战略 _policy 判别是否需求对 _value 履行 retain 操作。最终依据相关战略 _policy 判别是否需求将 _value 增加到主动开释池,并返回 _value

  1. 移除相关目标: 上面已经提到,假如想要铲除某一个特定相关目标,设置相关目标的 valuenil 即可。假如想要移除一切相关目标,则能够运用:

objc_removeAssociatedObjects(id _Nonnull object)

    1. id _Nonnull object: 移除指定目标的一切相关目标

其内部完成代码如下:

iOS 全面深入理解 Category 类别,+ (void)load 与 + (void)initialize及关联对象实现原理
当调用移除相关目标操作时,会先判别 object 是否为空及是否有相关目标存在,假如存储则会调用 _object_remove_assocations 函数。

从上图其内部完成代码能够看到,程序会获取大局的 AssociationsHashMap 然后从中获取目标对应的 ObjectAssociationMap ,注释说假如不是 deallocating,则体系的相关目标将会保留。而 objc_removeAssociatedObjects 函数传入的 deallocating 参数为 false,因而咱们能够揣度,免除相关必定不是在调用 objc_removeAssociatedObjects 时。

iOS 全面深入理解 Category 类别,+ (void)load 与 + (void)initialize及关联对象实现原理
于是,我查找了一下 _object_remove_assocations,发现了真实的调用机遇,即在 objc_destructInstance 函数调用时,如上图。

那什么时分会调用 objc_destructInstance 函数呢?带着这个疑问,我查了一下源码,这儿简单说一下调用流程,后续会专门针对 dealloc 写相关文章,其大体流程如下:

iOS 全面深入理解 Category 类别,+ (void)load 与 + (void)initialize及关联对象实现原理
图中函数调用流程十分清晰,此处不做过多解说。由此,咱们知道免除相关目标是在源目标 dealloc 时进行的。


拓展常识

  1. iOS 中变量修饰词 @public@protected@package@private 的效果:

@package // 常用于结构类的实例变量,运用 @private 太约束,运用 @protected 或许 @public 又太敞开,这时能够运用 @package

@private // 效果规模只能在本身类,即使子类也无法运用,但 category 及 extension 类中能够运用

@protected // 体系默许为 @protected,效果规模在本身类及子类

@public // 揭露类型,效果域大,只要能拿到所属实例目标就能够运用

实例变量规模图(@package 的规模图中未展现)

iOS 全面深入理解 Category 类别,+ (void)load 与 + (void)initialize及关联对象实现原理

@interface Person : NSObject {
@package
    NSString *_country; // 结构内拿到 Person 及其子类的实例变量都能够运用
@protected
    NSString *_birthday; // 只能在本身类及子类中运用,包含 category 及 extension
@private
    NSString *_weight; // 只能在本身类中运用,包含 category 及 extension
@public
    NSString *_height; // 大局任意拿到 Person 及其子类实例变量的当地都能够运用
}

详细实例如下: Son 承继自 Person

@interface Son : Person
@end

iOS 全面深入理解 Category 类别,+ (void)load 与 + (void)initialize及关联对象实现原理
@protected 从上图示例代码能够看到,在子类中是能够拜访父类的 @protected _birthday 成员变量,但不能拜访父类的 @private _weight 成员变量。

iOS 全面深入理解 Category 类别,+ (void)load 与 + (void)initialize及关联对象实现原理
从上图示例代码能够看到,在其他类中是能够拜访父类的 @protected _birthday 成员变量,但不能拜访父类的 @private _weight 成员变量。


总结

类别 category 和扩展 extension 涉及到的东西仍是挺多的,这儿仅对其中心要关注的一些点进行了详细介绍。另外还有关于 category 装载的过程,有爱好的同学能够查阅一下。以上便是本文对类别 category 和 扩展 extension 相关常识点的介绍,感谢阅览。


参阅资料:

  • Categories and Extensions

  • Defining a Class

  • Associated Objects


关于技能组

iOS 技能组主要用来学习、分享日常开发中运用到的技能,一同坚持学习,坚持前进。文章库房在这儿:github.com/minhechen/i… 微信公众号:iOS技能组,欢迎联系进群学习沟通,感谢阅览。

iOS 全面深入理解 Category 类别,+ (void)load 与 + (void)initialize及关联对象实现原理