美文网首页
iOS开发 常备的底层面试题合集

iOS开发 常备的底层面试题合集

作者: UILabelkell | 来源:发表于2019-10-31 11:46 被阅读0次

    一、Runtime

    1、iOS 一个objc对象的isa的指针指向什么?有什么作用?

    答:指向他的类对象,从而可以找到对象上的方法

    2、一个 NSObject 对象占用多少内存空间?一个 NSObject 对象占用多少内存空间?

    • 受限于内存分配的机制,一个 NSObject对象都会分配 16byte 的内存空间。
    • 但是实际上在 64位 下,只使用了 8byte;
    • 在32位下,只使用了 4byte
    • 一个 NSObject 实例对象成员变量所占的大小,实际上是 8 字节

    3、说一下对 class_rw_t 的理解?

    • rw代表可读可写。
    • ObjC 类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t 中:
    // 可读可写
    struct class_rw_t {
        // Be warned that Symbolication knows the layout of this structure.
        uint32_t flags;
        uint32_t version;
    
        const class_ro_t *ro; // 指向只读的结构体,存放类初始信息
    
        /*
         这三个都是二位数组,是可读可写的,包含了类的初始内容、分类的内容。
         methods中,存储 method_list_t ----> method_t
         二维数组,method_list_t --> method_t
         这三个二位数组中的数据有一部分是从class_ro_t中合并过来的。
         */
        method_array_t methods; // 方法列表(类对象存放对象方法,元类对象存放类方法)
        property_array_t properties; // 属性列表
        protocol_array_t protocols; //协议列表
    
        Class firstSubclass;
        Class nextSiblingClass;
        
        //...
        }
    

    4、说一下对 class_ro_t 的理解?

    存储了当前类在编译期就已经确定的属性、方法以及遵循的协议。

    struct class_ro_t {  
        uint32_t flags;
        uint32_t instanceStart;
        uint32_t instanceSize;
        uint32_t reserved;
    
        const uint8_t * ivarLayout;
    
        const char * name;
        method_list_t * baseMethodList;
        protocol_list_t * baseProtocols;
        const ivar_list_t * ivars;
    
        const uint8_t * weakIvarLayout;
        property_list_t *baseProperties;
    };
    baseMethodList,baseProtocols,ivars,baseProperties三个都是以为数组。
    
    

    5、对 isa 指针的理解

    对 isa 指针的理解, 对象的isa 指针指向哪里?isa 指针有哪两种类型?

    • isa 等价于 is kind of
    • 实例对象 isa 指向类对象
    • 类对象指 isa 向元类对象
    • 元类对象的 isa 指向元类的基类

    isa 有两种类型

    纯指针,指向内存地址
    NON_POINTER_ISA,除了内存地址,还存有一些其他信息

    6、 Runtime 的方法缓存?存储的形式、数据结构以及查找的过程?

    • cache_t增量扩展的哈希表结构。哈希表内部存储的 bucket_t。

    • bucket_t 中存储的是 SEL 和 IMP的键值对。

    • 如果是有序方法列表,采用二分查找

    • 如果是无序方法列表,直接遍历查找

    cache_t结构体

    // 缓存曾经调用过的方法,提高查找速率
    struct cache_t {
        struct bucket_t *_buckets; // 散列表
        mask_t _mask; //散列表的长度 - 1
        mask_t _occupied; // 已经缓存的方法数量,散列表的长度使大于已经缓存的数量的。
        //...
    }
    
    struct bucket_t {
        cache_key_t _key; //SEL作为Key @selector()
        IMP _imp; // 函数的内存地址
        //...
    }
    
    散列表查找过程,在objc-cache.mm文件中
    
    // 查询散列表,k
    bucket_t * cache_t::find(cache_key_t k, id receiver)
    {
        assert(k != 0); // 断言
    
        bucket_t *b = buckets(); // 获取散列表
        mask_t m = mask(); // 散列表长度 - 1
        mask_t begin = cache_hash(k, m); // & 操作
        mask_t i = begin; // 索引值
        do {
            if (b[i].key() == 0  ||  b[i].key() == k) {
                return &b[i];
            }
        } while ((i = cache_next(i, m)) != begin);
        // i 的值最大等于mask,最小等于0。
    
        // hack
        Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
        cache_t::bad_cache(receiver, (SEL)k, cls);
    }
    
    上面是查询散列表函数,其中cache_hash(k, m)是静态内联方法,将传入的key和mask进行&操作返回uint32_t索引值。do-while循环查找过程,当发生冲突cache_next方法将索引值减1。
    
    

    7、使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?

    无论在MRC下还是ARC下均不需要,被关联的对象在生命周期内要比对象本身释放的晚很多,它们会在被 NSObject -dealloc 调用的object_dispose()方法中释放。

    8、什么是method swizzling(俗称黑魔法)

    简单说就是进行方法交换

    在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。
    每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系,selector的本质其实就是方法名,IMP有点类似函数指针,指向具体的Method实现,通过selector就可以找到对应的IMP。
    换方法的几种实现方式

    • 利用 method_exchangeImplementations 交换两个方法的实现
    • 利用 class_replaceMethod替换方法的实现
    • 利用 method_setImplementation 来直接设置某个方法的IMP。

    9、什么时候会报unrecognized selector的异常?

    objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果,在最顶层的父类中依然找不到相应的方法时,会进入消息转发阶段,如果消息三次转发流程仍未实现,则程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX 。

    10、objc中向一个nil对象发送消息将会发生什么?

    如果向一个nil对象发送消息,首先在寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误。也不会崩溃。
    详解:

    如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil);

    如果方法返回值为指针类型,其指针大小为小于或者等于sizeof(void*) ,float,double,long double 或者long long的整型标量,发送给nil的消息将返回0;

    如果方法返回值为结构体,发送给nil的消息将返回0。结构体中各个字段的值将都是0;

    如果方法的返回值不是上述提到的几种情况,那么发送给nil的消息的返回值将是未定义的。

    11、isKindOfClass 与 isMemberOfClass

    @interface Sark : NSObject
    @end
    @implementation Sark
    @end
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
            BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
            BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
            BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];
            NSLog(@"%d %d %d %d", res1, res2, res3, res4);
        }
        return 0;
    }
    答案:1000
    
    详解:

    在isKindOfClass中有一个循环,先判断class是否等于meta class,不等就继续循环判断是否等于meta class的super class,不等再继续取super class,如此循环下去。

    [NSObject class]执行完之后调用isKindOfClass,第一次判断先判断NSObject和 NSObject的meta class是否相等,之前讲到meta class的时候放了一张很详细的图,从图上我们也可以看出,NSObject的meta class与本身不等。接着第二次循环判断NSObject与meta class的superclass是否相等。还是从那张图上面我们可以看到:Root class(meta) 的superclass就是 Root
    class(class),也就是NSObject本身。所以第二次循环相等,于是第一行res1输出应该为YES。

    同理,[Sark class]执行完之后调用isKindOfClass,第一次for循环,Sark的Meta Class与[Sark class]不等,第二次for循环,Sark Meta Class的super class 指向的是 NSObject Meta Class, 和Sark Class不相等。第三次for循环,NSObject Meta Class的super class指向的是NSObject Class,和 Sark Class 不相等。第四次循环,NSObject Class 的super class 指向 nil, 和 Sark Class不相等。第四次循环之后,退出循环,所以第三行的res3输出为NO。

    isMemberOfClass的源码实现是拿到自己的isa指针和自己比较,是否相等。

    第二行isa 指向 NSObject 的 Meta Class,所以和 NSObject Class不相等。第四行,isa指向Sark的Meta Class,和Sark Class也不等,所以第二行res2和第四行res4都输出NO。

    12、为什么 NSTimer 有时候不好使?

    因为创建的 NSTimer 默认是被加入到了 defaultMode,所以当 Runloop 的 Mode 变化时,当前的 NSTimer 就不会工作了。

    13、多线程面试题(任务、队列)

    任务

    • 就是执行操作的意思,也就是在线程中执行的那段代码。在 GCD 中是放在 block 中的。执行任务有两种方式:同步执行(sync)和异步执行(async)

    • 同步(Sync):同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行,即会阻塞线程。只能在当前线程中执行任务(是当前线程,不一定是主线程),不具备开启新线程的能力。

    • 异步(Async):线程会立即返回,无需等待就会继续执行下面的任务,不阻塞当前线程。可以在新的线程中执行任务,具备开启新线程的能力(并不一定开启新线程)。如果不是添加到主队列上,异步会在子线程中执行任务

    队列

    • 队列(Dispatch Queue):这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务
      在 GCD 中有两种队列:串行队列和并发队列。两者都符合 FIFO(先进先出)的原则。两者的主要区别是:执行顺序不同,以及开启线程数不同。

    • 串行队列(Serial Dispatch Queue):
      同一时间内,队列中只能执行一个任务,只有当前的任务执行完成之后,才能执行下一个任务。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)。主队列是主线程上的一个串行队列,是系统自动为我们创建的

    • 并发队列(Concurrent Dispatch Queue):
      同时允许多个任务并发执行。(可以开启多个线程,并且同时执行任务)。并发队列的并发功能只有在异步(dispatch_async)函数下才有效

    14、什么是死锁?

    死锁就是队列引起的循环等待

    15、GCD任务执行顺序

    1、串行队列先异步后同步

    dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
        
        NSLog(@"1");
        
        dispatch_async(serialQueue, ^{
            
             NSLog(@"2");
        });
        
        NSLog(@"3");
        
        dispatch_sync(serialQueue, ^{
            
            NSLog(@"4");
        });
        
        NSLog(@"5");
    
    打印顺序是13245
    原因是:
    首先先打印1
    接下来将任务2其添加至串行队列上,由于任务2是异步,不会阻塞线程,继续向下执行,打印3
    然后是任务4,将任务4添加至串行队列上,因为任务4和任务2在同一串行队列,根据队列先进先出原则,任务4必须等任务2执行后才能执行,又因为任务4是同步任务,会阻塞线程,只有执行完任务4才能继续向下执行打印5
    所以最终顺序就是13245。
    这里的任务4在主线程中执行,而任务2在子线程中执行。
    如果任务4是添加到另一个串行队列或者并行队列,则任务2和任务4无序执行(可以添加多个任务看效果)
    
    

    相关文章

      网友评论

          本文标题:iOS开发 常备的底层面试题合集

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