iOS 底层面试题

【面试-1】通过 Asssociate 方法关联的对象,需要在dealloc中释放

当对象释放时,系统会自动调用dealloc

dealloc释放步骤
  • 1、C++函数释放:objc_cxxDestruct
  • 2、移除关联属性:_object_remove_assocations
  • 3、将弱引用自动设置nil:weak_clear_no_lock(&table.weak_table, (id)this)
  • 4、引用计数清空:table.refcnts.erase(this)
  • 5、销毁对象:free(obj)

由此可看出关联对象不需要我们手动移除,会在对象析构(dealloc)时释放

dealloc源码
  • 在objc源码中搜索dealloc实现

    image.png

  • _objc_rootDealloc源码实现,主要是对象进行析构

    image.png

  • rootDealloc源码实现

    image.png

  • object_dispose源码实现,销毁实例对象

    image.png

  • objc_destructInstance源码实现,移除关联属性

    image.png

  • _object_remove_assocations源码,移除关联属性,主要是从全局哈希map中找到相关对象的迭代器,然后将迭代器中关联属性,彻底移除

    image.png

【面试-2】类的方法 和 分类方法 重名,方法调用顺序

  • 如果是普通方法同名,包括initialize,都会先调用分类方法

    • 因为分类的方法是类realize 后attach进去的,插在类的方法的前面,所有优先调用分类的方法(不是分类覆盖分类)
    • initialize方法在第一次消息时主动调用,为了不影响整个load,可以将提前加载的数据写到initialize
  • 如果同名方法时load方法,先调用主类load,后加载分类load(分类之间看编译顺序)

【面试-3】Runtime是什么?

  • runtime是由C和C++汇编实现的一套API,为OC语言加入了面向对象,以及运行时的功能

  • 将数据类型的确定从 编译时 推迟到了 运行时

  • 平时编写的OC代码,在程序运行的过程中,其实最终会转换成runtimeC语言代码, runtime是OC的幕后工作者

【面试-4】方法的本质,sel是什么?IMP是什么?两者之间的关系又是什么?

  • 方法的本质:发送消息

    • 快速查找objc_msgSend :从cache_t缓存消息中查找
    • 慢速查找:递归查找自己和父类lookUpImpOrForward
    • 查找不到消息
      • 动态方法解析resolveInstanceMethod
      • 消息快速转发forwardingTargetForSelector
      • 消息慢速转发methodSignatureForSelector & forwardInvocation
  • sel方法编号 -- 在read_images 期间就编译进了内存,相当于一本书的目录名字

  • imp函数实现指针找imp就是找函数的过程,相当于书本的页码

  • 查找具体的函数就是想看这本书具体内容

    • 1、首先知道想看什么,即目录title--sel
    • 2、根据目录找到对应的页码,即imp
    • 3、通过页码去找到具体内容

【面试-5】能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量

  • 1、不能向编译后得到的类中增加实例变量,因为编译好的实例变量存储的位置是ro,一旦编译完成,内存结构就确定了
  • 2、只要类没有注册到内存就可以添加,可以添加属性+方法

【面试-6】 [self class]和[super class]的区别以及原理分析

  • [self class]就是发送消息objc_msgSend,消息接收者是self,方法编号是class
  • [super class]本质是objc_msgSendSuper,消息接收者还是self,方法编号还是classsuper只是一个关键字,在运行时,底层调用的是_objc_msgSendSuper2,_objc_msgSendSuper2速度更快,会跳过self的查找
image.png
[self class]
  • [self class]中的class源码
- (Class)class {
    return object_getClass(self);
}

👇
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;`
}

其底层是获取对象的isa,当前的对象是LGTeacher,那么isa就是同名的LGTeacher,所有[self class]打印的是LGTeacher

[super class]
  • super是语法的关键字,我们可以通过命令行clang -rewrite-objc LGTeacher.m -o LGTeacher.cpp来查看super本质,这是编译时的底层源码,其中第一个参数是消息接收者,另外一个是__rw_objc_super结构

    image.png

  • 在底层源码中搜索__rw_objc_super,是一个中间结构体

    image.png

  • 在objc中搜索objc_msgSendSuper,看到了影藏参数

    image.png

  • 继续搜索struct objc_super

    image.png

通过clang的底层编译代码可知,当前的消息的接收者==self,而self==LGTeacher,所以[super class]进入class方法源码后,其中self是init后的实例对象,实例对象的isa指向本类,即消息接收者是LGTeacher本类

【面试-7】内存平移问题

LGPerson中有一个属性kc_name 和一个实例方法saySomething,通过下面代码,能否调用实例方法?

- (void)saySomething{
    NSLog(@"%s",__func__);
}

//下面这两种方式调用
//方式一
Class cls = [LGPerson class];
void  *kc = &cls; 
[(__bridge id)kc saySomething]; 

//方式二:常规调用
LGPerson *person = [LGPerson alloc];
 [person saySomething];

代码分析
  • [person saySomething]的本质是对象发送消息

    • personisa指向类LGPersonperson的首地址 指向 LGPerson的首地址,我们可以通过内存平移找到cache,在cache中查找方法
      image.png
  • [(__bridge id)kc saySomething]中的kc是来自于LGPerson 这个类,然后有一个指针kc,将其指向LGPerson的首地址

    image.png

所以,person是指向LGPerson类的结构,kc也是指向LGPerson类的结构,然后都是在LGPerson中的methodList中查找方法

image.png

修改:saySomething里面有属性 self.kc_name 的打印

代码如下所示

- (void)saySomething{
    NSLog(@"%s - %@",__func__,self.kc_name);
}

//下面这两种方式调用
//方式一
Class cls = [LGPerson class];
void  *kc = &cls; 
[(__bridge id)kc saySomething]; 

//方式二:常规调用
LGPerson *person = [LGPerson alloc];
 [person saySomething];
  • 打印结果
    • 方式一:kc的调用打印的kc_name<ViewController: 0x7fe29170b560>
    • 方式二:person方式的调用打印的kc_name(null)
      image.png

为什么会出现打印不一致的情况?

其中personkc_name 是由于 self指向person的内存结构,然后通过内存平移8字节,取出去kc_name,即self指针首地址平移8字节获得

image.png

  • 【方式一】其中kc表示8字节指针self.kc_name的获取,相当于kc首地址的指针也需要平移8字节找到kc_name,那么此时的kc的指针地址是多少?平移8字节获取的是什么?

    • kc是一个指针,存在中,因为栈是先进后出的结构,参数的传入是一个不断压栈的过程
      • 其中影藏参数也会压入栈,每个函数都会有两个隐藏参数(id self, sel _cmd),可以通过clang查看底层编译
      • 隐藏参数压栈的过程,其地址是递减的,而栈是从高地址->低地址 分配的,即在栈中,参数会从前往后一直压
      • 因为objc_msgSendSuper中第一个参数是一个结构体__rw_objc_super(self,class_getSuperclass)那么结构体中的属性是如何压栈的?可以通过自定义一个结构体,判断结构体内部成员的压栈情况
        image.png

        从图可知20是先加入的,再加入10,因此结构体内部的压栈情况是低地址->高地址递增的,栈中结构体内部成员是反向压入栈的
  • 所以到目前为止,栈中高地址->低地址的顺序是:self - _cmd - (id)class_getSuperclass(objc_getClass("ViewController")) - self - cls - kc - person

    • self和_cmdviewDidLoad方法的两个隐藏参数,是高地址->低地址正向压栈
    • class_getSuperClassselfobjc_msgSendSuper2中的结构体成员,即低地址->高地址反向压栈

我们可以通过下面这段代码打印栈的存储是否如上面所说

void *sp  = (void *)&self;
void *end = (void *)&person;
long count = (sp - end) / 0x8;
    
for (long i = 0; i<count; i++) {
    void *address = sp - 0x8 * i;
    if ( i == 1) {
        NSLog(@"%p : %s",address, *(char **)address);
    }else{
        NSLog(@"%p : %@",address, *(void **)address);
    }
}

运行结果如下


image.png

其中为什么class_getSuperclass是ViewController,因为objc_msgSendSuper2返回的是当前类,两个self,并不是同一个self,而是栈的指针不同,但是指向同一片内存空间

  • [(__bridge id)kc saySomething]调用时,此时kc是LGPerson: 0x7ffeec381098,所以saySomething方法中传入的self==LGPerson,但并不是我通常认为的LGPerson,即LGPerson: 0x7ffeec381098,是LGPerson的实例对象,可以通过LGPerson的地址内存平移8字节获得方法地址

    • 普通person流程:person -> kc_name - 内存平移8字节
    • kc流程:0x7ffeec381098 + 0x80 -> 0x7ffeec3810a0,即为self,指向<ViewController: 0x7fac45514f50>,如下图所示
      image.png

其中 personLGPerson的关系是 person是以LGPerson为模板的实例化对象,即alloc有一个指针地址,指向isa,isa指向LGPerson,它们之间关联是有一个isa指向

而kc也是指向LGPerson的关系,编译器会认为 kc也是LGPerson的一个实例化对象,即kc相当于isa,即首地址,指向LGPerson,具有和person一样的效果,简单来说,我们已经完全将编译器骗过了,即kc也有kc_name。由于person查找kc_name是通过内存平移8字节,所以kc也是通过内存平移8字节去查找kc_name

哪些东西在栈里 哪些在堆里
  • alloc的对象 都在

  • 指针、对象中,例如person指向的空间中,person所在的空间

  • 临时变量

  • 属性值,属性随对象是在

  • 是从小到大,即低地址->高地址
  • 是从大到小,即从高地址->低地址分配
    • 函数隐藏参数会从前往后一直压,即 从高地址->低地址 开始入栈
    • 结构体内部的成员是从低地址->高地址
  • 一般情况下,内存地址有如下规则
    • 0x60 开头表示在
    • 0x70 开头的地址表示在
    • 0x10 开头的地址表示在全局区域

【面试-8】 Runtime是如何实现weak的,为什么可以自动置nil

  • 1、通过SideTable 找到我们的 weak_table
  • 2、weak_table 根据referent找到或者创建 weak_entry_t
  • 3、然后append_referrer(entry,referrer)将我的新弱引用的对象加进去
  • 4、最后 weak_entry_insert,加入到我们的weak_table

底层源码调用流程如下图所示


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

推荐阅读更多精彩内容