iOS-OC底层15:面试题

1.我们关联的对象是否需要手动移除,为什么

不需要手动移除,在对象的dealloc中
在关联对象时,如果是第一次,我们会设置对象的has_assoc为true,看dealloc代码

- (void)dealloc {
    _objc_rootDealloc(self);
}

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}
RemoveAssocation.png

2.类的方法和分类的方法重名调用顺序

一般方法是优先调用分类的方法(包括initialize),因为在添加分类方法是把分类的方法插入到本类methodlist的前面。
load方法呢?
我们来看一下iOS对load的处理load_images,load_images中对load进行两个处理
,prepare_load_methods和call_load_methods

prepare_load_methods

1.schedule_class_load
通过递归,通过add_class_to_loadable_list把类加入到数组中,如果父类没有实现load方法则不加入,如果数组申请的内容小了,则扩容,扩容原则是之前容量的2倍加16.
2.add_category_to_loadable_list
通过加载顺序把分类和方法加入到数组中,扩容方式和schedule_class_load相同。

call_load_methods

调用顺序,先调用本类的,按照schedule_class_load的数组存入先后顺序,所以父类的load优于子类先调用,然后调用分类的load的方法。


image.png

补充 :initialize是系统自己调用的,在类或者对象第一次调用方法时系统调用initialize,先父类再子类,如果分类实现了initialize,会调用分类的initialize,不调用本类的initialize方法,因为分类的initialize在methodlist中在本类initialize的前面。

3.[self class]和[super class]的不解之缘

@interface LGPerson : NSObject
@end
@implementation LGPerson
@end
@interface LGTeacher : LGPerson

@end
@implementation LGTeacher
- (instancetype)init{
    self = [super init];
    if (self) {
        NSLog(@"%@ - %@",[self class],[super class]);
    }
    return self;
}
@end

我们调用[ LGTeacher alloc] init];打印日志如下
LGTeacher - LGTeacher,是不是出乎我们意料,但是super 到底做了什么呢?
看汇编[super class]调到objc_msgSendSuper2方法。
objc_msgSendSuper2做了什么呢?
objc_msgSendSuper2是从父类的cache中查询class方法,如果没有则从父类的方法列表中查询class,因为class的实现是在NSObject所以无论是[self class]还是[Super class]调用的方法都是从NSObject方法列表中找到的

- (Class)class {
    return object_getClass(self);
}

又因为[self class]和[Super class]的接收者不变,所以打印一直。
objc_msgSendSuper2的调用过程可以参考objc_msgSend缓存中读取IMPobjc_msgSend慢速查找两者流程大同小异,在此就不做分析了。

4.下面的调用会成功吗?

  Class cls = [LGPerson class];
    void  *kc = &cls;  // 
    LGPerson *person = [LGPerson alloc];
 [(__bridge id)kc saySomething]; // 1 2  - <ViewController: 0x7f7f7ec09490>
    
    [person saySomething];
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString* kc_name;
- (void)saySomething;
@end
@implementation LGPerson
- (void)saySomething{ 
    NSLog(@"%s ",__func__);
}
@end

打印日志

2020-10-22 22:24:19.290465+0800 004-内存平移问题[63882:4725050] -[LGPerson saySomething]
2020-10-22 22:24:19.290639+0800 004-内存平移问题[63882:4725050] -[LGPerson saySomething]

为什么呢?
方法调用实质上是发送消息,发送消息objc_msgSend(objc_msgSendSuper)里面自带两个参数接收着和方法编号,接收者实质上是内存地址,然后通过快速查找和慢速查找寻找方法的实现IMP。查看kc和person的内存地址情况

(lldb) x/4gx kc
0x7ffee54b6038: 0x000000010a74f648 0x00007ffea74046a0
0x7ffee54b6048: 0x000000010a74f580 0x00007fff5e0889bb
(lldb) x/4gx person
0x600000e0cca0: 0x000000010a74f648 0x0000000000000000
0x600000e0ccb0: 0x0000000000000000 0x0000000000000000
(lldb) 

[person saySomething]调用情况是,读0x600000e0cca0地址可知isa地址0x000000010a74f648,查找sel的imp实现消息的发送,
[(__bridge id)kc saySomething];调用情况,读0x7ffee54b6038地址可知isa地址0x000000010a74f648,和 [person saySomething]一样
修改saySomething方法

- (void)saySomething{ 
    NSLog(@"%s--%@",__func__,self.kc_name);
}
    Class cls = [LGPerson class];
    void  *kc = &cls;  // 
    LGPerson *person = [LGPerson alloc];
    person.kc_name = @"name";
    [(__bridge id)kc saySomething];     
    [person saySomething];

打印结果

[LGPerson saySomething]--<ViewController: 0x7fe4406066c0>
[LGPerson saySomething]--name

这又是为什么呢?
查看内存情况

(lldb) x/4gx kc
0x7ffee5bfb038: 0x000000010a00a5f0 0x00007fe4406066c0
0x7ffee5bfb048: 0x000000010a00a528 0x00007fff5e0889bb
(lldb) p  *(void **)0x7ffee5bfb040
(ViewController *) $1 = 0x00007fe4406066c0
(lldb) x/4gx person
0x600001b0c420: 0x000000010a00a5f0 0x000000010a005038
0x600001b0c430: 0x0000000000000000 0x0000000000000000
(lldb) p  *(void **)0x600001b0c428
(__NSCFConstantString *) $3 = 0x000000010a005038 "name"

我们去self.kc_name的值,根据LGPerson对象的内存布局,需要对象起始地址偏移8个字节,然后读取地址得到self.kc_name的值
kc的其实地址为0x7ffee5bfb038加8字节得到0x7ffee5bfb040读取地址得到(ViewController *) $1 = 0x00007fe4406066c0,同理person起始地址偏移8字节得到0x600001b0c428读取地址得到0x000000010a005038 "name"。那为什么kc偏移8地址读取到的是ViewController呢?下面就介绍压栈

5.压栈

函数的压栈规律

void kcFunction(id person, id kcSel, id kcSel2){
   NSLog(@"person = %p",&person);
   NSLog(@"person = %p",&kcSel);
   NSLog(@"person = %p",&kcSel2);
}

   LGPerson *person = [LGPerson alloc];
   kcFunction(person, person, person);

打印结果

person = 0x7ffeea97cfd8
person = 0x7ffeea97cfd0
person = 0x7ffeea97cfc8

函数中指针压栈过程是从高地址到低地址

结构体的压栈规律

struct kc_struct{
    NSNumber *num1;
    NSNumber *num2;
} kc_struct;
    struct kc_struct struct1 = {@(10),@(20)};

lldb调试

(lldb) p &struct1
(kc_struct *) $8 = 0x00007ffee39b9018
(lldb) p *(NSNumber **)0x00007ffee39b9018
(__NSCFNumber *) $9 = 0xa9b8175a99c3a687 (int)10
(lldb) p *(NSNumber **)0x00007ffee39b9020
(__NSCFNumber *) $10 = 0xa9b8175a99c3a767 (int)20

结构体中指针压栈过程是从低地址到高地址

viewDidLoad指针压栈情况

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // ViewController 当前的类
    // self cmd (id)class_getSuperclass(objc_getClass("LGTeacher")) self cls kc person
    
    Class cls = [LGPerson class];
    void  *kc = &cls;  // 
    LGPerson *person = [LGPerson alloc];

    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);
        }
    }

    
    // LGPerson  - 0x7ffeea0c50f8
    [(__bridge id)kc saySomething]; // 1 2  - <ViewController: 0x7f7f7ec09490>
    
    [person saySomething]; // self.kc_name = nil - (null)

    //
}

打印结果

 0x7ffee0ca3058 : <ViewController: 0x7f9be6408500>
0x7ffee0ca3050 : viewDidLoad
0x7ffee0ca3048 : ViewController
0x7ffee0ca3040 : <ViewController: 0x7f9be6408500>
0x7ffee0ca3038 : LGPerson
0x7ffee0ca3030 : <LGPerson: 0x7ffee0ca3038>

1.0x7ffee0ca3058 和0x7ffee0ca3050 :
viewDidLoad方法默认两个对象第一个是id第二个是cmd
所以打印ViewController对象和方法

  1. 0x7ffee0ca3048和0x7ffee0ca3040
    因为调用[super viewDidLoad];会产生一个结构题第一个是接收者第二个是Class
    有因为结构体压栈规律是低到高,所以class是被压倒高地址,接收者self被压倒低地址。

6.Runtime是什么

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

7.方法的本质,sel是什么?IMP是什么?两者之间的关系又是什么?

方法的本质:发送消息 , 消息会有以下几个流程
1:快速查找 (objc_msgSend)~ cache_t 缓存消息
2:慢速查找~ 递归自己| 父类 ~ lookUpImpOrForward
3:查找不到消息: 动态方法解析 ~ resolveInstanceMethod
4:消息快速转发~ forwardingTargetForSelector
5:消息慢速转发~ methodSignatureForSelector & forwardInvocation
sel 是方法编号 ~ 在read_images 期间就编译进入了内存 imp 就是我们函数实现指针 ,找imp 就是找函数的过程 sel 就相当于书本的目录 tittle
查找具体的函数就是想看这本书里面具体篇章的内容
1:我们首先知道想看什么 ~ tittle (sel)
2:根据目录对应的⻚码 (imp)
3.翻到具体的内容

imp与SEL 的关系

SEL : 方法编号
IMP : 函数指针地址
SEL 相当于书本目录的名称
IMP : 相当于书本目录的⻚码
1:首先明白我们要找到书本的什么内容 (sel 目录里面的名称)
2:通过名称找到对应的本⻚码 (imp)
3:通过⻚码去定位具体的内容

8能否向编译后的得到的类中增加实例变量?

能否想运行时创建的类中添加实例变量

答案:
1:不能向编译后的得到的类中增加实例变量
2:只要类没有注册到内存还是可以添加
原因:我们编译好的实例变量存储的位置在 ro,一旦编译完成,内存结构就完全确定 就无法修改
可以添加属性 + 方法

9.Runtime是如何实现weak的,为什么可以自动置nil

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


image.png

为什么可以自动置为nil呢
在对象dealloc时被置为nil的
dealloc->_objc_rootDealloc()->[ obj->rootDealloc()]->object_dispose((id)this)->objc_destructInstance(obj)->[obj->clearDeallocating()]-> clearDeallocating_slow();
补充 SideTable是什么时候初始化的
在map_images中初始化的
map_images->map_images_nolock->arr_init()->SideTablesMap.init();

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