IOS

iOS-底层原理 20:OC底层面试解析

2021-07-09  本文已影响0人  物非0人非

【面试-1】Runtime Asssociate方法关联的对象,需要在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 源码

dealloc的源码查找路径为:dealloc -> _objc_rootDealloc -> rootDealloc -> object_dispose(释放对象)-> objc_destructInstance -> _object_remove_assocations

image
image
image

【面试-2】方法的调用顺序

类的方法 和 分类方法 重名,如果调用,是什么情况?

  • 因为分类的方法是在类realize之后 attach进去的,插在类的方法的前面,所以优先调用分类的方法(注意:不是分类覆盖主类!!)
  • initialize方法什么时候调用? initialize方法也是主动调用,即第一次消息时调用,为了不影响整个load,可以将需要提前加载的数据写到initialize

【面试-3】Runtime是什么?

  • 举例:extension 和 category 的区别

1、category 类别、分类

  • 专门用来给类添加新的方法
  • 不能给类添加成员属性,添加了成员属性,也无法取到
  • 注意:其实可以通过runtime 给分类添加属性,即属性关联,重写setter、getter方法
  • 分类中用@property 定义变量,只会生成变量的setter、getter方法的声明不能生成方法实现 和 带下划线的成员变量

2、extension 类扩展

  • 可以说成是特殊的分类 ,也可称作 匿名分类
  • 可以给类添加成员属性,但是是私有变量
  • 可以给类添加方法,也是私有方法

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

  • 快速查找(objc_msgSend) - cache_t缓存消息中查找
  • 慢速查找 - 递归自己|父类 - lookUpImpOrForward
  • 查找不到消息:动态方法解析 - resolveInstanceMethod
  • 消息快速转发 - forwardingTargetForSelector
  • 消息慢速转发 - methodSignatureForSelector & forwardInvocation
  • sel方法编号 - 在read_images期间就编译进了内存
  • imp函数实现指针找imp就是找函数的过程
  • sel 相当于 一本书的目录title
  • imp 相当于 书本的页码
  • 1、首先知道想看什么,即目录 title - sel
  • 2、根据目录找到对应的页码 - imp
  • 3、通过页码去翻到具体的内容

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

  • 1、不能向编译后的得到的类中增加实例变量
  • 2、只要类没有注册到内存还是可以添加的
  • 3、可以添加属性+方法

【原因】:编译好的实例变量存储的位置是ro,一旦编译完成,内存结构就完全确定了

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

  • [self class]就是发送消息 objc_msgSend,消息接收者是self,方法编号 class
  • [super class] 本质就是objc_msgSendSuper,消息的接收者还是 self,方法编号 class,在运行时,底层调用的是_objc_msgSendSuper2【重点!!!】
  • 只是 objc_msgSendSuper2 会更快,直接跳过self的查找

代码调试

image
运行程序,打印结果如下
image
- (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

ENTRY _objc_msgSendSuper2
UNWIND _objc_msgSendSuper2, NoFrame

ldp p0, p16, [x0]       // p0 = real receiver, p16 = class 取出receiver 和 class
ldr p16, [x16, #SUPERCLASS] // p16 = class->superclass
CacheLookup NORMAL, _objc_msgSendSuper2//cache中查找--快速查找

END_ENTRY _objc_msgSendSuper2

完整回答

所以,最完整的回答如下

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

Class cls = [LGPerson class];
void  *kc = &cls;  //
[(__bridge id)kc saySomething];

LGPerson中有一个属性 kc_name 和一个实例方法saySomething,通过上面代码这种方式,能否调用实例方法?为什么?

代码调试

LGPerson *person = [LGPerson alloc];
[person saySomething];

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

image

修改: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];

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

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

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

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

其中 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

哪些东西在栈里 哪些在堆里

注意:

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

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

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

image
上一篇下一篇

猜你喜欢

热点阅读