美文网首页
iOS-OC底层面试题汇总1

iOS-OC底层面试题汇总1

作者: 泽泽伐木类 | 来源:发表于2020-10-26 16:47 被阅读0次
前言

本片文章对一些面试题做一些整理和记录,以便在之后的面试中复习。

问题1:关联对象需要我们手动移除吗?

答:关联对象不需要我们手动移除,它会在对象销毁时dealloc内移除。
这里我们详细的看下dealloc方法的实现:

- (void)dealloc {
    _objc_rootDealloc(self);
}
void
_objc_rootDealloc(id obj)
{
    ASSERT(obj);
    obj->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);
    }
}

id 
object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return 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;
}

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();
    }
}

从代码实现可以看到:dealloc->_objc_rootDealloc(self)->rootDealloc()
这里会对isa的一些标识做一次判断,这里我们知道当添加关联对象后,isa.has_assoc会标记为true,所以此时的if判断是不成立的,所以执行object_dispose()->objc_destructInstance()->_object_remove_assocations()
通过读取全局的AssociationHashMap,根据object查找对应的ObjectAssociationMap,然后通过erase()最终移除。
这里可以继续扩展:关联对象的使用场景以及内部的数据结构
关联对象移除流程图:

拓补图.010.jpeg
问题2:主类和分类同时实现+(void)load,哪个类先执行?

通过实际的代码打印,我们会看到主类load方法先执行,然后是分类的load方法执行。这个问题回答到此,就显的非常的初级,面对面试官没有一点的说服力,所以我们要更加深入的去表述,围绕这2个问题去表述:

  1. + (void)load()方法是什么时候被调用的?
    首先我们要知道,我们的代码最终会编译生成一个Mach-O 的可执行文件,程序在启动时,首先由libDyld介入,对Mach-O文件进行相应的初始化,链接,装载,具体:_dyld_start->_dyldbootstrap::star->dyld::_main()->dyly::initializeMainExecutable等。
    dyly::initializeMainExecutable中会触发libSystem_initializer->libdispatch_init->_os_object_init->_objc_init
    _objc_initlibObjc的方法,是对objc的各种初始化准备工作:
void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();     //初始化环境变量
    tls_init();         //初始化本地线程池
    static_init();      //初始化静态函数
    runtime_init();     //分类,类表
    exception_init();   //初始化异常处理相关 uncaught_handler
    cache_init();       //缓存相关处理
    _imp_implementationWithBlock_init(); //block初始化实现
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

这里面有一个比较重要的方法:

_dyld_objc_notify_register(&map_images, load_images, unmap_image);

Dyld在这里注册了3个回调:map_images,load_images,unmap_image,
关于load问题的答案就藏在load_images回调函数中,到此我们先看下堆栈信息:

截屏2020-10-22 上午11.53.19.png
_objc_init中我们只看到了注册回调,但是还不知道什么时候触发回调。通过对自定义类的load方法断点,查看堆栈信息发现,dyld::notifySingle()之后会触发load_image()->call_load_methods()->call_class_load()->[ZZStudent load]:
截屏2020-10-22 上午11.59.22.png
由此我们也就说明了load方法被触发的整个流程。其实这里我们可以先说一下应用程序的加载流程
  1. 为什么会先执行主类load,然后执行分类load?
    从问题1我们知道了关于load方法相关的内容是在load_images内部,所有我们要详细看下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();
}

从源码中我们可以看到2个核心的方法:

//准备load方法
prepare_load_methods((const headerType *)mh);
//调用load方法
call_load_methods();

来看下prepare_load_methods((const headerType *)mh);的实现:

void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertLocked();

    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);
    }
}

这里主要做了2件事:
获取classlist->schedule_class_load->add_class_to_loadable_list
获取categorylist->add_category_to_loadable_list
再看下void call_load_methods(void)

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;
}

从上面的代码,我们已经找到了答案,这里已经可以非常直观的看到在do while循环内先调用了call_class_loads(),然后执行call_category_loads(),所以主类的load方法先执行,然后是分类的load方法执行。
我们可以继续再看下call_class_loads()call_category_loads()的实现:

static void call_class_loads(void)
{
    int i;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, @selector(load));
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}
static bool call_category_loads(void)
{
    int i, shift;
    bool new_categories_added = NO;
    
    // Detach current loadable list.
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    int allocated = loadable_categories_allocated;
    loadable_categories = nil;
    loadable_categories_allocated = 0;
    loadable_categories_used = 0;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Category cat = cats[i].cat;
        load_method_t load_method = (load_method_t)cats[i].method;
        Class cls;
        if (!cat) continue;

        cls = _category_getClass(cat);
        if (cls  &&  cls->isLoadable()) {
            if (PrintLoading) {
                _objc_inform("LOAD: +[%s(%s) load]\n", 
                             cls->nameForLogging(), 
                             _category_getName(cat));
            }
            (*load_method)(cls, @selector(load));
            cats[i].cat = nil;
        }
    }
    //省略.......
}

都是通过遍历读取load_method_t,并调用(*load_method)(cls, @selector(load));,来触发了load方法的执行。
load_images流程图:

拓补图.009.jpeg
问题3:[self class]与[super class]
#import "ZZTeacher.h"

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

通常这个问题开始于输出结果的讨论,答案是输出结果是相同的。为什么呐?我们可以围绕下面几个问题展开回答:

  1. self,super 是什么?
    self:是oc 方法的隐藏参数1,还有一个隐藏参2是sel _cmd;
    super是一个关键字,super调用方法时会跳过本类,直接访问父类的同名方法。
  2. [self class][super class]的本质?
    通过clang来看下init代码段:
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_yy_htpy_x9s09v1zf7ms0jgytwr0000gn_T_LGTeacher_f84efe_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;
}

从编译后的源码可以看到:
[self class]最终转换为objc_msgSend(self,sel_registerName("class"));
[super class]最终转换为objc_msgSendSuper((objc_super){self,(id)class_getSuperclass(objc_getClass("LGTeacher"))},sel_registerName("class")) ),简化一下即class_getSuperclass((objc_super){self,superClass},sel)

OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

该方法接收两个参数:struct objc_super 结构体类型数据和sel
再看下struct 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 */
};

这里由receiver(消息接收者,这里是self)和super_class(父类self->isa->superClass)2个参数构成;
结构体的最后注释也说明了一切:

/* super_class is the first class to search */

父类是首先要查找的类。
这里我们也就看到了[super class]->objc_msgSendSuper({self,super_class},"class"),其中消息的接收者本质还是self,跟[self class]是一致的,所以返回的结果也是相同的。

  1. class方法做了什么?
    我们通过libObjc看下-(Class)class的实现:
- (Class)class {
    return object_getClass(self);
}
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

无论是[self class]还是[super class]最终都会定位到NSObjectclass方法中,从源码看本质是调用了object_getClass(self)方法,都是获取selfclass,即获取self->isa并返回,也就是当前实例对象的isa类对象(这里就是ZZTeacher类了),所以也证明了输出结果是相同的。

  1. 通过汇编进一步解释
    截屏2020-10-23 下午3.06.35.png
    我们会看到在运行时,super的调用发生了变化,跟我们通过clang后看到的稍微不一样,由原来的objc_msgSendSuper()->objc_msgSendSuper2指令,
    看下objc_msgSendSuper2的实现(这里基于objc-msg-arm64.s)
// no _objc_msgLookupSuper
ENTRY _objc_msgSendSuper2
UNWIND _objc_msgSendSuper2, NoFrame

ldp p0, p16, [x0]       // p0 = real receiver, p16 = class
ldr p16, [x16, #SUPERCLASS] // p16 = class->superclass
CacheLookup NORMAL, _objc_msgSendSuper2

END_ENTRY _objc_msgSendSuper2

从汇编的源码可以看到,最终p0 = receiver, p16 = class->superclass 然后跳转到CacheLookup指令开始从父类开始查找方法并调用。(关于CacheLookup指令之前已经详细探索过了,这里就不展开说了,可以参考objc_msgSend的本质)
这里再说一下[self class][super class]的简单查找流程:
[self class]->self->isa->cache_t->bits.data->methods(),如果没有找到,
->self->isa->superClass->cache_t........
[super class]->self->isa->superClass->cache_t->bits.data......
这里就是方法查找流程相关的内容了。
完。

问题4:内存平移问题?

首先我们定义了一个ZZPerson类,并添加了一些属性和方法

@interface ZZPerson : NSObject
@property (nonatomic,copy) NSString *zz_name;
- (void)saySomething;
@end
@implementation ZZPerson
- (void)saySomething
{
    NSLog(@"%s",__func__);
}
@end

ViewController中添加如下代码:

- (void)viewDidLoad {
    [super viewDidLoad];    
    Class cls = [ZZPerson class];
    void *test = &cls;
    [(__bridge id)test saySomething];

    ZZPerson *person = [ZZPerson alloc];
    [person saySomething];
}

问题一:test能否成功调起saySomething方法?
答案是肯定的,可以成功调用方法。

2020-10-26 15:05:21.852772+0800 TestMemoryShift[943:484379] -[ZZPerson saySomething]
2020-10-26 15:05:21.852900+0800 TestMemoryShift[943:484379] -[ZZPerson saySomething]

这里为什么呐?
首先person能调用saySomething方法是毋容置疑的,本质就是发送消息,并查找对应的IMP,即person通过isa,找到ZZPerson类,然后bits.data()->methods(),找到对应实现;
而这里的test指向的是cls,即ZZPerson类,最终都会到ZZPerson类里面,
对于编译器而言,两种方式的本质都是一样的,编译器将test也认为是一个对象,都是找到类查找方法,所以能调用到。指针指向入下图:

截屏2020-10-26 下午3.43.41.png
通过LLDB也可以很直观的看到:
截屏2020-10-26 下午3.45.25.png
testperson都指向ZZPerson的首地址0x10459d6d0
问题二:
我们修改下saySomething的打印
- (void)saySomething
{
    NSLog(@"%s,-%@",__func__,self.zz_name);
}

问:两次调用的打印结果是什么?为什么?
运行后我们看到打印结果:

2020-10-26 16:04:16.529901+0800 TestMemoryShift[955:494472] -[ZZPerson saySomething],-<ViewController: 0x10fe04720>
2020-10-26 16:04:16.530042+0800 TestMemoryShift[955:494472] -[ZZPerson saySomething],-(null)

[person saySomething]输出self.zz_name = (null)
[test saySomething]输出self.zz_name = <ViewController: 0x10fe04720>
这里关于person的输出(null)很好解释,因为zz_name没有赋值,但是test输出<ViewController: 0x10fe04720>就让人匪夷所思了。
这里我们就需要分析下viewDidLoad()代码块的整个栈帧排列了,首先我们要知道栈空间都是先进后出的排列方式,存放着各种指针数据;接下来我们转换一下viewDidLoad(),将一些隐藏参数都显示出来:

void viewDidLoad(id self, SEL _cmd) {
  objc_msgSendSuper({self , superClass} , sel_registerName("viewDidLoad"))
  Class cls = [ZZPerson class];
  void *test = &cls;
  [(__bridge id)test saySomething];
  ZZPerson *person = [ZZPerson alloc];
  [person saySomething];
}

栈帧分布图:

截屏2020-10-26 下午4.31.55.png
看到这个图之后,一切就变的一目了然了;
首先, [person saySomething],当前方法内的self = person对象,self.zz_name,也就是person通过平移8个字节去读取zz_name的值,此时zz_name没有赋值,所以为空;而[test saySomething],方法内的self=test,test本身大小只有8个字节,此时再想下查询就会读取到ViewController *self,所以就输出了ViewController对象。
通过LLDB看下test的内存分布情况:
(lldb) x/4gx test
0x16dd3dc58: 0x00000001020c96d8 0x0000000102707160
0x16dd3dc68: 0x00000001020c9610 0x00000001c799421e
(lldb) po 0x00000001020c96d8
ZZPerson

(lldb) po 0x0000000102707160
<ViewController: 0x102707160>

(lldb)

这里可以看到 test本身只有8字节并存储了ZZPerson,而下一个8字节就已经是<ViewController: 0x102707160>

总结

这里都是笔者自己的认知和理解,不对的地方请指出。

相关文章

网友评论

      本文标题:iOS-OC底层面试题汇总1

      本文链接:https://www.haomeiwen.com/subject/qbrumktx.html