文中咖啡图片及第一个图片来源百度图片,如涉及到侵权,请联系我删除图片
原创文章,转载请注明:转自:Try_Try_Try
更新
时间:2018.07.06
内容:添加几张结论图,使得结论更加的直观
背(吐)景(槽)
最近被问到了一个category问题,把我问的晕头转向,当时就很(想)佩(打)服(人),无疑是灰秃秃滚回去跪着搓板,面壁思过。
把这一块知识恶狠狠的补了一桶,奶奶的,了解完之后,发现so easy,被自己蠢哭了。
引用
网上也有相关的文章写的很好。我完全读下来的就是美团
的那篇深入理解Objective-C:Category。
感觉写的很好。我写这篇文章时,对着读了好多遍。
所以这次彻底对这篇文章分析一下(其实读很多遍,是因为写的很精简,内部的实现细节需要自己对照源码进行一一查看。这样才能把美团的这篇短短的文章读成一个体系,然后再进行精简,知识就变成我的了---想太多了可能)。
文章内容结构
- o 代码结构
- 1 分类的结构
- 1.1 题外话
- 1.2 撕破你这层面纱(让你再给我矫情)
- 1.3 .cpp文件
- 1.4 添加了category的消息发送流程总结
- 2 +load 方法的原理
- 2.1 题外话
- 2.2 一探究竟
- 3 initialize还有谁(有点捏花惹草)
- 3.1 添加initialize方法进行测试
- 3.2 猜测
- 3.3 源码分析-走-起-来
- 3.4 正经点
- 3.5 话外
- 4 联合起来才会更强(关联对象)
- 4.1 查看分类的结构
- 4.1.1 查看类的结构
- 4.1.2 分类结构
- 4.2 关联对象搞起来
- 4.2.1 扒关联对象的源码
- 4.2.2 内部具体实现细节分析
- 4.1 查看分类的结构
0. 代码结构
0.1 代码结构Dog 继承自Animate类,Dog中也有父类play方法,其中cat1和cat2中的方法一样,都有play方法,只是打印的内容不一致;
Animate.h
#import <Foundation/Foundation.h>
@interface Animate : NSObject
- (void)play;
@end
Animate.m
#import "Animate.h"
@implementation Animate
- (void)play
{
NSLog(@"Animate--%@", NSStringFromSelector(_cmd));
}
@end
Animate+cat1.h
#import "Animate.h"
@interface Animate (cat1)
- (void)play;
@end
Animate+cat1.m
#import "Animate+cat1.h"
@implementation Animate (cat1)
- (void)play
{
NSLog(@"Animate (cat1)--%@", NSStringFromSelector(_cmd));
}
@end
main.m
#import <Foundation/Foundation.h>
#import "Dog.h"
#import "Dog+cat1.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Dog *dog = [[Dog alloc] init];
[dog play];
}
return 0;
}
此后代码的分析,也都是基于上述的结构,进行测试。
-
compile 顺序1:
图0.2 代码编译顺序1
图0.2对应的执行结果:
Dog (cat2)--play
-
compile 顺序2:
图0.3 代码编译顺序2
图0.3对应的执行结果:
Dog (cat1)--play
ARE YOU READY?接下来是正文:
1. 分类的结构
1.1 题外话
之前看别人的代码时候,一直出现 clang -rewrite-objc filename.m
,这里是clang -rewrite-objc Animate+cat1.m
命令,然后就出现了神奇的.cpp文件,但是自己在terminal敲了一下,报了一堆错(尼玛,就失去了对.cpp的兴趣了)。
后来发现:电脑上安装了多个不同版本的Xcode,又更改了名称,所以就无法找到。当更换成Xcode
的真名时,重新在代码所处文件clang一下,神奇的.cpp出来了😁(哥终于要研究一番感(懵)人(逼)的c++代码了)。
生成的文件名称为Animate+cat1.cpp,层级结构如下:
图1.1 编译之后文件位置1.2 撕破你这层面纱(让你再给我矫情)
我了个曹操,竟然96763行左右,要吓死了(要是工资能达到96k该多好啊,我又做梦了)。这要从哪行开始看啊,拖着滚动条看了一圈,发现前面都是声明和定义,到最后才是真身(这庇护够强啊!你以为你是孙悟空啊,躲在这花果山的最深处)。
查了一下,发现之所以生成的文件这么大,可能的原因是不同的arm架构都有。
1.3 .cpp文件
以下就是编译后的部分关键代码:
struct _category_t {
const char *name;
struct _class_t *cls;
const struct _method_list_t *instance_methods;
const struct _method_list_t *class_methods;
const struct _protocol_list_t *protocols;
const struct _prop_list_t *properties;
};
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Animate_$_cat1 __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"play", "v16@0:8", (void *)_I_Animate_cat1_play}}
};
static struct _category_t _OBJC_$_CATEGORY_Animate_$_cat1 __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Animate",
0, // &OBJC_CLASS_$_Animate,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Animate_$_cat1,
0,
0,
0,
};
- 通过查看 libobjc.order文件,有之后加载的顺序。objc_init->map_images->map_images_nolock->_read_images。
以下是_read_images中,读取分类的部分代码(处理后的):
// Discover categories.
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
// Process this category.
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
}
}
}
- 为了找到本质,还需要继续沿着方法向下走:
_read_images->addUnattachedCategoryForClass->remethodizeClass ->attachCategories->attachLists
attachCategories():将分类compile的顺序进行逆序
重组到数组中,这里决定了最后编译的分类可能最先执行。
while (i--) {
auto& entry = cats->list[i];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
}
attachLists():将class中的方法和分类中的方法进行移动的操作,使得类中的方法放到数组的尾部,分类放到数组的头部;
{
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
至此,类按照编译顺序逆序
的过程结束,然后开始继续消息发送制。(关于消息发送的具体流程,网上很多资料,可以自己分析一波,源码好像是汇编)
1.4 添加了category的消息发送流程总结
通过上述分析:这些添加分类神马的,都是在编译阶段,编译器帮我们完成的。因此至于最终如何执行,就得按照运行时正常的消息发送流程,添加上分类后即:
- 类(array[cmplN,cmplN_1,cmplN_2.....,cls]->method)->
父类(array[cmplN,cmplN_1,cmplN_2.....,cls]->method)->
->
......
->
msgForward->...... - 先执行当前类对应的所有分类中最后编译的那个分类方法直到结束,否则一直按照数组顺序向后找。父类也类似。
好了,休息一下。
喝杯咖啡,缓缓2. +load 方法的原理
2.1 题外话
如果在上述的6个类中都添加load方法,那么实现的逻辑又是怎样的?
在6个类中都添加如下代码:
+ (void)load
{
NSLog(@"%@--%@", 类名/分类名, NSStringFromSelector(_cmd));
}
运行结果:
图2.1.1 编译顺序
图2.1.2 图2.1.1的运行结果
图2.1.3 编译顺序 图2.1.4 图2.1.3的运行结果
2.2 一探究竟
图2.2.1 load加载层次正如图2.2.1显示,关键代码如红色所示,prepare_load_methods
加载完才会对call_load_methods
进行调用。
如下是prepare_load_methods()
部分代码:
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertWriting();
classref_t *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}
category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
if (!cls) continue; // category for ignored weak-linked class
realizeClass(cls);
assert(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
}
从上述代码中可以并不能看出最终加载的顺序,但是能够看到class loads的顺序。
其中schedule_class_load
是个递归的调用,代码如下:
static void schedule_class_load(Class cls)
{
if (!cls) return;
assert(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
// Ensure superclass-first ordering
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
schedule_class_load
代码分析:
-
递归调用相当于数据结构中的栈的操作结构。栈底存放的可以看做是schedule_class_load(cls),每次将cls->supercls作为
schedule_class_load
的参数,然后将其入栈,继续判断cls是否为nil,如果为nil,则出栈,进行处理add_class_to_loadable_list
, 然后继续出栈,直到栈空为止。 -
从源码可以看出来:关于类的load方法调用,存储顺序是
先父类,再子类load
。
上述代码中的add_class_to_loadable_list
的部分源码如下:
void add_class_to_loadable_list(Class cls)
{
IMP method;
loadMethodLock.assertLocked();
method = cls->getLoadMethod();
if (!method) return; // Don't bother if cls has no +load method
...
...
...
loadable_classes[loadable_classes_used].cls = cls;
loadable_classes[loadable_classes_used].method = method;
loadable_classes_used++;
}
从上述代码中,有一点是注意的,if (!method) return;
如果该类中没有实现load方法,则直接返回,进行出栈的其他操作。
上述schedule_class_load
结束之后,开始add_category_to_loadable_list
。该方法的加载就是按照编译时的顺序进行存储。
当上述操作完成后,load的预加载也结束了;接下来就是真正的call_load_methods
的调用。
call_load_methods才能决定真正的调用流程有没有在这一步分生变化,如下所示(精简):
void call_load_methods(void)
{
do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. Call category +loads ONCE
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
}
从上述的call_load_methods方法可以看出,关键的代码是外层的do-while语句。
明显的可以看出来,是先call_class_loads
,然后call_category_loads
。因此,可以确定:cls -> category为大致的顺序
。
通过查看这两个函数的源码,内部是很常规的for循环,从头到尾。这里至少说是没有颠倒顺序的操作。
还发现了另一种情况。就是:load方法是主动执行的,就算什么消息都不手动发送,当程序运行起来的时候,它也会执行。毕竟如果按照消息发送机制的逻辑,得我调你,你才执行啊。它是运行时,系统进行调用的。
综上所述,可以得出结论:
- load方法的调用顺序为: 类->分类;
- 类中load调用顺序为:父类->类;
- 分类load调用顺序为:按照编译的顺序;
- 类(call[supercls,curcls])->分类(call[cmpl0.....cmplN])。
综上,就是load的完整流程。
好了,休息一下。
来来来,一起休息一下
3. initialize还有谁(有点捏花惹草)
3.1 添加initialize方法进行测试
在前边6个类中分别添加如下的测试代码
+ (void)initialize
{
NSLog(@"%@--%@", 类名/分类名, NSStringFromSelector(_cmd));
}
运行结果:
图3.1.1 编译顺序
图3.1.2 图3.1.1的运行结果
图3.1.3 编译顺序
图3.1.4 图3.1.3的运行结果
3.2 猜测
从结果可以预估:
-
initialize先执行父类,再执行类。
-
而且都只是执行了分类,且执行的分类的顺序是按照编译的逆序进行的,且只执行了一次。
-
从1的分析可以看出来加载load的影子。如果按照先加载父类方法这个尿性的话,是不是内部也通过一个递归实现的。那么它和load的递归有区别吗?(妈的,别再yy了。滚去看苹果粑粑的源码😆)
-
从2的分析可以看出,是消息发送的机制。 如果真真的如猜测的一样,关于3的消息发送机制,其实还有个小陷阱。即如果当前类没有实现initialize方法,那么按照消息机制的尿性,是不是要找他爹给摆平(毕竟官场气息太重,这社会没爹也是不行啊。没想到代码中早已告诉了我这个道理。[蠢哭])。
-
综上来说,initialize确实有点骚。这里摸一下,那里摸一下。所以一会得对照源码进行分析一波(希望脸不要太疼)。
3.3 源码分析-走-起-来
皮一下
写到这个标题,突然想到了我大渤哥(黄渤)在18年春晚的那首跳起来
。写到这,我脑袋里毅然神浮了这个魔曲。
我屁股就坐在办公的凳子上,一边敲着代码,一边左右摆动的甩了起来。突然,被地上的几个轮子亲了一下,疼的脚想踹人。
毕竟我再晃两下,我司的工学办公椅,它那仅留的两个轮子终将被我通通抛弃😰(看来越发展,这美曰其名的物件,坏在了质量做工啊。是道德的沦丧,还是人性的缺失?欢迎收看今晚的xxxx)。
3.4 正经点
当我运行initialize方法时,发现与load方法不一样。load方法是在运行时,主动调用的。而initialize,如果将main.m的代码段注释后,是不会执行。
这说明从调用的机制可以看出来两者加载的方式是不一样的。当还原.m后,类第一次发送消息时,又开始调用了。从这可以看出,是进行了_objc_msgSend()调用,和刚才的猜想相照应。
通过查找源码中的libobjc.order文本可以看出来具体的执行顺序。对于我等屌丝来说,他可是个万能的宝(宝?我信了你的袜)。
图3.4.1 initialize 调用的流程
- 从图图3.4.1中发现绿色部分代码,也使用了迭代操作。(哈哈,此刻也应征了它确实偷偷摸了一下load这家伙)。
_class_lookupMethodAndLoadCache3方法中的关键代码如下:
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
lookUpImpOrForward方法中的关键代码如下:
if (initialize && !cls->isInitialized()) {
runtimeLock.unlockRead();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.read();
// If sel == initialize, _class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}
_class_initialize() 的关键代码如下:
void _class_initialize(Class cls)
{
assert(!cls->isMetaClass());
Class supercls;
bool reallyInitialize = NO;
// Make sure super is done initializing BEFORE beginning to initialize cls.
// See note about deadlock above.
supercls = cls->superclass;
if (supercls && !supercls->isInitialized()) {
_class_initialize(supercls);
}
// Try to atomically set CLS_INITIALIZING.
{
monitor_locker_t lock(classInitLock);
if (!cls->isInitialized() && !cls->isInitializing()) {
cls->setInitializing();
reallyInitialize = YES;
}
}
callInitialize(cls);
}
该方法主要是为了让父类先执行,直到没有父类,或者父类初始化完成if (supercls && !supercls->isInitialized())
,然后执行callInitialize(cls)。
而callInitialize(cls)代码如下所示:
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
asm("");
}
哇塞(赛哇),真面目出来了,最后一步就是objc_msgSend
,哈哈,看你再躲。因此接下来走消息发送的流程(分类initialize-> else -> 类
)。这样所有问题到这里就又结束了。
综上得出以下结论:
- 父类(分类 else 类)->类(分类 else 类)`;
- 其中()的内容 array[cmplN,cmplN_1,cmplN_2.....,cls]->method。
- 即:supercls(array[cmplN,cmplN_1,cmplN_2.....,cls]->method)-curcls(array[cmplN,cmplN_1,cmplN_2.....,cls]->method)
3.5 话外
等等......
还有一个问题,就是刚才在代码中看到的一幕。
- 1.在上述
lookUpImpOrForward
贴出的源码中的注释,引起了我的兴趣(我相信也引起了你的兴趣。如果没有引起,再去看一遍😆):如果当前发的消息不是[[Dog alloc] init]
,而换成[Dog initialize]
。
结果如下所示:
图3.5.1 主动调用initialize
对于图3.5.1所示,多出了一次,无疑是狗狗主动发送initialize引起的。
其实源码中的注释也解释的非常清楚了(我从上边搬到了下边)。
if (initialize && !cls->isInitialized()) {
runtimeLock.unlockRead();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.read();
// If sel == initialize, _class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}
得出如下结论:initialize方法的调用是在该类在第一次使用时,调用的。而后该类再次使用时,是不会调用的(好像如有所思)。
等等......(艹,我的横杠分隔符都打上了,你才说还有)
还有一个问题(别墨迹,快说...)?
如果当前的子类、子分类中都没有实现initialize方法,只有父类、父分类中实现了initialize方法,那么运行结果如下图所示:
图3.5.2 只有父类、父分类实现initialize至于出现这种情况的原因,也是很容易分析的。
其实就是源码中一个很小的细节。这也是和load方法的区别。在递归调用方法时有一个条件判断;
在load中递归调用到最顶层时,开始执行add_class_to_loadable_list
方法,其中有一个代码片段是如下情况:
method = cls->getLoadMethod();
if (!method) return; // Don't bother if cls has no +load method
在load中,如果父类中没有load方法,就直接返回,出栈子类,进行下次的操作。而看一下initialize递归处相应的源码:
callInitialize(cls);
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
asm("");
}
从initialize源码中可以看到,并没有出现load中的没有该方法时跳出的情况,而是直接继续畅通无阻的执行(因为我是消息发送机制啊)。从而一路到达objc_msgSend。
这样即使子分类、子类都没有,此时也可以继续寻找父类.
😜,这次应该不用等了,没了。
以上。
好了,休息一下。
果汁咖啡也挺好喝的
4 联合起来才会更强(关联对象)
在Animate+cat1.h中添加如下的测试代码:
#import "Animate.h"
@interface Animate (cat1)
/** name */
@property (nonatomic, copy) NSString *name;
- (void)play;
@end
Animate+cat1.m
#import "Animate+cat1.h"
#import <objc/runtime.h>
@implementation Animate (cat1)
- (void)setName:(NSString *)name
{
objc_setAssociatedObject(self, "name", name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name
{
return (NSString *)objc_getAssociatedObject(self, "name");
}
@end
上述的第二个参数"name"也可以换成@selector(name),因为第二个参数的类型是:const void * _Nonnull key, 是void *类型,所以在C中,可以看做执行函数的指针类型,因此可以直接换成OC中的SEL类型,且这样写会有提示。
在main.m中进行简单的测试,赋值,取值操作,就可以看出来使用起来和属性差不多。
4.1 查看分类的结构
struct _category_t {
const char *name;
struct _class_t *cls;
const struct _method_list_t *instance_methods;
const struct _method_list_t *class_methods;
const struct _protocol_list_t *protocols;
const struct _prop_list_t *properties;
};
上述category结构中包含了:名称、所属类别、对象方法列表、类方法列表、协议列表、属性列表。
于是可以给分类中添加方法,协议和属性。但是好像没有实例变量列表,那是不是说明分类中不可以添加实例变量呢?
可以从以下两方面入手:
4.1.1 查看类的结构
如果在Animate基类中,添加一个属性和一个实例变量。代码如下:
Animate.h
#import <Foundation/Foundation.h>
@interface Animate : NSObject
/** name */
@property (nonatomic, copy) NSString *name;
- (void)play;
@end
Animate.m
#import "Animate.h"
@interface Animate()
{
NSString *_height;
}
@end
@implementation Animate
+ (void)load
{
NSLog(@"Animate--%@", NSStringFromSelector(_cmd));
}
+ (void)initialize
{
NSLog(@"Animate--%@", NSStringFromSelector(_cmd));
}
- (void)play
{
NSLog(@"Animate--%@", NSStringFromSelector(_cmd));
}
@end
从代码中,可以看到添加了一个属性name和一个实例变量_height,其中还包含有2个类方法,1个实例方法。
由属性name的特性可知,会自动生成实例变量:_name,-setName:和-name方法的声明及其实现。
因此:包含的内容应该是:1个属性,2个实例变量,2个类方法,3个对象方法的实现
。
通过将Animate.m文件进行clang,可以查看生成的c++关键源码如下(精简后):
struct _class_t OBJC_CLASS_$_Animate = {
0, // &OBJC_METACLASS_$_Animate,
0, // &OBJC_CLASS_$_NSObject,
0, // (void *)&_objc_empty_cache,
0, // unused, was (void *)&_objc_empty_vtable,
&_OBJC_CLASS_RO_$_Animate,
};
struct _class_t {
struct _class_t *isa;
struct _class_t *superclass;
void *cache;
void *vtable;
struct _class_ro_t *ro;
};
struct _class_ro_t {
unsigned int flags;
unsigned int instanceStart;
unsigned int instanceSize;
unsigned int reserved;
const unsigned char *ivarLayout;
const char *name;
const struct _method_list_t *baseMethods;
const struct _objc_protocol_list *baseProtocols;
const struct _ivar_list_t *ivars;
const unsigned char *weakIvarLayout;
const struct _prop_list_t *properties;
};
从源码中可以看出,结构体的声明和赋值操作;我们关心的内容在结构体中_class_ro_t
中:方法列表、协议列表、属性列表、实例变量列表、属性列表;然后看一下对应的各个列表的源码:
- 属性列表:
static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_Animate = {
sizeof(_prop_t),
1,
{{"name","T@\"NSString\",C,N,V_name"}}
};
struct _prop_t {
const char *name;
const char *attributes;
};
看到存储了一个结构体的属性数组中存放着唯一的name属性。
- 实例变量列表:
static struct /*_ivar_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count;
struct _ivar_t ivar_list[2];
} _OBJC_$_INSTANCE_VARIABLES_Animate = {
sizeof(_ivar_t),
2,
{{(unsigned long int *)&OBJC_IVAR_$_Animate$_height, "_height", "@\"NSString\"", 3, 8},
{(unsigned long int *)&OBJC_IVAR_$_Animate$_name, "_name", "@\"NSString\"", 3, 8}}
};
struct _ivar_t {
unsigned long int *offset; // pointer to ivar offset location
const char *name;
const char *type;
unsigned int alignment;
unsigned int size;
};
看到存储了2个结构体的数组包含了原有的_height和属性生成的_name。
- 对象方法列表:
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[3];
} _OBJC_$_INSTANCE_METHODS_Animate __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
3,
{{(struct objc_selector *)"play", "v16@0:8", (void *)_I_Animate_play},
{(struct objc_selector *)"name", "@16@0:8", (void *)_I_Animate_name},
{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_Animate_setName_}}
};
struct _objc_method {
struct objc_selector * _cmd;
const char *method_type;
void *_imp;
};
存储了3个结构体的数组,是原有的play方法和属性生成的setName和name方法。
也可以看到其他的类方法列表存储在metacls中等。通过查看类的结构,就能对之前的猜测进行有力的验证。
接下来就据此,对比的看一下,如果在分类中添加属性列表和实例变量,又如何呢?
4.1.2 分类结构
- 布置内容
首先在Animate+cat1.h中添加属性name。当我尝试添加实例变量时,发现没法添加,一写就报错💔;代码如下:
Animate+cat1.h
#import "Animate.h"
@interface Animate (cat1)
/** name */
@property (nonatomic, copy) NSString *name;
- (void)play;
@end
- 通过clang查看Animate+cat1.m的源码
我们只关心属性列表、方法列表、方法实现
static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_Animate_$_cat1 = {
sizeof(_prop_t),
1,
{{"name","T@\"NSString\",C,N"}}
};
wtf,瞅了一圈,愣是没有看到实例变量列表结构,不死心又去查看了一波实例方法列表如下:
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Animate_$_cat1 = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"play", "v16@0:8", (void *)_I_Animate_cat1_play}}
};
好吧,啥都没有,只有之前写的play方法。服了,其实这也可以理解的。
从4.1 查看分类的结构
开头,就直接说明了_category_t结构包含的内容。里边确实是没有ivar_lists
。这样一来,即时结构中给了一个属性列表,用处也不是很大啊。
没法使用其中的set和get方法进行操作,不能够存储内容。直接输入_name或者self->_name也是行不通的。因此为了能够存储数据,苹果粑粑又跳出来了。
4.2 关联对象搞起来
苹果粑粑托梦
粑粑
:小子,我给你属性列表了,你只需要重写相应的set和get方法。
我
:好像是啊😓。那......,那我该怎么实现呢?
我又没办法定义一个实例变量,莫非再让我定义一个属性,这样有没有set,get方法,这样又开始循环了(子子孙孙,无穷匮也!)。
但我又没办法声明一个实例变量。在分类中声明一个实例变量,想想就别扭啊。
如果在.m中声明一个实例变量,一般都是extention,()中也没有名称。在分类中,()中又是有内容的。如果这样写,又报了一对错误(啊啊啊啊,我疯了)。
粑粑
:傻儿子,继续想。拿出你C语言中长久不用的大招。
我
: 啥?奥(dingdong)我知道了,你让我用全局变量吗,这样也行。但是以后我每新加了一个属性,都要重新定义一个全局变量。这样粑粑会不会打死我,抢你太多的饭了(内存)。
那我定义一个字典就好了啊(等待粑粑夸我)。
粑粑
:不要抢老子的饭。
我
: 大哭(心想:屎粑粑,你那么有钱,已经从你的开发者中通过内购剥削了3分,还这样对我......)。
粑粑
:好吧,不逗你了,其实我已经给你提供了一个关联对象,方便你管理分类中的属性。至于在哪,你小子自己去找吧。毕竟粑粑有些东西是不能够给你说太清楚的,否则都要来我这里更改东西了。
我
: 恩,谢谢粑粑(😒,没有我找不到的东西)。
4.2.1 扒关联对象的源码
通过搜索associated关键字可以找到,步骤:objc_setAssociatedObject()->_object_set_associative_reference(), 如下:
/**********************************************************************
* Associative Reference Support
**********************************************************************/
id objc_getAssociatedObject(id object, const void *key) {
return _object_get_associative_reference(object, (void *)key);
}
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
_object_set_associative_reference(object, (void *)key, value, policy);
}
void objc_removeAssociatedObjects(id object)
{
if (object && object->hasAssociatedObjects()) {
_object_remove_assocations(object);
}
}
跳进_object_get_associative_reference
,可以看到里边出现了四个类:AssociationsManager、AssociationsHashMap、ObjectAssociationMap、ObjcAssociation。查看它们的结构如下:
class AssociationsManager {
// associative references: object pointer -> PtrPtrHashMap.
static AssociationsHashMap *_map;
public:
AssociationsManager() { AssociationsManagerLock.lock(); }
~AssociationsManager() { AssociationsManagerLock.unlock(); }
AssociationsHashMap &associations() {
if (_map == NULL)
_map = new AssociationsHashMap();
return *_map;
}
};
class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap *, DisguisedPointerHash, DisguisedPointerEqual, AssociationsHashMapAllocator> {
public:
void *operator new(size_t n) { return ::malloc(n); }
void operator delete(void *ptr) { ::free(ptr); }
};
class ObjectAssociationMap : public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjectAssociationMapAllocator> {
public:
void *operator new(size_t n) { return ::malloc(n); }
void operator delete(void *ptr) { ::free(ptr); }
};
class ObjcAssociation {
uintptr_t _policy;
id _value;
public:
ObjcAssociation(uintptr_t policy, id value) : _policy(policy), _value(value) {}
ObjcAssociation() : _policy(0), _value(nil) {}
uintptr_t policy() const { return _policy; }
id value() const { return _value; }
bool hasValue() { return _value != nil; }
};
这段代码怎么理解?
可以看到里边有两个map,类似于OC中的字典,后边加了类似泛型的东西。
再仔细一看,这不就是一个二维数组吗?
奶奶的,代码写这么多,为啥不加上一个注释说:都看好了,这一堆代码像极了一个二维数组。总结之后,结构如下图所示:
图4.2.1.1 4个类大致结构关系可以看出:
-
AssociationsManager中有一个对象AssociationsHashMap指针,它是二维数组的地址,相当于二位数组的名称。该值也是AssociationsManager的地址。他管理着内存中所有的关联对象。
-
纵坐标为当前的分类对象object,object下标对应的整行为ObjectAssociationMap。而该内部就是该分类对象下所有的关联对象。可能有name的关联对象,age,size等等。
-
横坐标为当前的具体key。而如果能够查找到,该单元格就是ObjcAssociation。它是value与policy通过运算的出的值。
以上就是基本的结构。 -
如果继续扩展,可以将它看到两个表的组合。而object是最外层表的主键,而内层可以看成内部表的主键。这样也能说得通。
-
也可以将他看成一张表,只不过是双主键罢了(object,key)。
综上,通过上边的图,很容易看清楚其结构。至于其添加值,获取值,销毁对象的过程,通过上表也可以很容易的分析。
4.2.2 内部具体实现细节分析
以objc_setAssociatedObject()为例进行分析:
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
// 其中acquireValue()函数是为了对value根据相应的内存策略进行处理
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
// DISGUISE() ,是将object转换为另一种类型:disguised_ptr_t
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
j->second = ObjcAssociation(policy, new_value);
} else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// setting the association to nil breaks the association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}
前边的代码加上了注释。从if开始继续分析:
-
如果new_value有值,则要存进去;否则相当于清空表中对应的数据;
-
if分析:如果AssociationsHashMap列表中有disguised_object这条记录。取出该条记录对应的ObjectAssociationMap指针,再根据key取得ObjcAssociation对象。然后将新的 ObjcAssociation(policy, new_value)填充到该位置即可。如果没有根据找到该key对应的值,则直接手动添加即可。
-
else分析:相当于拿到清空原先关联对象的值(或者成为初始化)。
相应的objc_getAssociatedObject、objc_removeAssociatedObjects也是如此,可以自己查看相应源码进行分析。
好了,休息一下。
这个小熊虎的可爱
- 本文相关的demo已放置github.
- 在阅读中,如果发现文章有不合理的地方,欢迎提出疑问至邮箱:B12050217@163.com.
- 原创文章,转载请注明:转自:Try_Try_Try
- OK, game over.
网友评论