iOS-OC底层面试题汇总1

前言

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

问题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>

总结

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343