底层探索 - OC中类的数据结构分析(总)

「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」。

通过之前对于Alloc底层的探索总结,过程中也对源码进行了一些简单的试探,本篇将对OC中的作一些研究、记录。(全篇较长,总约6728字)

本文重点:

  1. NSObject和Class在底层的具体类型、及继承关系
  2. objc_class数据结构分析
  3. isa数据结构及作用分析
  4. superclassAPI分析
  5. cache_t数据结构分析
  6. bits数据结构分析
资源准备:
  1. objc4-818源码

  2. 全篇动态分析使用的代码:

    //Person
    @interface UIPerson : NSObject 
    - (void)say3;
    @property(atomic, copy)NSString* name;
    @property(nonatomic, copy)NSString* age;
    @end
    @interface UIPerson ()
    - (void)say2;
    @end
    @implementation UIPerson
    - (void)say1{NSLog(@"say1");}    // 未声明
    - (void)say2{NSLog(@"say2");}    // 内部声明
    - (void)say3{NSLog(@"say3");}    // 外部声明
    @end
    
    //main
    //main.m
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            NSLog(@"I'm coming!");
            Class  cls = [UIPerson class];
        }return 0;
    }
    复制代码

一、Class的数据类型

OC中最常见的就是类,而且所有类的基类都是NSObject。那么NSObject和Class在底层具体是什么类型呢? 先利用Clang命令将准备好的代码转换一下。

 clang -rewrite-objc main.m -o outMain.cpp

执行完成后,在新生成的outMain.cpp文件中搜索分析可以得知:NSObject是objc_object结构体的别名,Class是objc_class的结构指针。 当然还发现了其他的类型,比如id类型。

通过转换后的代码可以初步理解 为什么id类型万能接收,原来是因为id是objc_object结构体的指针

image.png

现在已知类的底层是结构体objc_class,那就可以到源码中去搜索objc_class,去探究其内部究竟是什么样子的数据结构。通过对源码搜索的结果分析,最终终在objc-runtime-new.h找到了objc_class结构体的实现,且可以看到这个结构体的继承关系:继承自objc_object

那这样一来,就弄清楚了NSObject和Class在底层的数据类型,而且通过底层结构体的继承关系,以及其内部成员与其父类中的isa,不禁能想到官方文档中那张经典的isa关系图,且与OC层面类的继承关系有个关联。

image.png


二、isa 指针

1)isa 关系图分析

接着从类的isa开始分析。开篇探索类的底层类型时,已经分析出了objc_class中的isa来自其父类 (也就是根类)objc_object,那么要分析isa指针其作用还得先从这个官方文档中isa关系图开始。

sf9BFlcfMHpl3kcRhQiGteEBFB_OQ_B-2lSKhcQyTvw.png

通过这张图是可以初步分析出:在底层是不存在常说的对象方法、类方法的区别的,统统都是对象方法,只不过是通过isa类对象、元类对象查找的层级不同而已。

2)通过API理解isa

想要很好的认识isa、理解isa的作用,可以从OC层面中的API入手,其中对isa最直接的使用就是[xxx class]方法,其实现就是通过实例的isa查找到类对象,然后类找到元类、根元类以此类推,那么具体如何通过isa从实例对象找到类对象的?可以通过源码一探究竟。

yR2IQy0WPP6h7EtInxIlu1N_WJwyo1jr-O0C3WQAs7I.jpg

通过点击可以在NSObject.m中 找到了[xx class]的实现:类返回自己,实例通过isa去找到对应的类。静态分析看似好像很容易就找打了答案其实不然,使用动态调试将资源准备中的代码运行起来查看汇编可以看到[xx class]实际调用已经被LLVM修改成了objc_opt_class

guyhq-mgcmFjIGxd_9knZqAXvsR7Hs1BrzKuSQY7D1Q.jpg

而在objc_opt_class 中已经不区分实例调用还是类调用了,统一是obj -> objc_object::getIsa(),在getIsa()中判断了当前类的isa是否是优化指针,不是则使用bits&ISA_MSK,是则通过偏移和&操作来还原

-3RbandEhf4wzSzsQGSe2A32k8YH52RGVLMhldH9v-s.jpg

其中bits&ISA_MSK这点,可以通过p、x/4gx命令来打印验证。按照图片顺序来说,从代码 Class cls = [UIPerson class]; 调用开始,到3计算后得到其元类的uintptr_t数据,然后到4中验证。首先po一下,证明确实是其确实是UIPerson ,然后将uintptr_t转换成内存地址,在将UIPersonclass的首地址输出,比较其的isa指针就是图3计算后的结果。

注意一点:
objc_opt_class 中通过getisa得到cls后做的判断,如果结果是MetaClass,返回的还是当前的调用者obj

至此,完成了从API入手,分析其底层逻辑以达到对于isa关系图的实践:isaInstance的首地址isa&掩码可以找到类对象首地址,类对象的首地址isa&掩码后可以找到元类,元类可以找到根元类。

注意一点:

在分析验证过程中,不断的进行了控制台输出打印,发现isa到达类时,其isa地址直接就是其元类的地址,元类到根元类亦如此。 出现这种情况的原因,可能是isa不需要优化,不需要存储更多的信息了

3)通过API理解superclass

探究完isa的流程[xxx superclass]就简单的多了,上手直接使用动态调试来看底层调用的方法。

rakuedS5TKDEQ8zhdiZDPd0A4hB7TOKM6h52dSPkPt8.jpg

superclass是通过objc_msgSend进行方法调用,继续跟断点会直接定位到了NSObject.m中,这次竟然一步步都按常理调用了。

QGVSB2ETqbfCSi4HZzpnxW6j2vIb_U6d5DeSPBfEgF0.jpg

所以superclass的方法是去分+-的。如果是instance对象调用则先通过object_getClass->getIsa()获取到类对象,然后获取类对象objc_class结构体中的superclass即可,类则直接从其结构体中获取即可。

4)isa_t 数据结构

前面已知类的isa来自其父类objc_object,在定义的地方可以数据类型是isa_t,是一个union。这就意味着其成员共享内存空间,这点也能与常说的TaggedPointer从高到低的位标识相对应,这部分位标识成员以宏定义的方式定义在isa.h中的,并根据架构不同,各个标识位的长短及意义不同。

4EE1OZKWIqNycrGAxj_2s4mXvNIldk8hyW-Q3Jim_7U.jpg

接着进入isa.h看看ISA_BITFIELD的宏定义,其中:

  • nonpointer:表示是否是优化指针。0表示纯isa指针,1表示优化指针,其中包括对应平台定义的ISA_BITFILED内容,如对象的地址,引用计数、C++标识等并以位域方式存储。
  • has_assoc:关联对象标志位。0表示未关联,1表示关联。
  • has_cxx_dtor:该对象是否有C ++ 或者Objc的析构函数。如果有,则执行析构函数,没有跳过。
  • shiftcls:储存类指针的,开启指针优化的情况下。arm64架构33位用来存储类指针,x86_6444位。
  • magic:调试器判断当前对象是真的对象还是没有初始化的空间
  • weakly_referenced:是否指向一个ARC的弱变量。有弱引用需要先释放弱对象,没有则跳过。
  • has_sidetable_rc:当对象引用计数大于10时,则需要该变量存储。
  • extra_rc:该对象的引用计数值。值等于realcount-1如对象引用计数为10,此时extra_rc9,如果大于10,则用到上面的has_sidetable_rc
  • isDeallocating:标志对象是否正在释放。(结构体中的方法)

1JVuKkBGQdD6qBJmgsWCucfiVBHZeNxWS5tc2JxXM8M.jpg

对于一个类的isa构建、成员初始化赋值是在objc_object::initIsa()中进行的,在之前探索Alloc底层实现的三步中:

  1. 计算对象大小
  2. 开辟对应空间
  3. 绑定isa

就曾提到过类与isa的绑定。

T_L_ZLIl6mRKHgOMJ6kPpj8PGTnNjY5d9R2VheIg9xc.jpg

5)类对象比较的API分析

isa的数据结构、关系图、继承链都已经分析完了,可以开始分析两个比较容易混淆的比较类的APIisKindOfClassisMemberOfClass ,先来段代码看看输出。

 BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];       //y
 BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];     //f
 BOOL re3 = [(id)[UIPerson class] isKindOfClass:[UIPerson class]];       //f
 BOOL re4 = [(id)[UIPerson class] isMemberOfClass:[UIPerson class]];     //f
 NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);

 BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];       //y
 BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];     //y
 BOOL re7 = [(id)[UIPerson alloc] isKindOfClass:[UIPerson class]];       //y
 BOOL re8 = [(id)[UIPerson alloc] isMemberOfClass:[UIPerson class]];     //y
 NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
复制代码

W-FnXGgYNHjhh5fdu-BNj4W8jN3JsjZ2amttTfLL8n0.png

1. Kind

kind的实现在NSObject.m中有两处:objc_opt_isKindOfClassisKindOfClass,但是逻辑判断的核心逻辑一样,就是这就这个for循环。

   for (Class tcls = cls; tcls; tcls = tcls->getSuperclass()) {
     if (tcls == otherClass) return YES;
    }
复制代码

通过代码逻辑可以看出,isKindOfClass类的superclass继承链这一条的循环比较,归结来说 A isK B ,就是 for(A.isa.superclass) 中有没有B。 对于isK的实现逻辑总结就是:

obj -> obj->getIsa()->getSuperclass() == otherClass

再来分析一下打印结果的re1和re 3:都是cls的比较却只有NSObjcet输出1,因为NSObjcet的元类的superclass正好就是NSObjcet自己,恰好相同。而UIPerson已经是元类继承链在和类UIPerson比较了,永远都不可能相等。re5、re7同理


注意一点:
还有一点没有研究明白:使用动态调试断到isK调用时查看汇编,有时候执行的是NSObject中的+-方法,有时候会> 重定向objc_opt_isKindOfClass()。 这点同样适用于前面探索[xxx class]的API过程。

TJulvkiyGJ2MCxKuZBkkLqz8biB__I9tJAz4kinlNVc.png

2. Member

Member的实现相对就简单点了,只比较1次。点击即跳转NSObject.m中的实现,有+-两种方法但是核心逻辑一样:

self->ISA() ==? cls;

归结来说A isM B,就是A.isa() =? B,即A的类或元类是否是B。

再来分析一下打印结果中的re2和re4,它两个实际的比较都是 self与self.isa比较,怎样都不会xiang等的。


三、cache_t cache

1)cache_t 数据结构

cache顾名思义缓存,类中有缓存应该缓存什么?方法?属性? 带疑问先来试着分析一下cache_t的数据结构,看看是否能找到什么思路,cache\_t中:一个指针、一个union、一部分private方法、一部分public方法

H8lxI5ddfQntfZlxPRcaNW6-pAOPSb1JIfdtRAbsiS8.jpg

从成员来看是分析不出什么思路,那就继续向更深的层次分析。首先搜索_bucketsAndMaybeMask,通过下面方法的注释可知bucket_t指针。bucket_t又是什么结构呢? 点击进入其结构体,一目了然的是按框架区分的SEL和IMP。

iIpG9X-660dyKPXooVGFm3gAKj-nG0KbG8lD-pgHsM8.jpg

分析了一下这个结构体、属性的本质,看来类中的cache存的就是方法,而bucke_t就是其存储的容器。既然bucket_t作为容器,必然该有对应的存取方法存在于cache_t中用于操作,果如其然在结构体中找到了 insert()、buckets()、alloc()、empty() 等几个主要方法。

d8JCroLyMk-gKAmjLYXruDPe-fOCqdXuYAJXzw9MAFw.jpg

2)buckets的get()

先从 bucket_t *buckets()这个get方法开始分析,点进去可以看到其中addr是通过 _bucketsAndMaybeMask.load(memory_order_relaxed) 读出来的,而且在cache_t的结构体中_bucketsAndMaybeMask 是原子类型,那么读取有load方法,存储必然也会有store方法,八成store是在insert()中完成的。

知识点补充:
mory_order_relaxed:多线程内存同步中的宽松模式,即只保证当前操作的原子性,不考虑线程同步,其他线程可能读到新值,也可能读到旧值。

1O6z9LOdMd6jvnVcYlIut4rR0WLXza4RWIE9bzRg7ys.jpg

其中bucketsMask = ~0ul (0UL:就是无符号长整型的0,~:表示按位取反,即:0xffff。。。)

3)buckets的Insert()

接着分析insert(),一来是探究其内部结构与逻辑,二来确认一下刚才分析的store()是否真在其中。点击进入其结构体:

b0tvvNnXzvDBE4_3ZOSc_F78DSGgQPaBLge6elnlHNg.jpg

知识点补充:
在while判断条件时使用到了fastpath(),它是一个宏定义,其意思是告诉编译器当前判断的条件的成功概率很高。
可点击查看宏定义的具体内容:(__builtin_expect(bool(x), 1)) 这个指令是gcc引入的,作用是允许程序员将最有可能执行的分支告诉编译器。这个指令的写法为:__builtin_expect(EXP, N)意思:EXP==N的概率很大。

如图分析,insert() 中主要就是2部分:空间管理、插入缓存,分别来总结这两部分的主要逻辑。

1. 空间管理

  1. 初始占用:newOcupied = 0+1; 初始空间:capacity = oldCapacity = capacity(0) = 0;
  2. 75%扩容边界: newOccupied + CACHE_END_MARKER(1) > cache_fill_ratio(capacity) 等价于 _occupied +2 >= (_mybeMaks+1)*3/4
  3. 除了判断条件外,最重要的方法就是reallocate()

继续深入cache_t::reallocate() ,其内部逻辑主要也是2件事:

  1. newCapacity创建newBuckets,然后在setBucketsAndMask中对 _bucketsAndMaybeMask,_maybeMask,_occupied进行赋值。
  2. 如果是扩容调用则 -> free(oldBuckets)

H9QPak9C3ZO6Z1-Q4lNKcx4FawOuSp7yS_imfb8W1zc.jpg

2. 插入缓存

  1. 准备插入:取bucket首地址b,最大操作区间m,哈希开始插入位置begin,变动参数i,然后开始do...while循环。
  2. 循环中也就2件事:插入、已插入。循环的while条件cache_nextcache_hash的哈希算法一致,对变动参数i进行 +- 以进行b[i]的偏移查找。
  3. cache_hash 的算法可以看出,它的插入是并不是不是0...1..2..顺序插入的。

c9tAWLbEbpVph6f6mfHRGWWbuoOq4YOwIjXK04kkyPQ.jpg

知识点补充:
cache_hash中将sel进行了uintptr_t强转,那uintptr_t是个什么数据类型?用来干嘛的呢?

首先,uintptr_t不在C++中,而是位于C99 <stdint.h>中,作为可选类型,它被定义为“无符号整数类型“
主要作用:指针的按位操作,在C++中,无法对指针执行按位操作,原因见Why can't you do bitwise operations on pointer in C, and is there a way around this? 因此,为了对指针进行按位操作,需要将指针转换为类型unitpr_t,然后执行按位运算。同时其属性是任何有效的指向void的指针都可以转换为此类型,然后转换回指向void的指针,结果将等于原始指针

继续来分析循环中的buckett::set() 的逻辑:

首先在参数上传入了bucket*b、SEl、IMP、Cls,这也是缓存中存储所需要的全部信息。其次,尽管最终的存储方式在架构上有所区别,但是2部分中的IMP-Encode是相同的,位于struct bucket_t中的encodeImp(base, newImp, newSel, cls) 。最终对于 SEL、IMP 的存储在 __arm64__ 上使用了 LDP/STP (架构指令太底了,真不熟),其他情况则使用memory_order_relaxed (多线程内存同步) 进行存储,存储顺序与前面探索到的struct bucket_t 的sel、imp顺序一致。

1kwML5xnx8vfmfrHLSR5QWTq9XAhZQMlbULgLiM061Q.jpg

4)_maybeMask的变化

值得注意:其中对于_maybeMask的操作,在setBucketsAndMask()时进行了newCapacity -1,在capacity()中 +1 还原了占用。

那为什么_maybeMask = capcity - 1 ,或者 为什么这么操作?
reallocateallocateBuckets()方法中可以找到其中的答案,要在bucket_t末尾插入标记,根据框架不同有所区别。

  • arm: newbBucket - 1
  • other: newbBucket 首地址

E8ttAr_5UhxAho0x_x-pQ2THn2KRG4lT6AWuYwMlEtc.jpg


四、calss_data_bits_t bits

objc_class中,isa记录元类链superclass记录继承链cache 顾名思义缓存bits存储类的所有数据。前面的作用及其数据结构都已经分析完了,接着就开始从bits入手对类的数据结构进行更加深入的探究。

1)Method数据结构

  1. class_data_bits_t结构体的方法中,与bits相关的占多数,多是对bits的存取计算。
  2. 结合objc_class中的方法,可以分析出class_data_bits_t中的*data()及方法是比较关键的,根据其返回的数据类型最终可以定位到 class_rw_t这个数据结构。
  3. 进入class_rw_t后可以观察到其中包含了一个类本身的所有信息,比较好辨识的就是methods()properties()protocols()这3个方法,其他的一些较底层如ro_or_rw_ext_t()class_ro_t*ro等,稍后再做分析。

JQS9CDLf_WRQrIkfPVuX5RUWE5UB6xUaxJVcK5KBS84.jpg

  1. 先从methods()开始探究,按照方法的调用及其数据类型一层层深入,可以观察到类存储方法的列表数据类型:method_array_t:list_array_tt <method_t, method_list_t. method_list_t_authed_ptr> {...},其包含3个范型且list_array_tt已经是根类,而且进入list_array_tt后分析源码也没有梳理出其中的数据结构的存储关系。

vCKNeymDVaCmeus2cdQYcKNtVUMF_uZ3ptvixNlQbB0.png

  1. 看来静态分析只能止步于此,那就试试结合动态分析,通过llvm将上述分析得出来的bits.data一层层打印出来,直观的观察其结构
  2. 开始打印前,先分析一下objc_class中成员大小,然后才能计算出正确的偏移量:其中isa、superclass都是指针8字节cache_t cache中包含一个指针一个union总体算下来是16字节,那么要想打印出一个类的bits,从Class首地址向后偏移32个字节即可。

XNHhYaThynWH-oI6GKB52uz06uM3S02Qiu0ZwL-Jn6E.png

尽管静态分析时偏移算的正确了,但是其中数据结构分析不那么透测,打印过程还是进行了不少尝试才将最总想要的结果打出来。也理出了其中数据结构的关系:method_array_t 中包含 method_list_t,然后再下层是 method_t 。

分步测试的命令和过程已经合并省略,如查看过程分步打印只需要将命令拆分即可:

 p ((class_rw_t *)((class_data_bits_t *)Class_Index+32)->data())->methods().list.ptr->get(0).big()

2)Propert 和 Protocol数据结构

类的属性、协议与methods()相同,都是从class_rw_t中的方法取出来的,储存类型也都是list_array_tt对应的子类propert_array_tprotocol_array_t。所以以对于它们两的前几步都是相同,差别就集中在了各自的存储的基本数据结构(如method_t)上,所以根据本身的基本数据结构,去获取属性的内容即可,properties去掉big()即可。

p ((class_rw_t *)((class_data_bits_t *)Class-Index+32)->data())->properties().list.ptr->get(0)

协议的读取过程与方法、属性有一定的区别。读取到ptr时的数据结类型是protocol_list_t* 但是按上面的方式打印会报错没有get()方法:
no member named 'get' in 'protocol_list_t' ,都是同一个父类List,却没有get()那只能是去分析看看protocol_list_t结构体中它本身是怎么读取的,具体用了什么方法。

S24fKE43hLI5dcDqZbit2WYtxP2gDbDQSMtIrqv_nZ8.png

经过反复分析源码,注意到其中的的iterator xx方法和开始的list[0]并不断的尝试打印这些部分,终于获得到了protocol_list_t中的内容:

T_BTkMxRioe1ICnL5DL6nBl3pbS-NzrvQ6K7bfL5Kb8.png

最终是成功调用到protocol_list_t中的list[0] 才将内容输出,但是源码中xx的数据类型却是protocol_ref_t,而protocol_ref_t本质就是指针,只需要强转到protocol_t*就能拿到想要的内容了。最终打印出来后的内容与结构体protocol_list_t的内容一致。

dMHrm0rsk7EX2UWgozWKZT_rcUhtWjkJe4gN4Yv_2Kw.png

结构体protocol_t继承自objc_object,其中包含了method_list_tproperty_list_t等其他的数据结构,这些数据结构只需要按照之前分析方法、属性的方式展开即可获得其中内容。

至此,将探索过程中的分步获取协议protocol_t 的命令合成一下:

// protocol_t内容
p *(protocol_t*)((class_rw_t *)((class_data_bits_t *)Class_Index+32)->data())->protocols().list.ptr.list[0]

// @required 方法
p (*(protocol_t*)((class_rw_t *)((class_data_bits_t *)Class_Index+32)->data())->protocols().list.ptr.list[0]).instanceMethods->get(0).big()

// @optional 方法
p (*(protocol_t*)((class_rw_t *)((class_data_bits_t *)Class_Index+32))->data())->protocols().list.ptr.list[0]).optionalInstanceMethods->get(0).big()

3)bits中的class_rw_t

开始分析class_rw_t之前,首先需要声明两个概念dirty-memory(脏内存)clean-memory(干净内存
脏内存是在进程运行时更改的内存,干净内存是加载后不会更改的内存

脏内存比干净内存昂贵的多,尤其是在iOS中,因为只要进程正在运行,它就必须得保留,而干净内存可以被释放,在需要时总是可以去磁盘中重新加载。而且类只要被使用,类的数据结构就会变“脏”,如创建新的缓存、写入数据的存储等,所以保持干净的数据越多越好,这样可以节省出更多的空间用来存储App中的数据。

接着分析在方法的数据结构中提到的class_rw_t及其结构体中那些较底层的字段如:ro_or_rw_ext_t()class_ro_t*ro。这些内容归总来说也就是3部分内容class_ro_tclass_rw_extclass_rw_t

  • class_ro_t 是一个指向存储类更多信息的指针,名称中的“ro”是read-only(“rw”同理)的意思,所以当它被加载的时候是 clean-memory
  • class_rw_t 是进程运行时分配的存储用于读/写类数据的内存,也就是dirty-memory。
  • class_rw_ext 是将class_rw_t 中的方法、属性、协议等大约 90% 的类从不会在运行时动态修改的数据分离出来,从而让更多的数保持干净以节省空间,所有将class_rw_t 拆分成了2个部分。

DaCpplkPA2ueQBACiADwvG0xo2yprQvterdy7NlxDWo.jpg

class_rw_t 的结构体方法中通过get_ro_or_rwe()赋值了原子数据类型指针ro_or_rw_ext_t,并且在获取ro的时候对类型进行了判断:是class_rw_ext_t时要多去取一层 class_rw_ext_t->ro() ,而class_ro_t则直接返回ro

上述记录的数据结构,可以在动态调试过程中进行验证:将ro与rw两部分的properties()数据都打印出来进行比较,测试命令已整合:

p (class_ro_t *)((class_rw_t *)((class_data_bits_t *)Class_Index+32)->data())->ro())->baseProperties->get(0)

p (class_rw_t *)((class_data_bits_t *)Class_Index+32)->data())->properties().list.ptr->get(0)

以上关于class_rw_t的这部分内容,对于类的数据结构变化来自WWDC2020关于runtime的发展介绍-Advancements in the Objective-C runtime,除了上述变化外,还有 Objective-C 方法列表的更改和tagged pointers的改动。


五、类的属性

1)属性/成员变量

类的成员变量(ivars)是存储在class_ro_t中,在上小结class_rw_t拆分优化的图片中也可以看到。 类的属性 = ivar + set() + get()。

2)属性修饰

那么,get()set()方法是如何找到ivar的?

而且经常还在使用时加上修饰关键字,即使什么都不加也会有默认的strong、atomic修饰,那么前面的关键字不同会对查找ivar方式有所影响?

对于这2个问题,准备了一小段代码,将代码通过clang转换一下来进行观察比较。

@interface UIPerson : NSObject
@property(nonatomic, copy)  NSString* judy_NC;
@property(atomic, copy)     NSString* judy_AC;
@property(copy)             NSString* judy_3_C;
@property(nonatomic)        NSString* judy_N;
@property(atomic)           NSString* judy_A;
@end
@implementation UIPerson
@end
复制代码

 clang -rewrite-objc main.m -o outMain.cpp

dmRyVC__P4HlkI37iDQjDLCCDUjWqccEiW_EIXztIjI.jpg

经过比较转换后的cpp代码可以发现:

  • 当只有copy修饰属性修饰时set方法会用objc_setProperty()方法进行赋值。
  • copy基础上,如果方法且前面加上atomic时get方法会使用objc_getProperty()方法来取值。
  • 而其他关键字修饰时get、set均使用self+内存平移直接赋值、取值。
get() set()
n, c offset objc_setProperty()
a, c objc_getProperty() objc_setProperty()
n(s) offset offset
a(s) offset offset

至此,表格中的总结就是前面2个问题的答案了,但是又一个新的疑问出现了:为什么copy修饰时要使用objc_setPropertyobjc_getProperty 的方法来进行值的存取呢?

去源码中查找一下这两个方法的实现,通过分析这2个方法的源码,得到的结果是:
get过程中,要对取值的objc\_retain过程加锁 (spinlock_t)
set过程中,对赋值的过程同样也要加锁,不仅如此,还要判断copy或mutablecopy从而调用不同的copy方法。

jp5K7GTrV-AQ3C3DXSlWJ3hUeR5KzuKaVm6w7MCgGkU.jpg


六、总结

至此,基本完成了对类相关的底层探究、分析的记录,从OC层面的类入手,探究完成了底层Class的数据类型、各个数据结构、及类信息的存储等,现在对这些进行一下总结:

  1. NSObject和Class在底层的数据类型、及继承关系。
    1. NSObject是objc_object的别名
    2. Class是objc_class的结构体指针
    3. objc_class:objc_object{isa_t isa}
  2. objc_class数据结构分析
    1. isasuperclasscachebits 、public方法、private方法
  3. isa数据结构及作用分析
    1. 通过isa类对象、元类对象查找。
  4. superclassAPI分析 2. 通过superclass类对象的父类逐级查找。
  5. cache_t数据结构分析
    1. _bucketsAndMaybeMask指针、共用体(_maybeMask、_flags、_occupied)、private方法、public方法。
    2. 存储容器是bucket_t
  6. bits数据结构分析
    1. class_ro_tclass_rw_extclass_rw_t 和属性、协议、方法的获取方法。

七、链接汇总:

猜你喜欢

转载自juejin.im/post/7058261022494162951