iOS-底层原理16-类扩展和关联对象底层原理

《iOS底层原理文章汇总》

上一篇文章《iOS-底层原理15-类的加载下》详细介绍了类和分类的懒加载和非懒加载搭配情况下,方法的加载流程,本文介绍类的扩展和关联对象底层原理

LLVM源码下载地址

1.方法排序中类中的方法的name的内存地址排序,内存地址从哪儿得来,有什么规则?

我们都知道方法经过prepareMethodLists --> fixupMethodList 排序后变得有序,排序是通过name的内存地址进行排序的,name地址从哪儿取值呢?

name赋值@2x.png
static void 
fixupMethodList(method_list_t *mlist, bool bundleCopy, bool sort)
{
    runtimeLock.assertLocked();
    ASSERT(!mlist->isFixedUp());

    // fixme lock less in attachMethodLists ?
    // dyld3 may have already uniqued, but not sorted, the list
    if (!mlist->isUniqued()) {
        mutex_locker_t lock(selLock);
    
        // Unique selectors in list.
        for (auto& meth : *mlist) {
            const char *name = sel_cname(meth.name);
            meth.name = sel_registerNameNoLock(name, bundleCopy);
        }
    }
    // sel - imp
    // Sort by selector address.
    if (sort) {
        method_t::SortBySELAddress sorter;
        std::stable_sort(mlist->begin(), mlist->end(), sorter);
    }
    
    // Mark method list as uniqued and sorted
    mlist->setFixedUp();
}

SEL sel_registerNameNoLock(const char *name, bool copy) {
    return __sel_registerName(name, 0, copy);  // NO lock, maybe copy
}

static SEL __sel_registerName(const char *name, bool shouldLock, bool copy) 
{
    SEL result = 0;

    if (shouldLock) selLock.assertUnlocked();
    else selLock.assertLocked();

    if (!name) return (SEL)0;

    result = search_builtins(name);
    if (result) return result;
    
    conditional_mutex_locker_t lock(selLock, shouldLock);
    auto it = namedSelectors.get().insert(name);
    if (it.second) {
        // No match. Insert.
        *it.first = (const char *)sel_alloc(name, copy);
    }
    return (SEL)*it.first;
}

static SEL search_builtins(const char *name) 
{
#if SUPPORT_PREOPT
  if (builtins) {
      SEL result = 0;
      if ((result = (SEL)builtins->get(name)))
          return result;

      if ((result = (SEL)_dyld_get_objc_selector(name)))
          return result;
  } else if (useDyldSelectorLookup) {
      if (SEL result = (SEL)_dyld_get_objc_selector(name))
          return result;
  }
#endif
    return nil;
}

method.name从result = search_builtins(name)中取值,条件为useDyldSelectorLookup,查看条件的初始化发现map_images_nolock-->sel_init(selrefCount)-->useDyldSelectorLookup = true,程序进入(SEL)_dyld_get_objc_selector(name),走入dyld源码_dyld_get_objc_selector-->gAllImages.getObjCSelector(selName) --> _objcSelectorHashTable->getString(selName, _objcSelectorHashTableImages.array()),name的地址来源于段的基本地址+相应偏移量,name的内存地址是随机变化的,根据编译器调试处理的,新增库和方法会导致selectionBaseAddress地址变化和偏移量变化

useDyldSelectorLookup@2x.png
sel_init@2x.png
_dyld_get_objc_selector@2x.png
自定义类走dyld3-_dyld_get_objc_selector@2x.png
name地址动态变化@2x.png
const char* _dyld_get_objc_selector(const char* selName)
{
    // Check the shared cache table if it exists.
    if ( gObjCOpt != nullptr ) {
        if ( const objc_opt::objc_selopt_t* selopt = gObjCOpt->selopt() ) {
            const char* name = selopt->get(selName);
            if (name != nullptr)
                return name;
        }
    }

    if ( gUseDyld3 )
        return dyld3::_dyld_get_objc_selector(selName);

    return nullptr;
}

const char* _dyld_get_objc_selector(const char* selName)
{
    log_apis("dyld_get_objc_selector()\n");
    return gAllImages.getObjCSelector(selName);
}

2.MachO文件格式,读到相应的data(),read data()中的地址怎么直接变成class_ro_t格式的?那么什么时候编译成class_ro_t的格式编译到MachO中的呢?在MachO中是地址指针的形式存在

    auto ro = (const class_ro_t *)cls->data();
    auto isMeta = ro->flags & RO_META;
    if (ro->flags & RO_FUTURE) {
        // This was a future class. rw data is already allocated.
        rw = cls->data();
        ro = cls->data()->ro();
        ASSERT(!isMeta);
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        // Normal class. Allocate writeable class data.
        rw = objc::zalloc<class_rw_t>();
        rw->set_ro(ro);
        rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
        cls->setData(rw);
    }
  • 1.查看class_ro_t的数据结构
struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    // This field exists only when RO_HAS_SWIFT_INITIALIZER is set.
    _objc_swiftMetadataInitializer __ptrauth_objc_method_list_imp _swiftMetadataInitializer_NEVER_USE[0];

    _objc_swiftMetadataInitializer swiftMetadataInitializer() const {
        if (flags & RO_HAS_SWIFT_INITIALIZER) {
            return _swiftMetadataInitializer_NEVER_USE[0];
        } else {
            return nil;
        }
    }

    method_list_t *baseMethods() const {
        return baseMethodList;
    }

    class_ro_t *duplicate() const {
        if (flags & RO_HAS_SWIFT_INITIALIZER) {
            size_t size = sizeof(*this) + sizeof(_swiftMetadataInitializer_NEVER_USE[0]);
            class_ro_t *ro = (class_ro_t *)memdup(this, size);
            ro->_swiftMetadataInitializer_NEVER_USE[0] = this->_swiftMetadataInitializer_NEVER_USE[0];
            return ro;
        } else {
            size_t size = sizeof(*this);
            class_ro_t *ro = (class_ro_t *)memdup(this, size);
            return ro;
        }
    }
};

MachO文件在编译期完成,肯定有一个方法在编译期能读取data()中内存地址为class_ro_t,查看llvm源码

在llvm中查看 struct class_ro_t源码,llvm通过Read方法读取data()中的内存地址,给class_ro_t中的元素依次按顺序赋值,那么class_ro_t::Read方法在哪里调用的呢?

llvm-class_ro_t@2x.png

读取内存地址赋值class_ro_t@2x.png

Read_class_row中会连续调用class_rw->Readclass_ro->Read进而给class_rw_tclass_ro_t赋值,那什么时候调用Read_class_row的呢?Read_class_rowRead_objc_class相生相随,Read_class_row属于模板类,调用Describe方法之前都会调用ClassDescriptorV2进行初始化,初始化后调用Describe,GetClassName,GetInstanceSize都会调用Read_class_row,从而触发class_ro_tclass_rw_t赋值

Read_class_row@2x.png

3.类的扩展在编译期就会作为类的一部分编译进去,和分类的加载过程不一样,分类是为了动态的开辟,类扩展作为类的一部分跟类一起伴随着永生下去

A.category:类别,分类

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

B.extension:类扩展

  • I 可以说是特殊的分类,也称作匿名分类
  • II 可以给类添加成员属性,但是是私有变量
  • III 可以给类添加方法,也是私有方法

给LGPerson+LGA中增加属性cate_name,在main函数中调用person.cate_name = @"KC"变色但运行没有实现会崩溃

声明cate_name运行奔溃@2x.png
ClassDescriptorV2@2x.png

类扩展,类扩展必须放到类的声明之后,实现之前,即@interface和@implementation中间,否则编译报错,那么类扩展的本质是什么呢?我们通过clang查看类扩展的本质

类扩展必须在@interface和@implementation中间@2x.png

类扩展中声明的属性会自动生成带下划线的成员变量和setter、getter方法,类扩展中方法和类中方法一模一样,没有实现load方法会在第一次消息发送的时候将方法加载到类中,

realizeClassMaybeSwiftMaybeRelock: 这个是我要研究的 LGTeacher 
realizeClassMaybeSwiftMaybeRelock: 这个是我要研究的 LGTeacher 
realizeClassWithoutSwift: 这个是我要研究的 LGTeacher 
methodizeClass: 这个是我要研究的 LGTeacher 
prepareMethodLists: 这个是我要研究的 LGTeacher 
attachToClass: 这个是我要研究的 LGTeacher 

实现了load方法,类的扩展在编译期就会作为类的一部分编译进MachO文件中,方法在编译时期就进入ro文件中了,直接从MachO文件的data()中读取出来,过程和上一篇文章《iOS-底层原理15-类的加载下》分析的一致,分类是为了动态的开辟,类的扩展不需要动态开辟,作为类的一部分跟类一起加载

实现load方法加载到类中@2x.png
_ext_name@2x.png
_setExt_name@2x.png
类扩展中方法和类中方法一模一样@2x.png

4.类的扩展可以填加load方法变为非懒加载类吗?不能,没有.m文件和@implementation实现类

关联对象底层原理:分类无法添加属性,要添加属性的话,需要重写setter、getter方法添加关联对象,关联对象传入4个参数,对象,标识符,value值,关联策略

关联对象@2x.png
_base_objc_setAssociatedObject@2x.png

objc_setAssociatedObject --> SetAssocHook.get()(object, key, value, policy) --> _base_objc_setAssociatedObject --> _object_set_associative_reference(object, key, value, policy)

SetAssocHook.get()为一层接口模式,整个对外暴露的objc_setAssociatedObject永远不变,中间的这一层是有处理的,可以增加接口拦截之类做一些其他处理,类似于reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy)

调用SetAssocHook.get()(object, key, value, policy)相当于_base_objc_setAssociatedObject(object, key, value, policy)从而进入_object_set_associative_reference(object, key, value, policy)方法

_object_set_associative_referenece@2x.png

acquireValue对value的值进行处理,传入的策略是retain或copy做相应的操作,其他策略不做处理

acquireValue@2x.png

程序继续往下之心,构造函数和析构函数

构造函数和析构函数@2x.png
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }

类似于在main.m中写

struct LGObjc {
    LGObjc()   { printf("来了");}
    ~LGObjc()  {  printf("走了"); }
};
LGObjc构造函数和析构函数@2x.png

关联对象存在一张大的哈希表,AssociationsHashMap里面存储LGPerson和LGTeacher等的,此哈希表唯一,方便查找,AssociationsHashMap是静态变量获取出来的,全场唯一,但是AssociationsManager不唯一,加锁代表防止多线程重复创建,并不是代表不能创建

AssociationsHashMap唯一@2x.png
赋值前初始化@2x.png

查看refs_result的格式存在五个键值对

获取键值对@2x.png

根据对象去AssociationsHashMap总表中查找关联对象LGPerson桶子,若找到,直接返回LGPerson桶子和bool值为false,代表桶子已经存在,不是第一次进入,若没找到,插入一个新的空的LGPerson桶子和bool值为true,如果第一次执行object->setHasAssociatedObjects(),标记为nonpointerisa

setHasAssociatedObjects中非nonpointerisa@2x.png

若value传值为nil,则从AssociationsHashMap中移除桶子,关联对象也消除

value为nil@2x.png
AssociationsHashMap@2x.png

查看第一次时,TheBucket的值,和refs_result的键值对中最后一个键值对DenseMapPair完全一样, refs_result存在五个属性,TheBucket桶子藏在detail中

TheBucket@2x.png

首先去安放关联对象,查找关联对象作为key对应的桶子是否已经存在,若存在返回找到的关联对象LGPerson桶子,若不存在,返回一个空的关联对象LGPerson桶子并给默认值,空桶子赋值前后对比


查找关联对象对应的桶子@2x.png
空桶子@2x.png
桶子关联对象@2x.png

关联对象桶子和属性桶子赋值前后对比,属性桶子objc::detail::DenseMapPair<const void *, objc::ObjcAssociation>作为关联对象桶子(objc::detail::DenseMapPair<DisguisedPtr<objc_object>, objc::DenseMap<const void *, objc::ObjcAssociation, objc::DenseMapValueInfo<objc::ObjcAssociation>, objc::DenseMapInfo<const void *>, objc::detail::DenseMapPair<const void *, objc::ObjcAssociation> > > *)的最后一个属性存在于关联对象桶子中,知道属性桶子的结构为DenseMapPair后,通过方法获取存入的值

关联对象桶子赋值前后对比@2x.png
属性桶子赋值前后对比@2x.png
属性桶子通过方法获取存入的值@2x.png

取返回值的refs_result.first->second,此时安放key也就是"cate_name"作为key,association{policy, value}(3,"KC")作为Value到属性cate_name桶子中,若"cate_name"对应的桶子已经存在,则直接返回属性桶子,若不存在,再次返回一个以"cate_name"为key的空属性桶子

second的值@2x.png
返回cate_name为key的空桶子@2x.png

此时将key("cate_name"),policy(3),value("KC")插入到返回的以cate_name为key的属性桶子中,对象的属性就和对象产生了关联,返回std::make_pair( makeIterator(TheBucket, getBucketsEnd(), true), true),则result.second为true

key、policy、value插入到对应桶子中@2x.png
属性关联对象结果result@2x.png

关联对象

关联对象AssociationsHashMap->Buckets->DenseMapPair@2x.png

关联对象设值流程

关联对象设值流程@2x.png

关联对象取值流程

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

推荐阅读更多精彩内容