底层相关面试题分析

dealloc 释放的对象

首先查看dealloc的底层源码

obj->rootDealloc();
if (isTaggedPointer()) return;  // fixme necessary?
// 根据isa中相关存储格式进行判断
    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);
    }

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance. 判断是否还包含C++方法以及关联对象
        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;
}

根据上述代码得知:

  • 先查看isa相关信息来判断是否还包含信息1是0否
  • object_dispose -> objc_destructInstance来判断是否存在hasCxxDtorhasAssociatedObjects
    • 如果存在C++方法,就从缓存中释放object_cxxDestruct->object_cxxDestructFromClass->lookupMethodInClassAndLoadCache
    • 如果存在Associate,则从对应的哈希表中查找然后依次删除
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);
        }
    }

主类与分类的同名方法调用顺序

  • 如果方法为普通方法,包括initialize,这时会因为编译时分类方法存储在主类方法前面,故而优先调用分类方法,
    • initialize方法也是主动调用,即第一次消息时调用,为了不影响整个load,可以将需要提前加载的数据写到initialize
    • 其他分类方法通过attachCategories进行添加在主类方法之前,但不会覆盖主类方法
  • 如果为load方法,将先调用主类load方法,再调用分类load方法,可以在load_images中进行验证
void
load_images(const char *path __unused, const struct mach_header *mh)
{
    if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
        didInitialAttachCategories = true;
        loadAllCategories();
    }

    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}
void prepare_load_methods(const headerType *mhdr)
{


    classref_t const *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }

    category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[I];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class
        if (cls->isSwiftStable()) {
            _objc_fatal("Swift class extensions and categories on Swift "
                        "classes are not allowed to have +load methods");
        }
        realizeClassWithoutSwift(cls, nil);
        ASSERT(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}
void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

通过prepare_load_methods得知,先调用_getObjc2NonlazyClassList,然后才是_getObjc2NonlazyCategoryList
call_load_methods也是通过遍历先call_class_loads然后call_category_loads

【面试-3】Runtime是什么?

runtime是由CC++汇编实现的一套API,为OC语言加入了 面向对象、以及运行时的功能

运行时是指将数据类型确定编译时 推迟到了 运行时

举例:extensioncategory 的区别
平时编写的OC代码,在程序运行的过程中,其实最终会转换成runtimeC语言代码, runtimeOC的幕后工作者

1、category 类别、分类

专门用来给类添加新的方法

不能给类添加成员属性,添加了成员属性,也无法取到

注意:其实可以通过runtime 给分类添加属性,即属性关联,重写settergetter方法

分类中用@property 定义变量,只会生成变量的settergetter方法的声明(可以编译通过,是不能运行),不能生成方法实现带下划线成员变量

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的查找

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

通过clang编译成cpp文件可以看出

static instancetype _I_LGTeacher_init(LGTeacher * self, SEL _cmd) {
    self = ((LGTeacher *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("LGTeacher"))}, sel_registerName("init"));
    if (self) {
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_5c_6btl4svn6n5bqty614qwd7k80000gp_T_LGTeacher_d541e4_mi_0,((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")),((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("LGTeacher"))}, sel_registerName("class")));
    }
    return self;
}

通过实现方法可以看出,[super class]并不是通过LGTeacher的父类LGPerson进行调用,而消息接收者还是self即为LGTeacher,只是结构为__rw_objc_super

struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};

通过底层源码我们可以发现,super_class只是内部的一个参数,并非改变消息接收者,因此一开始打印的 NSLog(@"%@ - %@",[self class],[super class]);结果都为LGTeacher

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

下面方法调用打印结果是否一致

- (void)saySomething{ 
    NSLog(@"%s",__func__);
}
在ViewController中调用
Class cls = [LGPerson class];
void  *kc = &cls;  //
[(__bridge id)kc saySomething];
LGPerson * person = [LGPerson alloc];
[person saySomething];

结果发现两种打印是一致的,原因如下

2251862-1824c68a7400bb28-2.jpg

可以看出是通过personisa去获取类的相关信息,personisa指向的LGPerson,而上面通过 *kc = &cls也可以看出kc指向的也是LGPerson,他们都是通过获取LGPersonMethodList去查找saySomething方法,故而效果一致

@property (nonatomic, copy) NSString *kc_name;  // 12
- (void)saySomething{ 
    NSLog(@"%s - %@",__func__,self.kc_name);
}
2251862-6c51151a41e5b77b.jpg

如果进行上述更改,只增加一个属性的打印结果我们发现:

  • 正常情况下,读取属性是通过内存平移的方式进行读取,在person中存储的事isa以及相关属性,这时就可以通过person首地址平移8个单位获取kc_name的地址从而进行读取,kc_name没有赋值,打印为null可以理解
  • 如果是通过指针地址方式调用,发现打印了一个ViewController,这就很难理解,这时通过栈地址读取的方式来查看相关内容
void *sp  = (void *)&self;
void *end = (void *)&person;
long count = (sp - end) / 0x8;
    
for (long i = 0; i
2251862-f956699a7af79eb4.jpg

通过打印可以得知栈中现在保存的相关信息,由于栈是从高地址向低地址进行存储

  • 当进行方法调用时,首先传入的是每个函数都会包含的隐藏参数(id self,sel _cmd)
  • 通过实践可以得知结构体成员内部的压栈情况是 低地址->高地址递增的,故而如果是结构体进栈,则保存情况是后面变量先入栈
  • super通过clang查看底层的编译,是objc_msgSendSuper,其第一个参数是一个结构体__rw_objc_super(self,class_getSuperclass)
    因此可以得出目前栈中所保存的信息为self - _cmd - (id)class_getSuperclass(objc_getClass("ViewController")) - self - cls - kc - person

personLGPerson的关系是 person是以LGPerson为模板的实例化对象,即alloc有一个指针地址,指向isaisa指向LGPerson,它们之间关联是有一个isa指向

由于kc也是指向LGPerson的关系,编译器会认为 kc也是LGPerson的一个实例化对象,即kc相当于isa,即首地址,指向LGPerson,具有和person一样的效果,简单来说,我们已经完全将编译器骗过了,即kc也有kc_name。由于person查找kc_name是通过内存平移8字节,所以kc也是通过内存平移8字节去查找kc_name,目前kc的首地址为0x7ffeec381098,平移0x8得出0x7ffeec3810a0,正好与栈中 一致

【面试-8】 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

2251862-edad5fa01465390e.png

你可能感兴趣的:(底层相关面试题分析)