底层面试题(重要)面试合集

iOS 底层面试题

2020-11-05  本文已影响0人  Y丶舜禹

前言

我们类的底层探索已经告一段落,我们梳理一下常见的面试题,希望对你有些帮助。

问题

  • 1.runtime是什么?
  • 2.runtime如何实现weak,为什么可以自动置为nil?
  • 3.runtime Associate方法关联的对象,是否需要在dealloc中释放?
  • 4.关联对象AssociationsManager是否唯一?
  • 5.分类方法会覆盖本类方法吗?
  • 6.所有分类方法都优先于本类吗?
  • 7.方法的本质,SEL是什么?IMP是什么?两者之间关系是什么?
  • 8.编译后的类能否添加实例变量?能否向运行时创建的类添加实例变量?
  • 9.[self class][super class]区别和原理分析
  • 10.内存平移问题
问题一:runtime是什么?

runtime 是由CC++汇编实现的⼀套API,为OC语⾔加⼊了⾯向对象,运⾏时的功能。
运⾏时(Runtime)是指将数据类型的确定由编译时推迟到了运⾏时.
举个例⼦: extension - category 的区别(extension是编译期就确定了,但是懒加载的category是在运行时动态加入的)。
平时我们编写的OC代码,在程序运⾏过程中,其实最终会转换成RuntimeC语⾔代码,Runtime 是Object-C 的幕后⼯作者

问题二:runtime如何实现weak,为什么可以自动置为nil?
  1. 通过SideTable找到我们的weak_table
  2. weak_table 根据referent 找到或者创建 weak_entry_t
  3. 然后append_referrer(entry, referrer)将我的新弱引用的对象加进去entry
  4. 最后weak_entry_insertentry加入到我们的weak_table

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

weak底层调用
问题三:runtime Associate方法关联的对象,是否需要在dealloc中释放?

当我们创建的对象释放时,会调用dealloc方法,其中的大致流程如下:

所以,关联对象不需要我们手动移除,会在对象析构即dealloc时释放

dealloc 源码

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

// Replaced by NSZombies
- (void)dealloc {
    _objc_rootDealloc(self);
}
void
_objc_rootDealloc(id obj)
{
    ASSERT(obj);

    obj->rootDealloc();
}
/***********************************************************************
* object_dispose
* fixme
* Locking: none
**********************************************************************/
id 
object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}

/***********************************************************************
* objc_destructInstance
* Destroys an instance without freeing memory. 
* Calls C++ destructors.
* Calls ARC ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is nil.
**********************************************************************/
void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}
// Unlike setting/getting an associated reference,
// this function is performance sensitive because of
// raw isa objects (such as OS Objects) that can't track
// whether they have associated objects.
void
_object_remove_assocations(id object)
{
    ObjectAssociationMap refs{};

    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        //获取迭代器
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        //从头到尾逐个移除
        if (i != associations.end()) {
            refs.swap(i->second);
            associations.erase(i);
        }
    }

    // release everything (outside of the lock).
    for (auto &i: refs) {
        i.second.releaseHeldValue();
    }
}
问题四:关联对象AssociationsManager是否唯一?

AssociationsManager结构中,manager只是对外代言人,并不是唯一的,AssociationsHashMap哈希表才是唯一的。

1. 运行验证:
移除锁,这样可以同时存在2个manager了。

image
  • 加入测试代码,创建2个manager,都调用get(),发现2个读取的associations相同地址
  • 证明AssociationsHashMap在内存中是独一份的,而manager只是外层包装,可以创建多个。
测试
问题五:分类方法会覆盖本类方法吗?
问题六:所有分类方法都优先于本类吗?

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

image
问题七:方法的本质,SEL是什么?IMP是什么?两者之间关系是什么?

方法的本质:发送消息,消息会有以下几个流程

  • 快速查找(objc_msgSend) -cache_t缓存消息中查找
  • 慢速查找 - 递归自己|父类 -lookUpImpOrForward
  • 查找不到消息:动态方法解析 -resolveInstanceMethod
  • 消息快速转发 - forwardingTargetForSelector
  • 消息慢速转发 - methodSignatureForSelector & forwardInvocation

sel是方法编号 - 在read_images期间就编译进了内存

imp是函数实现指针 ,找imp就是找函数的过程

打个比方:加入你要从一本字典中查找某个字,那么sel相当于 字典的目录titleimp 相当于 字典的页码。

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

1、不可以。 因为编译好的实例变量存放的位置在类的ro,一旦编译完成,内存结构就完全确定了,无法修改。

2、运行时在register注册前,可以添加。但是调用运行时register注册后,就完成了内存的注入,内存结构确定了,无法修改。

问题九:[self class][super class]区别和原理分析

实际运行时,[super class]在汇编层执行的是objc_msgSendSuper2,直接从superclass父类开始搜索,节约了一轮查找资源

测试代码:

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

int main(int argc, const char * argv[]) {
   @autoreleasepool {
       ZGPerson * person = [[ZGPerson alloc] init];
  }
   return 0;
}


我们查看 [self class]中的class源码

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

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

其底层是获取对象的isa,当前的对象是ZGPerson,其isa是同名的ZGPerson,所以[self class]打印的是ZGPerson

[super class]中,其中super 是语法的 关键字,可以通过clangsuper的本质,clang生成cpp编译文件(clang -rewrite-objc ZGPerson.m -o ZGPerson.cpp),打开main.cpp文件:

ZGPerson.cpp

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

__rw_objc_super

objc中搜索objc_msgSendSuper,查看其隐藏参数

objc_msgSendSuper

搜索struct objc_super

objc_super

通过clang的底层编译代码可知,当前消息的接收者 等于 self,而self 等于 LGTeacher,所以 [super class]进入class方法源码后,其中的self是init后的实例对象,实例对象的isa指向的是本类,即消息接收者是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
总结:
问题十:runtime是什么?内存平移问题
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` 开头的地址表示在`全局区域`中

以上就是全部的内容了,如有错误,还望指正。

上一篇 下一篇

猜你喜欢

热点阅读