Objective-C—Class底层结构探索,真心分享给你!!!

news2024/12/26 14:43:16

isa 走位图

在讲 OC->Class 底层类结构之前,先看下下面这张图:

isa走位


 

通过isa走位图 得出的结论是:
1,类,父类,元类都包含了 isasuperclass

2,对象isa指向类对象,类对象的isa指向了元类,元类的 isa 指向了根元类,根元类 isa 指向自己

3,类的 superclass 指向父类,父类的 superclass 指向的根类,根类的superclass 指向的nil

4,元类的 superclass 指向父元类,父元类 superclass 指向的根元类,根元类 superclass 指向根类,根类 superclass 指向nil

这下又复习了isa ,superclass 走位;那么问题这些类,类对象,元类对象当中的在底层展现的数据结构是怎样呢,这是我需要探索的,于是把源码贴出来展开分析下:

struct objc_class

struct objc_class : objc_object {
    // Class ISA;
    Class superclass; 
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;  
    class_rw_t *data() const {
        return bits.data();
    }
    const class_ro_t *safe_ro() const {
        return bits.safe_ro();
    }
}

从源码没见 isa 属性,其实它继承了objc_object ,而 objc_object 中有个isa ,在运行时类图生成中会产生一个isa 指向objc_object 这个类图,而 superclass 指向它的父类;根据上面 isa , superclass 走位图就知道它的指向关系。

cache_t & class_data_bits_t

cache 方法缓存,这个作用将常调用的方法缓存下来;便于下次直接查找调用,提高查找效率。
它的结构:

struct cache_t {
	struct bucket_t *buckets() const;//存储方法的散列表
	mask_t mask() const;//散列表缓存长度
	mask_t occupied() const;//已缓存方法个数
}
struct class_data_bits_t {
    class_rw_t* data() const;//类信息
}

bits 存储具体类信息,它需要&FAST_DATA_MASK来计算得到类心所有信息,源码如下:

FAST_DATA_MASK 掩码值

imageng

bool has_rw_pointer() const {
	#if FAST_IS_RW_POINTER
	        return (bool)(bits & FAST_IS_RW_POINTER);
	#else
	        class_rw_t *maybe_rw = (class_rw_t *)(bits & FAST_DATA_MASK);
	        return maybe_rw && (bool)(maybe_rw->flags & RW_REALIZED);
	#endif
}

通过源码确实需要这种方式计算能得到类的存储信息;那为什么要用这种方式去处理呢。
比如说我要得到存储在 class_rw_t 类信息信息我只要通过 FAST_DATA_MASK 掩码值就能得到它的地址信息,通过地址信息就能从内存中拿到所有类的存储信息。

那这样我的FAST_DATA_MASK掩码值不一样,我通过&计算,得到的数据信息也就不一样,不得不说苹果工程师想的周到,而且这种方式不仅isa也是这样,很多地方都用这种方式取值,大大提高访问速度,数据提取效率。

class_rw_t ,class_ro_t,class_rw_ext_t

struct class_rw_t {
     const class_ro_t *ro() const ;
     const method_array_t methods() const ;//如果是类对象:放对象方法,元类:元类对象方法
     
     const property_array_t properties() const;
     const protocol_array_t protocols() const;
     class_rw_ext_t *ext() const;
}
struct class_rw_ext_t {
    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    uint32_t version;
}

可以看出类的信息具体就存储在class_rw_tclass_ro_tclass_rw_ext_t 中,

剖析下class_rw_t


先看看method_array_tproperty_array_tprotocol_array_t源码结构

class property_array_t : 
    public list_array_tt<property_t, property_list_t, RawPtr>
{
    typedef list_array_tt<property_t, property_list_t, RawPtr> Super;

 public:
    property_array_t() : Super() { }
    property_array_t(property_list_t *l) : Super(l) { }
};


class protocol_array_t : 
    public list_array_tt<protocol_ref_t, protocol_list_t, RawPtr>
{
    typedef list_array_tt<protocol_ref_t, protocol_list_t, RawPtr> Super;

 public:
    protocol_array_t() : Super() { }
    protocol_array_t(protocol_list_t *l) : Super(l) { }
};

看完之后,他们都继承list_array_tt,那么 list_array_tt 是什么鬼,它数据结构是怎样的,这下在取找下它。源码如下:

template <typename Element, typename List, template<typename> class Ptr>
class list_array_tt {
 protected:
    template <bool authenticated>
    class iteratorImpl {
        const Ptr<List> *lists;
        const Ptr<List> *listsEnd;
    }
        
    using iterator = iteratorImpl<false>;
    using signedIterator = iteratorImpl<true>;

 public:
    list_array_tt() : list(nullptr) { }
    list_array_tt(List *l) : list(l) { }
    list_array_tt(const list_array_tt &other) {
        *this = other;
    }

    void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        if (hasArray()) {
            // many lists -> many lists
            uint32_t oldCount = array()->count;
            uint32_t newCount = oldCount + addedCount;
            array_t *newArray =(array_t*)malloc(array_t::byteSize(newCount));
            newArray->count = newCount;
            array()->count = newCount;

            for (int i = oldCount - 1; i >= 0; i--)
                newArray->lists[i + addedCount] = array()->lists[i];
            for (unsigned i = 0; i < addedCount; i++)
                newArray->lists[i] = addedLists[i];
            free(array());
            setArray(newArray);
            validate();
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
            validate();
        } 
        else {
            // 1 list -> many lists
            Ptr<List> oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            array()->count = newCount;
            if (oldList) array()->lists[addedCount] = oldList;
            for (unsigned i = 0; i < addedCount; i++)
                array()->lists[i] = addedLists[i];
            validate();
        }
    }
    
}

我把主要地方拿去出来,可以看到 attachLists 它的目的是将一个或多个列表(List 类型)附加到某个 list_array_tt对象中。这个对象可以包含零个、一个或多个列表,这些列表可以是单个指针,也可以是指针数组。函数的输入参数是一个指向 List 指针数组的指针 addedLists 和一个无符号整数 addedCount,表示要添加的列表数量。

由此我推断它是一个数组,而且是一个二维数组存储的,所有由此得出 class_rw_t 中methodspropertiesprotocols这几个属性利用二维数组取存储类的方法,协议等信息,而且是可读可写的属性。

那它设计这种二维数组有什么好处呢?当然有好处,它可以动态的给数组里面增加删除方法,很方便我们分类方法的编写完进行存储。

那搞清楚了 class_rw_t 几个重要数据存储信息,那 class_rw_t 它的作用是干什么的呢;

class_rw_t 结构体定义来看;它是在应用运行时,将OC类,分类的信息直接写入到class_rw_t结构的数据结构中,在类的方法,协议进行调用时,从里面去读取,然后常调用的方法,又存储在cache_t这个结构体中,可想而知,苹果对OC类的处理,煞费苦心。

struct class_ro_t

在 class_rw_t结构体中有个 class_ro_t 结构体,在探索下这个东西做什么的,它的源码如下:

struct class_ro_t {
    WrappedPtr<method_list_t, method_list_t::Ptrauth> baseMethods;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;
    property_list_t *baseProperties;
}

先说说 ivars 这个属性修饰的结构体源码如下:

struct ivar_list_t : entsize_list_tt<ivar_t, ivar_list_t, 0> {
    bool containsIvar(Ivar ivar) const {
        return (ivar >= (Ivar)&*begin()  &&  ivar < (Ivar)&*end());
    }
};

这个貌似只有一个继承 entsize_list_tt,那在探索下源码:

struct entsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;
     struct iteratorImpl {
     uint32_t entsize;
        uint32_t index;  // keeping track of this saves a divide in operator-

        using ElementPtr = std::conditional_t<authenticated, Element * __ptrauth(ptrauth_key_process_dependent_data, 1, 0xdead), Element *>;

        ElementPtr element;

        typedef std::random_access_iterator_tag iterator_category;
        typedef Element value_type;
        typedef ptrdiff_t difference_type;
        typedef Element* pointer;
        typedef Element& reference;

        iteratorImpl() { }

        iteratorImpl(const List& list, uint32_t start = 0)
            : entsize(list.entsize())
            , index(start)
            , element(&list.getOrEnd(start))
        { }
     }
}

可以看出这段代码定义了一个结构体 entsize_list_tt,它内部包含一个嵌套的结构体 iteratorImpl,用于实现一个迭代器。遍历容器(如列表、数组等)的对象。

到此可以得出ivars 是一个 ivar_list_t 数组,它存储了类的属性变量信息,那protocol_list_t结构体内部也是数组形式构建的。

baseProtocolsbaseProperties 这两个属性对类的存储信息只能读取,不能写入。

所以总结的是:从 class_ro_t 结构体定义来看,它存储类的变量,方法,协议信息,而且这个结构体属于类的只读信息,它包含了类的初始信息。

class_rw_ext_t

这个结构体不在过多叙述,简单来说它是基于 class_rw_t 之后为了更好管理oc类的高级特性,比如关联属性等,衍生出来的一个结构体,包括:method_array_t ,property_arrat_t ,protocol_array_t 等定义属性类型

到这里类结构及存储所关联的信息都在这里了;来一张他们关联的结构思维图:

imageng

总结:一开始编译时,程序将类的初始信息放在 class_ro_t中,当程序运行时,将类的信息合并在一起的时候,它会将 class_ro_t 类的信息合并到 class_rw_t 结构体中去。

struct method_t

为什么要说method_t,因为它不仅在 class_ro_t 有使用,在OC底层其他地方也有使用;比如如下源码:

void method_exchangeImplementations(Method m1Signed, Method m2Signed)
{
    if (!m1Signed  ||  !m2Signed) return;

    method_t *m1 = _method_auth(m1Signed);
    method_t *m2 = _method_auth(m2Signed);

    mutex_locker_t lock(runtimeLock);

    IMP imp1 = m1->imp(false);
    IMP imp2 = m2->imp(false);
    SEL sel1 = m1->name();
    SEL sel2 = m2->name();

    m1->setImp(imp2);
    m2->setImp(imp1);


    // RR/AWZ updates are slow because class is unknown
    // Cache updates are slow because class is unknown
    // fixme build list of classes whose Methods are known externally?

    flushCaches(nil, __func__, [sel1, sel2, imp1, imp2](Class c){
        return c->cache.shouldFlush(sel1, imp1) || c->cache.shouldFlush(sel2, imp2);
    });

    adjustCustomFlagsForMethodChange(nil, m1);
    adjustCustomFlagsForMethodChange(nil, m2);
}

static IMP
_method_setImplementation(Class cls, method_t *m, IMP imp)
{
    lockdebug::assert_locked(&runtimeLock);

    if (!m) return nil;
    if (!imp) return nil;

    IMP old = m->imp(false);
    SEL sel = m->name();

    m->setImp(imp);

    // Cache updates are slow if cls is nil (i.e. unknown)
    // RR/AWZ updates are slow if cls is nil (i.e. unknown)
    // fixme build list of classes whose Methods are known externally?

    flushCaches(cls, __func__, [sel, old](Class c){
        return c->cache.shouldFlush(sel, old);
    });

    adjustCustomFlagsForMethodChange(cls, m);

    return old;
}


方法交换,实现中底层都有用到,我们探索下,先看看 method_t 源码:

struct method_t {

    // The representation of a "big" method. This is the traditional
    // representation of three pointers storing the selector, types
    // and implementation.
    struct big {
        SEL name;
        const char *types;
        MethodListIMP imp;
    };

    // A "big" method, but name is signed. Used for method lists created at runtime.
    struct bigSigned {
        SEL __ptrauth_objc_sel name;
        const char * ptrauth_method_list_types types;
        MethodListIMP imp;
    };

    // ***HACK: This is a TEMPORARY HACK FOR EXCLAVEKIT. It MUST go away.
    // rdar://96885136 (Disallow insecure un-signed big method lists for ExclaveKit)
#if TARGET_OS_EXCLAVEKIT
    struct bigStripped {
        SEL name;
        const char *types;
        MethodListIMP imp;
    };
#endif

}

可以看到这结构体中掐套了多个结构体;在把它简化下:

struct method_t {
    SEL name;//方法名
    const char *types;//包含函数具有参数编码的字符串类型的返回值
    MethodListIMP imp;//函数指针(指向函数地址的指针)
}

SEL :函数名,没特别的意义;

特点:
1,使用@selector()sel_registerName()获得
2,使用sel_getName()NSStringFromSelector()转成字符串
3,不同类中相同名字方法,对应的方法选择器是相同或相等的

底层代码结构:

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

types:包含了函数返回值、参数编码的字符串

imageng

imageng

可以看到types在值:v16@0:8 ,可以看出name,types,IMP其实都在class_ro_t结构体中,这样确实证明了之前说的;class_ro_t结构体在运行时存储着类的初始状态数据。

v16@0:8说明下:

v:方法返回类型,这里说void,

16:第一个参数,

@:id类型第二个参数,

0:第三个参数

: :selector类型

8:第四个参数

那这种types参数又是什么鬼东西,查下了资料这叫:Type Encoding(类型编码)
怎么证明了,使用如下代码:

imagepng

苹果官网types encoding表格:

imageng

IMP 其实就是指向函数的指针,感觉这个就没有必要讲了。

struct cache_t

cache_t 用于 class的方法缓存,对class常调用的方法缓存下来,提高查询效率,这个上之前都已经说过;接下来看看 bucket_t

struct bucket_t

struct bucket_t {
	cache_key_t _key;//函数名
	IMP _imp;//函数内存地址
}

这种散列表的模型,其实在底层用一个数组展现:

imagng

其实它的内部就是一个一维数组,那可能问了,数组难道它是循环查找吗,其实不然;在它元素超找时,它是拿到你的 函数名 & mask,而这个 mask 就是 cache_t 结构体中的 mask值;计算得到函数在 散列表 存储的索引值,在通过索引拿到函数地址,进行执行。

接下来看个事例:

int main(int argc, const char * argv[]) {

    @autoreleasepool {

        Student *stu=[Student new];

        [stu test];

        [stu test];

        [stu test];

        [stu test];

    }

    return 0;

}

如上方法:当首次调用它会去类对象中查找,在方法执行时,他会放入cache_t 缓存中,当第二次,第三次,第四次时,它就去缓存中查找。

imagpng

当方法执行后;我们看到 _mask 是:3,这个3代表了我类中定义了三个函数;而——_occupied 是一个随意的值;它其实代表了换存方法的个数。

那如何知道方法有缓存了,再继续往下执行:

imageng

这时候执行完 test02_mask的值从 3 变成了 7 ,说明散列表 bucket_t 做了扩容操作。在这里bucket_t 元素需要 _mask 个元素,所以最终 bucket_t 从原有的3个元素进行了 2倍 扩容。

在看下方法是否进行缓存:

imageng

可以看见当执行完 [stu test02] 时,数据做了扩容,并且扩容的数据使用(null) 进行填充。

在看个事例:

imageng

在执行 [stu test] 之前;其实bucket_t 就3个元素,并且存入了 init 方法;

imageng

当执行完 [stu test] 之后;就存入 test 方法。

但是注意的地方:它在扩容时对之前的缓存进行清除。

image.png

通过查看源码,我们知道了它如何进行清除操作,

imageng

当执行完 [stu test02]; ,[stu test03]; 之后,它先将缓存清空;这时候 init , test 方法被清空,bucket_t扩容完在存储:test02test03 方法。

那问题又来了,它是如何快速定位到方法的,然后执行的?接下来看看代码

imagepng

可以清楚看见,当我使用 @selector(test03)&stu_cache._mask 就可以得到下标,然后再从 bucket_t 拿到方法。

到这里 class结构,类的方法缓存到此结束了,从上面也可以思考下:如果自己去实现散列表数组,是不是思路就跟清晰了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1541202.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C语言操作符和数据类型的存储详解

CSDN成就一亿技术人 目录​​​​​​​ 一.操作符 一.算数操作符&#xff1a; 二.位移操作符&#xff1a; 三.位操作符&#xff1a; 四.赋值操作符&#xff1a; 五.单目操作符&#xff1a; 六.关系操作符&#xff1a; 七.逻辑操作符&#xff1a; 八.条件操作符&…

Java学习笔记 | JavaSE基础语法05 | 方法

文章目录 0.前言1. 方法概述2. 方法的定义和调用2.1 无参数方法定义和调用2.2 带参数方法定义和调用1 带参数方法定义和调用2 形参和实参3 带参数方法练习 2.3 带返回值方法的定义和调用1 带返回值方法定义和调用2 带返回值方法练习13 带返回值方法练习24 带返回值方法练习3 3.…

V R元宇宙平台的未来方向|V R主题馆加 盟|游戏体验馆

未来&#xff0c;VR元宇宙平台可能会呈现出以下发展趋势和可能性&#xff1a; 全面融合现实与虚拟世界&#xff1a; VR元宇宙平台将更加无缝地融合现实世界和虚拟世界&#xff0c;用户可以在虚拟环境中进行各种活动&#xff0c;与现实世界进行互动&#xff0c;并且体验到更加逼…

C程序编译、链接与项目构建

C程序编译、链接与项目构建 摘要C编译环境静、动态库介绍gcc与g和程序编译、链接Visual Studio创建和链接库动态库的显示调用 Make介绍安装使用 CMake介绍安装使用构建方式内部构建外部构建构建使用静/动态库常用[系统]变量常用指令CMake模块 Make与CMake的联系与区别 摘要 本…

代码随想录|Day26|贪心01|455.分发饼干、376.摆动序列、53.最大子数组和

455.分发饼干 大尺寸的饼干既可以满足胃口大的孩子也可以满足胃口小的孩子。 局部最优&#xff1a;尽量确保每块饼干被充分利用 全局最优&#xff1a;手上的饼干可以满足尽可能多的孩子 思路&#xff1a;大饼干 尽量分给 大胃口孩子 将小孩和饼干数组排序&#xff0c;我们从大到…

40+重量级DFLab合成模型含各种神丹底丹万能模型合集分享

之前玩DFL软件积累下来的资源&#xff0c;部分模型非常稀缺&#xff0c;之前买的都很贵&#xff0c;现在不玩了&#xff0c;分享给有缘人&#xff0c;懂货的自然懂。必须懂得怎么用再下载&#xff0c;否则对你没有任何价值。点击下载 所见即所得。其中包含几个重量级稀缺资源&…

新款理想L7一边增配一边减配,难怪大家都去买华为问界

文 | AUTO芯球 作者 | 雷歌 我真是要被理想汽车笑死了&#xff0c;真不愧是“定语榜单之王”。 几年前理想汽车搞了一个“中国市场新势力品牌销量周榜” 两个定语&#xff0c;将比亚迪&#xff0c;特斯拉排除在外&#xff0c;自己在自己打造的榜单里做了一年多的冠军宝座。…

试题E(求阶乘)

解题思路&#xff1a; 写不出来&#xff0c;看的题解。要想凑个10&#xff0c;就必须要有一个2和5&#xff0c;但是明显在一个阶乘里&#xff0c;因子为2的数量一定多余5的数量&#xff0c;所以计算5的数量。 解题代码&#xff1a; import java.util.Scanner; ​ public clas…

RabbitMQ 01

01.定义 02.功能

何为布控球?布控球的分类对比

主要的分类有&#xff1a; 根据内部的主控板卡的系统分类&#xff0c;典型的是基于海思芯片的嵌入式LINUX系统的&#xff0c;一般出国标GB28181&#xff0c;另外一种是剑走偏锋的安卓系统的&#xff0c;需要把球机的输出YUV转换为UVC接入安卓主板&#xff0c;作为外接USB摄像头…

Matplotlib数据可视化实战-2绘制折线图(1)

函数plot是matplotlib.pyplot模块中的一个重要函数&#xff0c;用于在图形中绘制折线图。其基本用法是plot(x, y, [fmt], **kwargs)&#xff0c;其中x和y分别代表X轴和Y轴上的数据点&#xff0c;通常是以列表、数组或一维序列的形式给出。通常用的参数有&#xff1a; 基本参数…

大学教材《C语言程序设计》(浙大版)课后习题解析 | 第一、二章

概述 本文主要提供《C语言程序设计》(浙大版) 第一、二章课后习题解析&#xff0c;以方便同学们完成题目后作为参考对照。后续将写出三、四章节课后习题解析&#xff0c;如想了解更多&#xff0c;请持续关注该专栏。 专栏直达链接&#xff1a;《C语言程序设计》(浙大版)_孟俊宇…

《深入浅出LLM 》(二):大模型基石知识

&#x1f389;AI学习星球推荐&#xff1a; GoAI的学习社区 知识星球是一个致力于提供《机器学习 | 深度学习 | CV | NLP | 大模型 | 多模态 | AIGC 》各个最新AI方向综述、论文等成体系的学习资料&#xff0c;配有全面而有深度的专栏内容&#xff0c;包括不限于 前沿论文解读、…

抖音IP属地怎么更改

抖音是一个非常受欢迎的短视频平台&#xff0c;吸引了无数用户在上面分享自己的生活和才艺。然而&#xff0c;随着快手的火爆&#xff0c;一些用户开始担心自己的IP地址会被他人获取&#xff0c;引起个人隐私风险。那么&#xff0c;抖音用户又该如何更改到别的地方呢&#xff1…

ArcGIS添加天地图底图服务

目录 一、注册天地图官网、申请Key 二、ArcGis配置和使用 1、配置 2、使用 三、其他方法 一、注册天地图官网、申请Key 进入官网&#xff0c;并注册账号。 地址&#xff1a;国家地理信息公共服务平台 天地图 (tianditu.gov.cn) 点击地图API&#xff0c;申请Key。 注意&am…

python闭包详解(实例)

“闭包”这个词语相信大多数学过编程的同学并不陌生&#xff0c;但是有时候理解起来还是有一定难度。 先看定义&#xff1a;闭包是由函数和与其相关的引用环境组合而成的实体。比如参考资源中就有这样的的定义&#xff1a;在实现深约束时&#xff0c;需要创建一个能显式表示引…

线段树(算法思想+模板+例题)

基础思想 : 介绍 线段树可以用来维护区间信息(区间和 &#xff0c; 区间最值 &#xff0c; 区间GCD等) &#xff0c;可以在log n的时间内执行区间修改 和 区间查询 ; 1 . 叶子结点的特点是左右结点相等 &#xff0c; 存储元素本身; 2 . 非叶子结点存储的是区间内的统计值 &…

力扣|两数相加|链表

给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请你将两个数相加&#xff0c;并以相同形式返回一个表示和的链表。 你可以假设除了数字 0 之外&#xff0c;这两个数都不会以 0 …

用户多部门切换部门,MySQL根据多个部门id递归获取所有上级(祖级)、获取部门的全路径(全结构名称)

背景 之前做过的项目&#xff0c;都是一个用户就一个部门的&#xff0c;现在碰到个一个用户在多个部门的需求&#xff0c;而且需要可以切换不同部门查看不同数据。 就比如说一个大公司下面有多个子公司&#xff0c;每个子公司有好多部门、子部门等等&#xff0c;然后有部分用…

JDK下载配置

一、JDK的作用 Java开发环境&#xff1a;JDK提供了完整的Java开发环境&#xff0c;包含编译器&#xff08;javac&#xff09;、解释器&#xff08;java&#xff09;、打包工具&#xff08;jar&#xff09;、文档生成工具&#xff08;javadoc&#xff09;等一系列工具&#xff0…