应用程序加载(五)-- 类扩展和关联对象

应用程序加载(一) -- dyld流程分析
应用程序加载(二) -- dyld&objc关联以及类的加载初探
应用程序加载(三)-- 类的加载
应用程序加载(四)-- 分类的加载
应用程序加载(五)-- 类扩展和关联对象


1、类扩展extension

类扩展需要对应着分类(category)比较研究。
分类的作用:

  1. 添加方法,可以添加实例方法和类方法
  2. 添加协议
  3. 添加属性(@property),只会生成gettersetter的声明,而不会生成方法的实现和带下划线的成员变量

类扩展的作用:

  1. 可以添加方法声明,需要在原类的.m中实现
  2. 可以添加属性
  3. 可以添加成员属性

扩展写法

通常在创建工程中时会有一个ViewController类或者创建自定义的UIViewController子类的时候,Xcode会在类中自动创建一个类扩展,如图:

我们经常把不想暴露的一些属性定义在这里,需要外部调用的属性写在.h文件中。

需要注意类扩展有一个位置要求,必须写在类的interfaceimplementation之间。写一个简单的例子,就在VierController.m文件中,写一个Person类,然后为Person添加一个类扩展。
正确的写法:

如果将类扩展写interface之前或者implementation之后,编译器会报错。
interface之前:

implementation之后:

扩展和分类的区别

分类中添加属性是没有gettersetter实现的,而扩展中是有的,我们可以通过c++层看到扩展添加属性会有什么效果。
首先在扩展中添加一个dzName属性

然后进入到ViewController.m文件所在的路径,执行xcrun -sdk iphonesimulator clang -rewrite-objc ViewController.m命令,生成ViewController.cpp文件,打开文件,搜索dzName

通过c++的代码可以看到,生成了属性对应的settergetter方法,还有带有下划线的成员变量(_dzName

在扩展中还写了一个ext_method方法,在c++中也能够看到

直接加入到方法列表中。这点可以说明,扩展是类的一部分,而且方法在编译期就加入到类中。


2、关联对象

上面了解了,扩展可以给类增加属性。但是分类添加属性的时候只是增加了gettersetter的声明。例如:

有如图中的分类,里面定义了一个属性cat_name。当我们调用的时候:

此时编译器是不会报错的,说明cat_namesetter方法的sel是可以找到的。当运行起来的时候,就会报错。因为找不到对应的imp。所以说分类中添加属性只是添加了gettersetter方法的声明,而没有方法的实现。

那么如何分类中的属性的实现呢?就是通过系统给我们提供的关联对象。

关联对象用法

#import <objc/runtime.h>
@interface Person (DZ)
@property (nonatomic, copy) NSString *cate_name;
@end

@implementation Person (DZ)
- (void)setCate_name:(NSString *)cate_name{
    /**
     1: 对象
     2: 标识符
     3: value
     4: 策略
     */
    objc_setAssociatedObject(self, "cate_name", cate_name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)cate_name{
    return  objc_getAssociatedObject(self, "cate_name");
}
@end
  • 分类中定义了一个属性cate_name
  • 实现了cate_namegettersetter方法,方法中调用了关联对象的两个方法:
    • objc_setAssociatedObject:需要四个参数,分别是对象、标识符、值和关联策略
    • objc_getAssociatedObject:相对简单,只有两个参数,分别是对象和标识符
    • 注意,使用关联对象要引入runtime的头文件

用法很容易,但是底层是如何实现的呢?接下来我们来探索一下底层实现。

关联对象的底层实现

还是在objc的源码中,可以看到相关源码,先来看看关联对象的set实现:

关联对象set
void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
    SetAssocHook.get()(object, key, value, policy);
}
  • 此处与以往的objc开源代码不同,在“781”的这个版本的源码中,增加了一层代码封装,通过command点击SetAssocHook,能够看到这行代码:static ChainedHookFunction<objc_hook_setAssociatedObject> SetAssocHook{_base_objc_setAssociatedObject};
  • 底层调用的就是_base_objc_setAssociatedObject函数
static void
_base_objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
  _object_set_associative_reference(object, key, value, policy);
}
  • _base_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
    //1、相关的容错处理
    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));
    //2、包装了一下 对象
    DisguisedPtr<objc_object> disguised{(objc_object *)object};
    //3、 包装一下 policy - value
    ObjcAssociation association{policy, value};

    //4、根据关联策略对值处理
    // retain the new value (if any) outside the lock.
    association.acquireValue();
    
    // 5、核心代码
    {
        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 */
                object->setHasAssociatedObjects();
            }

            /* 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);

                    }
                }
            }
        }
    }

    // release the old value (outside of the lock).
    association.releaseHeldValue();
}
  1. 首先代码进行了相关的容错处理,这些不用关心,简单看看就好

  2. 将传入的参数object进行了一次包装。

  3. 对第三个参数value和第四个参数policy包装一下。

  4. association.acquireValue();根据关联策略(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;
                }
            }
        }
    
    • 此处根据关联策略对value进行retain或者copy的操作。
  5. 此处进入核心代码部分

    {
        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 */
                object->setHasAssociatedObjects();
            }
    
            /* 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);
    
                    }
                }
            }
        }
    }
    
    • 初始化一个AssociationsManager变量manager,它不是一个单例,只是用它来给下面访问哈希表进行加锁

构造函数和析构函数就是加锁解锁操作。
- 通过manager.get()函数获取到关联对象的哈希表的引用&associations,类型是AssociationsHashMap
- 判断value存在时:
- 从哈希表中读取disguised对应的结果refs_resultdisguised就是传入的object参数。
- 通过结果refs_result中的second判断是不是第一次来。如果第一次来就会调用object->setHasAssociatedObjects();函数
- setHasAssociatedObjects就是给对象object设置状态。代表这个对象是有关联对象存在的。如果objectnonpointer,那么这个关联状态就存储在isa
- 然后根据参数key创建关联,如果之前关联过就进行替换关联。
- 此处就可以知道,是两层哈希表。第一层是以对象作为哈希函数,第二层是以闯入的参数key为哈希函数。
- value不存在时:传入的参数value为nil。就进行擦除。因为是双层哈希,所以得做两层擦除。

关联对象set流程图:


关联对象get

此时对关联对象的set有所了解了,再看看get实现就很简单,就是通过传入的两个参数,进行哈希的双层查找。源码如下:

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

id
_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();
}
关联对象双层哈希示意图
  • 图中包含示例中的关联对象例子
关联对象表的移除

表的创建我们了解后,就得看看什么时候对表进行移除操作的。移除是在dealloc中处理的,看看源码:

- (void)dealloc {
    _objc_rootDealloc(self);
}

⏬⏬⏬

void
_objc_rootDealloc(id obj)
{
    ASSERT(obj);

    obj->rootDealloc();
}

⏬⏬⏬

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}

⏬⏬⏬

id 
object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}

⏬⏬⏬

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}

⏬⏬⏬

void
_object_remove_assocations(id object)
{
    ObjectAssociationMap refs{};

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

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