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);
    }
}
iOS-OC底层15:面试题_第1张图片
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的方法。


iOS-OC底层15:面试题_第2张图片
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缓存中读取IMP和objc_msgSend慢速查找两者流程大同小异,在此就不做分析了。

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

  Class cls = [LGPerson class];
    void  *kc = &cls;  // 
    LGPerson *person = [LGPerson alloc];
 [(__bridge id)kc saySomething]; // 1 2  - 
    
    [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]--
[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
    
    [person saySomething]; // self.kc_name = nil - (null)

    //
}

打印结果

 0x7ffee0ca3058 : 
0x7ffee0ca3050 : viewDidLoad
0x7ffee0ca3048 : ViewController
0x7ffee0ca3040 : 
0x7ffee0ca3038 : LGPerson
0x7ffee0ca3030 : 

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


iOS-OC底层15:面试题_第3张图片
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();

你可能感兴趣的:(iOS-OC底层15:面试题)