复习-OC常见面试题

OC语言相关

分类Category
  1. 使用场景

    • 可以减少单个类的体积,降低耦合性,同一个类可以多人进行开发
    • 可以为系统类添加分类进行拓展
    • 模拟多继承
    • 把静态库的私有方法公开
  2. 特点

    • 分类可以添加属性,但是并不会自动生成成员变量及set/get方法。
      因为category_t结构体中并不存在成员变量。
      成员变量是存放在实例对象中的,并且编译的那一刻就已经决定好了。
      而分类是在运行时才去加载的。那么我们就无法再程序运行时将分类的成员变量中添加到实例对象的结构体中。因此分类中不可以添加成员变量。
    • 如果分类中有和原有类同名的方法, 会优先调用分类中的方法, 就是说会忽略原有类的方法。
    • 如果多个分类中都有和原有类中同名的方法, 那么调用该方法的时候执行谁由编译器决定,编译器会执行最后一个参与编译的分类中的方法。
  3. 为什么分类中不能增加实例变量?
    category本身是个结构体,而结构体没有设计实例变量,对应也就没有用于存储的内存空间,无法在类内部进行存储。
    @property 本身就是用于两个方法(setter、getter),当实例变量没地方存储时,对应也就取不到值

  4. 为什么覆盖原方法

    • Category是在runtime时候加载,而不是在编译的时候
    • 结构体中有存储实例方法的列表、存储类方法的列表、存储协议的和属性的列表
    • 方法的调用就是消息的发送,如果调用实例方法,就是通过isa去类对象中查找对应的方法,如果调用类方法、就是通过isa去元类对象中查找对应的方法。
    • 当分类、原来类、原来类的父类中有相同方法时,方法调用的优先级:分类(最后参与编译的分类优先) –> 原来类 –> 父类,即先去调用分类中的方法,分类中没这个方法再去原来类中找,原来类中没有再去父类中找。
  5. 多个分类中有同名方法如何生效?

    • 这个是与编译顺序有关,最后编译的分类中对应的信息会在整合在类或元类对应列表的最前边。所以是调用最后编译的分类中的方法!可以查看Build Phases ->Complie Source 中的编译顺序!
  6. 分类的加载流程?
    1)runtime初始化入口函数(objc-os.mm)
    2)objc-runtime-new.mm中的map_images函数
    3)_read_images函数内部通过dyld搜索Category并加载分类
    4) 来自objc-os.mm文件的 _dyld_objc_notify_register()函数调用
    5)通过attachCategories函数将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面
    分类加载流程详细
    category加载流程(懒加载和非懒加载load方法的不同)

  7. 主类和分类中有同名方法,如何调用主类方法?

    • 类方法,因为分类并没有覆盖主类的同名方法,只是Category的方法排在方法列表前面,而主类的方法被移到了方法列表的后面。 我们可以利用Runtime提供的API,从方法列表里拿回原方法,从而调用。如下所示
-(void) exchangeMainCategoryMethod:(id)target selector:(SEL)selector{
    // Get the class method list
    uint count;
    Method *methodList = class_copyMethodList([target class], &count);
    
    Method firstMethod = nil ;
    Method lastMethod = nil ;
    for (int i = 0; i < count; i++) {
        Method method = methodList[i];
        NSLog(@"Category catch selector : %d %@", i, NSStringFromSelector(method_getName(method)));
        SEL name = method_getName(method);
        if (name == selector) {
            if(!firstMethod){
                firstMethod = method ;
                lastMethod = method ;
            }else{
                lastMethod = method ;
            }
        }
    }
    if(firstMethod && lastMethod && firstMethod!=lastMethod){
        method_exchangeImplementations(firstMethod, lastMethod) ;
    }
}

在调用方法前,先将主类和分类的IMP交换,将主类的IMP放在前面,分类的IMP放在后面,就会优先执行主类的方法。方法调用完后,再将主类和分类的IMP交换,恢复初始的IMP顺序。

  • 如果是实例方法,分类同名方法会覆盖主类的方法,获取到的IMP永远是一个,,所以只能调用分类的方法,没有办法解决
关联对象
  1. 本质,有几层,如何运作?
    在OC中,runtime提供了动态添加属性和获得属性的API:objc_setAssociatedObjectobjc_getAssociatedObject

至少是2层

关联对象OC底层实现
iOS 关联对象的实现原理

  1. 关联对象的key为什么void

用void 来修饰key,更小的分配内存

扩展Extension
  1. 使用场景
    • 声明私有属性
    • 声明私有方法
    • 声明私有成员变量
  2. 特点
    • 它是编译时决议
    • 只以声明的形式存在,一般都写在.m中
    • 不能为系统类添加扩展
  3. 扩展与分类的区别
    • 分类有名字,扩展没有名字,是一个匿名的分类
    • 分类是运行时决议,而扩展是编译时决议;
      所以分类中的方法没有实现不会警告,而扩展声明的方法不实现会被警告。
    • 分类原则上可以增加属性,实例方法,类方法,而且外部类是可以访问的。扩展能添加属性,方法,实例变量,默认是不对外公开的。
    • 分类有自己的实现部分,扩展没有自己的实现部分,只能依赖对应的类本身来实现。
    • 可以为系统类添加分类,而不能为系统类添加扩展。
协议Protocol

iOS 之 Protocol 详解

  1. 可以定义哪些?

  2. 用Swift 将协议(protocol)中的部分方法设计成可选(optional),该怎样实现?
    @optional@required是Objective-C中特有的关键字。
    Swift中,默认所有方法在协议中都是必须实现的。而且,协议里方法不可以直接定义optional。先给出两种解决方案:

    • 在协议和方法前都加上@objc关键字,然后再在方法前加上 optional关键字。该方法实际上是把协议转化为Objective-C的方式然后进行可选定义
    • 用扩展(extension)来规定可选方法。Swift中,协议扩展(protocol extension)可以定义部分方法的默认实现,这样这些方法在实际调用中就是可选实现的了
      用Swift 将协议(protocol)中的部分方法设计成可选(optional)
NSNotification

一文全解iOS通知机制

KVO

iOS-底层原理 23:KVO 底层原理
iOS 设计模式(五)-KVO 详解

属性关键字
  1. 属性关键字有哪些
    iOS 属性关键字
  2. 对比weak和assign
    weak和assign的区别-正确使用weak、assign
  3. weak是如何将指针置为nil?
    iOS weak指针置nil具体过程
  4. 深拷贝 浅拷贝
    iOS-深拷贝和浅拷贝

OC内存管理相关

内存五大分区

堆区、栈区、全局区、常量区、代码区

内存管理方案
  1. 有哪些方案,分别是用于什么场景?
    • TaggedPointer
      对一些小对象,如NSNumber等,采用的是TaggedPointer这种内存管理方案。
    • NONPOINTER_ISA
      对于64位架构下的iOS应用程序采用的是NONPOINTER_ISA这种内存管理方案。
      在64位架构下,ISA这个指针本身是占64个bit位的,但其实有32位或者40位就够用了,剩余的bit位其实是浪费的,苹果为了提高内存的利用率,在iSA剩余的这些bit位当中,存储了一些关于内存管理方面的相关内容,这个叫非指针型的ISA。
    • 散列表
      是一种很复杂的结构,其中包含了引用计数表和弱引用表。

iOS内存布局&内存管理方案&数据结构

  1. 介绍NONOPINTER_ISA
    isa分为POINTER_ISA(指针类型)和NONPOINTER_ISA(非指针类型)

POINTER_ISA指针类型只有一个内存地址

NONPOINTER_ISA除了有地址,还包含其他字段:

//arm64 架构
struct 
{
    uintptr_t nonpointer        : 1;  // 0:普通指针,1:优化过,使用位域存储更多信息
    uintptr_t has_assoc         : 1;  // 对象是否含有或曾经含有关联引用,如果没有,则析构时会更快
    uintptr_t has_cxx_dtor      : 1;  // 表示是否有C++析构函数或OC的dealloc,如果没有,则析构时会更快
    uintptr_t shiftcls          : 33; // 类的指针,存放着 Class、Meta-Class 对象的内存地址信息
    uintptr_t magic             : 6;  // 固定值为 0xd2,用于在调试时分辨对象是否未完成初始化
    uintptr_t weakly_referenced : 1;  // 是否被弱引用指向,如果没有,则析构时更快
    uintptr_t deallocating      : 1;  // 对象是否正在释放
    uintptr_t has_sidetable_rc  : 1;  // 是否需要使用 sidetable 来存储引用计数
    uintptr_t extra_rc          : 19;  // 引用计数能够用 19 个二进制位存储时,直接存储在这里
  };

has_sidetable_rc表明该对象的引用计数器是否过大而无法储存到isa指针,如果过大,则其会存入相应的sideTable(散列表)中,正常则存入extra_rc中,且extra_rc保存的是引用计数减1后的结果

  1. 介绍散列表(哈希表),如何解决冲突
  2. sideTable结构,有哪些SideTable?
  3. 为什么不是一个sideTable,用了什么方案?
    iOS内存布局&内存管理方案&数据结构
弱引用表
  1. 介绍
    iOS底层原理探索 -- 内存管理之弱引用表
  2. weak从引用到置于nil的整个流程
    • 释放时,调⽤clearDeallocating函数。clearDeallocating函数⾸先根据对象地址
      获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个
      entry从weak表中删除,最后清理对象的记录

    • 1.实现weak后,为什么对象释放后会⾃动为nil?
      runtime对注册的类, 会进⾏布局,对于weak对象会放⼊⼀个hash表中。
      weak指向的对象内存地址作为key,当此对象的引⽤计数为0的时候会dealloc,假如weak指向的对象内存地址是a,那么就会以a为键, 在这个weak表中搜索,找到所有以a为键的weak对象,从⽽设置为nil
      追问的问题⼆:
      2.当weak引⽤指向的对象被释放时,⼜是如何去处理weak指针的呢?
      1、调⽤objc_release
      2、因为对象的引⽤计数为0,所以执⾏dealloc
      3、在dealloc中,调⽤了_objc_rootDealloc函数
      4、在_objc_rootDealloc中,调⽤了object_dispose函数
      5、调⽤objc_destructInstance
      6、最后调⽤objc_clear_deallocating,详细过程如下:
      a. 从weak表中获取废弃对象的地址为键值的记录
      b. 将包含在记录中的所有附有 weak修饰符变量的地址,赋值为 nil
      c. 将weak表中该记录删除
      d. 从引⽤计数表中删除废弃对象的地址为键值的记录
      weak的一生

引用计数
  1. 什么是引用计数?

    • 引用计数是一个简单而有效的管理对象生命周期的方式
    • 当我们创建一个新对象时,它的引用计数为1,当有一个新的指针指向这个对象时,引用计数+1,当指针不在指向这个对象时,我们将其引用计数-1,当对象的引用计数变为0时,说明这个对象不再被任何指针指向了,这个时候我们就可以将这个对象销毁,回收内存。
  2. alloc的对象引用计数为多少?打印出来的话,引用计数为多少?为什么?

    • 引用计数的总值就是isa里面extra_rc的取值和散列表中引用计数表的取值外加1,我们新alloc的对象,引用计数打印为1就是因为这个加的这个1,其实新alloc出来的对象引用计数为0
      iOS-内存管理(二)-引用计数
Dealloc
  1. 具体描述dealloc的流程


    image.png

iOS Dealloc流程解析 Dealloc 实现原理

  1. 关联对象和弱引用对象需要我们手动清理吗?为什么?
    • 不需要
    • 1.执行了object_cxxDestruct函数
      2.执行_object_remove_assocations,去除了关联对象.(这也是为什么category添加属性时,在释放时没有必要remove)
      3.就是上面写的那个,清空引用计数表并清除弱引用表,将weak指针置为nil
      object_cxxDestruct是由编译器生成,这个方法原本是为了++对象析构,ARC借用了这个方法插入代码实现了自动内存释放的工作.
自动释放池
  1. 什么是自动释放池?他的结构是怎么样的?
    iOS的自动释放池(AutoReleasePool)
    • OC中的一种内存自动回收机制,它可以延迟加入AutoreleasePool中的变量release的时机,即当我们创建了一个对象,并把他加入到了自动释放池中时,他不会立即被释放,会等到一次runloop结束或者作用域超出{}或者超出[pool release]之后再被释放
    • 自动释放池的数据结构:
      是以栈为结点通过双向链表的形式组合而成
  2. 自动释放池与线程的关系
    • 一一对应
  3. 描述自动释放池的运作
    iOS开发进阶:自动释放池的实现原理分析
  4. 一个对象在作用域结束后什么时候被释放?
    • 分两种情况:⼿动⼲预释放时机系统⾃动去释放
      ⼿动⼲预释放时机--指定autoreleasepool 就是所谓的:当前作⽤域⼤括号结束时释
      放。
      系统⾃动去释放--不⼿动指定autoreleasepool
      Autorelease对象出了作⽤域之后,会被添加到最近⼀次创建的⾃动释放池中,并会在
      当前的 runloop 迭代结束时释放。
      释放的时机总结起来,可以⽤下图来表示:
      autoreleasepool与 runloop 的关系图
      image.png

下⾯对这张图进⾏详细的解释:
从程序启动到加载完成是⼀个完整的运⾏循环,然后会停下来,等待⽤户交互,⽤户的每⼀次交互都会启动⼀次运⾏循环,来处理⽤户所有的点击事件、触摸事件。
我们都知道: 所有 autorelease的对象,在出了作⽤域之后,会被⾃动添加到最近创建的⾃动释放池中。
但是如果每次都放进应⽤程序的main.m中的autoreleasepool中,迟早有被撑满的⼀刻。

这个过程中必定有⼀个释放的动作。何时?
在⼀次完整的运⾏循环结束之前,会被销毁。
那什么时间会创建⾃动释放池?
运⾏循环检测到事件并启动后,就会创建⾃动释放池。
⼦线程的 runloop 默认是不⼯作,⽆法主动创建,必须⼿动创建。
⾃定义的 NSOperation 和 NSThread 需要⼿动创建⾃动释放池。⽐如: ⾃定义的
NSOperation 类中的 main ⽅法⾥就必须添加⾃动释放池。否则出了作⽤域后,⾃动释放对象会因为没有⾃动释放池去处理它,⽽造成内存泄露。
但对于 blockOperation 和 invocationOperation 这种默认的Operation ,系统已经帮我们封装好了,不需要⼿动创建⾃动释放池。
@autoreleasepool当⾃动释放池被销毁或者耗尽时,会向⾃动释放池中的所有对象发送 release 消息,释放⾃动释放池中的所有对象。
如果在⼀个vc的viewDidLoad中创建⼀个 Autorelease对象,那么该对象会在
viewDidAppear ⽅法执⾏前就被销毁了

你可能感兴趣的:(复习-OC常见面试题)