美文网首页iOS问题集
问题集 基础部分

问题集 基础部分

作者: 9d8c8692519b | 来源:发表于2018-11-27 14:53 被阅读42次

    基础题部分

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

    3: 被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的实现原理

    4: 关联对象有什么应用,系统如何管理关联对象?其被释放的时候需要手动将所有的关联对象的指针置空么?

    关联对象(AssociatedObject)是Objective-C 2.0运行时的一个特性,允许开发者对已经存在的类在扩展中添加自定义的属性。在实际生产过程中,比较常用的方式是给分类(Category)添加成员变量。

    关联对象有什么应用?

    关联对象可以在运行时给指定对象绑定一个有生命周期的变量。
    1.由于不改变原类的实现,所以可以给原生类或者是打包的库进行扩展,一般配合Category实现完整的功能。
    2.ObjC类定义的变量,由于runtime的特性,都会暴露到外部,使用关联对象可以隐藏关键变量,保证安全。
    3.可以用于KVO,使用关联对象作为观察者,可以避免观察自身导致循环。

    系统如何管理关联对象?

    系统通过管理一个全局哈希表,通过对象指针地址和传递的固定参数地址来获取关联对象。根据setter传入的参数协议,来管理对象的生命周期。

    其被释放的时候需要手动将其指针置空么?

    当对象被释放时,如果设置的协议是OBJC_ASSOCIATION_ASSIGN,那么他的关联对象不会减少引用计数,其他的协议都会减少从而释放关联对象。
    unsafe_unretain一般认为外部有对象控制,所以对象不用处理,因此不管什么协议,对象释放时都无需手动讲关联对象置空。

    AssociatedObject关联对象原理实现

    5: KVO的底层实现?如何自己动手实现 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: Autoreleasepool所使用的数据结构是什么?AutoreleasePoolPage结构体了解么

    AutoreleasePool是一个堆栈,里面装着指针。那么栈的底层实现是什么呢?是数组。
    AutoreleasePool全名叫NSAutoreleasePool。它就是一个对象引用计数自动处理器,在官方文档中被称为是一个类。

    AutoreleasePoolPage的数据结构如下:
    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
    

    AutoreleasePool没有单独的数据结构,是由AutoreleasePage组成的双向数据链表,每一个AutoreleasePage对应一个thread,前向节点为parent,后向节点为child,next为page内部对象的指针,指向当前对象。

    AutoreleasePoolPage::push()
    static inline void *push() 
    {
        id *dest;
        if (DebugPoolAllocation) {
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }
    

    对链表进行查询,将新生成的page赋给当前page的child。

    AutoreleasePoolPage::pop()
    static inline void pop(void *token) 
    {
        AutoreleasePoolPage *page;
        id *stop;
    
        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
            // Popping the top-level placeholder pool.
            if (hotPage()) {
                // Pool was used. Pop its contents normally.
                // Pool pages remain allocated for re-use as usual.
                pop(coldPage()->begin());
            } else {
                // Pool was never used. Clear the placeholder.
                setHotPage(nil);
            }
            return;
        }
    
        page = pageForPointer(token);
        stop = (id *)token;
        if (*stop != POOL_BOUNDARY) {
            if (stop == page->begin()  &&  !page->parent) {
                // Start of coldest page may correctly not be POOL_BOUNDARY:
                // 1. top-level pool is popped, leaving the cold page in place
                // 2. an object is autoreleased with no pool
            } else {
                // Error. For bincompat purposes this is not 
                // fatal in executables built with old SDKs.
                return badPop(token);
            }
        }
    
        if (PrintPoolHiwat) printHiwat();
    
        page->releaseUntil(stop);
    
        // memory: delete empty children
        if (DebugPoolAllocation  &&  page->empty()) {
            // special case: delete everything during page-per-pool debugging
            AutoreleasePoolPage *parent = page->parent;
            page->kill();
            setHotPage(parent);
        } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
            // special case: delete everything for pop(top) 
            // when debugging missing autorelease pools
            page->kill();
            setHotPage(nil);
        } 
        else if (page->child) {
            // hysteresis: keep one empty child if page is more than half full
            if (page->lessThanHalfFull()) {
                page->child->kill();
            }
            else if (page->child->child) {
                page->child->child->kill();
            }
        }
    }
    

    AutoreleasePool 浅析
    autoreleasepool 源码解析

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

    对象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对象如何进行内存布局

    8: class_ro_t 和 class_rw_t 的区别?

    class_ro_t与class_rw_t的最大区别在于一个是只读的,一个是可读写的,实质上ro就是readonly的简写,rw是readwrite的简写。

    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: 一个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实现原理

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

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

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

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

    官方中有如下一段话:

    copy.png

    总结别人的话来说:

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

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

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

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

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

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

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

    RunLoop .png

    参考

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

    13:谈谈消息转发机制实现。

    先会调用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 消息发送与转发机制原理
    深入浅出理解消息的传递和转发机制

    14: 哪些场景可以触发离屏渲染?(知道多少说多少)

    官方公开的的资料里关于离屏渲染的信息最早是在 2011年的 WWDC, 在多个 session 里都提到了尽量避免会触发离屏渲染的效果,包括:mask, shadow, group opacity, edge antialiasing。

    shouldRasterize(光栅化)
    
    masks(遮罩)
    
    shadows(阴影)
    
    edge antialiasing(抗锯齿)
    
    group opacity(不透明)
    
    复杂形状设置圆角等
    
    渐变
    
    Text(UILabel, CATextLayer, Core Text, etc)...
    

    相关文章

      网友评论

        本文标题:问题集 基础部分

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