美文网首页iOS复习资料
几个iOS基础题目总结

几个iOS基础题目总结

作者: 临川慕容吹雪 | 来源:发表于2018-07-30 10:44 被阅读327次

    前言:
    最近看到大佬汇集的iOS面试题,个人感觉还不错,打算试着探索一下这些问题的答案,也巩固一下我自己基础知识。这篇文章先总结一下基础知识的答案吧。其中有些错误或不全的地方望指教。

    -----------------------------------------持续更新中---------------------------------

    iOS 基础题

    1:分类和扩展有什么区别?可以分别用来做什么?分类有哪些局限性?分类的结构体里面有哪些成员?

    分类和扩展的作用

    1:category的主要作用是为已经存在的类添加方法
    下面也有其他作用可以了解下:
    2:可以把类的实现分开在几个不同的文件里面,
    (可以减少单个文件的体积
    可以把不同的功能组织到不同的category里
    可以由多个开发者共同完成一个类
    可以按需加载想要的category)
    3:模拟多继承
    4:把framework的私有方法公开
    扩展的作用:为一个类添加额外的原来没有变量,方法和属性

    类别与类扩展的区别

    1:extension在编译期决定,它就是类的一部分,
    在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,
    它伴随类的产生而产生,亦随之一起消亡。
    extension一般用来隐藏类的私有信息,
    你必须有一个类的源码才能为一个类添加extension,所以你无法为系统的类比如NSString添加extension
    但是category则完全不一样,它是在运行时候决定的.
    类扩展是在编译阶段被添加到类中,而类别是在运行时添加到类中。
    extension可以添加实例变量,而category是无法添加实例变量的
    2:类扩展中声明的方法没被实现,编译器会报警,但是类别中的方法没被实现编译器是不会有任何警告的。

    分类局限性

    (1)无法向类中添加新的实例变量。
    (2)名称冲突,即当类别中的方法与原始类方法名称冲突时,类别具有更高的优先级。
    (3)如果多个分类中都有和原有类中同名的方法, 那么调用该方法的时候执行谁由编译器决定;编译器会执行最后一个参与编译的分类中的方法

    在runtime层,category用结构体category_t

    typedef struct category_t {
        const char *name;
        classref_t cls;
        struct method_list_t *instanceMethods;
        struct method_list_t *classMethods;
        struct protocol_list_t *protocols;
        struct property_list_t *instanceProperties;
    } category_t;
    

    从源码中我们可以看出分类结构体成员:

    1)类的名字(name)
    2)类(cls)
    3)category中所有给类添加的实例方法的列表(instanceMethods)
    4)category中所有添加的类方法的列表(classMethods)
    5)category实现的所有协议的列表(protocols)
    6)category中添加的所有属性(instanceProperties)

    参考链接:
    分类和扩展说明参考
    美团关于分类的源码解析说明
    官方分类源码地址

    2:atomic的实现机制;为什么不能保证绝对的线程安全

    这个问题我觉得看这个就够了stackoverflow关于atomic和nonatomic的一个问题
    当然也可以看别人根据stackoverflow这个问题总结好的中文说明

    简单来说:atomic 会加一个锁来保障线程安全,也就是保证了读写操作是安全的,并且引用计数会 +1,来向调用者保证这个对象会一直存在.
    但是不能保证线程安全,比如当线程A setter操作时,这时B线程的setter操作会等待。当A线程的setter结束后,B线程进行setter操作,
    然后当A线程需要getter操作时,却有可能获得了在B线程中的值,这就破坏了线程安全

    3:哪些场景可以触发离屏渲染?

    首先我们要知道什么是离屏渲染:
    离屏渲染Off-Screen Rendering 指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。
    离屏渲染会先在屏幕外创建新缓冲区,离屏渲染结束后,再从离屏切到当前屏幕
    还有另外一种屏幕渲染方式-当前屏幕渲染On-Screen Rendering ,
    指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。
    以下方式会触发离屏幕渲染
    1:使用系统提供的圆角效果也会触发离屏渲染.(masksToBounds = true&&cornerRadius>0才会引发离屏渲染)
    2:重写drawRect
    3:layer.shadow(Shawdow 可以通过指定路径来取消离屏渲染)
    4:layer.mask(Mask 效果无法取消离屏渲染,使用混合图层的方法来模拟 mask 效果,性能各方面都是和无效果持平。)
    5:layer.allowsGroupOpacity(GroupOpacity 是指 CALayer 的allowsGroupOpacity属性,UIView 的alpha属性等同于 CALayer opacity属性,
    开启离屏渲染的条件是:layer.opacity != 1.0并且有子 layer 或者背景图。

    layer.allowsEdgeAntialiasing(该属性用于消除锯齿,离屏渲染条件旋转视图并且设置layer.allowsEdgeAntialiasing = true)
    6:layer.shouldRasterize(光栅化会触发离屏渲染,开启 Rasterization=true 后,GPU 只合成一次内容,然后复用合成的结果;合成的内容超过 100ms 没有使用会从缓存里移除,在更新内容时还会产生更多的离屏渲染。对于内容不发生变化的视图,原本拖后腿的离屏渲染就成为了助力)

    参考:
    离屏渲染优化详解
    Instruments性能优化-Core Animation
    绘制像素到屏幕上
    界面流畅性优化

    4:被weak修饰的对象在被释放的时候会发生什么?是如何实现的?知道sideTable么?里面的结构可以画出来么?

    释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录.
    objc_clear_deallocating该函数的动作如下:
    1、从weak表中获取废弃对象的地址为键值的记录
    2、将包含在记录中的所有附有 weak修饰符变量的地址,赋值为nil
    3、将weak表中该记录删除
    4、从引用计数表中删除废弃对象的地址为键值的记录
    SideTable 这个结构体主要用于管理对象的引用计数和 weak 表。在 NSObject.mm 中声明其数据结构:

    struct SideTable {
        spinlock_t slock;//保证原子操作的自旋锁
        RefcountMap refcnts;//引用计数的 hash 表
        weak_table_t weak_table;//weak 引用全局 hash 表
    
        SideTable() {
            memset(&weak_table, 0, sizeof(weak_table));
        }
    
        ~SideTable() {
            _objc_fatal("Do not delete SideTable.");
        }
    
        void lock() { slock.lock(); }
        void unlock() { slock.unlock(); }
        void forceReset() { slock.forceReset(); }
    
        // Address-ordered lock discipline for a pair of side tables.
    
        template<HaveOld, HaveNew>
        static void lockTwo(SideTable *lock1, SideTable *lock2);
        template<HaveOld, HaveNew>
        static void unlockTwo(SideTable *lock1, SideTable *lock2);
    };
    
    

    参考:
    objc-weak.mm源码
    weak 弱引用的实现方式
    iOS 底层解析weak的实现原理

    5:KVO的底层实现?如何取消系统默认的KVO并手动触发(给KVO的触发设定条件:改变的值符合某个条件时再触发KVO)?

    当你观察一个对象时,一个新的类会被动态创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象:值的更改。最后通过 isa 混写(isa-swizzling) 把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,
    对象就神奇的变成了新创建的子类的实例

    关闭默认的KVO重写方法

    +(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
        return NO;
    }//如果返回NO,KVO无法自动运作,需手动触发
    

    键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey
    在一个被观察属性发生改变之前, willChangeValueForKey: 一定会被调用,这就 会记录旧的值。而当改变发生后,
    observeValueForKey:ofObject:change:context: 会被调用,
    并且 didChangeValueForKey: 也会被调用。如果可以手动实现这些调用,就可以实现手动触发.

    参考:
    如何自己动手实现 KVO
    apple用什么方式实现对一个对象的KVO

    6:一个int变量被__block修饰与否的区别?

    Block不允许修改外部变量的值,这里所说的外部变量的值,指的是栈中指针的内存地址。
    __block 所起到的作用就是只要观察到该变量被 block 所持有。
    __block 后,实际上成为了一个结构体,block内截获了 该结构体的指针。
    在block中使用自动变量时,使用的是 指针指向的结构体中的 自动变量。
    ARC环境下,会被copy到堆上。(ARC环境下,一旦Block赋值就会触发copy,__block就会copy到堆上,Block也是__NSMallocBlock。
    ARC环境下也是存在__NSStackBlock的时候,这种情况下,__block就在栈上。)
    MRC环境下,只有copy,__block才会被复制到堆上,否则,__block一直都在栈上。

    测试,其实最好的方法是动手测试,这边我只测试了ARC环境下的。我在.main.m的测试代码如下:

      __block int a1 = 1;
            int a2 = 1;
            NSLog(@"__block定义前a1:%p", &a1);
            NSLog(@"__block定义前a2:%p", &a2);;
            void (^foo)(void) = ^{
                a1 = 2;
            
                NSLog(@"block内部a1:%p", &a1);
                NSLog(@"block内部a2:%p", &a2);
            };
            NSLog(@"重新定义后a1:%p", &a1);
            NSLog(@"重新定义后a2:%p", &a2);
            NSLog(@"foo =%@",foo);
            foo();
        ——---------------------- 输出结果如下:-------------------------------    
     
            __block定义前a1:0x7fff53814128 
            __block定义前a2:0x7fff5381410c  
            重新定义后a1:0x60400003dd98
            重新定义后a2:0x7fff5381410c
            foo =<__NSMallocBlock__: 0x60c000244830>
            block内部a1:0x60400003dd98
            block内部a2:0x60400025dbd8
    

    通知打印结果可以发现a1,a2blcok内部和定义前的地址字节数相差很大,堆地址要小于栈地址,又因为iOS中一个进程的栈区内存只有1M,Mac也只有8M,所以a1和a2在block内部都会被copy到堆上,只不过一个值的copy,一个是地址copy。

    然后clang -rewrite-objc main.m查看一下源码,如果clang -rewrite-objc报错,可以像我一样尝试
    xcrun -sdk iphonesimulator11.0 clang -rewrite-objc main.m

    源码如下:

    //加上__block 后,实际上成为了一个结构体,block内截获了 该结构体的指针
    struct __Block_byref_a1_0 {
      void *__isa;
    __Block_byref_a1_0 *__forwarding;
     int __flags;
     int __size;
     int a1;
    };
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int a2;
      ////截获的结构体指针
      __Block_byref_a1_0 *a1; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a2, __Block_byref_a1_0 *_a1, int flags=0) : a2(_a2), a1(_a1->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      //指针引用
      __Block_byref_a1_0 *a1 = __cself->a1; // bound by ref
      //a2只是单纯的值拷贝,。Block仅仅捕获了a2的值,并没有捕获a2的内存地址。
      int a2 = __cself->a2; // bound by copy
    
                (a1->__forwarding->a1) = 2;
    
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_dd_4kldckw11bv3zn6tgktzys440000gn_T_main_5a4382_mi_2, &(a1->__forwarding->a1));
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_dd_4kldckw11bv3zn6tgktzys440000gn_T_main_5a4382_mi_3, &a2);
            }
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a1, (void*)src->a1, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a1, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    

    从源码中可以看出:

    带有 __block的变量也被转化成了一个结构体__Block_byref_i_0,很清楚看到了__block的引用过程。
    而Block仅仅捕获了a2的值,并没有捕获a2的内存地址。所以在__main_block_func_0这个函数中即使我们重写这个自动变量a2的值,
    也无法改变Block外面自动变量a2的值

    参考:
    iOS中__block 关键字的底层实现原理
    深入研究Block捕获外部变量和__block实现原理

    7:为什么在block外部使用__weak修饰的同时需要在内部使用__strong修饰

    _weak是为了解决循环引用问题,(如果block和对象相互持有就会形成循环引用)
    而__strong在Block内部修饰的对象,会保证,在使用这个对象在block内,
    这个对象都不会被释放,strongSelf仅仅是个局部变量,存在栈中,会在block执行结束后回收,不会再造成循环引用。
    __strong主要是用在多线程中,防止对象被提前释放。

    参考:
    iOS __weak和__strong在Block中的使用

    题外话:

    有时候我们经常也会被问到block为什么 常使用copy关键字?

    官方中有如下一段话:

    block 应该用copy

    总结别人的话来说:

    block 使用 copy 是从 MRC遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区.在 ARC 中写不写都行:对于 block 使用 copy 还是 strong 效果是一样的,但写上 copy 也无伤大雅,还能时刻提醒我们:编译器自动对 block 进行了 copy 操作。
    如果不写 copy ,该类的调用者有可能会忘记或者根本不知道“编译器会自动对 block 进行了 copy 操作”

    8:讲一下对象,类对象,元类,跟元类结构体的组成以及他们是如何相关联的?为什么对象方法没有保存的对象结构体里,而是保存在类对象的结构体里.

    对象isa指向类对象,类对象的isa指向元类。元类isa指向根元类。
    根元类的isa指针指向自己,superclass指针指向NSObject类
    实例对象结构体只有一个isa变量,指向实例对象所属的类。
    类对象有isa,superclass,方法,属性,协议列表,以及成员变量的
    描述。
    所有的对象调用方法都是一样的,没有必要存在对象中,对象可以有
    无数个,类对象就有一个所以只需存放在类对象中

    可以从官方objc.h源码里面找到实例定义

    
    /// Represents an instance of a class.
    struct objc_object {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    };
    

    可以在runtime.h里面找到类对象的定义

    struct objc_class {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    
    #if !__OBJC2__
        //向该类所继承的父类对象 
        Class _Nullable super_class                              OBJC2_UNAVAILABLE;
        const char * _Nonnull name                               OBJC2_UNAVAILABLE;
        long version                                             OBJC2_UNAVAILABLE;
        long info                                                OBJC2_UNAVAILABLE;
        long instance_size                                       OBJC2_UNAVAILABLE;
        //成员变量列表   
        struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
        //方法列表
        struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;//方法列表
        //用于缓存调用过的方法    
        struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
        //协议链表用来存储声明遵守的正式协议
        struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
    #endif
    
    }
    

    参考:
    iOS开发·runtime原理与实践: 基本知识篇
    一个objc对象如何进行内存布局

    9:iOS 中内省的几个方法?class方法和objc_getClass方法有什么区别?

    题外话:原谅我看了这道面试题,第一次听说内省,才疏学浅,太菜了,只能好好搜索学习了一番。

    内省是对象揭示自己作为一个运行时对象的详细信息的一种能力。包括对象在继承树上的位置,对象是否遵循特定的协议,以及是否可以响应特定的消息。NSObject协议和类定义了很多内省方法,用于查询运行时信息,以便根据对象的特征进行识别。
    isKindOfClass:Class
    检查对象是否是那个类或者其继承类实例化的对象
    isMemberOfClass:Class
    检查对象是否是那个类但不包括继承类而实例化的对象
    respondToSelector:selector
    检查对象是否包含这个方法
    conformsToProtocol:protocol
    检查对象是否符合协议,是否实现了协议中所有的必选方法。
    object_getClass(obj)返回的是obj中的isa指针;
    而[obj class]则分两种情况:
    一:当obj为实例对象时,
    [obj class]中class是实例方法:- (Class)class,
    返回的obj对象中的isa指针,返回的是类对象;
    二:当obj为类对象(包括元类和根类以及根元类)时,调用的是类方法:+ (Class)class,返回的结果为其本身

    可以在ViewController通过简单代码验证一下

    //currentClass现在是类对象   
    Class currentClass = [self class];
         //都指向实例对象isa指定的类对象
         NSLog(@"currentClass = %p getClass=%p",currentClass ,object_getClass(self));
         //class指向类对象本身  getClass指向类对象isa指向元类
         NSLog(@"currentClass = %p  getClass=%p",[currentClass class],object_getClass(currentClass));
          const char *getClassName = object_getClassName(currentClass);
         //实例对象指向类,类执行元类,元类指向根元类,根元类指向自己
         for (int i = 1; i < 5; i++) {
             NSLog(@"Following the isa pointer %d times gives %p %@---%s", i, currentClass,currentClass,getClassName);
             currentClass = object_getClass(currentClass);
             getClassName = object_getClassName(currentClass);
         }
    

    输出结果如下:

    currentClass = 0x10ab29198 getClass=0x10ab29198
    currentClass = 0x10ab29198  getClass=0x10ab291c0
    Following the isa pointer 1 times gives 0x10ab29198 ViewController---ViewController
    Following the isa pointer 2 times gives 0x10ab291c0 ViewController---NSObject
    Following the isa pointer 3 times gives 0x10b819e58 NSObject---NSObject
    Following the isa pointer 4 times gives 0x10b819e58 NSObject---NSObject
    
    

    参考
    Objective-C的内省(Introspection)小结

    10:RunLoop的作用是什么?它的内部工作机制了解么?(最好结合线程和内存管理来说)

    这一块平时用的比较少,了解不是很多。其有时间真的好好静下心来看一下相关东西了。

    字面意思是“消息循环、运行循环”,runloop内部实际上就是一个do-while循环,它在循环监听着各种事件源、消息,对他们进行管理并分发给线程来执行。
    线程和 RunLoop 之间是一一对应的。
    运行机制从官方文档说明
    翻译过来如下:
    1.通知观察者将要进入运行循环。
    2.通知观察者将要处理计时器。
    3.通知观察者任何非基于端口的输入源即将触发。
    4.触发任何准备触发的基于非端口的输入源。
    5.如果基于端口的输入源准备就绪并等待触发,请立即处理该事件。转到第9步。
    6.通知观察者线程即将睡眠。
    7.将线程置于睡眠状态,直到发生以下事件之一:

    • 事件到达基于端口的输入源。
    • 计时器运行。
    • 为运行循环设置的超时值到期。
    • 运行循环被明确唤醒。

    8.通知观察者线程被唤醒。
    9.处理待处理事件。

    • 如果触发了用户定义的计时器,则处理计时器事件并重新启动循环。转到第2步。
    • 如果输入源被触发,则传递事件。
    • 如果运行循环被明确唤醒但尚未超时,请重新启动循环。转到第2步。

    10.通知观察者运行循环已退出。
    这里借用一下这里的图片

    RunLoop_1.png

    参考

    深入理解RunLoop
    关于Runloop的原理探究及基本使用

    11:谈谈消息转发机制实现

    先会调用objc_msgSend方法,首先在Class中的缓存查找IMP,没有缓存则初始化缓存。如果没有找到,则向父类的Class查找。如果一直查找到根类仍旧没有实现,则执行消息转发。
    1、调用resolveInstanceMethod:方法。允许用户在此时为该Class动态添加实现。如果有实现了,则调用并返回YES,重新开始objc_msgSend流程。这次对象会响应这个选择器,一般是因为它已经调用过了class_addMethod。如果仍没有实现,继续下面的动作。
    2、调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,返回非nil对象。否则返回nil,继续下面的动作。注意这里不要返回self,否则会形成死循环。
    3、调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。如果能获取,则返回非nil;传给一个NSInvocation并传给forwardInvocation:
    4、调用forwardInvocation:方法,将第三步获取到的方法签名包装成Invocation传入,如何处理就在这里面了,并返回非nil。
    5、调用doesNotRecognizeSelector:,默认的实现是抛出异常。如果第三步没能获得一个方法签名,执行该步骤 。

    参考:
    Objective-C 消息发送与转发机制原理
    深入浅出理解消息的传递和转发机制
    -----------------------------------------未完待续-----------------------------------

    相关文章

      网友评论

        本文标题:几个iOS基础题目总结

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