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地址变化和偏移量变化

自定义类走[email protected]
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();
        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方法在哪里调用的呢?


读取内存地址赋值[email protected]

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赋值

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

类扩展,类扩展必须放到类的声明之后,实现之前,即@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
类扩展中方法和类中方法一模一样@2x.png

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

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

关联对象@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)方法

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

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

构造函数和析构函数@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中非[email protected]

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

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

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


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

关联对象桶子和属性桶子赋值前后对比,属性桶子objc::detail::DenseMapPair作为关联对象桶子(objc::detail::DenseMapPair, objc::DenseMap, objc::DenseMapInfo, objc::detail::DenseMapPair > > *)的最后一个属性存在于关联对象桶子中,知道属性桶子的结构为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
属性关联对象结果[email protected]

关联对象

关联对象AssociationsHashMap->Buckets->[email protected]

关联对象设值流程

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

关联对象取值流程

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

你可能感兴趣的:(iOS-底层原理16-类扩展和关联对象底层原理)