iOS开发面试题

1. KVO的实现原理

KVO的底层实现是使用runtime实现的,在程序运行过程中,创建一个继承于对象观察者的类 (例如观察者对象的类为Person,动态创建NSKVONofitying_Person类,并且继承于Person)使用断点查看发现age属性虽然属于Person类,但是内部的isa指针指向的是 NSKVONofitying_Person
对应的.m文件,假设被观察的对象为age:
- (void)setAge:(int)age {
[super setAge:age];
//在该类中调用下面的的两个方法,这两个方法会触发观察者的observeValueForKeyPath:ofObject:change:context 方法
[self willChangeValueForKey:@"age"];
[self didChangeValueForKey:@"age"];

}

2.代理属性的引用 weak & strong

一般情况下都使用weak,为了避免循环引用。
特殊情况:苹果使用strong引用的代理
NSUrlSession :官方文档
Important
The session object keeps a strong reference to the delegate until your app exits or explicitly invalidates the session. If you do not invalidate the session, your app leaks memory until it exits.
苹果要求所有的网络请求都使用一个类去操作,并且该类应该成为session的代理,将其设置为单例,可以全局使用,不会被销毁(后台下载)

3.代理可以一对多

正常下,代理与通知的区别就是,前者为一对一,后者为一对多。但是代理是可以一对多的,多播代理 ,xmpp 就是这样,即时通讯的时候,多个界面在接受到消息时做出相应的改变

4.runloop

NSRunLoop & CFRunLoop
主线程的runloop是由UIApplicationMain函数开启的
runloop:
保证程序正常运行;
  1. 负责监听系统内核时间,时钟,触摸,网络时间;observe,timer,source(事件源)
  2. 如果没有时间需要处理,就暂时休息
  3. 在一次循环中,需要绘制屏幕上的点

4.1NSRunLoop中的5种mode

  1. NSDefaultRunLoopMode— 默认模式
  2. UITrackingRunLoopMode — 用户交互模式
  3. NSRunLoopCommonMode — 占位模式,也可以理解为兼容以上两种模式
  4. NSConnectionReplyMode
  5. NSModalPanelRunLoopMode
    子线程异步销毁,eg:子线程中创建NSTimer ,timer的方法并不会执行;
    解决方案:常驻线程。每个线程都有一个runloop,主线程不会挂掉,子线程上面的runloop默认不启动,因此子线程执行完代码就会销毁
    [[NSRunLoop currentRunLoop] run];//runloop启动后就保证运行,后续代码无法执行,子线程如果需要执行timer,使用dipatch就可以。

4.2 runloop应用案例,性能优化

  1. 耗时操作放子线程,更新UI放主线程
  2. 假设更新UI做了很多操作,如何进行性能优化
    案例:UITableView上加载多张高清大图,并且不让tableView复用,在滚动时runloop会造成卡段。runloop在进行UITracking时,会绘制很多的点,这个是很消耗的资源的。
    思路:
  3. 减少性能开销
  4. 分步加载
    分步加载原理,监听runloop循环,将耗时操作放在一个数组中,不去执行,一次runloop循环,从数组中拿出来一个执行
    使用CFRunLoop解决出现的问题
    NULL指针赋值,对象赋值nil

5.nonatomic与natomic

前者非原子性,线程不安全;后者原子性,性能安全. Apple这么设计是为了提高性能,所以会出现非原子性的属性。
使用时nonatomic我们不能使用多个线程去访问,会出现抢夺资源的现象

6.NSURLConnection与NSURLSession

connection复杂的网络请求默认是同步的(虽然也有异步网络请求),一旦异步做下载,Connection需要手动开启runloop(使用后关闭runloop以及线程),否则会出现子线程销毁.

7.将 UIImage 保存到磁盘,用什么方式最好?

目前来说,保存 UIImage 有三种方式:1.直接用 NSKeyedArchiver 把 UIImage 序列化保存,2.用 UIImagePNGRepresentation() 先把图片转为 PNG 保存,3.用 UIImageJPEGRepresentation() 把图片压缩成 JPEG 保存。
实际上,NSKeyedArchiver 是调用了 UIImagePNGRepresentation 进行序列化的,用它来保存图片是消耗最大的。苹果对 JPEG 有硬编码和硬解码,保存成 JPEG 会大大缩减编码解码时间,也能减小文件体积。所以如果图片不包含透明像素时,UIImageJPEGRepresentation(0.9) 是最佳的图片保存方式,其次是 UIImagePNGRepresentation()。

8. weak&assign

  1. assign除了可以修饰OC对象以外,还可以修饰基本数据类型
  2. weak修改的属性的成员变量为__ weak 修饰的,assign修饰的属性的成员变量是使用 __ unsafe __unretained 修饰的
__weak_unsafe__unretained
  1. 都不是强指针,不能保证对象不被销毁
  2. __weak:所指向的对象销毁后,会自动变成nil指针(空指针);指针被销毁
  3. _unsafe__unretained:所指向的对象销毁后,仍旧指向已经销毁的对象,从而引起BAD_ACCESS崩溃。指针未被销毁
    1.在 ARC 中,在有可能出现循环引用的时候,往往要通过让其中一端使用 weak 来解决,比如: delegate 代理属性。
    2.自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用 weak,自定义 IBOutlet 控件属性一般也使用 weak;当然,也可以使用strong。
    IBOutlet连出来的视图属性为什么可以被设置成weak?
    因为父控件的subViews数组已经对它有一个强引用。
    不同点:
    assign 可以用非 OC 对象,而 weak 必须用于 OC 对象。
    weak 表明该属性定义了一种“非拥有关系”。在属性所指的对象销毁时,属性值会自动清空(nil)。

9 消息转发

[receiver message]调用方法时,如果在message方法在receiver对象的类继承体系中没有找到方法,那怎么办?一般情况下,程序在运行时就会Crash掉,抛出unrecognized selector sent to…类似这样的异常信息。但在抛出异常之前,还有三次机会按以下顺序让你拯救程序。
  1. Method Resolution
    动态方法解析:向当前类发送 resolveInstanceMethod: 信号,检查是否动态向该类添加了方法
  2. Fast Forwarding
    快速消息转发:检查该类是否实现了 ·之间传递之间是地址传递
    3.苹果推荐我们在swift开发中多使用结构体。使用结构体遵守协议实现类似于类的继承效果

野指针&僵尸对象

weak:被修饰指针指向的对象释放以后,该指针会自动指向0x0(nil)。
unsafe_unretain 特点,当指向的对象释放以后,被其修饰的指针依旧会指向原来的地址;
僵尸对象:指向的地址 在原来对象释放以后还没有被分配。此时指向的即为僵尸对象
野指针:指向的地址,在原来对象释放完成以后,重新分配给其他对象使用。此时该指针为野指针,即指向了不该指向的内存地址 。

帧数

一秒小于16帧,用户就可以明显感觉到卡顿,一般电影是20帧或者30帧

10.hitTest 事件分发分析

userInteractionEnabled = NO(这个属性大家肯定不陌生,UILabel和UIImageView无法接收点击事件就是因为这个属性默认设置为了NO),类似还有Hidden=YES/alpha<0.01等都可以使某个视图不响应点击事件。userInteractionEnabled = NO等价于使当前的hitTest:withEvent返回nil。
UIView是如何判定这个事件是否是自己应该处理的呢?iOS系统检测到一个触摸操作时会打包一个UIEvent对象,并放入Application的队列,Application从队列中取出事件后交给UIWindow来处理,UIWindow会使用hitTest:withEvent:方法来递归的寻找操作初始点所在的view,这个过程成为hit-test view.
苹果官方文档:
The hitTest:withEvent: method returns the hit test view for a given CGPoint and UIEvent. The hitTest:withEvent: method begins by calling the pointInside:withEvent: method on itself. If the point passed into hitTest:withEvent: is inside the bounds of the view, pointInside:withEvent: returns YES. Then, the method recursively calls hitTest:withEvent: on every subview that returns YES.
If the point passed into hitTest:withEvent: is not inside the bounds of the view, the first call to the pointInside:withEvent: method returns NO, the point is ignored, and hitTest:withEvent: returns nil. If a subview returns NO, that whole branch of the view hierarchy is ignored, because if the touch did not occur in that subview, it also did not occur in any of that subview’s subviews. This means that any point in a subview that is outside of its superview can’t receive touch events because the touch point has to be within the bounds of the superview and the subview. This can occur if the subview’s clipsToBounds property is set to NO.
翻译下,差不多就是:
1.首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内;
2.若pointInside:withEvent:方法返回NO,说明触摸点不在当前视图内,则当前视图的hitTest:withEvent:返回nil.
3.若pointInside:withEvent:方法返回YES,说明触摸点在当前视图内,则遍历当前视图的所有子视图(subviews),调用子视图的hitTest:withEvent:方法重复前面的步骤,子视图的遍历顺序是从top到bottom,即从subviews数组的末尾向前遍历,直到有子视图的hitTest:withEvent:方法返回非空对象或者全部子视图遍历完毕.
4.若第一次有子视图的hitTest:withEvent:方法返回非空对象,则当前视图的hitTest:withEvent:方法就返回此对象,处理结束;
5.若所有子视图的hitTest:withEvent:方法都返回nil,则当前视图的hitTest:withEvent:方法返回当前视图自身(self).
MJ大神的事件分发示意图

11.堆与栈

Objective-C的对象在内存中是以堆的方式分配空间的,并且堆内存是由你释放的,即release
栈由编译器管理自动释放的,在方法中(函数体)定义的变量通常是在栈内,因此如果你的变量要跨函数的话就需要将其定义为成员变量。
1.栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量等值。其操作方式类似于数据结构中的栈。
2.堆区(heap):一般由程序员分配释放,若程序员不释放,则可能会引起内存泄漏。注堆和数据结构中的堆栈不一样,其类是与链表。
操作系统iOS 中应用程序使用的计算机内存不是统一分配空间,运行代码使用的空间在三个不同的内存区域,分成三个段:“text segment “,“stack segment ”,“heap segment ”。
段“text segment ”是应用程序运行时应用程序代码存在的内存段。每一个指令,每一个单个函数、过程、方法和执行代码都存在这个内存段中直到应用程序退出。一般情况下,你不会真的不得不知道这个段的任何事情。
当应用开始以后,函数main() 被调用,一些空间分配在”stack” 中。这是为应用分配的另一个段的内存空间,这是为了函数变量存储需要而分配的 内存。每一次在应用中调用一个函数,“stack ”的一部分会被分配在”stack” 中,称之为”frame” 。新函数的本地变量分配在这里。
正如名称所示,“stack ”是后进先出(LIFO )结构。当函数调用其他的函数时,“stack frame ”会被创建;当其他函数退出后,这个“frame ”会自动被破坏。
“heap” 段也称为”data” 段,提供一个保存中介贯穿函数的执行过程,全局和静态变量保存在“heap”中,直到应用退出。
为了访问你创建在heap 中的数据,你最少要求有一个保存在stack 中的指针,因为你的CPU 通过stack 中的指针访问heap 中的数据。
你可以认为stack 中的一个指针仅仅是一个整型变量,保存了heap 中特定内存地址的数据。实际上,它有一点点复杂,但这是它的基本结构。
简而言之,操作系统使用stack 段中的指针值访问heap 段中的对象。如果stack 对象的指针没有了,则heap 中的对象就不能访问。这也是内存泄露的原因。
在iOS 操作系统的stack 段和heap 段中,你都可以创建数据对象。
stack 对象的优点主要有两点,一是创建速度快,二是管理简单,它有严格的生命周期。stack 对象的缺点是它不灵活。创建时长度是多大就一直是多 大,创建时是哪个函数创建的,它的owner 就一直是它。不像heap 对象那样有多个owner ,其实多个owner 等同于引用计数。只有 heap 对象才是采用“引用计数”方法管理它。

stack 对象的创建

只要栈的剩余空间大于stack 对象申请创建的空间,操作系统就会为程序提供这段内存空间,否则将报异常提示栈溢出。

heap 对象的创建

操作系统对于内存heap 段是采用链表进行管理的。操作系统有一个记录空闲内存地址的链表,当收到程序的申请时,会遍历链表,寻找第一个空间大于所申请的heap 节点,然后将该节点从空闲节点链表中删除,并将该节点的空间分配给程序。
例如:
NSString 的对象就是stack 中的对象,NSMutableString 的对象就是heap 中的对象。前者创建时分配的内存长度固定且不可修改;后者是分配内存长度是可变的,可有多个owner, 适用于计数管理内存管理模式。
两类对象的创建方法也不同,前者直接创建“NSString * str1=@”welcome”; “,而后者需要先分配再初始化“ NSMutableString * mstr1=[[NSMutableString alloc] initWithString:@”welcome”]; ”。
ios中堆栈的区别

管理方式:

对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来讲,释放工作有程序员控制,容易产生memory Leak。

申请大小:

栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存区域。这句话的意思是栈顶上的地址和栈的最大容量是系统预先规定好的,在Windows下,栈的大小是2M(也有的说1M,总之是编译器确定的一个常数),如果申请的空间超过了栈的剩余空间时候,就overflow。因此,能获得栈的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大笑受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

碎片的问题:

对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存快从栈中弹出。

分配方式:

堆都是动态分配的,没有静态分配的堆。栈有两种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配是有alloc函数进行分配的,但是栈的动态分配和堆是不同的,他的动态分配由编译器进行释放,无需我们手工实现。
分配效率:
栈是机器系统提供的数据结构,计算机会在底层堆栈提供支持,分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,他的机制是很复杂的。

12.浅谈数据库的存储过程

13.僵尸对象、野指针、空指针分别指什么,有什么区别?

僵尸对象:一个OC对象引用计数为0被释放后就变成僵尸对象了,僵尸对象的内存已经被系统回收,虽然可能该对象还存在,数据依然在内存中,但僵尸对象已经是不稳定对象了,不可以再访问或者使用,它的内存是随时可能被别的对象申请而占用的;
野指针:野指针出现的原因是指针没有赋值,或者指针指向的对象已经被释放掉了,野指针指向一块随机的垃圾内存,向他们发送消息会报EXC_BAD_ACCESS错误导致程序崩溃;
空指针:空指针不同于野指针,它是一个没有指向任何东西的指针,空指针是有效指针,值为nil、NULL、Nil或0等,给空指针发送消息不会报错,只是不响应消息而已,应该给野指针及时赋予零值变成有效的空指针,避免内存报错。

14.Objective-C有GC垃圾回收机制吗?

GC(Garbage Collection),垃圾回收机制,简单地说就是程序中及时处理废弃不用的内存对象的机制,防止内存中废弃对象堆积过多造成内存泄漏。Objective-C语言本身是支持垃圾回收机制的,但有平台局限性,仅限于Mac桌面系统开发中,而在iPhone和iPad等苹果移动终端设备中是不支持垃圾回收机制的。在移动设备开发中的内存管理是采用MRC(Manual Reference Counting)以及iOS5以后的ARC(Automatic Reference Counting),本质都是RC引用计数,通过引用计数的方式来管理内存的分配与释放,从而防止内存泄漏。
另外引用计数RC和垃圾回收GC是有区别的。垃圾回收是宏观的,对整体进行内存管理,虽然不同平台垃圾回收机制有异,但基本原理都是一样的:将所有对象看做一个集合,然后在GC循环中定时检测活动对象和非活动对象,及时将用不到的非活动对象释放掉来避免内存泄漏,也就是说用不到的垃圾对象是交给GC来管理释放的,而无需开发者关心,典型的是Java中的垃圾回收机制;相比于GC,引用计数是局部性的,开发者要管理控制每个对象的引用计数,单个对象引用计数为0后会马上被释放掉。ARC自动引用计数则是一种改进,由编译器帮助开发者自动管理控制引用计数(自动在合适的时机发送release和retain消息)。另外自动释放池autorelease pool则像是一个局部的垃圾回收,将部分垃圾对象集中释放,相对於单个释放会有一定延迟。

15.自动释放池跟GC(垃圾回收)有什么区别?iPhone上有GC么?[pool release]和[pool drain]有什么区别?

[pool release]和[pool drain]在作用上是一样的,都是倾倒自动释放池,区别是drain在支持GC垃圾回收的系统中(Mac系统)可以引起GC回收操作,而release不可以。对于自动释放池一般还是使用[pool drain]了,一是它的功能对系统兼容性更强,二者也是为了跟普通对象的release释放区别开。自动释放池的释放操作指的是向池内所有的对象发送release消息,以让系统及时释放池内的所有对象。

16.如果一个对象释放前被加到了NotificationCenter中,不在NotificationCenter中remove这个对象可能会出现什么问题?

首先对于NotificationCenter的使用,我们都知道,只要添加对象到消息中心进行通知注册,之后就一定要对其remove进行通知注销。将对象添加到消息中心后,消息中心只是保存该对象的地址,消息中心到时候会根据地址发送通知给该对象,但并没有取得该对象的强引用,对象的引用计数不会加1。如果对象释放后却没有从消息中心remove掉进行通知注销,也就是通知中心还保存着那个指针,而那个指针指的对象可能已经被释放销毁了,那个指针就成为一个野指针,当通知发生时,会向这个野指针发送消息导致程序崩溃。

17.Objective-C是如何实现内存管理的?autorealease pool自动释放池是什么?autorelease的对象是在什么时候被release的?autorelease和release有什么区别?

引用计数
Objective-C的内存管理本质上是通过引用计数实现的,每次RunLoop都会检查对象的引用计数,如果引用计数为0,说明该对象已经没人用了,可以对其进行释放了。其中引用计数可以大体分为三种:MRC(手动内存计数)、ARC(自动内存计数,iOS5以后)和内存池。
其中引用计数是如何操作的呢?不论哪种引用计数方式,本质都是在合适的时机将对象的引用计数加1或者减1。
这里简单归结一下:
使对象引用计数加1的常见操作有:alloc、copy、retain
使对象引用计数减1的常见操作有:release、autorealease
自动释放池
自动释放池是一个统一来释放一组对象的容器,在向对象发送autorelease消息时,对象并没有立即释放,而是将对象加入到最新的自动释放池(即将该对象的引用交给自动释放池,之后统一调用release),自动释放池会在程序执行到作用域结束的位置时进行drain释放操作,这个时候会对池中的每一个对象都发送release消息来释放所有对象。这样其实就实现了这些对象的延迟释放。
自动释放池释放的时机,也就是自动释放池内的所有对象是在什么时候释放的,这里要提到程序的运行周期RunLoop。对于每一个新的RunLoop,系统都会隐式的创建一个autorelease pool,RunLoop结束时自动释放池便会进行对象释放操作。

autorelease和release的区别主要是引用计数减一的时机不同,autorelease会在对象的使用真正结束的时候才做引用计数减1,而不是收到消息立马释放。
retain、release和autorelease的底层实现
最后通过了解这三者的较底层实现来理解它们的本质区别:
-(id)retain {
// 对象引用计数加1
NSIncrementExtraRefCount(self);
return self;
}

-(void)release {
// 对象引用计数减1,之后如果引用计数为0则释放
if(NSDecrementExtraRefCountWasZero(self)) {
NSDeallocateObject(self);
}
}

-(id)autorelease {
// 添加对象到自动释放池
[NSAutoreleasePool addObject:self];
return self;
}

18. 为什么很多内置的类,如TableViewController的delegate的属性是assign不是retain?

delegate代理的属性通常设置为assign或者weak是为了避免循环引用,所有的引用计数系统,都存在循环引用的问题,但也有个别特殊情况,个别类的代理例如CAAnimation的delegate就是使用strong强引用。
其他问法: 委托的property声明用什么属性?为什么?

19. CAAnimation的delegate代理是强引用还是弱引用?

CAAnimation的代理是强引用,是内存管理中的其中一个罕见的特例。我们知道为了避免循环引用问题,delegate代理一般都使用weak修饰表示弱引用的,而CAAnimation动画是异步的,如果动画的代理是弱应用不是强应用的话,会导致其随时都可能被释放掉。在使用动画时要注意采取措施避免循环引用,例如及时在视图移除之前的合适时机移除动画。
CAAnimation的代理定义如下,明确说了动画的代理在动画对象整个生命周期间是被强引用的,默认为nil。
/* The delegate of the animation. This object is retained for the
* lifetime of the animation object. Defaults to nil. See below for the
* supported delegate methods. */

@property(nullable, strong) id <CAAnimationDelegate> delegate;

20. OC中,与alloc语义相反的方法是dealloc还是release?与retain语义相反的方法是dealloc还是release?需要与alloc配对使用的方法是dealloc还是release,为什么?

alloc与dealloc语意相反,alloc是创建变量,dealloc是释放变量;
retain与release语义相反,retain保留一个对象,调用后使变量的引用计数加1,而release释放一个对象,调用后使变量的引用计数减1。
虽然alloc对应dealloc,retain对应release,但是与alloc配对使用的方法是release,而不是dealloc。为什么呢?这要从他们的实际效果来看。事实上alloc和release配对使用只是表象,本质上其实还是retain和release的配对使用。alloc用来创建对象,刚创建的对象默认引用计数为1,相当于调用alloc创建对象过程中同时会调用一次retain使对象引用计数加1,自然要有对应的release的一次调用,使对象在不用时能够被释放掉防止内存泄漏。
此外,dealloc是在对象引用计数为0以后系统自动调用的,dealloc没有使对象引用计数减1的作用,只是在对象引用计数为0后被系统调用进行内存回收的收尾工作。

21.以下每行代码执行后,person对象的retain count分别是多少

Person *person = [[Person alloc] init];
[person retain];
[person release];
[person release];
1-2-1-0。开始alloc创建对象并持有对象,初始引用计数为1,retain一次引用计数加1变为2,之后release对象两次,引用计数减1两次,先后变为1、0。

22.问题:执行下面的代码会发生什么后果?

Ball *ball = [[[[Ball alloc] init] autorelease] autorelease];
程序会因其而崩溃,因为对象被加入到自动释放池两次,当对象被移除时,自动释放池将其释放了不止一次,其中第二次释放必定导致崩溃。

23.内存管理的几条原则是什么?按照默认法则,哪些关键字生成的对象需要手动释放?哪些对象不需要手动释放会自动进入释放池?在和property结合的时候怎样有效的避免内存泄露?

起初MRC中开发者要自己手动管理内存,基本原则是:谁创建,谁释放,谁引用,谁管理。其中创建主要始于关键词new、alloc和copy的使用,创建并持有开始引用计数为1,如果引用要通过发送retain消息增加引用计数,通过发送release消息减少引用计数,引用计数为0后对象会被系统清理释放。现在有了ARC后编译器会自动管理引用计数,开发者不再需要也不可以再手动显示管理引用计数。
当使用new、alloc或copy方法创建一个对象时,该对象引用计数器为1。不再需要使用该对象时可以向其发送release或autorelease消息,在其使用完毕时被系统释放。如果retain了某个对象,需要对应release或autorelease该对象,保持retain方法和release方法使用次数相等。
使用new、alloc、copy关键字生成的对象和retain了的对象需要手动释放。设置为autorelease的对象不需要手动释放,会直接进入自动释放池。
下面代码的输出依次为:
NSMutableArray* ary = [[NSMutableArray array] retain];
NSString *str = [NSString stringWithFormat:@"test"];
[str retain];
[ary addObject:str];
NSLog(@"%@%d",str,[str retainCount]);
[str retain];
[str release];
[str release];
NSLog(@"%@%d",str,[str retainCount]);
[ary removeAllObjects];
NSLog(@"%@%d",str,[str retainCount]);
  • 2,3,1
  • 3,2,1(right)
  • 1,2,3
  • 2,1,3
    此问题考查的是非MRC下引用计数的使用(只有在MRC下才可以通过retain和release关键字手动管理内存对象,才可以向对象发送retainCount消息获取当前引用计数的值),开始使用类方法stringWithFormat在堆上新创建了一个字符串对象str,str创建并持有该字符串对象默认引用计数为1,之后retain使引用计数加1变为2,然后又动态添加到数组中且该过程同样会让其引用计数加1变为3(数组的add操作是添加对成员对象的强引用),此时打印结果引用计数为3;之后的三次操作使引用计数加1后又减2,变为2,此时打印引用计数结果为2;最后数组清空成员对象,数组的remove操作会让移除的对象引用计数减1,因此str的引用计数变为了1,打印结果为1。因此先后引用计数的打印结果为:3,2,1。
这里要特别注意上面为何说stringWithFormat方法是在堆上创建的字符串对象,这里涉及到NSString的内存管理,下面单独对其进行扩展和分析。
OC中常用的创建NSString字符串对象的方法主要有以下五种:
// 字面量直接创建
NSString *str1 = @"string";
// 类方法创建
NSString *str2 = [NSString stringWithFormat:@"string"];
NSString *str3 = [NSString stringWithString:@"string"]; // 编译器优化后弃用,效果等同于str1的字面量创建方式
// 实例方法创建
NSString *str4 = [[NSString alloc] initWithFormat:@"string"];
NSString *str5 = [[NSString alloc] initWithString:@"string"]; // 编译器优化后弃用,效果等同于str1的字面量创建方式
开发中推荐的是前两种str1和str2的创建方式,分别用来创建不可变字符串和格式化字符串。最新的编译器优化后弃用了str3的stringWithString和str5的initWithString创建方式,现在这样创建会报警告,说这样创建是多余的,因为实际效果和直接用字面量创建相同,也都是在常量内存区创建一个不可变字符串。另外,此处由于字符串的内容都是“string”,使用str1、str3和str5创建的的字符串对象实际在常量内存区只有一个备份,这是编译器的优化效果,而str2和str4由于是在堆上创建因此各自有自己的备份。
此外最重要的是这五种方法创建的字符串对象所处的内存类型,str1、str3和str5都是创建的不可变字符串,是位于常量内存区的,由系统管理内存;stringWithFormat和initWithFormat创建的都是格式化的动态字符串对象,在堆上创建,需要手动管理内存。
相关问题:当你用stringWithString来创建一个新NSString对象的时候,你可以认为:
这个新创建的字符串对象已经被autorelease了(right)
这个新创建的字符串对象已经被retain了
全都不对
这个新创建的字符串对象已经被release了

22.什么是安全释放?

释放掉不再使用的对象同时不会造成内存泄漏或指针悬挂问题称其为安全释放。

23.这段代码有什么问题,如何修改?

for (int i = 0; i < someLargeNumber; i++) {
NSString *string = @”Abc”;
string = [string lowercaseString];
string = [string stringByAppendingString:@"xyz"];
NSLog(@“%@”, string);
}
代码通过循环短时间内创建了大量的NSString对象,在默认的自动释放池释放之前这些对象无法被立即释放,会占用大量内存,造成内存高峰以致内存不足。
为了防止大量对象堆积应该在循环内手动添加自动释放池,这样在每一次循环结束,循环内的自动释放池都会被自动释放及时腾出内存,从而大大缩短了循环内对象的生命周期,避免内存占用高峰。
代码改进方法是在循环内部嵌套一个自动释放池:
for (int i = 0; i < 1000000; i++) {
@autoreleasepool {
NSString *string = @"Abc";
string = [string lowercaseString];
string = [string stringByAppendingString:@"xyz"];
NSLog(@"%@",string);
}
}
相关问题:这段代码有什么问题?会不会造成内存泄露(多线程)?在内存紧张的设备上做大循环时自动释放池是写在循环内好还是循环外好?为什么?
for(int index = 0; index < 20; index++) {
NSString *tempStr = @”tempStr”;
NSLog(tempStr);
NSNumber *tempNumber = [NSNumber numberWithInt:2];
NSLog(tempNumber);
}

24.在block内如何修改block外部变量?

在block内部修改block外部变量会编译不通过,提示变量缺少block修饰,不可赋值。要想在block内部修改block外部变量,则必须在外部定义变量时,前面加上block修饰符:
/* block外部变量 */
__block int var1 = 0;
int var2 = 0;
/* 定义block */
void (^block)(void) = ^{
/* 试图修改block外部变量 */
var1 = 100;
/* 编译错误,在block内部不可对var2赋值 */
// var2 = 1;
};
/* 执行block */
block();
NSLog(@"修改后的var1:%d",var1); // 修改后的var1:100

25.使用block时什么情况会发生引用循环,如何解决?

常见的使用block引起引用循环的情况为:在一个对象中强引用了一个block,在该block中又强引用了该对象,此时就出现了该对象和该block的循环引用,例如:
/* Test.h */
#import <Foundation/Foundation.h>
/* 声明一个名为MYBlock的block,参数为空,返回值为void */
typedef void (^MYBlock)();

@interface Test : NSObject
/* 定义并强引用一个MYBlock */
@property (nonatomic, strong) MYBlock block;
/* 对象属性 */
@property (nonatomic, copy) NSString *name;

- (void)print;

@end

/* Test.m */
#import "Test.h"
@implementation Test

- (void)print {
self.block = ^{
NSLog(@"%@",self.name);
};
self.block();
}

@end
解决上面的引用循环的方法一个是强制将一方置nil,破坏引用循环,另外一种方法是将对象使用weak或者block修饰符修饰之后再在block中使用(注意是在ARC下):
- (void)print {
__weak typeof(self) weakSelf = self;
self.block = ^{
NSLog(@"%@",weakSelf.name);
};
self.block();
}

26.http://yulingtianxia.com/blog/2016/06/15/Objective-C-Message-Sending-and-Forwarding/“>消息发送和转发机制

27.Block相关

  1. block的实质是什么?一共有几种block?都是什么情况下生成的?
  2. 为什么在默认情况下无法修改被block捕获的变量? __block都做了什么?
  3. 模拟一下循环引用的一个情况?block实现界面反向传值如何实现?

28.Runtime

  1. objc在向一个对象发送消息时,发生了什么?
  2. 什么时候会报unrecognized selector错误?iOS有哪些机制来避免走到这一步?
  3. 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
  4. runtime如何实现weak变量的自动置nil?
  5. 给类添加一个属性后,在类结构体里哪些元素会发生变化?

29.RunLoop

  1. runloop是来做什么的?runloop和线程有什么关系?主线程默认开启了runloop么?子线程呢?
  2. runloop的mode是用来做什么的?有几种mode?
  3. 为什么把NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环以后,滑动scrollview的时候NSTimer却不动了?
  4. 苹果是如何实现Autorelease Pool的?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章