今天给大家带来文章讲述IOS程序员招聘面试时候可能出现的部分问题以及答案,对于希望担任IOS程序员的各位来说是必读的干货。
IOS程序员面试中被面试官问到的问题以及答案
1. 请你谈谈static和宏定义的区别。什么时候用static什么时候用宏定义。
让你声明的常量只在你声明的文件里有作用要不编译器会保存
宏定义:
1). 一般来说我们使用宏定义最常见的是定义一些常量 简单的”函数”(比如求两个数的最大小值)
例如:定义常量PI
#define PI 3.1415926
定义函数
#define MIN(A,B) ((A) < (B) ? (A):(B))
我们不对宏定义进行修改
2) . 使用宏定义可以在很大程度上可以简化我们的代码
例如:我们在写单例的时候 之前我们写的是
#import "ShareSingleton.h"@implementation ShareSingleton+(instancetype)shareSingleton { static ShareSingleton *leader = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{
leader = [[ShareSingleton alloc]init];
}); return leader;
}@end//如果我们使用宏定义的话我们可以这样写:#define DISPATCH_ONCE_BLOCK(onceBlock) static dispatch_once_t onceToken; dispatch_once(&onceToken, onceBlock);#import "ShareSingleton.h"@implementation ShareSingleton+(instancetype)shareSingleton { static ShareSingleton *leader = nil;
DISPATCH_ONCE_BLOCK(^{
leader = [[ShareSingleton alloc]init];
}) return leader;
}@end
其实#define的原理就是不管三七二十一,直接做替换,所以我们完全可以利用这个特点,发挥自己的想象,简化代码~ 宏定义实质是一个预编译指令,在程序未运行之前将某些指令付给相应的变量。
小结一下: static标记的变量会存储到全局变量区,生命周期和程序相同。而宏定义所定义的生命周期与所在的载体的生命周期有关.
static只在声明的类中可见。
在声明的类中结束后,再次使用还是之前的值。
2.你是怎么看待代理 通知的 他们有什么区别?
首先,我们把代理通知 放到一起来讨论第一反映是传值。 ok,下面慢慢来说各个的用法和区别。
通知中心
通过NSNotification可以给多个对象传递数据和消息(多个传递) 代理 通过protocol(代理模式)只能给一个对象传递数据和消息(单一传递)“一对一”,对同一个协议,一个对象只能设置一个代理delegate,所以单例对象就不能用代理(可以用多点回调,下面见解)。
代理更注重过程信息的传输:比如发起一个网络请求,可能想要知道此时请求是否已经开始、是否收到了数据、数据是否已经接受完成、数据接收失败。
区别: 代理和通知的区别应该主要是一对一和一对多的关系。delegate的多点回调相对notification更加便捷,更多方便,让项目更好维护。
3.说说你对内存管理的理解。
内存管理原则
引用计数的增加和减少相等,当引用计数降为0之后,不应该再使用这块内存空间。 凡是用alloc retain 或者copy让内存的引用计数增加了。就需要使用release或者autorelease让内存的引用 计数减少。在一段代码内。增加和减少的次数要相等。
autoreleasepool的使用
通过autoreleasepool控制autorelease对象的释放 向一个对象发送autorelease消息。这个对象何时释放取决于autoreleasepool
copy方法
跟retain不同,一个对象想要copy,生成自己的副本,需要实现NSCopying协议,定义copy的细节(如何copy)如果类没有接受NSCoping协议而给类发送copy消息,会引起crash 总结: OC借助引用计数机制去管理内存,凡是使用了alloc copy retain 等 方法,增加了引用计数,就要使用release 和autorelease 减少引用计数,引用计数为0的时候,对象所占的内存,被系统回收。
autorelease是未来某个时间(出autorelease)引用减一,不是即时的。
不是任何对象都可以接受copy消息。只有接受了NSCoping协议的对象才接受copy消息。
4.谈谈你对iOS性能优化的理解.
谈起iOS的性能优化我们首先想到的是应该是tableview表视图的优化。关于表视图的优化我们可以从以下几个方面来看:
1).tableviewcell渲染
绘制时要尽可能的避免分配资源,比如UIFont,NSDateFormatter或者任何在绘制时 需要的对象,推荐使用类层级的初始化方法中执行分配,并将其存储为静态变量。
2).图层渲染的问题
透明图层对渲染性能会有一定的影响,系统必须将透明图层与下面的视图混合起来计算颜色,并绘制出来。减少透明图层并使用不透明的图层来替代它们,可以极大地提高渲染速度。
3).为代理方法瘦身
我们要尽量避免在tableview的cellforrowatindexpath的代理方法里写那么多代码,这样做不仅可以简化代码方便维护和管理,这对程序的运行也有帮助。
4).复杂视图尽量采用纯代码的方式
当 UITableViewCell拥有多个子视图时,IOS的渲染机制会拖慢速度。重写drawRect直接绘制内容的方式可 以提高性能,而不是在类初始化的时候初始化一些label或者imageview等。
下面就是些CPU 资源消耗原因和解决方案 还有GPU资源消耗原因和解决方案
对象的创建会分配内存、调整属性、甚至还有读取文件等操作,比较消耗 CPU 资源。尽量用轻量的对象代替重量的对象,可以对性能有所优化。比如 CALayer 比 UIView 要轻量许多,那么不需要响应触摸事件的控件,用 CALayer 显示会更加合适。如果对象不涉及 UI 操作,则尽量放到后台线程去创建,但可惜的是包含有 CALayer 的控件,都只能在主线程创建和操作。通过 Storyboard 创建视图对象时,其资源消耗会比直接通过代码创建对象要大非常多,在性能敏感的界面里,Storyboard 并不是一个好的技术选择。
1).对象的创建
尽量推迟对象创建的时间,并把对象的创建分散到多个任务中去。尽管这实现起来比较麻烦,并且带来的优势并不多,但如果有能力做,还是要尽量尝试一下。如果对象可以复用,并且复用的代价比释放、创建新对象要小,那么这类对象应当尽量放到一个缓存池里复用。
2).对象调整
对象的调整也经常是消耗 CPU 资源的地方。这里特别说一下 CALayer:CALayer 内部并没有属性,当调用属性方法时,它内部是通过运行时 resolveInstanceMethod 为对象临时添加一个方法,并把对应属性值保存到内部的一个 Dictionary 里,同时还会通知 delegate、创建动画等等,非常消耗资源。UIView 的关于显示相关的属性(比如 frame/bounds/transform)等实际上都是 CALayer 属性映射来的,所以对 UIView 的这些属性进行调整时,消耗的资源要远大于一般的属性。对此你在应用中,应该尽量减少不必要的属性修改。 当视图层次调整时,UIView、CALayer 之间会出现很多方法调用与通知,所以在优化性能时,应该尽量避免调整视图层次、添加和移除视图。
3). 对象销毁
对象的销毁虽然消耗资源不多,但累积起来也是不容忽视的。通常当容器类持有大量对象时,其销毁时的资源消耗就非常明显。同样的,如果对象可以放到后台线程去释放,那就挪到后台线程去。这里有个小 Tip:把对象捕获到 block 中,然后扔到后台队列去随便发送个消息以避免编译器警告,就可以让对象在后台线程销毁了。 例如:
NSArray *tmp = self.array; self.array = nil; dispatch_async(queue, ^{
[tmp class];
});
4). 一些计算
视图布局的计算是 App 中最为常见的消耗 CPU 资源的地方。如果能在后台线程提前计算好视图布局、并且对视图布局进行缓存,那么这个地方基本就不会产生性能问题了。 不论通过何种技术对视图进行布局,其最终都会落到对 UIView.frame/bounds/center 等属性的调整上。上面也说过,对这些属性的调整非常消耗资源,所以尽量提前计算好布局,在需要时一次性调整好对应属性,而不要多次、频繁的计算和调整这些属性。 Autolayout 是苹果本身提倡的技术,在大部分情况下也能很好的提升开发效率,但是 Autolayout 对于复杂视图来说常常会产生严重的性能问题。随着视图数量的增长,Autolayout 带来的 CPU 消耗会呈指数级上升。具体数据可以看这个文章:http://pilky.me/36/。 如果你不想手动调整 frame 等属性,你可以用一些工具方法替代(比如常见的 left/right/top/bottom/width/height 快捷属性),或者使用 ComponentKit、AsyncDisplayKit 等框架.
如果一个界面中包含大量文本(比如微博微信朋友圈等),文本的宽高计算会占用很大一部分资源,并且不可避免。如果你对文本显示没有特殊要求,可以参考下 UILabel 内部的实现方式:用 [NSAttributedString boundingRectWithSize:options:context:] 来计算文本宽高,用 -[NSAttributedString drawWithRect:options:context:] 来绘制文本。尽管这两个方法性能不错,但仍旧需要放到后台线程进行以避免阻塞主线程。
5).文本的绘制
如果你用 CoreText 绘制文本,那就可以先生成 CoreText 排版对象,然后自己计算了,并且 CoreText 对象还能保留以供稍后绘制使用。 屏幕上能看到的所有文本内容控件,包括 UIWebView,在底层都是通过 CoreText 排版、绘制为 Bitmap 显示的。常见的文本控件 (UILabel、UITextView 等),其排版和绘制都是在主线程进行的,当显示大量文本时,CPU 的压力会非常大。对此解决方案只有一个,那就是自定义文本控件,用 TextKit 或最底层的 CoreText 对文本异步绘制。尽管这实现起来非常麻烦,但其带来的优势也非常大,CoreText 对象创建好后,能直接获取文本的宽高等信息,避免了多次计算(调整 UILabel 大小时算一遍、UILabel 绘制时内部再算一遍);CoreText 对象占用内存较少,可以缓存下来以备稍后多次渲染.
6).图片的解码
当你用 UIImage 或 CGImageSource 的那几个方法创建图片时,图片数据并不会立刻解码。图片设置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的数据才会得到解码。这一步是发生在主线程的,并且不可避免。如果想要绕开这个机制,常见的做法是在后台线程先把图片绘制到 CGBitmapContext 中,然后从 Bitmap 直接创建图片。目前常见的网络图片库都自带这个功能。
7).图像的绘制
图像的绘制通常是指用那些以 CG 开头的方法把图像绘制到画布中,然后从画布创建图片并显示这样一个过程。这个最常见的地方就是 [UIView drawRect:] 里面了。由于 CoreGraphic 方法通常都是线程安全的,所以图像的绘制可以很容易的放到后台线程进行。一个简单异步绘制的过程大致如下(实际情况会比这个复杂得多,但原理基本一致)
dispatch_async(backgroundQueue, ^{
CGContextRef ctx = CGBitmapContextCreate(...); // draw in context...
CGImageRef img = CGBitmapContextCreateImage(ctx);
CFRelease(ctx); dispatch_async(mainQueue, ^{
layer.contents = img;
});
});
GPU 资源消耗原因和解决方案
1. 纹理的渲染
所有的 Bitmap,包括图片、文本、栅格化的内容,最终都要由内存提交到显存,绑定为 GPU Texture。不论是提交到显存的过程,还是 GPU 调整和渲染 Texture 的过程,都要消耗不少 GPU 资源。当在较短时间显示大量图片时(比如 TableView 存在非常多的图片并且快速滑动时),CPU 占用率很低,GPU 占用非常高,界面仍然会掉帧。避免这种情况的方法只能是尽量减少在短时间内大量图片的显示,尽可能将多张图片合成为一张进行显示。 当图片过大,超过 GPU 的最大纹理尺寸时,图片需要先由 CPU 进行预处理,这对 CPU 和 GPU 都会带来额外的资源消耗。目前来说,iPhone 4S 以上机型,纹理尺寸上限都是 4096x4096,更详细的资料可以看这里:iosres.com。所以,尽量不要让图片和视图的大小超过这个值。
2.视图的混合
当多个视图(或者说 CALayer)重叠在一起显示时,GPU 会首先把他们混合到一起。如果视图结构过于复杂,混合的过程也会消耗很多 GPU 资源。为了减轻这种情况的 GPU 消耗,应用应当尽量减少视图数量和层次,并在不透明的视图里标明 opaque 属性以避免无用的 Alpha 通道合成。当然,这也可以用上面的方法,把多个视图预先渲染为一张图片来显示
3.图像的生成
CALayer 的 border、圆角、阴影、遮罩(mask),CASharpLayer 的矢量图形显示,通常会触发离屏渲染(offscreen rendering),而离屏渲染通常发生在 GPU 中。当一个列表视图中出现大量圆角的 CALayer,并且快速滑动时,可以观察到 GPU 资源已经占满,而 CPU 资源消耗很少。这时界面仍然能正常滑动,但平均帧数会降到很低。为了避免这种情况,可以尝试开启 CALayer.shouldRasterize 属性,但这会把原本离屏渲染的操作转嫁到 CPU 上去。对于只需要圆角的某些场合,也可以用一张已经绘制好的圆角图片覆盖到原本视图上面来模拟相同的视觉效果。最彻底的解决办法,就是把需要显示的图形在后台线程绘制为图片,避免使用圆角、阴影、遮罩等属性。
如何检测应用的流畅度?
“过早的优化是万恶之源”,在需求未定,性能问题不明显时,没必要尝试做优化,而要尽量正确的实现功能。做性能优化时,也最好是走修改代码 -> Profile -> 修改代码这样一个流程,优先解决最值得优化的地方。 如果你需要一个明确的 FPS 指示器,可以尝试一下 KMCGeigerCounter。对于 CPU 的卡顿,它可以通过内置的 CADisplayLink 检测出来;对于 GPU 带来的卡顿,它用了一个 1x1 的 SKView 来进行监视。这个项目有两个小问题:SKView 虽然能监视到 GPU 的卡顿,但引入 SKView 本身就会对 CPU/GPU 带来额外的一点的资源消耗;这个项目在 iOS 9 下有一些兼容问题,需要稍作调整。
5.你用过单元测试吗?怎么才能做好单元测试?
什么是单元测试?
单元测试:以下内容来自维基百科单元测试
在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
通常来说,程序员每修改一次程序就会进行最少一次单元测试,在编写程序的过程中前后很可能要进行多次单元测试,以证实程序达到软件规格书要求的工作目标,没有程序错误;虽然单元测试不是什么必须的,但也不坏,这牵涉到项目管理的政策决定。
每个理想的测试案例独立于其它案例;为测试时隔离模块,经常使用stubs、mock[1]或fake等测试马甲程序。单元测试通常由软件开发人员编写,用于确保他们所写的代码符合软件需求和遵循开发目标。它的实施方式可以是非常手动的(通过纸笔),或者是做成构建自动化的一部分。
单元测试有什么好处?
单元测试的一个好处就是我们可以只测试单个模块,我们可以测试 单一模块有没有问题。比如说我们在开发中经常会写一些测试性的demo。我们写的测试性demo运行正常达到了我们需要的效果,那么我们就可以把demo的效果运用到我们的工程中进行调试。
适应变更
单元测试允许程序员在未来重构代码,并且确保模块依然工作正确(复合测试)。这个过程就是为所有函数和方法编写单元测试,一旦变更导致错误发生,借助于单元测试可以快速定位并修复错误。
可读性强的单元测试可以使程序员方便地检查代码片断是否依然正常工作。良好设计的单元测试案例覆盖程序单元分支和循环条件的所有路径。
在连续的单元测试环境,通过其固有的持续维护工作,单元测试可以延续用于准确反映当任何变更发生时可执行程序和代码的表现。借助于上述开发实践和单元测试的覆盖,可以分分秒秒维持准确性。
简化集成
单元测试消除程序单元的不可靠,采用自底向上的测试路径。通过先测试程序部件再测试部件组装,使集成测试变得更加简单。
业界对于人工集成测试的必要性存在较大争议。尽管精心设计的单元测试体系看上去实现了集成测试,因为集成测试需要人为评估一些人为因素才能证实的方面,单元测试替代集成测试不可信。一些人认为在足够的自动化测试系统的条件下,人力集成测试组不再是必需的。事实上,真实的需求最终取决于开发产品的特点和使用目标。另外,人工或手动测试很大程度上依赖于组织的可用资源。
文档记录
单元测试提供了系统的一种文档记录。借助于查看单元测试提供的功能和单元测试中如何使用程序单元,开发人员可以直观的理解程序单元的基础API。
单元测试具体表现了程序单元成功的关键特点。这些特点可以指出正确使用和非正确使用程序单元,也能指出需要捕获的程序单元的负面表现(译注:异常和错误)。尽管很多软件开发环境不仅依赖于代码做为产品文档,在单元测试中和单元测试本身确实文档化了程序单元的上述关键特点。
另一方面,传统文档易受程序本身实现的影响,并且时效性难以保证(如设计变更、功能扩展等在不太严格时经常不能保持文档同步更新)。
表达设计
在测试驱动开发的软件实践中,单元测试可以取代正式的设计。每一个单元测试案例均可以视为一项类、方法和待观察行为等设计元素。下面的Java例可以帮助说明这一点。 当然,单元测试缺乏图的可读性,但UML图可以在自由工具(通常可从IDE扩展获取)中为大多数现代程序语言生成UML图,很难要求采购昂贵的UML设计套装软件。自由工具,类似于基于xUnit框架的工具,测试结果输出到一些可生成供人工识读的图形化工具系统中去。
分离接口和实现
因为很多类会引用其它类,对这个类的测试经常会要求测试其它的类。一个最普遍的例子是依赖于数据库的类:为了测试它,测试人员通常编写代码去操作数据库。这是不对的,因为单元测试不应超出待测试的类边界。
作为替代,软件开发人员应创建一个数据库连接的抽象接口,然后实现这个接口的模拟对象。通过对代码所需附件的抽象(临时降低了网状的耦合效应),这些独立程序单元较前者更能被完整测试。高质量的代码单元也可提供更好的可维护性。
局限
测试不可能发现所有的程序错误,单元测试也不例外。按定义,单元测试只测试程序单元自身的功能。因此,它不能发现集成错误、性能问题、或者其他系统级别的问题。单元测试结合其他软件测试活动更为有效。与其它形式的软件测试类似,单元测试只能表明测到的问题,不能表明不存在未测试到的错误。
软件测试是一个组合问题。例如,每一个布尔型的决断语句需要至少两种测试:一个返回真,一个返回假。因此,针对每行书写的代码,程序员通常需要写3至5行的测试代码。[3]这很明显地很花时间而且对此的投入可能并不值得。也有些问题是根本不能简单地检测出来的——例如具不确定性的或牵扯到多线程的问题。此外,替单元测试写的代码可能就像要测试的代码一样有程序错误。佛瑞德·布鲁克斯在人月神话一书中举例说明:“绝对不要带两个计时器去海边。最好总是带一或三个”。意味着,如果两个计时器互相冲突的话,你该怎么知道哪个是对的?为了获得单元测试的好处,在软件开发过程中应形成一套严格纪律意识。仔细保留记录是必要的,不仅仅只保留执行的测试,也包括保留对应的源码和其它软件单元的变更历史。即,使用版本控制系统是必要的。如果后续版本不能通过一个以前测试通过的单元测试,版本控制系统可以提供对应时间段对源代码所做的变更清单。
每天养成查看单元测试案例失败测试并及时确定错误原因的习惯是必要的。如果没有这样的流程,没有在团队工作流程中体现,单元测试系列将走向不同步,造成越来越多的错误和越来越低效的单元测试案例系列。
iOS中的单元测试
在开发中,经常用到的单元测试一是测试某个模块的功能,也就是说把这个模块独立起来,单独进行测试。用到最多的应该是测试模块功能和接口调试功能。当然单元测试还有一些高级的用法自动测试和自动发布等。
OCUnit(即用XCTest进行测试)其实就是苹果自带的测试框架,我们主要讲的就是这个。GHUnit是一个可视化的测试框架。(有了它,你可以点击APP来决定测试哪个方法,并且可以点击查看测试结果等。)OCMock就是模拟某个方法或者属性的返回值,你可能会疑惑为什么要这样做?使用用模型生成的模型对象,再传进去不就可以了?答案是可以的,但是有特殊的情况。比如你测试的是方法A,方法A里面调用到了方法B,而且方法B是有参数传入,但又不是方法A所提供。这时候,你可以使用OCMock来模拟方法B返回的值。(在不影响测试的情况下,就可以这样去模拟。)除了这些,在没有网络的情况下,也可以通过OCMock模拟返回的数据。UITests就是通过代码化来实现自动点击界面,输入文字等功能。靠人工操作的方式来覆盖所有测试用例是非常困难的,尤其是加入新功能以后,旧的功能也要重新测试一遍,这导致了测试需要花非常多的时间来进行回归测试,这里产生了大量重复的工作,而这些重复的工作有些是可以自动完成的,这时候UITests就可以帮助解决这个问题了。
关于单元测试可以参考文章
6.你知道的的本地数据持久化都有哪些。你比较喜欢用哪些 为什么?
采用的数据存储的方式有以下几种:
1、 FMDB(常用)
2、 Sqlite(次之)
3、 Coredata(次之)
4、 NSUserdefaults(最多使用)
5、 序列化反序列化(归档和解档)
6、 MongoDB(小众型的)
大家讨论用的最多的是FMDB,原因很简单,关系型数据库,使用方便(相对于没经过封装和加工的Sqlite来说)。其次就是sqlite和coredata 当然使用者三种主要是为了缓存。因为我们在开发中为了给用户更好的体验,就采用缓存的形式。一般情况下要做的操作就是在本地建立一个数据库(本地后台)。