----

作者: DeCori | 来源:发表于2019-07-07 19:02 被阅读0次

    第一部分

    iOS基础

    1、 常用关键字,retain,copy,mutablecopy,weak,assign,strong,属性的本质是什么

    IOS const static extern 深度解析
    1).getter=getterName,setter=setterName,设置setter与 getter的方法名
    2).readwrite,readonly,设置可供访问级别
    3).assign,setter方法直接赋值,不进行任何retain操作,为了解决原类型与环循引用问题
    4).retain,setter方法对参数进行release旧值再retain新值,所有实现都是这个顺序(CC上有相关资料)
    5).copy,setter方法进行Copy操作,与retain处理流程一样,先旧值release,再 Copy出新的对象,retainCount为1。这是为了减少对上下文的依赖而引入的机制。
    6).nonatomic,非原子性访问,不加同步,多线程并发访问会提高性能。注意,如果不加此属性,则默认是两个访问方法都为原子型事务访问。锁被加到所属对象实例级。

    • 一类是表示原子性
      atomic只是保证了getter和setter存取方法的线程安全,并不能保证整个对象是线程安全的,因此在多线程编程时,线程安全还需要开发者自己来处理.
      关于选择:atomic系统生成的getter、setter会保证get、set操作的安全性,但相对nonatomic来说,atomic要更耗费资源,且速度要慢,故在iPhone等小型设备上,如果没有多线程之间的通讯,使用nonatomic是更好的选择

    因为getter/setter方法有加锁的缘故,故在别的线程来读写这个属性之前,会先执行完当前操作.

    • 一类是表示引用计数的,有assign(iOS5以前用unsafe_unretained),strong,weak,copy。
      assign: assign用于非指针变量,一般用于基础类型和C数据类型,这些类型不是对象,统一由系统栈进行内存管理。
      weak:对对象的弱引用,不增加对象的引用计数,也不持有对象,对象销毁后会自动置为nil,防止野指针。
      strong:对对象的强引用,会增加对象的引用计数,如果指向了一个空对象,会造成野指针,平常我们用得最多的应该也是strong了。
      retain:声明属性时用strong或者retain效果是一样的(貌似更多开发者更倾向于用strong)。不过在声明Block时,使用strong和retain会有截然不同的效果。strong会等于copy,而retain竟然等于assign
      copy:建立一个引用计数为1的新对象,赋值时对传入值进行一份拷贝,所以使用copy关键字的时候,你将一个对象复制给该属性,该属性并不会持有那个对象,而是会创建一个新对象,并将那个对象的值拷贝给它。而使用copy关键字的对象必须要实现NSCopying协议。
      unsafe_unretained:跟 weak 类似,声明一个弱引用,但是当引用计数为 0 时,变量不会自动设置为 nil,现在基本都用weak了。

    • 一类是表示读写权限的,默认是readwrite(可读可写),还有就是readonly,当你希望暴露出来的属性不能被外界修改时就需要申明为readonly。

    • 原子性--- nonatomic 特质
      在默认情况下,由编译器合成的方法会通过锁定机制确保其原子性(atomicity)。如果属性具备 nonatomic 特质,则不使用自旋锁。请注意,尽管没有名为“atomic”的特质(如果某属性不具备 nonatomic 特质,那它就是“原子的” ( atomic) ),但是仍然可以在属性特质中写明这一点,编译器不会报错。若是自己定义存取方法,那么就应该遵从与属性特质相符的原子性。
      读/写权限---readwrite(读写)、readonly (只读)
      内存管理语义---assign、strong、 weak、unsafe_unretained、copy
      方法名---getter=<name> 、setter=<name>

    • @property 的本质是实例变量(ivar)+存取方法(access method = getter + setter),即 @property = ivar + getter + setter;

      “属性” (property)作为 Objective-C 的一项特性,主要的作用就在于封装对象中的数据。 Objective-C 对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”(access method)来访问。其中,“获取方法” (getter)用于读取变量值,而“设置方法” (setter)用于写入变量值。

    • ivar、getter、setter 是自动合成这个类中的

      完成属性定义后,编译器会自动编写访问这些属性所需的方法,此过程叫做“自动合成”(autosynthesis)。需要强调的是,这个过程由编译 器在编译期执行,所以编辑器里看不到这些“合成方法”(synthesized method)的源代码。除了生成方法代码 getter、setter 之外,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。在前例中,会生成两个实例变量,其名称分别为 _firstName 与 _lastName。也可以在类的实现代码里通过 @synthesize 语法来指定实例变量的名字.

    2、 沙盒目录结构,存放数据

    沙盒结构:

    • Application:存放程序源文件,上架前经过数字签名,上架后不可修改。
    • Documents:常用目录,保存应用程序在运行时生成的一些需要长久保存的重要数据(比如: 个人设置等信息)。(这里不能存缓存大文件,否则上架不被通过)--iCloud备份目录。
    • Library:
      1.Caches:存放体积大又不需要备份的数据。(常用的缓存路径)--iCloud非备份目录
      2.Preference:用户偏好设置目录,使用 NSUserDefault 直接读写--iCloud备份目录。
    • tmp:存放临时文件,这个目录下的数据有随时被清除的可能--iCloud非备份目录。

    3、 tableview的复用原理

    • 重用机制主要用到了一个可变数组visibleCells和一个可变的字典类型reusableTableCells,其中visiableCells用来存储当前UITableView显示的cell,reusableTableCells用来存储已经用'identify'缓存的cell。当UITableView滚动的时候,会先在reusableTableCells中根据identify找是否有有已经缓存的cell,如果有直接用,没有再去初始化。

    4、 bounds和frame区别

    • Frame :视图在父视图坐标系统中的位置和大小。(参照点是父视图的坐标系统)
    • Bounds:视图在本身坐标系统中 的位置和大小。(参照点是自身坐标系统)
      设置bounds可以修改自己坐标系的原点位置,进而影响到其“子view”的显示位置。

    5、 内存管理

    内存分配

    首先既然我们需要对内存进行管理,就需要知道内存是怎么分配的,是分配在哪里的?
    在iOS中数据是存在在堆和栈中的,然而我们的内存管理管理的是堆上的内存,栈上的内存并不需要我们管理。

    • 非OC对象(基础数据类型)存储在栈上
    • OC对象存储在堆上

    OC中的内存管理也就是引用计数--是计算机编程语言中的一种内存管理技术。

    什么是引用计数

    当我们创建一个新对象的时候,它的引用计数为 1,当有一个新的指针指向这个对象时,我们将其引用计数加 1,当某个指针不再指向这个对象时,我们将其引用计数减 1,当对象的引用计数变为 0 时,说明这个对象不再被任何指针指向了,这个时候我们就可以将对象销毁,回收内存。

    内存管理方式

    无论是MRC(manual reference counting )还是ARC(automatic reference counting)情况下,Objective-C采用的是引用计数式的内存管理方式,这一方式的特点:

    自己生成的对象,自己持有。
    非自己生成的对象,自己也能持有。
    不再需要自己持有对象时释放。
    非自己持有的对象无法释放。
    ARC自动管理引用计数

    MRC

    在MRC模式下,开发者自己手动管理内存,这往往要占用开发者大量的时间和精力去调试工程,基于这种情况
    苹果在 2011 年的时候,在 WWDC 大会上提出了自动的引用计数(ARC)。

    ARC

    ARC其实也是基于引用计数,背后的原理是依赖编译器的静态分析能力,在编译时期自动在已有代码中插入合适的内存管理代码(包括 retain、release、copy、autorelease、autoreleasepool)以及在 Runtime 做一些优化。
    现在的iOS开发基本都是基于ARC的,所以开发人员大部分情况都是不需要考虑内存管理的。
    个别情况下如:

    • 底层的 Core Foundation 对象由于不在 ARC 的管理下,所以需要自己手动管理这些对象的引用计数。
    • 还有就是循环引情况下,由于对象互相之间强引用,引用计数永远不会减到0,所以需要自己主动断开循环引用,使引用计数能够减少。
    所有权修饰符

    Objective-C编程中为了处理对象,可将变量类型定义为id类型或各种对象类型。 ARC中id类型和对象类其类型必须附加所有权修饰符。
    其中有以下4种所有权修饰符:
    __strong
    __weak
    __unsafe_unretaied
    __autoreleasing

    • __strong 表示强引用,对应定义 property 时用到的 strong。当对象没有任何一个强引用指向它时,它才会被释放。如果在声明引用时不加修饰符,那么引用将默认是强引用。当需要释放强引用指向的对象时,需要保证所有指向对象强引用置为 nil。__strong 修饰符是 id 类型和对象类型默认的所有权修饰符。

    • __weak 表示弱引用,对应定义 property 时用到的 weak。弱引用不会影响对象的释放,而当对象被释放时,所有指向它的弱引用都会自定被置为 nil,这样可以防止野指针。使用__weak修饰的变量,即是使用注册到autoreleasePool中的对象。__weak 最常见的一个作用就是用来避免循环循环。需要注意的是,__weak 修饰符只能用于 iOS5 以上的版本,在 iOS4 及更低的版本中使用 __unsafe_unretained 修饰符来代替。

    • __unsafe_unretained
      ARC 是在 iOS5 引入的,而 __unsafe_unretained 这个修饰符主要是为了在ARC刚发布时兼容iOS4以及版本更低的系统,因为这些版本没有弱引用机制。这个修饰符在定义property时对应的是unsafe_unretained。__unsafe_unretained 修饰的指针纯粹只是指向对象,没有任何额外的操作,不会去持有对象使得对象的 retainCount +1。而在指向的对象被释放时依然原原本本地指向原来的对象地址,不会被自动置为 nil,所以成为了野指针,非常不安全。

    • __autoreleasing
      将对象赋值给附有__autoreleasing修饰符的变量等同于MRC时调用对象的autorelease方法。

    内存管理问题--循环引用

    什么是循环引用?循环引用是指两个对象相互之间成为了强引用关系,引用计数都会加1,从而导致对象永远无法释放。这样就造成了内存泄漏。
    最容易产生循环引用的两种情况就是Delegate和Block。所以引入了弱引用的概念来修饰对象,使用weak关键字修饰的对象,对象的引用计数不会+1,在对象释放的时候会将引用的对象置为nil,这样就避免了循环引用的产生。
    原理:简单的描述就是每一个拥有弱引用的对象都有一张表来保存弱引用的指针地址,这个弱引用并不会使对象引用计数加1,当这个对象的引用计数变为0时,系统就通过这张表,找到所有的弱引用指针把它们都置成nil。
    weak表是Runtime维护了一个hash(哈希)表,用于存储指向某个对象的所有weak指针。Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象指针的地址)数组。
    目前,在ARC环境下,导致内存泄漏的根本原因是代码中存在循环引用,从而导致一些内存无法释放,最终导致dealloc()方法无法被调用。
    所以在ARC中做内存管理主要就是发现这些内存泄漏,关于内存泄漏Instrument为我们提供了 Allocations/Leaks 这样的工具用来检测。
    iOS 内存泄漏排查方法及原因分析

    总结

    在 ARC 的帮助下,iOS 开发者的内存管理工作已经被大大减轻,但是我们仍然需要理解引用计数这种内存管理方式的优点和常见问题,特别要注意解决循环引用问题。对于循环引用问题有两种主要的解决办法,一是主动断开循环引用,二是使用弱引用的方式避免循环引用。对于 Core Foundation 对象,由于不在 ARC 管理之下,我们仍然需要延续以前手工管理引用计数的办法。
    在调试内存问题时,Instruments 工具可以很好地对我们进行辅助,善用 Instruments 可以节省我们大量的调试时间。

    6、 安全单例

    当我们创建单例对象的步骤分为
    1:申请内存(alloc)
    2:初始化(init) 这两个步骤
    为了确保单例对象的唯一性,我们应该在alloc阶段拦截。
    当我们调用alloc方法时,OC内部会调用allocWithZone这个方法来申请内存,我们覆写这个方法,然后在这个方法中调用shareInstance方法返回单例对象,这样就可以达到我们的目的。
    拷贝对象也是同样的原理,覆写copyWithZone方法,然后在这个方法中调用shareInstance方法返回单例对象。

    #import "Single.h"
    static Single *single = nil;
    @implementation Single
    +(instancetype) shareInstance{
        static dispatch_once_t onceToken ;
        dispatch_once(&onceToken, ^{
            single = [[super allocWithZone:NULL] init];
        }) ;
        return single ;
    }
    +(id) allocWithZone:(struct _NSZone *)zone{
        return [Single shareInstance] ;
    }
    -(id) copyWithZone:(struct _NSZone *)zone{
        return [Single shareInstance] ;
    }
    @end
    
    

    7、 堆栈的区别

    • 结构层面上
      栈:栈是向低地址扩展的数据结构,是一块连续的内存区域。
      堆:堆是向高地址扩展的数据结构,是不连续的内存区域。因为系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
    • 按管理方式分
      对于栈来讲,是由系统编译器自动管理,不需要程序员手动管理
      对于堆来讲,释放工作由程序员手动管理,不及时回收容易产生内存泄露
    • 按分配方式分
      堆是动态分配和回收内存的,没有静态分配的堆
      栈有两种分配方式:静态分配和动态分配
      静态分配是系统编译器完成的,比如局部变量的分配
      动态分配是由alloc函数进行分配的,但是栈的动态分配和堆是不同的,它的动态分配也由系统编译器进行释放,不需要程序员手动管理

    区别

    • 堆是一种经过排序的树形数据结构,每个节点都有一个值;堆常用来实现优先队列,它的存取是随意的。
    • 栈是一种具有后进先出的线性数据结构,又称为后进先出的线性表,限定仅在表尾进行插入和删除操作。
      答:
      管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。
      申请大小:
      栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因 此,能从栈获得的空间较小。
      堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
      碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出
      分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
      分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的。

    介绍

    • 堆是一种经过排序的树形数据结构,每个节点都有一个值,通常我们所说的堆的数据结构是指二叉树。所以堆在数据结构中通常可以被看做是一棵树的数组对象。而且堆需要满足一下两个性质:

      1)堆中某个节点的值总是不大于或不小于其父节点的值;

      2)堆总是一棵完全二叉树。

    • 堆分为两种情况,有最大堆和最小堆。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆,在一个摆放好元素的最小堆中,父结点中的元素一定比子结点的元素要小,但对于左右结点的大小则没有规定谁大谁小。

    • 堆常用来实现优先队列,堆的存取是随意的,这就如同我们在图书馆的书架上取书,虽然书的摆放是有顺序的,但是我们想取任意一本时不必像栈一样,先取出前面所有的书,书架这种机制不同于箱子,我们可以直接取出我们想要的书。

    • 栈是限定仅在表尾进行插入和删除操作的线性表。我们把允许插入和删除的一端称为栈顶,另一端称为栈底,不含任何数据元素的栈称为空栈。栈的特殊之处在于它限制了这个线性表的插入和删除位置,它始终只在栈顶进行。

    • 栈是一种具有后进先出的数据结构,又称为后进先出的线性表,简称 LIFO(Last In First Out)结构。也就是说后存放的先取,先存放的后取,这就类似于我们要在取放在箱子底部的东西(放进去比较早的物体),我们首先要移开压在它上面的物体(放进去比较晚的物体)。

    • 堆栈中定义了一些操作。两个最重要的是PUSH和POP。PUSH操作在堆栈的顶部加入一个元素。POP操作相反,在堆栈顶部移去一个元素,并将堆栈的大小减一。

    • 栈的应用—递归

    队列

    • 队列是只允许在一端进行插入操作、而在另一端进行删除操作的线性表。允许插入的一端称为队尾,允许删除的一端称为队头。它是一种特殊的线性表,特殊之处在于它只允许在表的前端进行删除操作,而在表的后端进行插入操作,和栈一样,队列是一种操作受限制的线性表。

    • 队列是一种先进先出的数据结构,又称为先进先出的线性表,简称 FIFO(First In First Out)结构。也就是说先放的先取,后放的后取,就如同行李过安检的时候,先放进去的行李在另一端总是先出来,后放入的行李会在最后面出来。

    8、数组跟链表的区别

    • 数组:有序、下标取值、内存连续。
      查找:所以使用下标取值(因为内存连续,且存储单元大小已知,所以可以快速定位下标所在的内存地址)的时候很快能找到,比链表快多了。
      插入:但是插入的时候,需要挪动本下标后边的所有内存地址,往后移一个单元。所以如果插入不是在尾部,并且数据量很大的情况下,会比链表慢。但是:OC中的数组对此采用了环形缓冲区的优化,它的作用是可以计算出移动最少的内存来达到目的。比如,如果距离开头近,就会移动头部的内存;距离尾部近,就会移动尾部的内存。而环形缓冲区是靠链表实现的。

    • 链表:有序、本节点保存有下一节点的地址、内存不连续(不能使用下标取值)。
      查找:只能从头开始查找,所以查找比数组慢。
      插入:插入的成本很低,只需要打断两个节点插入后重新连接上就行,对其他不相关节点毫无影响。比数组快。

    • 所以,app开发中大部分情况下,数组比链表更强更合适,这应该也是为什么链表很少被人使用的原因吧。
      数组、链表、Hash的优缺点

    第二部分

    iOS比较深一点问题

    1、load和initialize 方法什么时候调用

    1、load方法在类或分类加载到runtime的时候调用;(APP启动后初始化的时候)
    2、load方法只调用一次;
    3、load方法调用顺序:父类----子类----分类
    如果一个类没有实现load方法,那么就不会调用它父类的load方法.
    1、initialize在类第一次收到消息的时候调用;(在首次使用类时,会生成类对象,该方法在此时调用)
    2、initialize有可能调用多次;(如果子类没有实现initialize方法,则子类第一次收到消息的时候会先调用父类的initialize方法)
    3、initialize调用顺序:父类----子类(如果有分类,分类中的initialize会覆盖子类,子类中的方法不会调用)

    • load和initialize方法都会在实例化对象之前调用,以main函数为分水岭,前者在main函数之前调用,后者在之后调用。这两个方法会被自动调用,不能手动调用它们。
    • load和initialize方法都不用显示的调用父类的方法而是自动调用,即使子类没有initialize方法也会调用父类的方法,而load方法则不会调用父类。
      load方法通常用来进行Method Swizzle,initialize方法一般用于初始化全局变量或静态变量。
    • load和initialize方法内部使用了锁,因此它们是线程安全的。实现时要尽可能保持简单,避免阻塞线程,不要再使用锁

    2、响应链,事件的传递

    响应者对象(UIResponder)
    • 学习触摸事件首先要了解一个比较重要的概念-响应者对象(UIResponder)。
      在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接受并处理事件,我们称之为“响应者对象”。以下都是继承自UIResponder的,所以都能接收并处理事件。
    事件的产生
    • 发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中,为什么是队列而不是栈?因为队列的特点是FIFO,即先进先出,先产生的事件先处理才符合常理,所以把事件添加到队列。
    • UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)。
    • 主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步。
      找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理。
    事件的传递
    • 触摸事件的传递是从父控件传递到子控件
      也就是UIApplication->window->寻找处理事件最合适的view
      注 意: 如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件
    • 寻找合适视图的过程
      1.首先判断自己是否能接收触摸事件。(hitTest:withEvent: 不返回nil 。不能接收事件:1、userInteractionEnabled = NO 2、隐藏 3、透明度<0.01)。
      2.判断触摸点是否在自己身上。(pointInside方法) 。
      3.子控件数组倒序遍历,即从最上层往下遍历并,子控件重复前两个步骤。
      4.如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view。(hitTest:withEvent: return self)
    事件响应
    • 我们事件传递找到合适的视图view的时候首先看view能否处理这个事件,如果能处理则交由其处理并停止该事件的向上响应(各种事件、滑动、touches...),如果不能则会将事件传递给其上级视图(view的superView);如果上级视图仍然无法处理则会继续往上传递;一直到 window,如果window还是不能处理此事件则继续交给application处理,如果最后application还是不能处理此事件则将其丢弃。
    总结
    • 1.点击一个UIView或产生一个触摸事件A,这个触摸事件A会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。
      2.UIApplication会从事件对列中取出最前面的事件(此处假设为触摸事件A),把事件A传递给应用程序的主窗口(keyWindow)。
      3.窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。(至此,第一步已完成)

    • 响应者链
      响应链是从最合适的view开始传递,处理事件传递给下一个响应者,响应者链的传递方法是事件传递的反方法,如果所有响应者都不处理事件,则事件被丢弃。我们通常用响应者链来获取上几级响应者,方法是UIResponder的nextResponder方法。

    (1)如果一个button有一部分超出父控件的范围了,这部分无法响应点击,如果想让它响应点击应该怎么做

    iOS UIButton 点击无响应的解决办法
    button 超出父控件的范围的响应问题解决
    iOS中多层视图中,响应事件的透传
    重写button父视图的- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event的方法

    • (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
      //if内的条件应该为,当触摸点point超出父视图部分,但在button部分时
      if (.....){
      return YES;
      }
      return NO;
      }

    3、runtime,isa指针,应用场景

    iOS Runtime详解
    Runtime-iOS运行时基础篇
    Runtime重要知识点以及使用场景
    iOS Runtime探索之旅

    简单总结下Runtime
    • Runtime准确来说是一套由C、C++、汇编实现的API。Runtime是用C++编写的运行时库,为 C 添加了面向对象的能力,这也是区别于C语言这样静态语言的重要特性,C语言函数调用在编译期间就已经决定了,在编译完成之后直接顺序执行。OC是动态语言(多态和运行时),函数调用就是消息发送,在编译期间不知道具体调用哪个函数,所以Runtime就是去解决运行时找到调用哪个方法的问题
      多态:OC中的多态是不同对象对同一消息的不同响应方式,子类通过重写父类的方法来改变同一方法的实现,体现多态性
      总结:三个能力
      首地址是isa的struct指针,都可以被认为是objc中的对象
      1.面向对象能力 (继承,封装,多态)
      2.动态加载类信息,进行消息的分发 (isa横向查找,继承纵向查找,找不到调用_objc_msgForward用于消息转发)
      3.消息转发 1.动态方法解析并动态添加方法 2.备用接收者 3.完整消息转发 4.抛出异常
      顺序:
      instance—>class—->method—–>sel—(Cache)–>imp—->实现函数(找不到就进入转发流程)
      实例对象中存放isa指针,有isa指针就可以找到实力变量对应的所属类,类中存放着实例方法列表,SEL为Key,IMP为value,在编译期间,根据方法名会生成一个唯一的int标识符,这就是SEL标识,IMP就是函数指针,指向最终函数实现。runtime核心就是objc_msgSend函数,通过给SEL传递消息,找到匹配的IMP
      Runtime--运行时系统来动态得创建类和对象、进行消息传递和转发。核心是消息传递 (Messaging)
      OC代码最终都会被编译器转化为运行时代码,通过消息机制决定函数调用方式,这也是OC作为动态语言使用的基础。
    Runtime消息传递
    • 系统首先找到消息的接收对象,然后通过对象的isa找到它的类。
      在它的类中查找method_list,是否有selector方法。
      没有则查找父类的method_list。
      找到对应的method,执行它的IMP。
      转发IMP的return值。
    Runtime消息转发

    前文介绍了进行一次发送消息会在相关的类对象中搜索方法列表,如果找不到则会沿着继承树向上一直搜索直到继承树根部(通常为NSObject),如果还是找不到并且消息转发都失败了就回执行doesNotRecognizeSelector:方法报unrecognized selector错。那么消息转发到底是什么呢?接下来将会逐一介绍最后的三次机会。

    • 动态方法解析
      首先,Objective-C运行时会调用 +resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数并返回YES, 那运行时系统就会重新启动一次消息发送的过程。
      如果resolve方法返回 NO ,运行时就会移到下一步:forwardingTargetForSelector。
    • 备用接收者
      如果目标对象实现了-forwardingTargetForSelector:,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。
    • 完整消息转发
      如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。
      首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nil ,Runtime则会发出 -doesNotRecognizeSelector: 消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation 对象并发送 -forwardInvocation:消息给目标对象。
    isa指针
    • isa:是类指针,之所以说isa是指针是因为Class其实是一个指向objc_class结构体的指针,而isa 是它唯一的私有成员变量,即所有对象都有isa指针(isa位置在成员变量第一个位置)
      Objective-C 中对象的定义:
    struct objc_object {
        Class isa;
    };
    typedef struct objc_object *id;
    
    • 这里看到了我们熟悉的 id ,一般我们用它来实现类似于 C++ 中泛型的一些操作,该类型的对象可以转换为任意一种对象。在这里 id 被定义为一个指向 objc_object 的指针。说明 objc_object 就是我们平时常用的对象的定义,它只包含一个 isa 指针。
      也就是说,一个对象唯一保存的信息就是它的 Class 的地址。当我们调用一个对象的方法时,它会通过 isa 去找到对应的 objc_class,然后再在 objc_class 的 methodLists 中找到我们调用的方法,然后执行。
    Runtime应用

    Runtime简直就是做大型框架的利器。它的应用场景非常多,下面就介绍一些常见的应用场景。

    • 关联对象(Objective-C Associated Objects)给分类增加属性
    • 方法魔法(Method Swizzling)方法添加和替换和KVO实现
    • 消息转发(热更新)解决Bug(JSPatch)
    • 实现NSCoding的自动归档和自动解档
    • 实现字典和模型的自动转换(MJExtension)

    4、kvo

    KVO-键值观察机制,原理如下:

    • 1.当给A类添加KVO的时候,利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
    • 2.当修改instance对象的属性时(重写监听属性的setter方法,在setter方法内部调用了Foundation ),会调用Foundation的_NSSetObjectValueAndNotify函数
    • 3._NSSetObjectValueAndNotify函数内部
      a) 首先会调用 willChangeValueForKey
      b) 然后给属性赋值(调用set方法)
      c) 最后调用 didChangeValueForKey
      d) didChangeValueForKey方法内部调用 observer 的 observeValueForKeyPath 去告诉监听器属性值发生了改变 .
    • 4.重写了dealloc做一些 KVO 内存释放
    kvc

    当一个对象调用setValue方法时,方法内部会做以下操作:
    1). 检查是否存在相应的key的set方法,如果存在,就调用set方法。
    2). 如果set方法不存在,就会查找与key相同名称并且带下划线的成员变量,如果有,则直接给成员变量属性赋值。
    3). 如果没有找到_key,就会查找相同名称的属性key,如果有就直接赋值。
    4). 如果还没有找到,则调用valueForUndefinedKey:和setValue:forUndefinedKey:方法。
    这些方法的默认实现都是抛出异常,我们可以根据需要重写它们。

    5、runloop,timer和perform延迟 在子线程调用

    什么是RunLoop
    Runloop

    • RunLoop其实就是一种处理事件、消息的机制被面向对象化,它就是一个对象。其实它的本质就是一个do-while循环,通常的代码逻辑是这样的:
    function loop() {
        initialize();
        do {
            var message = get_next_message();//没事件/消息或者都处理完了就沉睡
            process_message(message);//如果有事件/消息,就处理
        } while (message != quit);
    }
    
    • 这种模型通常被称作 Event Loop。 Event Loop 在很多系统和框架里都有实现,比如 Node.js 的事件处理,比如 Windows 程序的消息循环,再比如 OSX/iOS 里的 RunLoop。实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。
      所以,RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。
    • RunLoop如何实现Event Loop模型逻辑
      RunLoop 的核心是基于 mach port 的,其进入休眠时调用的函数是 mach_msg()。为了实现消息的发送和接收,mach_msg() 函数实际上是调用了一个 Mach 陷阱 (trap),即函数mach_msg_trap()。当你在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到内核态;内核态中内核实现的 mach_msg() 函数会完成实际的工作。
      RunLoop 的核心就是一个 mach_msg() ,RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。

    Runloop-题
    只有主线程会在创建的时候默认自动运行一个runloop,并且有timer,普通的子线程是没有这些的。这样在子线程中被调用的时候,我们的代码中的延时调用的代码就会一直等待timer的调度,但是实际上在子线程中又没有这样的timer,这样我们的代码就永远不会被调到。因此,perform使用时需要注意环境!
    原来performSelector withObject afterDelay这个方法在子线程中,并不会调用SEL方法,而performSelect withObject 方法会直接调用。原因是:
    1. afterDelay 方式是使用当前线程的定时器在一定时间后调用SEL,NO AfterDelay方式是直接调用SEL.
    2. 主线程的runloop默认开启,子线程的runloop默认不开启,所以timer在子线程中是不会执行的,需要手动开启runloop

    1.NSTimer是Foundation库提供的一个类,基于runloop实现.
    可以只执行一次,也可定期反复执行(设置repeat参数).其中只执行一次时,执行后自动销毁.重复执行的,必须手动调用invalidate才能销毁.
    使用时应注意:
    1.)必须在有runloop的线程中使用.而根据runloop的特性,如果不处于Timer的mode时,就无法响应Timer事件.
    2.)创建和销毁必须在同一线程.
    3.)会对要执行方法的对象,造成强引用.容易产生循环引用.
    2.performSelector是NSObject实现的成员方法.
    依赖于NSTimer实现,故存在跟NSTimer的问题.
    提供了功能的封装,故使用起来较方便.较NSTimer比,最大的问题是不能重复执行.
    3.dispatch_after是GCD层面提供的c接口.
    虽不存在NSTimer的一系列问题.该接口的最大问题就是:无法Cancel.

    6、应用启动过程

    App 启动速度怎么做优化与监控

    • 一般而言,App 的启动时间,指的是从用户点击 App 开始,到用户看到第一个界面之间的时间。总结来说,App 的启动主要包括三个阶段:
      1)main() 函数执行前;
      2)main() 函数执行后;
      3)首屏渲染完成后。
    • main() 函数执行前
      在 main() 函数执行前,系统主要会做下面几件事情
      1)加载可执行文件(App 的.o 文件的集合);
      2)加载动态链接库,进行 rebase 指针调整和 bind 符号绑定;
      3)Objc 运行时的初始处理,包括 Objc 相关类的注册、category 注册、selector 唯一性检查等;
      4)初始化,包括了执行 +load() 方法、attribute((constructor)) 修饰的函数的调用、创建 C++ 静态全局变量。
    • main() 函数执行后
      main() 函数执行后的阶段,指的是从 main() 函数执行开始,到 appDelegate 的 didFinishLaunchingWithOptions方法里首屏渲染相关方法执行完成。
      首页的业务代码都是要在这个阶段,也就是首屏渲染前执行的,主要包括了:
      1)首屏初始化所需配置文件的读写操作;
      2)首屏列表大数据的读取;
      3)首屏渲染的大量计算等。
    • 首屏渲染完成后
      首屏渲染后的这个阶段,主要完成的是,非首屏其他业务服务模块的初始化、监听的注册、配置文件的读取等。从函数上来看,这个阶段指的就是截止到didFinishLaunchingWithOptions方法作用域内执行首屏渲染之后的所有方法执行完成。简单说的话,这个阶段就是从渲染完成时开始,到 didFinishLaunchingWithOptions方法作用域结束时结束。

    7、iOS 静态库使用的配置 -ObjC

    1、如果静态库中有category,那么需要添加 -ObjC 参数标识,否则可能会报:unrecognized selector sent to instance

    2、参数说明(引用自:http://www.cnblogs.com/robinkey/archive/2013/05/27/3101095.html

    -ObjC:加了这个参数后,链接器就会把静态库中所有的Objective-C类和分类都加载到最后的可执行文件中

    -all_load:会让链接器把所有找到的目标文件都加载到可执行文件中,但是千万不要随便使用这个参数!假如你使用了不止一个静态库文件,然后又使用了这个参数,那么你很有可能会遇到ld: duplicate symbol错误,因为不同的库文件里面可能会有相同的目标文件,所以建议在遇到-ObjC失效的情况下使用-force_load参数。

    -force_load:所做的事情跟-all_load其实是一样的,但是-force_load需要指定要进行全部加载的库文件的路径,这样的话,你就只是完全加载了一个库文件,不影响其余库文件的按需加载

    第三部分

    项目相关

    项目的架构,重点和难点技术,项目设计思路

    项目大了人员多了,架构怎么设计更合理
    ToB和ToC产品的差异,知道这3点就够了

    第四部分

    简单的算法

    1、 有序数组找特定元素

    2、 选择排序

    3.常用的排序算法

    选择排序、冒泡排序、插入排序三种排序算法可以总结为如下:
    
    都将数组分为已排序部分和未排序部分。
    
    选择排序将已排序部分定义在左端,然后选择未排序部分的最小元素和未排序部分的第一个元素交换。
    
    冒泡排序将已排序部分定义在右端,在遍历未排序部分的过程执行交换,将最大元素交换到最右端。
    
    插入排序将已排序部分定义在左端,将未排序部分元的第一个元素插入到已排序部分合适的位置。
    
    /** 
     *  【选择排序】:最值出现在起始端
     *  
     *  第1趟:在n个数中找到最小(大)数与第一个数交换位置
     *  第2趟:在剩下n-1个数中找到最小(大)数与第二个数交换位置
     *  重复这样的操作...依次与第三个、第四个...数交换位置
     *  第n-1趟,最终可实现数据的升序(降序)排列。
     *
     */
    void selectSort(int *arr, int length) {
        for (int i = 0; i < length - 1; i++) { //趟数
            for (int j = i + 1; j < length; j++) { //比较次数
                if (arr[i] > arr[j]) {
                    int temp = arr[i];
                    arr[i] = arr[j];
                    arr[j] = temp;
                }
            }
        }
    }
     
    /** 
     *  【冒泡排序】:相邻元素两两比较,比较完一趟,最值出现在末尾
     *  第1趟:依次比较相邻的两个数,不断交换(小数放前,大数放后)逐个推进,最值最后出现在第n个元素位置
     *  第2趟:依次比较相邻的两个数,不断交换(小数放前,大数放后)逐个推进,最值最后出现在第n-1个元素位置
     *   ……   ……
     *  第n-1趟:依次比较相邻的两个数,不断交换(小数放前,大数放后)逐个推进,最值最后出现在第2个元素位置 
     */
    void bublleSort(int *arr, int length) {
        for(int i = 0; i < length - 1; i++) { //趟数
            for(int j = 0; j < length - i - 1; j++) { //比较次数
                if(arr[j] > arr[j+1]) {
                    int temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            } 
        }
    }
     
    /**
     *  折半查找:优化查找时间(不用遍历全部数据)
     *
     *  折半查找的原理:
     *   1> 数组必须是有序的
     *   2> 必须已知min和max(知道范围)
     *   3> 动态计算mid的值,取出mid对应的值进行比较
     *   4> 如果mid对应的值大于要查找的值,那么max要变小为mid-1
     *   5> 如果mid对应的值小于要查找的值,那么min要变大为mid+1
     *
     */ 
     
    // 已知一个有序数组, 和一个key, 要求从数组中找到key对应的索引位置 
    int binarySearch(int *arr, int length, int key) {
        int min = 0, max = length - 1, mid;
        while (min <= max) {
            mid = (min + max) / 2; //计算中间值
            if (key > arr[mid]) {
                min = mid + 1;
            } else if (key < arr[mid]) {
                max = mid - 1;
            } else {
                return mid;
            }
        }
        return -1;
    }
    
    

    iOS冒泡排序、插入排序、选择排序、快速排序、二分查找

    iOS 开发中常用的排序(冒泡、选择、快速、插入、希尔、归并、基数)算法

    /**
     冒泡排序
     1. 首先将所有待排序的数字放入工作列表中;
     2. 从列表的第一个数字到倒数第二个数字,逐个检查:若某一位上的数字大于他的下一位,则将它与它的下一位交换;
     3. 重复2号步骤(倒数的数字加1。例如:第一次到倒数第二个数字,第二次到倒数第三个数字,依此类推...),直至再也不能交换。
     
     最好的时间复杂度为O(n)
     最坏的时间复杂度为O(n^2)
     平均时间复杂度为O(n^2)
     */
    - (NSMutableArray *)bubbleSortWithArray:(NSArray *)array
    {
        id temp;
        NSUInteger i, j;
        NSMutableArray *a = [NSMutableArray arrayWithArray:array];
        
        for (i = 0; i < a.count-1; i++) {
            for (j = 0; j < a.count-1-i; j++) {
                if (a[j] > a[j+1]) {        // 升序
                    temp = a[j];
                    a[j] = a[j+1];
                    a[j+1] = temp;
                }
            }
        }
        
        NSLog(@"bubbleSort:%@", a);
        return a;
    }
    
    /**
     插入排序
     1.初始时,a[0]自成1个有序区,无序区为a[1..n-1]。令i=1
     2.将a[i]并入当前的有序区a[0…i-1]中形成a[0…i]的有序区间。
     3.i++并重复第二步直到i==n-1。排序完成。
     
     最好的时间复杂度为O(n)
     最坏的时间复杂度为O(n^2)
     平均时间复杂度为O(n^2)
     */
    - (NSMutableArray *)insertSortWithArray:(NSArray *)array
    {
        id temp;
        NSUInteger i, j;
        NSMutableArray *a = [NSMutableArray arrayWithArray:array];
        
        for (i = 1; i < a.count; i++) {
            temp = a[i];
            for (j = i; j>0 && a[j-1]>temp; j--) {
                a[j] = a[j-1];
            }
            a[j] = temp;
        }
        
        NSLog(@"insertSort:%@", a);
        return a;
    }
    
    /**
     选择排序
     1. 设数组内存放了n个待排数字,数组下标从0开始,到n-1结束;
     3. 从数组的第a[i+1]个元素开始到第n个元素,寻找最小的元素。(具体过程为:先设a[i](i=0)为最小,逐一比较,若遇到比之小的则交换);
     4. 将上一步找到的最小元素和a[i]元素交换;
     5. 如果i=n-1算法结束,否则回到第3步。
     
     最好的时间复杂度为O(n^2)
     最坏的时间复杂度为O(n^2)
     平均时间复杂度为O(n^2)
     */
    - (NSMutableArray *)selectSortWithArray:(NSArray *)array
    {
        id temp;
        NSUInteger min, i, j;
        NSMutableArray *a = [NSMutableArray arrayWithArray:array];
        
        for (i = 0; i < array.count; i++) {
            min = i;
            for (j = i+1; j < array.count; j++) {
                if (a[min] > array[j]) {
                    min = j;
                }
            }
            if (min != i) {
                temp = a[min];
                a[min] = a[i];
                a[i] = temp;
            }
        }
        
        NSLog(@"insertSort:%@", a);
        return a;
    }
    
    /**
     快速排序
     1.先从数列中取出一个数作为基准数;
     2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边;
     3.再对左右区间重复第二步,直到各区间只有一个数。
     
     最好的时间复杂度为O(nlog2n)
     最坏的时间复杂度为O(n^2)
     平均时间复杂度为O(nlog2n)
     */
    - (void)quickSortWithArray:(NSMutableArray *)array
    {
        [self quickSortWithArray:array left:0 right:array.count-1];
    }
    
    - (void)quickSortWithArray:(NSMutableArray *)a left:(NSUInteger)left right:(NSUInteger)right
    {
        if (left >= right) {
            return;
        }
        NSUInteger i = left;
        NSUInteger j = right;
        id key = a[left];
        
        while (i < j) {
            while (i < j && key <= a[j]) {
                j--;
            }
            a[i] = a[j];
            
            while (i < j && key >= a[i]) {
                i++;
            }
            a[j] = a[i];
        }
        
        a[i] = key;
        [self quickSortWithArray:a left:left right:i-1];
        [self quickSortWithArray:a left:i+1 right:right];
    }
    
    
    #pragma mark - 二分查找法
    /**
     *  当数据量很大适宜采用该方法。
        采用二分法查找时,数据需是排好序的。 
        基本思想:假设数据是按升序排序的,对于给定值x,从序列的中间位置开始比较,如果当前位置值等于x,则查找成功;若x小于当前位置值,则在数列的前半段 中查找;若x大于当前位置值则在数列的后半段中继续查找,直到找到为止。
    时间复杂度:O(logn)
     */
    - (NSInteger)BinarySearch:(NSArray *)array target:(id)key
    {
        NSInteger left = 0;
        NSInteger right = [array count] - 1;
        NSInteger middle = [array count] / 2;
        
        while (right >= left) {
            middle = (right + left) / 2;
            
            if (array[middle] == key) {
                return middle;
            }
            if (array[middle] > key) {
                right = middle - 1;
            }
            else if (array[middle] < key) {
                left = middle + 1;
            }
        }
        return -1;
    }
    
    - (void)print:(NSArray *)array
    {
        for (id m in array) {
            NSLog(@"%@", m);
        }
        NSLog(@"-----------------");
    }
    

    产品相关:假如给你一个新产品,你将从哪些方面来保障它的质量?

    • 可以从代码开发、测试保障、线上质量三个方面来保障。
      在代码开发阶段,有单元测试、代码Review、静态代码扫描等;
      测试保障阶段,有功能测试、性能测试、高可用测试、稳定性测试、兼容性测试等;
      在线上质量方面,有灰度发布、紧急回滚、故障演练、线上监控和巡检等。

    五加密算法

    iOS加密详解
    iOS中常见的几种加密方式总结!

    iOS开发——BAT面试题合集(持续更新中)

    相关文章

      网友评论

          本文标题:----

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