OC运行时机制Runtime(三):关联对象Associated Object和分类Category

Runtime最全总结

本系列详细讲解Runtime知识点,由于运行时的内容较多,所以将内容拆分成以下几个方面,可以自行选择想要查看的部分

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;
}  

这里看到结构体里分别有分类名,类名,实例方法列表,类方法列表,协议列表这几项,所以我们通常可以在分类中动态添加方法而不能添加实例变量,当然我们也可以更详细的分析一下,方法列表和实例变量列表的区别。

struct objc_class {
    Class isa;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

struct objc_ivar {
    char *ivar_name                                          OBJC2_UNAVAILABLE;
    char *ivar_type                                          OBJC2_UNAVAILABLE;
    int ivar_offset                                          OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
}                                                            OBJC2_UNAVAILABLE;

struct objc_ivar_list {
    int ivar_count                                           OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;


struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

struct objc_method_list {
    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;

    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

这里看到,objc_ivar_list结构体里面有ivar_count以及一个变长结构体ivar_list,这个结构体里面存储了ivar的各种属性,包括name、type等,同样objc_method_list里面也存储了method_count变长结构体method_list,方法结构体包括了方法名和方法实现,这里SEL类型表示的是选择子的名字,IMP类型表示的方法的具体实现,这里有一个问题*ivars和**methodLists分别是一级指针和二级指针,二者的区别是使用一级指针做参数传递时,如果函数改变传入参数的值,原参数指针指向的值不会改变,而使用二级指针做参数传递时,原参数指针指向的值是可以改变的,所以当使用添加方法时,*methodList值可以改变,所以可以添加方法。那如果添加属性是否可以呢,我们写一个分类尝试一下

//UIImage+Detection.h
@interface UIImage (Detection)
@property (nonatomic, copy) NSString * remarkName;
@end


//ViewController.m
- (void)viewDidLoad {
    UIImage * image = [UIImage imageNamed:@"image_name"];
    image.imageName = @"image_name";
    NSLog(@"%@", image.imageName);
}

可以看到控制台报出-[UIImage setImageName:]: unrecognized selector sent to instance 0x6000000b1040找不到setter的错,是因为Category没有给属性自动添加setter和getter方法,但是我们如果使用_remarkName = remarkName;这种方式重写setter方法,会因为Category不能添加变量而报错,所以这里引出一个概念Associated Object

Associated Object——关联对象

当我们给一个系统类添加方法,我们常用的是使用类别来进行扩展,但是如果我们想添加一个系统类的属性,我们通常是使用继承的方式,但是只是添加一个属性就使用继承有些小题大做,这里我们可以使用Associated Object,下面继续之前的例子,将setter和getter用关联对象的方式实现,首先请出两个当事人,哦不当事函数。

//为一个实例对象添加一个关联对象,用键来区分,由于是C函数只能使用C字符串,这个key就是关联对象的名称,value为具体的关联对象的值,policy为存储策略,用以维护相应的内存管理语义
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
//通过key和实例对象获取关联对象的值
id objc_getAssociatedObject(id object, const void *key);
//删除实例对象的关联对象
void objc_removeAssociatedObjects(id object);

由于都是c函数,所以oc的内存管理语义在这里也受到了影响,需要做相应的改变,详细的objc_AssociationPolicy如下

/**
 * Policies related to associative references.
 * These are options to objc_setAssociatedObject()
 */
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};

这里语义内容很清晰了,就不多做解释了,那针对上面的案例,如何用关联对象完成setter和getter

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

- (NSString *)imageName {
    return objc_getAssociatedObject(self, _cmd);
}

这里的key,可以使用c字符串等形式,我这里放入表示方法名的SEL类型也可以,_cmd 关键字表示当前方法选择子,也就是@selector(imageName),当然这里也可以用静态指针static void *但是为了保证key的唯一性,我们还是用当前的方法,可以看到成功打印出结果image_name,下面对个函数进行详细分析。

objc_setAssociatedObject

我们在ojbc-runtime.m文件中找到以下代码片段

#if SUPPORT_GC
PRIVATE_EXTERN void objc_setAssociatedObject_gc(id object, const void *key, id value, objc_AssociationPolicy policy) {
    if ((policy & OBJC_ASSOCIATION_COPY_NONATOMIC) == OBJC_ASSOCIATION_COPY_NONATOMIC) {
        value = objc_msgSend(value, SEL_copy);
    }
    auto_zone_set_associative_ref(gc_zone, object, (void *)key, value);
}
#endif

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

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
#if SUPPORT_GC
    if (UseGC) {
        if ((policy & OBJC_ASSOCIATION_COPY_NONATOMIC) == OBJC_ASSOCIATION_COPY_NONATOMIC) {
            value = objc_msgSend(value, SEL_copy);
        }
        auto_zone_set_associative_ref(gc_zone, object, (void *)key, value);
    } else 
#endif
    {
        // Note, creates a retained reference in non-GC.
        _object_set_associative_reference(object, (void *)key, value, policy);
    }
}

在这里我们确定方法调用栈的内部方法为_objc_set_associative_reference这个方法,那么我们跳转到这个方法内部看其具体实现。

PRIVATE_EXTERN void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    // retain the new value (if any) outside the lock.
    uintptr_t old_policy = 0; // NOTE:  old_policy is always assigned to when old_value is non-nil.
    id new_value = value ? acquireValue(value, policy) : nil, old_value = nil;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        if (new_value) {
            // break any existing association.
            AssociationsHashMap::iterator i = associations.find(object);
            if (i != associations.end()) {
                // secondary table exists
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    ObjcAssociation &old_entry = j->second;
                    old_policy = old_entry.policy;
                    old_value = old_entry.value;
                    old_entry.policy = policy;
                    old_entry.value = new_value;
                } else {
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // create the new association (first time).
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                _class_setInstancesHaveAssociatedObjects(_object_getClass(object));
            }
        } else {
            // setting the association to nil breaks the association.
            AssociationsHashMap::iterator i = associations.find(object);
            if (i !=  associations.end()) {
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    ObjcAssociation &old_entry = j->second;
                    old_policy = old_entry.policy;
                    old_value = (id) old_entry.value;
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    if (old_value) releaseValue(old_value, old_policy);
}

由于代码量较多,我们忽略大部分的逻辑部分,看到产生作用的有如下几个类:AssociationsManager,AssociationsHashMap,ObjectAssociationMap,ObjcAssociation

下面一个个看这几个类的作用

AssociationsManager关联对象管理类
class AssociationsManager {
    static OSSpinLock _lock;
    static AssociationsHashMap *_map;               // associative references:  object pointer -> PtrPtrHashMap.
public:
    AssociationsManager()   { OSSpinLockLock(&_lock); }
    ~AssociationsManager()  { OSSpinLockUnlock(&_lock); }
    
    AssociationsHashMap &associations() {
        if (_map == NULL)
            _map = new(::_malloc_internal(sizeof(AssociationsHashMap))) AssociationsHashMap();
        return *_map;
    }
};

OSSpinLock AssociationsManager::_lock = OS_SPINLOCK_INIT;
AssociationsHashMap *AssociationsManager::_map = NULL;

这里实现了OSSpinLockAssociationsHashMap这两个单例,在构造这个方法的时候,会调用OSSpinLockLock,而在析构的时候会调用OSSpinLockUnlockassociations方法可以取得一个AssociationsHashMap单例,很明显这个管理类通过持有一个自旋锁保证了操作AssociationsHashMap是线程安全的,所以每次只有一个线程可以对AssociationsHashMap进行操作

ObjcAssociation关联对象实际存储的方式

ObjectAssociation,ObjectAssociationMap结构体名定义来看,这两个分别是关联对象结构体和关联对象结构体映射表,下面上源码

struct ObjcAssociation {
        uintptr_t policy;
        id value;
        ObjcAssociation(uintptr_t newPolicy, id newValue) : policy(newPolicy), value(newValue) { }
        ObjcAssociation() : policy(0), value(0) { }
    };

class ObjectAssociationMap : public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjcAllocator<std::pair<void * const, ObjcAssociation> > > {
    public:
        void *operator new(size_t n) { return ::_malloc_internal(n); }
        void operator delete(void *ptr) { ::_free_internal(ptr); }
    };
    typedef hash_map<void *, ObjectAssociationMap *, ObjcPointerHash, ObjcPointerEqual, ObjcAllocator<void *> > AssociationsHashMap;

这里ObjcAssociation关联对象结构体存储了policy存储策略value关联对象值这两个重要属性,ObjcAssociationMap这里关联了keyObjcAssociation的映射,这个类存储了所有这个对象所关联对象的信息。
拿出我们的经典demo案例,

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

用图来表示一下这段代码的具体实现


关联对象的底层实现

下面接着分析_objc_set_associative_reference这个方法,根据new_value出现了第一次逻辑判断

uintptr_t old_policy = 0; // NOTE:  old_policy is always assigned to when old_value is non-nil.
id new_value = value ? acquireValue(value, policy) : nil, old_value = nil;

用个demo看一下这个new_value的作用

UIImage * image = [UIImage imageNamed:@"image_name"];
NSLog(@"%@", objc_getAssociatedObject(image, @selector(imageName)));
    
objc_setAssociatedObject(image, @selector(imageName), @"image_name", OBJC_ASSOCIATION_COPY_NONATOMIC);
NSLog(@"%@", objc_getAssociatedObject(image, @selector(imageName)));
    
objc_setAssociatedObject(image, @selector(imageName), nil, OBJC_ASSOCIATION_COPY_NONATOMIC);
NSLog(@"%@", objc_getAssociatedObject(image, @selector(imageName)));
2019-03-19 18:21:08.667240+0800 Demo[5301:197907] (null)
2019-03-19 18:21:08.667557+0800 Demo[5301:197907] image_name
2019-03-19 18:21:08.667789+0800 Demo[5301:197907] (null)

这里看得出,如果我们在value字段设置为nil,相当于清除了这个关联对象的key,所以我们初步可以认为,如果new_value为真,那么逻辑中应该是创建一个关联对象或者修改一个关联对象的值,如果new_value为假,那么应该是清除这个关联对象。为了验证,我们找到逻辑为假这部分代码。

if (j != refs->end()) {
    ObjcAssociation &old_entry = j->second;
    old_policy = old_entry.policy;
    old_value = (id) old_entry.value;
    refs->erase(j);
}

这里明显看到调用了erase函数擦出了这个关联对象的key

所以_objc_set_associative_reference方法流程如下
1.从AssociationsManager单例中,在线程安全的状态下,取得全局关联对象哈希表AssociationsHashMap
2.根据关联对象所属类,从AssociationsHashMap取得这个类的关联对象哈希表ObjectAssociationMap,如果ObjectAssociationMap这个表不存在则创建一个新的表。
3.根据void * key,从ObjectAssociationMap中查找到ObjcAssociation结构体,如果没有这个结构体则新创建这个结构体。
4.如果new_value为空,ObjectAssociationMap会调用erase函数擦除这个key
4.ObjcAssociation结构体中应存有policy存储策略value值

以上就是objc_setAssociatedObject方法的实现流程,另外两个方法原理雷同这里不加以多赘述。

总结

Category结构体中不可以添加实例变量可以添加方法,添加属性时不会自动生成setter和getter方法,需要我们手动实现,由于不能添加实例变量所以实现这两个方法需要用到关联对象,它的实质是ObjcAssociation这个结构体,里面主要存储了存储策略和具体值,这个结构体存在于ObjectAssociationMap哈希表中,这个表存储了每个对象具体的关联对象,键为void *类型的一段字符串,这个哈希表存在于AssociationsHashMap这个表中,这个表实际根据对象的不同为key存储了全部关联对象,这个表被AssociationsManager这个单例所持有,每次调用方法时会以单例的形式创建它是线程安全的

后续

到这里已经将最重要的三个部分分析好了,分别是运行时结构和消息机制以及关联对象,感兴趣的朋友们可以移步下一篇文章 OC运行时机制Runtime(四):尝试使用黑魔法 Method Swizzling,如果觉得本文对您有些作用,请在下方点个赞再走哈~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,978评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,954评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,623评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,324评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,390评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,741评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,892评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,655评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,104评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,451评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,569评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,254评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,834评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,725评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,950评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,260评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,446评论 2 348