iOS底层原理之部分面试题分析

1,817 阅读5分钟

Runtime Asssociate方法关联的对象,是否需要在dealloc中释放?

不需要释放

分析

我们知道当一个对象销毁的时候会调用dealloc方法,那么我们先看下dealloc都进行了哪些操作。

  • dealloc函数调用了_objc_rootDealloc函数
  • _objc_rootDealloc函数调用rootDealloc函数
void
_objc_rootDealloc(id obj)
{
    ASSERT(obj);

    obj->rootDealloc();
}
  • rootDealloc函数查看
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);
    }
}

rootDealloc函数中我们看到了判断isa相关属性的地方,实际上当一个对象存在会进入else中,即object_dispose函数

  • object_dispose函数查看
id 
object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}

通过objc_destructInstance函数找到对象,然后free,我们看objc_destructInstance函数

  • objc_destructInstance函数 重点查看_object_remove_assocations函数
  • _object_remove_assocations函数分析

类、分类方法同名时调用顺序是怎样的?

非+load方法同名时,分类的方法在类的方法前面(注意不是覆盖),因为分类的方法是在类realize之后 attach进去的,所以 优先分类,其次类

+load方法同名时,优先类,其次分类

分类与类的扩展

分类

  • 专门用来给类添加新的方法
  • 不能添加属性,但是可以通过runtime动态添加属性(因为我们在前面的篇章中分析过,分类底层代码中有属性列表)
  • 分类中@property定义的变量只会生成setter以及getter方法的声明,但是不会生成对应的方法实现以及带有下划线的成员变量

类的扩展

  • 可以添加给类添加属性,但是属于私有变量,比如ViewController的.m文件中@property定义的变量只能是这个ViewController的.m文件使用
  • 添加的方法也是私有方法

什么是Runtime?

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

  • 运行时:将数据类型的确定由编译阶段推迟到了运行阶段。我们平时所写的OC代码,最终转换为runtime的C语言代码。

方法的本质是什么?SEL、IMP是什么?两者之间的关系是什么?

方法的本质

方法的本质是消息的发送,涉及到消息发送的流程有

  • 快速查找:objc_msgSend ~cache_t中查找
  • 慢速查找:递归自己以及父类查找,即lookUpImpOrForward
  • 动态解析:当查找不到消息时执行动态解析,即resolveInstanceMethod
  • 消息快速转发:当动态解析也没有找到消息,则进行消息快速转发,即forwardingTargetForSelector
  • 消息慢速转发:消息快速转发没有找到消息,则进行消息慢速转发,即mesthodSignatureForSelector & forwardInvocation
  • 以上流程均没有找到消息则crash

SEL、IMP

  • sel:方法编号,类比一本书的目录
  • imp:方法函数指针地址,类比一本书的页数
  • sel与imp关系:sel是方法编号,通过sel找到imp的函数指针地址,通过imp就能找到函数的实现

能否向编译后的类中添加实例变量?能否向运行时创建的类添加实例变量?

  • 不能向编译后的类中添加实例变量,因为编译后实例变量存储到 ro 中,一旦编译完成,内存结构就完全确定了,无法再次修改
  • 只要类还没有注册到内存还是可以添加的
  • 可以添加属性与方法

[self class] 与 [super class]的区别

我们先看以下如下代码打印结果,其中self是LGTeacher类,LGTeacher继承于LGPerson,LGPerson继承于NSObject从打印结果中我们看到无论是[self class]还是[super class]的结果是一样的,为什么呢?

分析

  • 我们知道任何方法调用都会隐藏两个参数,即(id self , sel _cmd),其中self是消息接收者。对于[self class]来说,它的消息接收者是 自身LGTeacher没什么可说的,所以打印的是LGTeacher
  • 首先我们要知道super只是关键字,它意思是说从父类调用方法,因此[super class]就是直接调用的就是父类的class方法,它的本质是objc_msgSendSuper,只是objc_msgSendSuper速度更快,直接跳过self。但需要注意的是,[super class]的消息接受者依然是LGTeacher,所以最终打印的是LGTeacher

内存偏移相关问题

我们先准备代码,定义IFPerson类,代码如下我们再看ViewController代码 从上述代码中我们延伸出两个问题:代码是否崩溃doSomething打印结果是什么。先不回答这两个问题,我们运行代码看结果如何,运行结果如下图 从运行结果中我们可以看出代码不会崩溃且运行结果也出来了.

[(__bridge id)kc doSomething]为什么不会崩溃?

首先我们知道对于一个对象,它的指针地址指向的是isa,同时isa地址指向当前的class,所以kc指向的是IFPersonisa,而person的指针指向的也是isa,这样它们都是isacache_t中查找doSomething方法,因此不会崩溃。

为什么[(__bridge id)kc doSomething]打印的结果是ViewController

  • 从打印结果中[person doSomething]打印出出来shifx是没有什么问题的,毕竟给person.name赋值shifx,但是[(__bridge id)kc doSomething]打印的结果是ViewController呢?要解决这个问题首先我们需要知道person能够找到name指针从isa内存平移了8个字节移动到了name。那么对于kc来说,它也需要指针平移,但是为什么平移后的结果是viewController呢?这就需要明白栈地址是从高到低存储的,且是先进后出,由于前面先调用了[super viewDidLoad]方法,且viewDidLoad的隐藏参数是(id self, SEL _cmd),所以self会先入栈,其次是cls->kc->person,出栈的顺序刚好相反,由于[(__bridge id)kc doSomething]时需要指针平移,自然指向了self(即ViewController),所以打印的结果是ViewController
  • 为了验证我们上面分析是否正确,我们修改代码位置,将声明IFPerson *person = [[IFPerson alloc] init]放在[super viewDidLoad]之后,即 此时我们按照我们上面的分析self会先入栈,其次是person->cls->kc,猜测[(__bridge id)kc doSomething]打印结果应该是IFPerson (person的isa指向其Class),我们运行代码结果可以看出我们的分析是正确的。