017-分类关联对象

995 阅读6分钟

类扩展与分类的区别

category:类别、分类

  • 专门用来给类添加新的方法
  • 不能给类添加成员属性,添加了成员变量,也无法获取到
  • 可以通过runtime给分类添加属性
  • 分类中用@property定义变量,只会生成变量的getter、setter方法的声明,不能自动生成方法实现和带下划线的成员变量

extension:类扩展

  • 可以说成是特殊的分类,也称作匿名分类
  • 可以给类添加成员属性,但是是私有变量
  • 可以给类添加方法,也是私有方法
  • 类扩展要写在类的声明之后,实现之前(如果在类中直接添加扩展)

示例代码

@interface FFBoys : NSObject
@property (nonatomic, copy) NSString * name;
@property (nonatomic, assign) int age;

- (void)instanceMethod;
+ (void)classMethod;

@end

@interface FFBoys (BBLv)
@property (nonatomic, copy) NSString * ext_name;
@property (nonatomic, assign) int ext_age;

- (void)ext_instanceMethod;
+ (void)ext_classMethod;

@end

@implementation FFBoys

- (void)instanceMethod {
    NSLog(@"%s",__func__);
}

+ (void)classMethod {
    NSLog(@"%s",__func__);
}

- (void)ext_instanceMethod {
    NSLog(@"%s",__func__);
}

+ (void)ext_classMethod {
    NSLog(@"%s",__func__);
}

@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSLog(@"Hello, World!");
        FFPerson *objc = [FFPerson alloc];
        [objc likeFood];

    }
    return 0;
}

通过.cpp文件查看extension的表现

分类在编译成c++的阶段已经被注释了

#ifndef _REWRITER_typedef_FFBoys
#define _REWRITER_typedef_FFBoys
typedef struct objc_object FFBoys;
typedef struct {} _objc_exc_FFBoys;
#endif

extern "C" unsigned long OBJC_IVAR_$_FFBoys$_name;
extern "C" unsigned long OBJC_IVAR_$_FFBoys$_age;
struct FFBoys_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	int _age;
	NSString *_name;
};

// @property (nonatomic, copy) NSString * name;
// @property (nonatomic, assign) int age;

// - (void)instanceMethod;
// + (void)classMethod;

/* @end */


// @interface FFBoys (BBLv)
// @property (nonatomic, copy) NSString * ext_name;
// @property (nonatomic, assign) int ext_age;

// - (void)ext_instanceMethod;
// + (void)ext_classMethod;

/* @end */


// @implementation FFBoys

实例方法和类方法直接被合成到类和元类的方法列表里面了

static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[10];
} _OBJC_$_INSTANCE_METHODS_FFBoys __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	10,
	{{(struct objc_selector *)"instanceMethod", "v16@0:8", (void *)_I_FFBoys_instanceMethod},
	{(struct objc_selector *)"ext_instanceMethod", "v16@0:8", (void *)_I_FFBoys_ext_instanceMethod},
	{(struct objc_selector *)"name", "@16@0:8", (void *)_I_FFBoys_name},
	{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_FFBoys_setName_},
	{(struct objc_selector *)"age", "i16@0:8", (void *)_I_FFBoys_age},
	{(struct objc_selector *)"setAge:", "v20@0:8i16", (void *)_I_FFBoys_setAge_},
	{(struct objc_selector *)"name", "@16@0:8", (void *)_I_FFBoys_name},
	{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_FFBoys_setName_},
	{(struct objc_selector *)"age", "i16@0:8", (void *)_I_FFBoys_age},
	{(struct objc_selector *)"setAge:", "v20@0:8i16", (void *)_I_FFBoys_setAge_}}
};

static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[2];
} _OBJC_$_CLASS_METHODS_FFBoys __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	2,
	{{(struct objc_selector *)"classMethod", "v16@0:8", (void *)_C_FFBoys_classMethod},
	{(struct objc_selector *)"ext_classMethod", "v16@0:8", (void *)_C_FFBoys_ext_classMethod}}
};

通过lldb动态调试源码查看extension的表现

iShot2021-08-03 10.34.19.png

关联对象

当分类声明了属性的时候,是不会自动生成setter、getter方法和带有下划线的成员变量的,属性的赋值就无法完成,这个时候分类会报出警告,提示你缺少getter、setter方法

2161627975255_.pic.jpg

手动实现getter、setter方法

@implementation FFPerson (BBLv)

- (NSString *)cate_name {
    return objc_getAssociatedObject(self, "cate_name");
}

- (void)setCate_name:(NSString *)cate_name {
    objc_setAssociatedObject(self, "cate_name", cate_name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)cate_hobby {
    return objc_getAssociatedObject(self, "cate_hobby");
}

- (void)setCate_hobby:(NSString *)cate_hobby {
    objc_setAssociatedObject(self, "cate_age", cate_hobby, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end

疑问点一:关联对象存在的意义是什么

源码

通过找objc_setAssociatedObject找到了下一层的实现函数_object_set_associative_reference,这里在不同的Objc4源码里面的实现都有差异,例如779版本、756版本与当前818版本都是不一样的,这样做的道理做到了API稳定,在下层的实现过程中,源码都是在变的,但是在上层调用的过程中是不变的,这是一种简单的架构思维。

objc_setAssociatedObject

void
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)
{
    // 当为对象和键传递 nil 时,此代码曾经起作用。一些代码可能依赖于它不会崩溃。明确检查和处理。
    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));
    //对object对象的包装成统一的数据结构
    DisguisedPtr<objc_object> disguised{(objc_object *)object};
    ObjcAssociation association{policy, value};

    // 在锁外保留新值(如果有)。
    association.acquireValue();

    bool isFirstAssociation = false;
    //关联对象的核心代码
    {
        //针对这个{}作用域通过构造与析构函数进行锁操作
        AssociationsManager manager;
        //单例的方式获取AssociationsHashMap关联表
        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);

                    }
                }
            }
        }
    }

    /**
     在锁外调用 setHasAssociatedObjects ,因为如果它有一个,
     这将调用对象的 _noteAssociatedObjects 方法,
     这可能会触发 +initialize ,这可能会做任意的事情,包括设置更多关联的对象。
     */
    if (isFirstAssociation)
        object->setHasAssociatedObjects();

    // 释放旧值(锁外)。
    association.releaseHeldValue();
}

_object_set_associative_reference包含了4个参数:

  • id object:被关联者
  • const void *key:key值,比如cate_name
  • id value:value值
  • objc_AssociationPolicy policy:缓存策略,对应声明属性设定的retain、copy等

try_emplace

在AssociationsHashMap中找到ObjectAssociationMap,判断值是否在映射表中,如果不在则将键插入,如果键不在,则创建一个bucket,一个空的空间。这个bucket就是ObjcAssociation,里面包含了_policy、_value。
  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);
  }

AssociationsManager

AssociationsManager manager;manager并不是单例,只是针对这个{}作用域通过构造与析构函数进行锁操作,负责管理AssociationsHashMap这个哈希表单例。

AssociationsHashMap这个哈希表才是单例!!!

static Storage _mapStorage;这是一个全局静态变量_mapStorage的声明,跟是否写AssociationsManager内并无任何关系,写在内部意味着需要用AssociationsManager去调用,A创建AssociationsManager与B创建AssociationsManager的对象调用_mapStorage是一模一样的,因为AssociationsHashMap是通过return _mapStorage.get()得到的,所以AssociationsHashMap这张表是全局唯一的。

// 类 AssociationsManager 管理锁/哈希表单例对。
// 分配实例获取锁
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();
    }
};

模仿操作

struct BBLvManager {
    BBLvManager() {NSLog(@"开始");}
    ~BBLvManager() {NSLog(@"结束");}
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        BBLvManager bblvManager;
        NSLog(@"Hello, World!");
    }
    return 0;
}

执行结果

2021-08-03 16:27:17.876511+0800 KCObjcBuild[6325:172143] 开始
2021-08-03 16:27:17.876601+0800 KCObjcBuild[6325:172143] Hello, World!
2021-08-03 16:27:21.097917+0800 KCObjcBuild[6325:172143] 结束

疑问点二:为什么底层源码总是喜欢在调用的地方再次嵌套一层实现,而不是直接实现?

类的关联对象结构图

16280485206065.jpg

根据图可以得知关联对象是双层hashmap的结构:

  • 第一层:AssociationsHashMap包含多个对象的ObjectAssociationMap
  • 第二层:ObjectAssociationMap包含多个ObjcAssociation
  • 每一个ObjcAssociation都有policy与value

解决探索过程中的疑问点

疑问点一:关联对象存在的意义是什么?

用于存储,由于关联对象没有getter、setter方法,无法像正常的类声明的对象,可以通过指针偏移找到其具体的地址进行getter、setter操作,由于关联对象是动态添加的属性,只能通过关联对象来进行存储,模拟getter、setter操作。

疑问点二:为什么底层源码总是喜欢在调用的地方再次嵌套一层实现,而不是直接实现?

通过一段时间对底层源码探索,发现不同版本的源码改动是有一些的,但是并不对上层暴露给开发者的API有影响,也就是说不管底层代码怎样改动,都保证了上层API的稳定。这也方便了改动下层代码的时候,控制中间层参数就可以搞定了。在设计上最大程度上保证开发者不需要每次都需要跟随系统api的更细而改动自己的代码。SDK的设计思维也是如此,要最大程度上的保证使用者的便利性。

类扩展写在声明之后,实现之前