iOS细节知识收录

iOS 2018经典面试题

2018-09-05  本文已影响78人  3dc2075a325d

针对今年找工作,把一些偏底层以及一些比较不常见的东西记录下来!有不对的地方,下方道友留步!不BB,由浅入深!答案来自网上,如有侵权请联系删除!后面如遇到经典的面试题会持续更新!

1,常见的OC的数据类型?和C的基础数据类型有什么不一样的地方?

答:OC的数据类型:NSString,NSNumber,NSArray,NSData等,创建后都是对象!C的基础类型是int,只是一定字节的内存空间。而OC的NSInteger也是基础类型但是并不是NSNumber的子类也和NSObject这个根类没有关系,它是根据系统是32位还是64位来决定自己是int还是Long。

2,谈谈对OC的内存管理机制的了解?

答:(1),arc自动管理内存计数:相当于在你程序运行的时候有一个大牛帮你收拾垃圾(无用的对象或者资源),类似于我只用资源 ,我不管资源,有大牛来搞定。其实也是针对引用计数的操作,只是编译器会自动帮程序员添加retain和release的代码而已。

(2),mrc手动管理内存计数:简单理解为一个对象被创建后有专门的一个变量用来记录这个对象被使用的次数(计数器),当计数器为0的时候就是这个对象被销毁其内存区域被收回。当你alloc init或者new了一个对象的时候,那么不用的时候要对其release操作(谁申请谁释放),retain一次就要release一次(retain和release是配套的)!

(3),自动释放池:可以通过创建和释放内存池来管理内存申请与回收的时机。当向一个对象发送autorelease消息,cocoa会将这个对象一个引入放入最新的一个自动释放池,但是这个对象仍然是一个正常的对象,因此自动释放池作用域的其他对象仍然可以向这个对象发送消息。当作用域结束时,自动释放池会被释放,池子内部的所有对象都要被释放。

注:一般由类的静态方法创建的对象创建后引用计数+0,在函数出栈后释放相当于一个栈上的局部变量(可以对其retaion延长其对象的生命周起)。

3,atomic和non-atomic对比

答:使用前者会使用同步锁额外生成一些代码提供多线程安全,防止在使用的时候被另外一个线程使用,造成数据错误,但是这会带来一些性能问题。使用后者访问器只是简单的返回这个值。前者比后者安全,后者比前者性能高。一般情况下并不要求属性必须是原子的,因为这并不能保证线程安全,要想保证线程安全还需要更深层次的锁定机制才行。例如:一个线程在连续多次读取某一个属性值的过程中还有别的线程也在同时改写这个属性值,那么这个属性即便是atomic也还是会读到不同的属性值。

4,深浅copy的区别

答:对不可变对象的copy是指针copy浅copy,其余的情况不管是mutablecopy还是可变对象对象copy都是深copy,浅copy就是你和你的影子,你GG的话你的影子也GG。深copy就是你和你的克隆人,你GG的话你的克隆人还活着。

5,代理的作用

答:改变和传递控制链。允许一个类在某些特定时刻通知到其他类而不用知道那些类的指针。可以减少框架复杂度。

6,OC是动态运行时语言

答:将数据类型的确定从编译时推迟到了运行时。简单来讲,直到运行时才去确定一个对象具体是什么类型。多态:不同对象对相同消息做出不同相应的能力叫做多态(子类指针可以赋值给父类)。

7,简述响应者链

答:首先我们要知道只有继承了UIResponder的对象才能接受并处理事件,我们称之为“响应者对象”,例如:UIApplication,UIViewController,UIView等。   

下面介绍事件传递过程:1.点击一个UIView或产生一个触摸事件A,这个触摸事件A会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。

2.UIApplication会从事件对列中取出最前面的事件(此处假设为触摸事件A),把事件A传递给应用程序的主窗口(keyWindow)。

3.窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。

(1).首先判断主窗口(keyWindow)自己是否能接受触摸事件

(2).判断触摸点是否在自己身上

(3).子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1、2步骤)

(4).view,比如叫做fitView,那么会把这个事件交给这个fitView,再遍历这个fitView的子控件,直至没有更合适的view为止。

(5).如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view。

注意:控件不能响应的情况(1).userInteractionEnabled = NO

(2).hidden = YES

(3).透明度 alpha 小于等于0.01

(4).子视图超出了父视图区域

8,方法和选择器

答:selector是一个方法的名字,method是一个组合体,包含了名字和实现。

9,简便构造方法

答:一般由CocoaTouch提供,例如NSNumber的+numberWithBool  +numberWithInt等。由简便构造方法创建的对象并不需要释放。

10,OC堆和栈的区别

答:栈是向低地址扩展的数据结构,是一串连续的内存区域。栈顶的地址和栈的最大容量是系统预先设计好的,是一个编译时就确定的常数。堆是向高地址扩展的数据结构,并不一定是连续的内存区域。堆的大小受限于计算机系统有效的虚拟内存,堆比栈的空间答更灵活.。堆都i是动态分配的,没有静态分配的堆。而栈的分配方式有两种:静态分配和动态分配。静态分配是由编译器来完成的,比如局部变量。动态分配是由alloc函数来分配的,但是栈的动态分配由编译器来释放,无需程序员手动释放。

11,死锁的必要条件

答:互斥,请求保持,不可剥夺,环路(线程A占有a资源但是又想占有b资源,而线程B占有b资源又想占有a资源)

12,内存分区情况

答:数据区(全局变量,静态变量,常量),代码区(函数二进制代码),堆区(程序员动态申请),栈区(局部变量,函数参数)

13,frame和bounds

答:(1)、frame不管对于位置还是大小,改变的都是自己本身

(2)、frame的位置是以父视图的坐标系为参照,从而确定当前视图在父视图中的位置

(3)、frame的大小改变时,当前视图的左上角位置不会发生改变,只是大小发生改变

(4)、bounds改变位置时,改变的是子视图的位置,自身没有影响;其实就是改变了本身的坐标系原点,默认本身坐标系的原点是左上角

(1)、bounds的大小改变时,当前视图的中心点不会发生改变,当前视图的大小发生改变,看起来效果就想缩放一样

14,UIView和CALayer

答:1.事件响应:UIView可以响应触摸事件,CALayer不可以.UIKit使用UIResponder作为响应对象,来响应系统传递过来的事件并进行处理.UIApplication、UIViewController、UIView、和所有从UIView派生出来的UIKit类(包括UIWindow)都直接或间接地继承自UIResponder类.在 UIResponder中定义了处理各种事件和事件传递的接口, 而 CALayer直接继承 NSObject,并没有相应的处理事件的接口.

2.坐标:UIView坐标通过frame,bounds来显示,最终也是通过CALayer来控制.但是CALayer可以通过position与anchorPoint计算位置.

3.动画:UIView本身是由CoreAnimation来实现的.CALayer类来管理实际绘图.CALayer 内部维护着三分 layer tree,分别是 presentLayer Tree(显示树),modeLayer Tree(模型树), Render Tree (渲染树).iOS动画修改的的动画属性,实际上是修改presentLayer的属性值,而最终展示在界面上的是modelLayer.

4.渲染:当更新CALayer视图层,改变不能立即显示在屏幕上.当所有的层都准备好时,可以调用setNeedsDisplay方法来重绘显示.

view.layer.setNeedsDisplay()

5.变换与变形:CALayer层可以添加3D或仿射变换,可以分别设置层的transform或affineTransform属性.CATransform3D的数据结构定义了一个同质的三维变换(4x4 CGFloat值的矩阵,用于图层的旋转,缩放,偏移,歪斜和应用的透视.

15,load和initialize

答:共同点:.如果父类和子类都被调用,父类的调用一定在子类之前

load:当类被引用进项目的时候就会执行load函数(在main函数开始执行之前),与这个类是否被用到无关,每个类的load函数只会自动调用一次.由于load函数是系统自动加载的,因此不需要再调用[super load],否则父类的load函数会多次执行。

1.当父类和子类都实现load函数时,父类的load方法执行顺序要优先于子类

2.当一个类未实现load方法时,不会调用父类load方法

3.类中的load方法执行顺序要优先于类别(Category)

4.当有多个类别(Category)都实现了load方法,这几个load方法都会执行,但执行顺序不确定(其执行顺序与类别在Compile Sources中出现的顺序一致)

5.当然当有多个不同的类的时候,每个类load 执行顺序与其在Compile Sources出现的顺序一致

注意:

load调用时机比较早,当load调用时,其他类可能还没加载完成,运行环境不安全.

initialize:initialize在类或者其子类的第一个方法被调用前调用。即使类文件被引用进项目,但是没有使用,initialize不会被调用。由于是系统自动调用,也不需要显式的调用父类的initialize,否则父类的initialize会被多次执行。假如这个类放到代码中,而这段代码并没有被执行,这个函数是不会被执行的。

1.父类的initialize方法会比子类先执行

2.当子类不实现initialize方法,会把父类的实现继承过来调用一遍。在此之前,父类的方法会被优先调用一次

3.当有多个Category都实现了initialize方法,会覆盖类中的方法,只执行一个(会执行Compile Sources 列表中最后一个Category 的initialize方法)

16,kvc的底层实现

答:(1),检查是否存在相应的key的set方法,如果存在就调用set方法

(2),如果不存在set方法,就会查找与key相同名称带下划线的成员变量,如果有直接给成员变量赋值

(3),如果没有_key就会找相同名称的属性key,如果有直接赋值

(4),如果没有就调用valueForUndefinedKey抛出异常

17,isa指针问题

答:isa是一个class类型的指针,每个实例对象都有一个isa指针指向对象的类,而Class里也有一个isa指针指向元类,元类保存了类方法列表,当类方法被调用先从本身查找类方法如果没有则去向父类查找。元类也是类也是对象,它也有isa指针指向根元类,而根元类的isa指针指向自己。

注:一个对象的isa指针指向它的类对象,从而找到类对象上的方法。

18,self和super

答:self是类的隐藏参数,指向当前调用方法的这个类的实例。super本质是一个编译器标识符,和self指向的是同一个消息接收者。而不同的是self和super调用方法时,self是调用本类的而super是去调用父类的方法,而接收者都是当前类的实例对象(self)。

19,udp和tcp

答:1、TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接

2、TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保   证可靠交付

3、TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的

UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)

4、每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信

5、TCP首部开销20字节;UDP的首部开销小,只有8个字节6、TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道

20,get和post

答:低级回答:GET在浏览器回退时是无害的,而POST会再次提交请求。

GET产生的URL地址可以被Bookmark,而POST不可以。

GET请求会被浏览器主动cache,而POST不会,除非手动设置。

GET请求只能进行url编码,而POST支持多种编码方式。

GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。

GET请求在URL中传送的参数是有长度限制的,而POST么有。

对参数的数据类型,GET只接受ASCII字符,而POST没有限制。

GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。

GET参数通过URL传递,POST放在Request body中。

高级回答:HTTP的底层是TCP/IP。所以GET和POST的底层也是TCP/IP,也就是说,GET/POST都是TCP链接。GET和POST能做的事情是一样一样的。你要给GET加上request body,给POST带上url参数,技术上是完全行的通的。 

GET和POST一个重大区别,简单的说:GET产生一个TCP数据包;POST产生两个TCP数据包。

长的说:对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。

21,weak的使用和assign的区别

答:arc情况下为了防止循环引用让其中一端使用weak来修饰例如:delagate,本身已经对其强引用,自定义IBOutlet控件属性一般也用weak

weak修饰的属性表明一种非拥有关系。为这个属性赋值set的时候,set方法既不保留新值也不释放旧值,属性所指向的对象遭到销毁时,属性值会自动置为nil。而在oc中assgin一般用来修饰基本类型(int,float等),assign可以修饰非oc对象而weak必须修饰oc对象。

22,怎么使用copy关键字

答:1,NSString,NSArray,NSDictary等经常使用copy关键字,它们有对应的可变类型

2,block也经常使用copy关键字,block使用copy是从mrc遗留下来的习惯,在mrc中方法内部的block是在栈区的,使用block可以把它放在堆区。在arc中写不写都可以:对于block而言用copy还是strong效果是一样

注:copy修饰可变类型的话,添加删除操作的时候会因为找不到对应的方法而崩溃,因为copy返回的是一个不可变类型

23,让自己的类使用copy修饰符?

答:需要实现NSCopying协议 的 copyWithZone方法,加入对象还包含一个数组,那么这个数组(数组的内容)也要copy过来

24,@property的本质是什么?ivar,getter,setter是如何生成并添加到类中的?

答:@property = ivar + getter + setter

自动合成:完成属性定义后,编译器会自动编写访问属性所需要的方法,在编译期执行的所以编译器中看不到这些代码。除了生成setter和getter方法,编译器还要向类中自动添加适当类型的实例变量,并且在属性名前加下划线,以此作为实例变量的名称。

每次我们添加一个属性,系统就会在成员变量列表中添加一个成员变量的描述,在方法列表中添加setter和getter的描述,在属性列表中添加一个属性的描述,然后计算属性在当前类中的偏移量,然后给出setter和getter方法的实现,在偏移量处利用setter和getter方法存取值。

25,@protocol和category中如何使用@property

答:只会生成setter和getter的声明,在协议中使用是希望遵守协议的对象能实现该属性。而在category中我们需要借助runtime运行时的两个函数增加属性的实现:(1)objc_setAssociatedObject(2)objc_getAssociatedObject

26,runtime如何实现weak属性

答:runtime对注册的类,会进行布局,对于weak对象会加入一个hash表中。用weak指向的对象内存地址作为key,当这个对象的二引用计数是0时候会dealloc。(依照内存地址为key在hash表中搜索所有以内存地址为key的对象,从而置为nil)

27,@synthesize和@dynamic

答:两者都没有的话默认就是@synthesize

@synthesize的语义是如果你没有实现setter和getter方法,那么编译器会自动帮你加上这个方法

@dynam告诉编译器:属性的setter和getter方法由程序员自己实现,不使用自动生成。但是如果程序员没有实现,编译的时候没问题,当调用属性赋值的时候会因为缺少setter方法导致崩溃。

28,NSString以及NSArray等为什么使用copy关键字?如果strong呢?

答:因为父类指针可以指向子类对象,使用copy为了让对象的属性不受外界影响,如论传入的是一个可变对象还是不可变对象,我本身持有的就是一个不可变的副本。如果使用strong那么有可能指向一个可变对象,如果这个可变对象在外部被修改了那么就会影响到该属性。

29,@synthesize合成实例变量的规则

答:实例变量=成员变量=ivar

1,如果制定了成员变量的名称,会生成一个制定名称的成员变量

2,如果这个成员变量已经存在了就不再生成了

3,@sythesize uzi;如果没有指定成员变量的名称,会自动生成一个属性同名的成员变量

4,如果@sythesize uzi = _uzi就不会生成成员变量了

@property NSString *uzi;

可以通过@sythesize uzi = _myUzi改变成员变量的名称

30,OC中向一个nil对象发送消息会发生什么?

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

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

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

objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,然后在发送消息的时候,objc_msgSend方法不会返回值,所谓的返回内容都是具体调用时执行的。 那么,回到本题,如果向一个nil对象发送消息,首先在寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误。

31,空指针,野指针,僵尸对象

答:空指针是指没有指向任何东西的指针(存储的东西是nil、NULL、0),给空指针发送消息不会报错

指针指向的对象已经被回收掉了.这个指针就叫做野指针.

一个OC对象引用计数为0被释放后就变成僵尸对象了,僵尸对象的内存已经被系统回收,虽然可能该对象还存在,数据依然在内存中,但僵尸对象已经是不稳定对象了,不可以再访问或者使用,它的内存是随时可能被别的对象申请而占用的

32,nil Nil NULL等

答:nil:OC中的对象的空指针

Nil:OC中类的空指针

NULL:C类型的空指针

NSNull:数值类的空对象

33,objc中向一个对象发送消息[obj foo]和objc_msgSend()函数之间有什么关系?

答:[obj foo];在objc动态编译时,会被转意为:objc_msgSend(obj, @selector(foo));。

34,什么时候会报unrecognized selector的异常?

答:objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果,在最顶层的父类中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX 。有三次拯救程序崩溃的办法:

Method resolution

objc运行时会调用+resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数并返回 YES,那运行时系统就会重新启动一次消息发送的过程,如果 resolve 方法返回 NO ,运行时就会移到下一步,消息转发(Message Forwarding)。

Fast forwarding

如果目标对象实现了-forwardingTargetForSelector:,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。 只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。否则,就会继续Normal Fowarding。 这里叫Fast,只是为了区别下一步的转发机制。因为这一步不会创建任何新的对象,但下一步转发会创建一个NSInvocation对象,所以相对更快点。

Normal forwarding

这一步是Runtime最后一次给你挽救的机会。首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nil,Runtime则会发出-doesNotRecognizeSelector:消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送-forwardInvocation:消息给目标对象。

35,一个objc对象如何进行内存布局?(考虑有父类的情况)

答:所有父类的成员变量和自己的成员变量都会存放在该对象所对应的存储空间中.

每一个对象内部都有一个isa指针,指向他的类对象,类对象中存放着本对象的对象方法列表(对象能够接收的消息列表,保存在它所对应的类对象中)和成员变量的列表以及属性列表

1)根对象就是NSobject,它的superclass指针指向nil。

2)类对象既然称为对象,那它也是一个实例。类对象中也有一个isa指针指向它的元类(meta class),即类对象是元类的实例。元类内部存放的是类方法列表,根元类的isa指针指向自己,superclass指针指向NSObject类。

36,runtime如何通过selector找到对应的IMP地址?(分别考虑类方法和实例方法)

答:每一个类对象中都一个方法列表,方法列表中记录着方法的名称,方法实现,以及参数类型,其实selector本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现.

37,objc中的类方法和实例方法有什么本质区别和联系?

答:类方法:

*类方法是属于类对象的

*类方法只能通过类对象调用

*类方法中的self是类对象

*类方法可以调用其他的类方法

*类方法中不能访问成员变量

*类方法中不定直接调用对象方法

实例方法:

*实例方法是属于实例对象的

*实例方法只能通过实例对象调用

*实例方法中的self是实例对象

*实例方法中可以访问成员变量

*实例方法中直接调用实例方法

*实例方法中也可以调用类方法(通过类名)

38,_objc_msgForward函数是做什么的?

答:_objc_msgForward是一个函数指针(和 IMP 的类型一样),是用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发。

在“消息传递”过程中,objc_msgSend的动作比较清晰:首先在 Class 中的缓存查找 IMP (没缓存则初始化缓存),如果没找到,则向父类的 Class 查找。如果一直查找到根类仍旧没有实现,则用_objc_msgForward函数指针代替 IMP 。最后,执行这个 IMP 。

_objc_msgForward消息转发做的几件事:

调用resolveInstanceMethod:方法 (或 resolveClassMethod:)。允许用户在此时为该 Class 动态添加实现。如果有实现了,则调用并返回YES,那么重新开始objc_msgSend流程。这一次对象会响应这个选择器,一般是因为它已经调用过class_addMethod。如果仍没实现,继续下面的动作。

调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,返回非 nil 对象。否则返回 nil ,继续下面的动作。注意,这里不要返回 self ,否则会形成死循环。

调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。如果能获取,则返回非nil:创建一个 NSlnvocation 并传给forwardInvocation:。

调用forwardInvocation:方法,将第3步获取到的方法签名包装成 Invocation 传入,如何处理就在这里面了,并返回非ni。

调用doesNotRecognizeSelector: ,默认的实现是抛出异常。如果第3步没能获得一个方法签名,执行该步骤。

上面前4个方法均是模板方法,开发者可以override,由 runtime 来调用。最常见的实现消息转发:就是重写方法3和4。

39,能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?

答:因为编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表 和 instance_size 实例变量的内存大小已经确定,同时runtime 会调用 class_setIvarLayout 或 class_setWeakIvarLayout 来处理 strong weak 引用。所以不能向存在的类中添加实例变量;

运行时创建的类是可以添加实例变量,调用 class_addIvar 函数。但是得在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前。

40,runloop和线程有什么关系?

答:loop表示某种循环,和run放在一起就表示一直在运行着的循环。实际上,run loop和线程一一对应的。

主线程的run loop默认是启动的

对其它线程来说,run loop默认是没有启动的,如果你需要更多的线程交互则可以手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要。

在任何一个 Cocoa 程序的线程中,都可以通过以下代码来获取到当前线程的 run loop 

41,runloop的mode作用是什么?

答:model 主要是用来指定事件在运行循环中的优先级的,分为:

NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态

UITrackingRunLoopMode:ScrollView滑动时

UIInitializationRunLoopMode:启动时

NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合

42,以+ scheduledTimerWithTimeInterval...的方式触发的timer,在滑动页面上的列表时,timer会暂定回调,为什么?如何解决?

答:RunLoop只能运行在一种mode下,如果要换mode,当前的loop也需要停下重启成新的。利用这个机制,ScrollView滚动过程中NSDefaultRunLoopMode(kCFRunLoopDefaultMode)的mode会切换到UITrackingRunLoopMode来保证ScrollView的流畅滑动:只能在NSDefaultRunLoopMode模式下处理的事件会影响scrllView的滑动。

如果我们把一个NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环中的时候, ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。

解决办法:通过将timer添加到NSRunLoopCommonModes

43,autoreleasepool的实现原理

答:释放池是由n个page组成的双向链表,线程和释放池一一对应;释放池push时会放入哨兵对象,根据next指针放置添加进来的自动释放对象;释放池pop时会将hotpage中next指针依次向前移动,对所指对象调用release直到遇到结束标志,清理过程可跨越page;

App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

44,BAD_ACCESS在什么情况下出现?

答:访问了野指针,比如对一个已经释放的对象执行了release、访问已经释放对象的成员变量或者发消息。 死循环

45,__block做了什么

答:没有__block修饰的外部变量,block会把这个变量复制为自己私有的const变量,也就是说block会捕获栈上的变量(或指针),将其复制为自己私有的const变量。

__block修饰的外部变量,加入__block修饰符所起到的作用就是只要观察到该变量被block所持有,就将该变量在栈中的内存地址放到堆中,此时不管block外部还是内部的变量的内存地址都是一样的,进而不管在block外部还是内部都可以修改变量的值

46,kvo的实现

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

键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey: 。在一个被观察属性发生改变之前, willChangeValueForKey: 一定会被调用,这就 会记录旧的值。而当改变发生后, didChangeValueForKey: 会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。

47,IBOutlet连出来的视图属性为什么可以被设置成weak?

答:既然有外链那么视图在xib或者storyboard中肯定存在,视图已经对它有一个强引用了。

不过这个回答漏了个重要知识,使用storyboard(xib不行)创建的vc,会有一个叫_topLevelObjectsToKeepAliveFromStoryboard的私有数组强引用所有top level的对象,所以这时即便outlet声明成weak也没关系

48,在子线程中创建一个timer会出现什么情况

答:没有作用,子线程中runlop是默认没有开启的,开启子线程的runlop

49,如何让timer调用类方法

答:timer事件中再去调用类方法

50,id和NSObject *的区别

答:id是一个 objc_object 结构体指针,定义是typedefstructobjc_object *idid可以理解为指向对象的指针。所有oc的对象id都可以指向,编译器不会做类型检查,id调用任何存在的方法都不会在编译阶段报错,当然如果这个id指向的对象没有这个方法,该崩溃还是会崩溃的。NSObject*指向的必须是NSObject的子类,调用的也只能是NSObjec里面的方法否则就要做强制类型转换。不是所有的OC对象都是NSObject的子类,还有一些继承自NSProxy。NSObject*可指向的类型是id的子集。

51,weak修饰符的实现原理

答:Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组。

1、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。

2、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。

3、释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。

52,当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. 从引用计数表中删除废弃对象的地址为键值的记录

53,main()加载之前做了哪些事

答:1)dyld 开始将程序二进制文件初始化

2)交由ImageLoader 读取 image,其中包含了我们的类,方法等各种符号(Class、Protocol 、Selector、 IMP)

3)由于runtime 向dyld 绑定了回调,当image加载到内存后,dyld会通知runtime进行处理

4)runtime 接手后调用map_images做解析和处理

5)接下来load_images 中调用call_load_methods方法,遍历所有加载进来的Class,按继承层次依次调用Class的+load和其他Category的+load方法

6)至此 所有的信息都被加载到内存中

7)最后dyld调用真正的main函数

注:dyld会缓存上一次把信息加载内存的缓存,所以第二次比第一次启动快一点

54,动态库和静态库

答:静态库:以.a 和 .framework为文件后缀名。

动态库:以.tbd(之前叫.dylib) 和 .framework 为文件后缀名。

静态库:链接时会被完整的复制到可执行文件中,被多次使用就有多份拷贝。

动态库:链接时不复制,程序运行时由系统动态加载到内存,系统只加载一次,多个程序共用(如系统的UIKit.framework等),节省内存。

模拟器:

iPhone4s-iPnone5:i386

iPhone5s-iPhone7 Plus:x86_64

真机:

iPhone3gs-iPhone4s:armv7

iPhone5-iPhone5c:armv7s

iPhone5s-iPhone7 Plus:arm64

支持armv7的静态库可以在armv7s上正常运行。

55,通知发送在哪个线程

答:NSNotificationCenter消息的接受线程是基于发送消息的线程的。也就是同步的,因此,有时候,你发送的消息可能不在主线程,而大家都知道操作UI必须在主线程,不然会出现不响应的情况。所以,在你收到消息通知的时候,注意选择你要执行的线程。

56,为什么只能在主线程更新UI

答:1、在子线程中是不能进行UI 更新的,而可以更新的结果只是一个幻像:因为子线程代码执行完毕了,又自动进入到了主线程,执行了子线程中的UI更新的函数栈,这中间的时间非常的短,就让大家误以为分线程可以更新UI。如果子线程一直在运行,则子线程中的UI更新的函数栈 主线程无法获知,即无法更新

2、只有极少数的UI能,因为开辟线程时会获取当前环境,如点击某个按钮,这个按钮响应的方法是开辟一个子线程,在子线程中对该按钮进行UI 更新是能及时的,如换标题,换背景图,但这没有任何意义

如果不在主线程中操作UI,可能会造成如下的情况:

(1)两个线程同时设置同一个背景图片,那么很有可能因为当前图片被释放了两次而导致应用崩溃。

(2)两个线程同时设置同一个UIView的背景颜色,那么很有可能渲染显示的是颜色A,而此时在UIView逻辑树上的背景颜色属性为B。

(3)两个线程同时操作view的树形结构:在线程A中for循环遍历并操作当前View的所有subView,然后此时线程B中将某个subView直接删除,这就导致了错乱还可能导致应用崩溃。

57,内存泄漏的几种情况

答:循环引用:delegate,block,timer等

图片没释放,instrument调试后,发现没被释放的全是imageIO,差不多就知道了,把读图的方式,从[UIImage imageNamed:@""],改成imageWithContentsOfFile,就可以了。

CoreFoundation对象(C对象) : 只要函数中包含了create\new\copy\retain等关键字, 那么这些方法产生的对象, 就必须在不再使用的时候调用1次CFRelease或者其他release函数

地图,蓝牙等框架的使用,在根据文档实现某地图,蓝牙相关功能的同时,我们需要注意内存的正确释放,大体需要注意的有需在使用完毕时将地图、代理等滞空为nil,注意地图中标注(大头针)的复用,保存蓝牙设备的数据源清空,使用完毕时清空标注数组等。

大数量的循环,该循环内产生大量的临时对象,直至循环结束才释放,可能导致内存泄漏,解决方法为在循环中创建自己的autoReleasePool,及时释放占用内存大的临时变量,减少内存占用峰值。

58,OC中字典的实现原理

答:1,使用 hash表来实现key和value之间的映射和存储的, hash函数设计的好坏影响着数据的查找访问效率。- (void)setObject:(id)anObject forKey:(id )aKey;

   2,Objective-C中的字典 NSDictionary 底层其实是一个哈希表,实际上绝大多数语言中字典都通过哈希表实现。

那么hash的原理:哈希表的本质是一个数组,数组中每一个元素称为一个箱子(bin),箱子中存放的是键值对。

hash的存储过程:1. 根据 key计算出它的哈希值 h。

    2.假设箱子的个数为 n,那么这个键值对应该放在第 (h % n) 个箱子中。

    3.如果该箱子中已经有了键值对,就使用开放寻址法或者拉链法解决冲突。

    在使用拉链法解决哈希冲突时,每个箱子其实是一个链表,属于同一个箱子的所有键值对都会排列在链表中。

    哈希表还有一个重要的属性:负载因子(load factor),它用来衡量哈希表的 空/满 程度,一定程度上也可以体现查询的效率,计算公式为:

   负载因子 =总键值对数 /箱子个数

   负载因子越大,意味着哈希表越满,越容易导致冲突,性能也就越低。因此,一般来说,当负载因子大于某个常数(可能是1,或者0.75等)时,哈希表将自动扩容。

重哈希概念:

   哈希表在自动扩容时,一般会创建两倍于原来个数的箱子,因此即使 key的哈希值不变,对箱子个数取余的结果也会发生改变,因此所有键值对的存放位置都有可能发生改变,这个过程也称为重哈希(rehash)。

59,block和函数指针

答:相似点:

函数指针和Block都可以实现回调的操作,声明上也很相似,实现上都可以看成是一个代码片段。

函数指针类型和Block类型都可以作为变量和函数参数的类型。(typedef定义别名之后,这个别名就是一个类型)

不同点:

函数指针只能指向预先定义好的函数代码块(可以是其他文件里面定义,通过函数参数动态传入的),函数地址是在编译链接时就已经确定好的。

Block本质是Objective-C对象,是NSObject的子类,可以接收消息。

函数里面只能访问全局变量,而Block代码块不光能访问全局变量,还拥有当前栈内存和堆内存变量的可读性(当然通过__block访问指示符修饰的局部变量还可以在block代码块里面进行修改)。

从内存的角度看,函数指针只不过是指向代码区的一段可执行代码,而block实际上是程序运行过程中在栈内存动态创建的对象,可以向其发送copy消息将block对象拷贝到堆内存,以延长其生命周期。

60,GCD的执行原理

答:GCD有一个底层线程池,这个池中存放的是一个个的线程。之所以称为“池”,很容易理解出这个“池”中的线程是可以重用的,当一段时间后这个线程没有被调用胡话,这个线程就会被销毁。注意:开多少条线程是由底层线程池决定的(线程建议控制再3~5条),池是系统自动来维护,不需要我们程序员来维护。

如果队列中存放的是同步任务,则任务出队后,底层线程池中会提供一条线程供这个任务执行,任务执行完毕后这条线程再回到线程池。这样队列中的任务反复调度,因为是同步的,所以当我们用currentThread打印的时候,就是同一条线程。

如果队列中存放的是异步的任务,(注意异步可以开线程),当任务出队后,底层线程池会提供一个线程供任务执行,因为是异步执行,队列中的任务不需等待当前任务执行完毕就可以调度下一个任务,这时底层线程池中会再次提供一个线程供第二个任务执行,执行完毕后再回到底层线程池中。

这样就对线程完成一个复用,而不需要每一个任务执行都开启新的线程,也就从而节约的系统的开销,提高了效率。在iOS7.0的时候,使用GCD系统通常只能开5~8条线程,iOS8.0以后,系统可以开启很多条线程,但是实在开发应用中,建议开启线程条数:3~5条最为合理。

61,OC中类和结构体

1:类指针赋值时只是复制了地址,结构体是复制内容;

2:类不能有同名同参数个数的方法,结构体可以;

3:结构体方法实现编译时就确定了,类方法实现可动态改变;

4:内存分配不一样,结构体在栈(new方式创建则在堆),类在堆;

5:结构体可以多重继承,类只能单继承。

上一篇下一篇

猜你喜欢

热点阅读