iOS-底层原理17-类的扩展补充和面试题

《iOS底层原理文章汇总》

上一篇文章《iOS-底层原理16-类扩展和关联对象底层原理》介绍了关联对象底层原理

1.分析AssociationsManager不唯一和AssociationsHashMap唯一

模拟新建AssociationsManager和AssociationsHashMap两个,查看内存地址是否一致,先将AssociationsManager构造函数中的锁去掉,否则会死锁

不同的AssociationsManager manager和manager1创建的associations和associations1内存地址一样,唯一


image.png

AssociationsManager构造函数去掉锁


image.png

manager不唯一,manager内存地址不可读,调用初始化方法,并不会调用init方法,那么init方法在什么时候调用的呢?断点调试下

    AssociationsManager()   {  }
    ~AssociationsManager()  {  }
image.png

在arr_init()中调用AssociationsManager::init()方法,属于类方法对AssociationsManager进行环境准备并没有初始化,init()方法并没有返回值,map_images-->arr_init()-->_objc_associations_init()-->AssociationsManager::init()-->_mapStorage.init()


image.png

2.加锁的原因:会对唯一的表AssociationsHashMap中的数据进行读取和安放,防止多线程对数据进行篡改而导致数据不同步

image.png

相当于如下:操作前加锁,操作后解锁


image.png

3.整体结构:

AssociationsHashMap:整个项目
LGPerson LGTeacher LGStudent
对象 key -> ObjctAssociationMap (LGPerson)(name age hobby)
key -> ObjcAssociation(policy value)

Buckets桶子里面包含桶子,Buckets桶子的结构为objc::detail::DenseMapPair<const void *, objc::ObjcAssociation>,结构复用,代码复用


image.png
image.png
image.png

4.面试题:请问关联对象设置后是否应该移除?不需要移除,为什么呢?

对象在释放的时候会自动移除,进入dealloc方法,- (void)dealloc-->_objc_rootDealloc(self) --> obj->rootDealloc() --> object_dispose((id)this) --> objc_destructInstance(obj) --> _object_remove_assocations(obj),从总表AssociationsHashMap中挨个移除,Bucket移除

image.png

image.png

image.png

5.面试题:主类和分类同名方法,优先调用哪一个?

    1. 非load方法会先调用分类中的
  • 2.load方法会先调用主类的,再调用分类的,为什么会是先主类后分类呢?


    image.png

    image.png

5.load_images分析

image.png

prepare_load_methods((const headerType *)mh) --> schedule_class_load(remapClass(classlist[i])) --> add_class_to_loadable_list(cls)

在schedule_class_load方法中进行递归,将父类中的方法进行添加loadable_classes,若类有load方法,将类添加到loadable_classes中,已经开辟的和正在使用的classes_loadable类数量是否相等,若相等,则进行扩容

method = cls->getLoadMethod(),判断是否是load方法,返回imp


image.png
image.png
image.png

类中的load方法加载完成,再加载分类中的load方法,加到loadable_categories,若已经使用的分类和开辟的相等,则进行扩容

image.png
image.png

调用load方法call_load_methods(),循环先调用类中load方法,后调用分类中load方法,函数指针消息发送(*load_method)(cls, @selector(load))传入两个参数cls和@selector(load)


image.png
image.png
image.png

调用完毕后,调用自动释放池回收整片内存空间


image.png

load_images分析流程图


image.png

面试题

initialize方法在第一次消息发送的时候调用
load方法调用是先主类后分类
其他方法并不是分类覆盖了主类,而是分类中的方法编译时写在了前面,会先调用

Runtime是什么

runtime是由C和C++汇编实现的一套API,为OC语言加入了面向对象,运行时的功能
运行时(Runtime)是指将数据类型的确定由编译时推迟到了运行时
举例子:extension-category的区别
平常编写的OC代码,在程序运行过程中,其实最终会转换成Runtime的C语言代码,RuntimeObject-C的幕后工作者

6.[self class]和[super class]的区别

image.png
2020-12-27 19:12:35.385708+0800 KCObjc[65535:2691177] LGTeacher - LGTeacher
2020-12-27 19:12:35.386547+0800 KCObjc[65535:2691177] <LGTeacher: 0x100507b90>
  • 1.[self class]会调用object_getClass(self),返回对象的isa,也就是类LGTeacher
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}
  • 2.super是关键字,clang LGTeacher.m查看LGTeacher的源码得到


    image.png

[super class]编译为了如下代码,__rw_objc_super为中间形态,搜objc_msgSendSuper源码

((Class (*)(__rw_objc_super *, SEL))(void
*)objc_msgSendSuper)((__rw_objc_super){(id)self, 
(id)class_getSuperclass(objc_getClass("LGTeacher"))}, 
sel_registerName("class"))

objc_msgSendSuper结构第一个参数为结构体struct objc_super *super,结构体中有两个参数id receiver,class super_class,故源码中是self调用sel_registerName("class"),在方法instancetype _I_LGTeacher_init中的self为LGTeacher对象,[super class]相当于是[self class],和[self class]是同一套调用源码流程顺序,会调用object_getClass(self),返回对象的isa,也就是类LGTeacher


image.png

image.png

image.png

// super : 关键字
// [super class] (class)(id self , sel _cmd)
// self->isa LGTeacher
// self 消息的接受者 LGTeacher

运行程序,断点object_getClass,发现方法会进入两次,间接证明输出的是LGTeacher


image.png

image.png

3.[self class]和[super class]的区别,self再去查找class方法的时候,不先从本类中去查找了,直接从父类中去查找,跳过了本类的查找流程,比[self class]查找速度更快super_class is the first class to search,消息的接收者还是self,查找的方式变化了


image.png

假设将[super class]改为[LGPerson class]呢,查看源码


image.png

由上面可知编译时期[super class]调用的方法为objc_msgSendSuper,那么运行时期呢?实质上调用的方法为objc_msgSendSuper2


image.png
image.png

将当前类传入结构体struct objc_super中,在结构体内部再取当前类的父类,而不是现在就把当前类的父类传进去,结构体内部objc_super中会从类的父类super_class开始查找,这一点和objc_msgSendSuper不一样,消息的接收者receiver还是本类self,[super class]还是输出LGTeacher
[self class]从本类中查找class方法,[super class]从父类中开始查找class方法
查看汇编代码:是直接从superclass中查找class方法


image.png

完整回答:[self class]和[super class]两个都会输出LGTeacher,[self class]调用的本质是消息发送msgSend,通过调用class底层方法获取到对象的isa即LGPerson元类型,类已经加载到内存,获取元类类型,在map_images的readClass方法中类名已经加载到类名表中,读取%@时是一个字符串类型,打印LGTeacher,super是一个关键字,底层调用objc_msgSendSuper2,消息接收者为self,和[self class]消息接收者一模一样,返回LGTeacher

7.内存平移

- (void)viewDidLoad {
    [super viewDidLoad];
    Class cls = [LGPerson class];
    void  *kc = &cls; 
    [(__bridge id)kc saySomething];
 }
 #import "LGPerson.h"
@implementation LGPerson
- (void)saySomething{
 NSLog(@"%s",__func__);
}

输出的结果一模一样,可以正常调用实例方法,kc调用saySomething和person对象调用saySomething指向的内存空间一致,可以调用


image.png

image.png
  • 在saySomething方法中增加获取self.kc_name,[person saySomething]打印的self.kc_name为nil,[(__bridge id)kc saySomething]打印的self.kc_name为什么呢?


    image.png

打印出来的结果为-[LGPerson saySomething] - <ViewController: 0x7f984b6063a0>,为什么打印的是ViewController???

image.png

  • 1.分析[person saySomething]调用self.kc_name的内存平移情况,在函数-(void)viewDidLoad中,栈的情况,栈是先进后出,最先压栈的是-(void)viewDidLoad方法中的隐藏的两个参数(self,_cmd),之后[super viewDidLoad]方法clang编译会生成一个结构体{(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}结构体两个参数self和super_class分别压入栈中

    image.png

  • 2.分析栈的情况

  • 隐藏参数会压入栈帧:参数从前往后一直压栈,栈区是从高地址到低地址,person先压入栈分配的是高地址,person2和person3后压入栈,依次分配比person低的地址


    image.png
  • 函数调用的压栈情况[super viewDidLoad]分析:查看编译源码分析能得到,会生成一个结构体{(id)self, (id)class_getSuperclass(objc_getClass("LGTeacher"))},会传入结构体属性self和(id)class_getSuperclass(objc_getClass("ViewController")),那么结构体属性,是怎么一个压栈情况呢
objc_msgSendSuper({(id)self, (id)class_getSuperclass(objc_getClass("LGTeacher"))}, sel_registerName("class")));
  • 结构体属性的压栈情况:结构体里面的属性num2的内存地址为0x00007ffeed838178,num1的内存地址为0x00007ffeed838170,person3的内存地址为0x00007ffeed838168,说明结构体中的属性,后面的属性先压栈,即先压入20,再压入10,因此objc_msgSendSuper方法中的结构体{(id)self, (id)class_getSuperclass(objc_getClass("LGTeacher"))}先入栈的是super_class,后入栈的是self,压栈顺序为self-->cmd-->(id)class_getSuperclass(objc_getClass("ViewController"))-->self


    image.png
  • 完整压栈顺序:self-->cmd-->(id)class_getSuperclass(objc_getClass("ViewController"))-->self-->cls-->kc-->person


    image.png
  • 理解指针和地址的概念:p是指针变量,在栈中赋值前后p和q的地址0x7ffee5373160、0x7ffee5373158不变,给指针变量p赋值&a(a的地址0x7ffee537316c),p的地址0x7ffee5373160中存放a的地址0x7ffee537316c,a的内存地址0x7ffee537316c中存放了整形数值10,q的地址0x7ffee5373158存放p的地址0x7ffee5373160,q取双重指针得到10


    image.png

    image.png
  • 3.理解了指针和地址的概念,查看当前栈中内存地址情况,*(void *)address打印address这一个内存地址中存放的地址所
    指向的区域,比如address的内存地址中存放
    kc的内存地址,则
    (void **)address为kc的内存
    地址中所存放的对象或值的内存情况,kc的内存地址中指向的内容为cls的内存地址
    image.png

kc的内存地址中存放的内容为cls的内存地址,cls的内存地址中存放的是[LGPerson class],address的内存地址中存放的是kc的内存地址

image.png

address的内存地址
image.png

image.png

self为消息接收者 - LGPerson <LGPerson: 0x7ffee119e178>
从person对象中找到唯一的属性kc_name,需要将person对象内存地址平移一个isa指针8字节的位置,获取kc_name的值,0x7ffee119e178平移8字节得0x7ffee119e180,0x7ffee119e178+0x8 = 0x7ffee119e180正好是ViewController的内存地址(0x7ffee119e180 - <ViewController: 0x7fd47c40b3a0>),故会输出-[LGPerson saySomething] - <ViewController: 0x7fd47c40b3a0>
//LGPerson: 0x7ffee119e178
//person VS LGPerson(实例化)(isa)
//kc -> LGPerson (实例化) kc_name
image.png

第二个问题:为什么第三个参数super_class返回的是ViewController?(id)class_getSuperclass(objc_getClass("ViewController")),因为objc_msgSendSuper2返回的是当前的类ViewController,为什么不是ViewController的父类UIViewController呢???

[super viewDidLoad]方法,运行时经过汇编走的代码是objc_msgSendSuper2,在进入汇编之前要传入两个参数,一个是结构体指针struct objc_super * _Nonnull super,一个是SEL _Nonnull op为sel_registerName("viewDidLoad"),查看结构体struct objc_super中存在两个参数一个是receiver,一个是super_class,消息的接收者为self本类,super_class为多少呢???

image.png

查看objc_msgSendSuper2的解释,super_class传入的是当前类ViewController并不是当前类的父类UIViewController,在汇编中去查找方法viewDidLoad时才去查找当前类的父类,若传入的是当前类的父类UIViewController,则在汇编中查找的是父类的父类UIResponer,
所以第三项打印为当前类(本类)ViewControler,objc_msgSendSuper2的汇编代码实质为objc_msgSendSuper2({self, objc_getClass("ViewController")}, sel_registerName("viewDidLoad"));
image.png

image.png

改一下LGPerson中的属性结构,结果变化如何:增加一个属性,则平移isa + kc_hobby总共16字节,0x7ffee7808178 + 0x8 + 0x8 = 0x7ffee7808188

为self ——> ViewController


image.png

再改下LGPerson的属性结构

image.png

image.png

此时person对象<LGPerson: 0x7ffee5d98178>平移isa(8字节)再平移int类型(4字节),即将内存地址0x7ffee5d98180 - <ViewController: 0x7ff545f0ac70>劈开一半,读取出来为数值1463859280,不是一段完整的数据,程序没有崩溃

此面试题的一次,外层传一个cls,无论是什么cls,都可以用kc接收,更加实现多态化,但是不安全,对象属性变化,内存访问不到,会崩溃

image.png

kc为嘛能调用LGPerson中的对象方法

Class cls = [LGPerson class];
void *kc = &cls;//ISA
LGPerson *person = [LGPerson alloc];// person - 指针 - ISA -> LGPerson

person指针地址里面有ISA,ISA指向LGPerson,cls的首地址是ISA,kc相当于ISA,故能调用类中的方法

alloc出来的内存空间都在堆里面,常量,指针在栈中,0x70一般代表栈,0x60一般代表堆

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

推荐阅读更多精彩内容

  • 【面试-1】Runtime Asssociate方法关联的对象,需要在dealloc中释放? 当我们对象释放时,会...
    CrazySnow阅读 409评论 0 3
  • iOS 底层原理 文章汇总[https://www.jianshu.com/p/412b20d9a0f6] 【面试...
    Style_月月阅读 3,480评论 6 12
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,401评论 16 21
  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,498评论 0 11
  • 可爱进取,孤独成精。努力飞翔,天堂翱翔。战争美好,孤独进取。胆大飞翔,成就辉煌。努力进取,遥望,和谐家园。可爱游走...
    赵原野阅读 2,681评论 1 1