iOS常见面试题汇总
iOS常见面试题汇总
1. 什么是 ARC? (ARC 是为了解决什么问题而诞生的?)
ARC 是 Automatic Reference Counting 的缩写, 即自动引用计数. 这是苹果在 iOS5 中引入的内存管理机制. Objective-C 和 Swift 使用 ARC 追踪和管理应用的内存使用. 这一机制使得开发者无需键入retain
和 release
, 这不仅能够降低程序崩溃和内存泄露的风险, 而且可以减少开发者的工作量, 能够大幅度提升程序的流畅性和可预测性. 但是 ARC 不适用于 Core Foundation 框架中, 仍然需要手动管理内存.
2. 以下 keywords
有什么区别: assign vs weak
, __block vs __weak
-
assign
和weak
是用于在声明属性时, 为属性指定内存管理的语义.
assign 用于简单的赋值, 不改变属性的引用计数, 用于 Objective-C 中的NSInteger
,CGFloat
以及C
语言中int
,float
,double
等数据类型. -
assign
看起来跟weak
一样,其实不能混用的,assign
的变量在释放后并不设置为nil
(和weak不同), 当你再去引用时候就会发生错误,崩溃,EXC_BAD_ACCESS
.
Has an assign attribute, AND Has been deallocated. Thus when you attempt to call the respondsToSelector method on delegate, you get a EXC_BAD_ACCESS. This is because objects that use the assign property will not be set to nil when they are deallocated. (Hence why doing a !self.delegate before the respondsToSelector does not prevent the responseToSelector from being called on a deallocated object, and still crashes your code) The solution is to use the weak attribute, because when the object deallocates, the pointer WILL be set the nil . So when your code calls respondsToSelector on a nil, Objective C will ignore the call, and not crash.
-
weak
用于对象类型, 由于weak
同样不改变对象的引用计数且不持有对象实例, 当该对象废弃时,该弱引用自动失效并且被赋值为 nil , 所以它可以用于避免两个强引用产生的 循环引用导致内存无法释放的问题.__block
和__weak
之间的区别确实极大的, 不过它们都用于修饰变量. -
前者用于指明当前声明的变量在被 block 捕获之后, 可以在 block 中改变变量的值. 因为在 block 声明的同时会截获该 block 所使用的全部自动变量的值, 而这些值只在 block 中 只具有"使用权"而不具有"修改权" . 而
__block
说明符就为 block 提供了变量的修改权. -
后者是 所有权修饰符 , 什么是所有权修饰符? 这里涉及到另一个问题, 因为在 ARC 有效时, id 类型和对象类型同 C 语言中的其他类型不同, 必须附加所有权修饰符. 所有权修饰符一种有 4 种:
1、__strong
2、__weak
3、__unsafe_unretained
4、__autorelease -
block
不能修改局部变量,如果需要修改需要加上__block
. -
block
会对对象强引用,引起retain-cycle,需要使用weak
__weak __typeof(&*self)weakSelf =self;
注意:在block种使用weakSelf可避免这种强引用。(两个指针,指向同一块地址(self));
__weak
与weak
的区别只在于, 前者用于变量的声明, 而后者用于属性的声明.
3. __block
在 ARC 和非 ARC 下含义一样吗?
__block
在 ARC 下捕获的变量会被 block retain , 这样可能导致循环引用, 所以必须要使用弱引用才能解决该问题. 而在非 ARC 下, 可以直接使用 __block
说明符修饰变量, 因为在非 ARC 下, block 不会 retain 捕获的变量.
4. 使用 nonatomic 一定是线程安全的吗
nonatomic 的内存管理语义是 非原子 的, 非原子的操作本来就是线程不安全的, 而 atomic 的操作是原子的, 但是 并不意味着它是线程安全的 , 它会增加正确的几率, 能够更好的避免线程的错误, 但是它仍然是线程不安全的.
当使用 nonatomic 的时候, 属性的 setter 和 getter 操作是非原子的, 所以当多个线程同时对某一属性进行读和写的操作, 属性的最终结果是不能预测的.
当使用 atomic 时, 虽然对属性的读和写是原子的, 但是仍然可能出现线程错误: 当线程 A 进行写操作, 这时其他线程的读或写操作会因为该操作的进行而等待. 当 A 线程的写操作结束后, B 线程进行写操作, 然后当 A 线程进行读操作时, 却获得了在 B 线程中的值, 这就破坏了线程安全, 如果有线程 C 在 A 线程读操作前 release 了该属性, 那么还会导致程序崩溃. 所以仅仅使用 atomic 并不会使得线程安全, 我们还需要为线程添加 lock 来确保线程的安全.
atomic 都不是一定线程安全的, nonatomic 就更不必多说了.
5. + (void)load;
和 + (void)initialize;
有什么用处?
当类对象被引入项目时, runtime 会向每一个类对象发送 load 消息. load 方法还是非常的神奇的, 因为它会在 每一个类甚至分类 被引入时仅调用一次, 调用的顺序是父类优先于子类, 子类优先于分类. 而且 load 方法不会被类自动继承, 每一个类中的 load 方法都不需要像 viewDidLoad 方法一样调用父类的方法.
initialize 方法和 load 方法有一些不同, 它虽然也会在整个 runtime 过程中调用一次, 但是它是在 该类的第一个方法执行之前 调用, 也就是说 initialize 的调用是 惰性 的, 它的实现也与我们在平时使用的惰性初始化属性时基本相同. 在该方法中主要做 静态变量的设置 并用于确保在实例初始化前某些条件必须满足.
+(void)load
方法只要加入了工程种,进行了编译,且.m中实现了这个方法,都会调用一次,值得注意的时没实现的子类是不会调用的,就算父类实现了也不行。categories,都实现了这个方法,只会调用其中一个,具有不确定性。
+(void)initialize
在发送第一条消息给类的时候进行调用,跟load方法的不同之处在于,比较迟,可实现懒加载,且父类.m实现了该方法,子类不实现也会调用父类,跟正常的方法一样。
6.为什么其他语言里叫函数调用, Objective-C 中是给对象发送消息 (谈下对 runtime 的理解)
我们在其他语言中比如说: C, Python, Java, C++, Haskell ... 中提到函数调用或者方法调用(面向对象). 函数调用是在编译期就已经决定了会调用哪个函数(方法), 编译器在编译期就能检查出函数的执行是否正确.
然而 Objective-C(ObjC) 是一门动态的语言, 整个 ObjC 语言都是尽可能的将所有的工作推迟到运行时才决定. 它基于 runtime 来工作, runtime 就是 ObjC 的灵魂, 其核心就是消息发送 objc_msgSend
.
What makes Objective-C truly powerful is its runtime.
所有的消息都会在运行时才会确定, [obj message]
在运行时会被转化为 objc_msgSend(id self, SEL cmd, ...)
来执行, 它会在运行时从 选择子表中寻找对应的选择子 并将选择子与实现进行绑定. 而如果没有找到对应的实现, 就会进入类似黑魔法的消息转发流程. 调用 + (BOOL)resolveInstanceMethod:(SEL)aSelector
方法, 我们可以在这个方法中 为类动态地生成方法 .
我们几乎可以使用 runtime 魔改 Objective-C 中的一切: class property object ivar method protocol
, 而下面就是它的主要应用:
- 内省
- 为分类动态的添加属性
- 使用方法调剂修改原有的方法实现
...
7.什么是 Method Swizzling?
method swizzling 实际上就是一种在运行时动态修改原有方法的技术, 它实际上是基于 ObjC runtime 的特性, 而 method swizzling 的核心方法就是 method_exchangeImplementations(SEL origin, SEL swizzle)
. 使用这个方法就可以在运行时动态地改变原有的方法实现.
8. UIView 和 CALayer 有什么关系?
每一个 UIView 的身后对应一个 Core Animation 框架中的 CALayer .
Many of the methods you call on UIView simply delegate to the layer
在 iOS 上 当你处理一个一个有一个的 UIView 时实际上是在操作 CALayer . 尽管有的时候你并不知道 (直接操作 CALayer 并不会在对效率有着显著的提升).
UIView 实际上就是对 CALayer 的轻量级的封装. UIView 继承自 UIResponder 处理来自用户的事件; CALayer 继承自 NSObject 主要用于图层的渲染和动画. 这么设计有以下几个原因:
- 你可以通过操作 UIView 在一个更高的层级上处理与用户的交互, 触摸, 点击, 拖拽等事件, 这些都是在 UIKit 这个层级上完成的.
- UIView 和 NSView(AppKit) 的实现极其不同, 而使用 Core Animation 可以实现底层代码地重用, 因为在 Mac 和 iOS 平台上都使用着近乎相同的 Core Animation 代码, 这样我们可以对这个层级进行抽象在两种平台上产生 UIKit 和 AppKit 用于不同平台的框架.
使用 CALayer 的唯一原因大概是便于移植到不同的平台, 如果仅仅使用 Core Animation 层级, 处理用户的交互时间需要写更多的代码.
9. 如何高性能的给 UIImageView 加个圆角? (不准说 layer.cornerRadius !)
一般情况下给 UIImageView 或者说 UIKit 的控件添加圆角都是改变 clipsToBounds
和 layer.cornerRadius
, 这样大约两行代码就可以解决. 但是, 这样使用这样的方法会 强制 Core Animation 提前渲染屏幕的离屏绘制 , 而离屏绘制就会为性能带来负面影响.
我们也可以使用另一种比较复杂的方式来为图片添加圆角, 这里就用到了贝塞尔曲线.
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
imageView.center = CGPointMake(200, 300);
UIImage *anotherImage = [UIImage imageNamed:@"image"];
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);
[[UIBezierPath bezierPathWithRoundedRect:imageView.bounds
cornerRadius:50] addClip];
[anotherImage drawInRect:imageView.bounds];
imageView.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[self.view addSubview:imageView];
在这里使用了贝塞尔曲线"切割"个这个图片, 给 UIImageView 添加了的圆角.
10. 使用 drawRect: 有什么影响?
这个方法的主要作用是根据传入的 rect 来绘制图像. 这个方法的默认实现没有做任何事情, 我们 可以 在这个方法中使用 Core Graphics 和 UIKit 来绘制视图的内容.
这个方法的调用机制也是非常特别. 当你调用 setNeedsDisplay
方法时, UIKit 将会把当前图层标记为 dirty, 但还是会显示原来的内容, 直到下一次的视图渲染周期, 才会重新建立 Core Graphics
上下文, 然后将内存中的数据恢复出来, 使用 CGContextRef
进行绘制.
用来画图,这个方法会在intiWithRect时候调用。
这个方法的影响在于有touch event的时候之后,会重新绘制,很多这样的按钮的话就会比较影响效率。以下都会被调用
1、如果在UIView初始化时没有设置rect大小,将直接导致drawRect不被自动调用。drawRect 掉用是在Controller->loadView
, Controller->viewDidLoad
两方法之后掉用的.所以不用担心在 控制器中,这些View的drawRect就开始画了.这样可以在控制器中设置一些值给View(如果这些View draw的时候需要用到某些变量 值).
2、该方法在调用sizeToFit后被调用,所以可以先调用sizeToFit
计算出size。然后系统自动调用drawRect:
方法。
3、通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:。
4、直接调用setNeedsDisplay
,或者setNeedsDisplayInRect:
触发drawRect:
,但是有个前提条件是rect不能为0。
11. ASIHttpRequest 或者 SDWebImage 里面给 UIImageView 加载图片的逻辑是什么样的?
SDWebImage 中为 UIView 提供了一个分类叫做 WebCache
, 这个分类中有一个最常用的接口, sd_setImageWithURL:placeholderImage:
, 这个分类同时提供了很多类似的方法, 这些方法最终会调用一个同时具有 option progressBlock completionBlock
的方法, 而在这个类最终被调用的方法首先会检查是否传入了 placeholderImage
以及对应的参数, 并设置 placeholderImage
.
然后会获取 SDWebImageManager 中的单例调用一个 downloadImageWithURL:...
的方法来获取图片, 而这个 manager
获取图片的过程有大体上分为两部分, 它首先会在 SDWebImageCache
中寻找图片是否有对应的缓存, 它会以 url 作为数据的索引先在内存中寻找是否有对应的缓存, 如果缓存未命中就会在磁盘中利用 MD5 处理过的 key 来继续查询对应的数据, 如果找到了, 就会把磁盘中的缓存备份到内存中.
然而, 假设我们在内存和磁盘缓存中都没有命中, 那么 manager
就会调用它持有的一个 SDWebImageDownloader
对象的方法 downloadImageWithURL:...
来下载图片, 这个方法会在执行的过程中调用另一个方法 addProgressCallback:andCompletedBlock:fotURL:createCallback:
来存储下载过程中和下载完成的回调, 当回调块是第一次添加的时候, 方法会实例化一个 NSMutableURLRequest
和 SDWebImageDownloaderOperation
, 并将后者加入 downloader
持有的下载队列开始图片的异步下载.
而在图片下载完成之后, 就会在主线程设置 image
, 完成整个图像的异步下载和配置.
12. loadView 的作用是什么?
loadView 是 UIViewController 的实例方法, 我们永远不要直接调用这个方法 [self loadView]
. 这在苹果的 官方文档 中已经明确的写出了. loadView
会在获取视图控制器的 view
但是却得到 nil
时被调用.
loadView
的具体实现会做下面两件事情中的一件:
1、如果你的视图控制器关联了一个 storyboard
, 那么它就会加载 storyboard
中的视图.
2、如果试图控制器没有关联的 storyboard
, 那么就会创建一个空的视图, 并分配给 view 属性
如果你需要覆写 loadView
方法:
1、你需要创建一个根视图.
2、创建并初始化 view
的子视图, 调用 addSubview:
方法将它们添加到父视图上.
3、如果你使用了自动布局, 提供足够的约束来保证视图的位置.
4、将根视图分配给 view
属性.
5、永远不要在这个方法中调用 [super loadView]
.
13. viewWillLayoutSubviews 的作用是什么?
viewWillLayoutSubviews
方法会在视图的 bounds
改变时, 视图会调整子视图的位置, 我们可以在视图控制器中覆写这个方法在视图放置子视图前做出改变, 当屏幕的方向改变时, 这个方法会被调用.
layoutSubviews在以下情况下会被调用:
1、init初始化不会触发layoutSubviews
2、addSubview会触发layoutSubviews
3、设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化
4、滚动一个UIScrollView会触发layoutSubviews
5、旋转Screen会触发父UIView上的layoutSubviews事件
6、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件
14. GCD 里面有哪几种 Queue? 背后的线程模型是什么样的?
GCD 中 Queue 的种类还要看我们怎么进行分类, 如果根据同一时间内处理的操作数分类的话, GCD 中的 Queue
分为两类
1、Serial Dispatch Queue
2、Concurrent Dispatch Queue
一类是串行派发队列, 它只使用一个线程, 会等待当前执行的操作结束后才会执行下一个操作, 它按照追加的顺序进行处理. 另一类是并行派发队列, 它同时使用多个线程, 如果当前的线程数足够, 那么就不会等待正在执行的操作, 使用多个线程同时执行多个处理.
另外的一种分类方式如下:
1、Main Dispatch Queue
2、Global Dispatch Queue
3、Custom Dispatch Queue
主线程只有一个, 它是一个串行的进程. 所有追加到 Main Dispatch Queue
中的处理都会在 RunLoop
在执行. Global Dispatch Queue
是所有应用程序都能使用的并行派发队列, 它有 4 个执行优先级 High, Default, Low, Background
. 当然我们也可以使用 dispatch_queue_create
创建派发队列.
15. Core Data 或者 sqlite 的读写是分线程的吗? 死锁如何解决?
数据库读取操作一般都是多线程的, 在对数据进行读取的时候, 我们要确保当前的状态不会被修改, 所以加锁, 防止由于线程竞争而出现的错误. 在 Core Data
中使用并行的最重要的规则是: 每一个 NSManagedObjectContext
必须只从创建它的进程中访问 .
16. http 的 POST 和 GET 有什么区别?
根据 HTTP 协议的定义 GET 类型的请求是幂等的, 而 POST 请求是有副作用的, 也就是说 GET 用于获取一些资源, 而 POST 用于改变一些资源, 这可能会创建新的资源或更新已有的资源.
POST 请求比 GET 请求更加的安全, 因为你不会把信息添加到 URL 上的查询字符串上. 所以使用 GET 来收集密码或者一些敏感信息并不是什么好主意.
最后, POST 请求比 GET 请求也可以传输更多的信息.
17. 什么是 Binary search tree, 它的时间复杂度是多少?
二叉搜索树是一棵以二叉树来组织的, 它搜索的时间复杂度 $O(h)$ 与树的高度成正比, 最坏的运行时间是 $\Theta(\lg n)$.