笔者之前在魔都从事iOS相关工作,前段时间决定来到帝都发展,找工作的过程中准备了些基础的面试题目,现分享给大家。
通常耗时的操作都放在子线程处理,然后到主线程更新UI,如
这里建立了两个字典 :
1.iconCache:保存缓存的图片
2.blockOperation 用来保存下载任务
每当进入或退出程序时,会进行图片文件的管理:超过一星期的文件会被清除,如果设置了最大缓存,超过这个缓存就会删除最旧的文件,直到当前缓存文件为最大缓存文件的一半大小;
如何处理事件
界面刷新: 当UI改变( Frame变化、 UIView/CALayer 的继承结构变化等)时,或手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理。 苹果注册了一个用来监听BeforeWaiting和Exit的Observer,在它的回调函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
手势识别: 如果上一步的 _UIApplicationHandleEventQueue() 识别到是一个guesture手势,会调用Cancel方法将当前的touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。 苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,其回调函数为 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。 当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
网络请求:最底层是CFSocket层,然后是CFNetwork将其封装,然后是NSURLConnection对CFNetwork进行面向对象的封装,NSURLConnection是iOS7中新增的接口。当网络开始传输时,NSURLConnection创建了两个新线程:com.apple.NSURLConnectionLoader和com.apple.CFSocket.private。其中CFSocket线程是处理底层socket连接的。NSURLConnectionLoader这个线程内部会使用RunLoop来接受底层socket的事件,并添加到上层的Delegate
应用
滑动与图片刷新:当tableView的cell上有需要从网络获取的图片的时候,滚动tableView,异步线程回去加载图片,加载完成后主线程会设置cell的图片,但是会造成卡顿。可以设置图片的任务在CFRunloopDefaultMode下进行,当滚动tableView的时候,Runloop切换到UITrackingRunLoopMode,不去设置图片,而是而是当停止的时候,再去设置图片。(在viewDidLoad中调用self.imageView performSelector@selector(setImage) withObject:...afterDelay:...inModes@[NSDefayltRunLoopMode])
常驻子线程,保持子线程一直处理事件 为了保证线程长期运转,可以在子线程中加入RunLoop,并且给Runloop设置item,防止Runloop自动退出
//1.获得NSUserDefaults文件
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
//2.向文件中写入内容
[userDefaults setObject:@"AAA" forKey:@"a"];
[userDefaults setBool:YES forKey:@"sex"];
[userDefaults setInteger:21 forKey:@"age"];
//2.1立即同步
[userDefaults synchronize];
//3.读取文件
NSString *name = [userDefaults objectForKey:@"a"];
BOOL sex = [userDefaults boolForKey:@"sex"];
NSInteger age = [userDefaults integerForKey:@"age"];
NSLog(@"%@, %d, %ld", name, sex, age);
归档(Archiver)、解档(unArchiver),利用NSKeyedArchiver实现归档、利用NSKeyedUnarchiver反接的那个
// 反归档
- (id)initWithCoder:(NSCoder *)aDecoder {
if ([super init]) {
self.avatar = [aDecoder decodeObjectForKey:@"avatar"];
self.name = [aDecoder decodeObjectForKey:@"name"];
self.age = [aDecoder decodeIntegerForKey:@"age"];
}
return self;
}
// 归档
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.avatar forKey:@"avatar"];
[aCoder encodeObject:self.name forKey:@"name"];
[aCoder encodeInteger:self.age forKey:@"age"];
}
归档,把对象归档时需要调用NSKeyedArchiver的工厂方法archiveRootObject: toFile: 方法
NSString *file = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"person.data"];
Person *person = [[Person alloc] init];
person.avatar = self.avatarView.image;
person.name = self.nameField.text;
person.age = [self.ageField.text integerValue];
[NSKeyedArchiver archiveRootObject:person toFile:file];
反归档
NSString *file = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"person.data"];
Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:file];
if (person) {
self.avatarView.image = person.avatar;
self.nameField.text = person.name;
self.ageField.text = [NSString stringWithFormat:@"%ld", person.age];
}
属性列表
数据库:SQLite
从存储数据大小来看,归档、偏好设置、属性列表三种方法适合存储数据量较小的数据,数据库、CoreData方法适合存储数据量较大的数据
从加密性来看,其中归档会将数据进行加密,而偏好设置是直接保存到属性列表中,不会对数据进行加密
从存储类型来看,属性列表只能存放固定的七种类型(可在plist文件中看到),归档对存储类型无限制
KVC缺点:一旦使用KVC,编译器无法检查出错误,即不会对设置的键、键路径进行错误检查,且执行效率低于自定义的setter和getter方法,因为使用KVC键值编值,必须先解析字符串,然后设置或访问对象的实例变量
通过KVO(key-value-observing,典型的观察者模式,被观察的对象必须使用KVC键值编码来修改它的实例变量,这样才能被观察者观察到)监听person对象中name属性发生改变
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
do something....
}
GET请求的数据会负载URL之后,即把数据放在HTTP协议头中,以?区分URL和传输数据,参数之间以&相连,英文字母/数字,原样发送,如果是空格,转化为+,如果是中文,把字符串用BASE64加密;POST就是把提交的数据放在HTTP包的包体中
GET一般用于提交少量数据(最多提交1k,浏览器限制),POST用于提交大量数据(理论上无限制,收服务器限制)
GET提交的数据可以在浏览器历史记录中看到,安全性不好,别人可以拿到账号密码
Get是向服务器发索取数据的一种请求,而POST是向服务器发提交数据的一种请求,只是发送机制不同
GET不可以设置书签,POST可以设置书签
什么情况下用POST:
什么情况下用GET:
其实MVVM这货有点被神化了,好吧,其实我项目中也没用到过,只是写过稍大点的demo
实际用例
如果添加视图模型,仅仅需要映射一下原始数据
struct AccountViewModel {
let dispalyBalance: String
init(mode: cardAccount) {
let formattedBalance = model.balance.currencyValue;
displayBalance = "Your balance is \(formatterBalance)"
}
}
在去年WWDC上,Andy(iOS4.1-8的UIKit框架维护者)演讲中有一个关于zoetrope(西洋镜)的例子
var viewModel = ViewModel(model: Account)
其实是个面试官八个不懂Swift,而且一般不懂得就问个Swift与OC的不同。。题主也只是在自学Swift,等到3.0出了之后再深入研究,而且项目中可能也要开始从混编逐渐向Swift靠拢了。
Swift是一门更加现代化的语言,但是目前还在成长阶段,更新改动比较大,虽然说其底层思想不变,变的是API和接口,但是每次更新完Xcode看到自己的Swift项目还是有些淡淡的忧伤,而且目前Swift开发都要转成OC的runtime,包略大,因此题主认为成熟项目最好还是采用OC
先记住一句话:OC底层面向对象,而Swift底层更加面向协议
我们已经见识过Apple使用了大量协议,比如在tableView当中,我们可以通过协议来告诉Apple需要多少个表视图单元格,而不是每时每刻都要继承UITableViewController
在这里以MVVM作为测试用例:比如现在需要建立一个类似设置界面的tableView,每个cell需要一个label和一个switch,自定义SwitchWithTextTableViewCell,在其内部建立一个configure方法中对label的title,titleFont,titleColor,switch的switchOn和switchColor等进行初始化,但这种方式非常累赘,比如添加一个副标题,就需要额外添加三个属性
但是利用协议SwitchWithTextCellProtocol,让视图模型实现这个协议,然后在这里设置所有的属性
protocol SwitchWithTextCellProtocol {
var title: String { get }
var titleFont: UIFont { get }
var titleColor: UIColor { get }
var switchOn: Bool { get }
var switchColor: UIColor { get }
func onSwitchTogglenOn(onL Bool)
}
通过swift2.0重点饿协议扩展,就可以通过默认值来做一些处理了,如果对于大多数单元格来说,可以确定某一种颜色的话,就可以对其建立扩展,然后设置颜色即可,所有实现此协议的视图就没有必要再去设置这个颜色了
现在,我的configure方法里面只要实现此协议的值就可以了
// 这个方法只需要一个参数,相比于之前的多个参数简便了很多
class SwitchWithTextTableViewCell: UITableViewCell {
func configure(withDelegate delagate: SwitchWithTextCellProtocol) {
// 在这里配置方法
}
}
现在的视图模型
struct MinionModeViewController: SwitchWithTextCellProtocol {
var title = "excellent!!"
var switchOn = true
var switchColor: UIColor {
return .yellowColor()
}
func onSwitchToggleOn(on: Bool) {
if on {
print("The Minions are here to stay!")
} else {
print("The Minions went out to play!")
}
}
}
let cell = tableView.dequeueReuseableCellWithIdentifier("SwitchWithTextTableViewCell", forIndexPath: indexPath) as! SwitchWithTextTableViewCell
cell.configure(withDelegate: MinionModeViewModel())
return cell
再把模型放在视图模型层级,一遍对其进行跟踪,再视图模型中传递这些信息,这样单元格就可以生成了
protocol SwitchWithTextCellDataSource {
var title: String { get }
var switchOn: Bool { get }
}
protocol SwitchWithTextCellDelegate {
func onSwitchTogglenOn(on: Bool)
var switchColor: UIColor { get }
var textColor: UIColor { get }
var font: UIFont { get }
}
接下来,再让configure方法同时接受这两个协议。因为委托可以全部在协议扩展中使用默认值进行配置,比如说字体、颜色之类的信息,这样在理论上就可以不用向里面传递任何东西进去,只需要创建一个模型就可以了
// SwitchWithTextTableViewCell
func configure(withDataSource dataSource: SwitchWithTextCellDataSource, delegate: SwitchWithTextCellDelegate?) {
// 在这里配置视图
}
然后就要开始通过扩展来改进视图模型了,使用一个实际数据源的代码块,然后给定要传递的视图当中的原始信息
struct MinionModeViewModel: SwiftWithTextCellDataSource {
var title = "Minion Mode!!"
var switchOn = true
}
接下来会在一个单独的视图模型的部分使用处理字体、颜色之类的委托,然后在其中进行相关的配置
extension MinionModeViewModel: SwitchWithTextCellDelegate {
var switchColor: UIColor {
return .yellowColor()
}
func onSwitchToggleOn(on: Bool) {
if on {
print("The Minions are here to stay!")
} else {
print("The Minions went out to play!")
}
}
}
最终,表格视图单元格变得非常简单
// SettingViewController
let viewModel = MinionModeViewModel()
cell.configure(withDataSource:viewModel, delegate: viewModel)
return cell
仅仅需要创建视图模型,然后将其传递到配置方法当中,最后返回单元格,就可以了
在游戏开发中通常会有一个很庞大的层级关系,以及一系列的继承,比如各种怪,继承在这里显得十分有意义,但是随着层级的扩展,这个项目就会变得凌乱起来
比如说需要设计一个可以射击的怪物,但这时候塔防顶部的大炮也会射击,就需要把“射击辅助类”提取出来,但是如果一直这样提取子类,代码后面会一团乱麻
将这个代码重构,不再去提取能够射击或者能够加血的子类,而是将其提取为协议,通过协议扩展来实现这个功能,代码更加简洁,更利于理解
// 一看这个对象的类型,就知道他有哪些功能,而不是一个个去查找她的实现
class ZapMonster: GameObject, GunTraint, HealthTraint, MovementTraint {
...
}
虽然说这种设计模式是游戏方面的,但是我们平时的代码也可以参考这种设计模式:这样就不需要让实际的单元格实现这个协议了,只需要将其根更广泛的TextPresentable 联系在一起就可以了,这样,任何拥有标签的视图,而不仅仅是单元格,都可以实现这个协议来弯沉相关的功能。这样就可以说这个标签有什么样的温恩,什么样的颜色,以及什么样的字体
protocol TextPresentable {
var text: String { get }
var textColor: UIColor { get }
var font: UIFont { get }
}
protocol SwitchPresentable {
var switchOn: Bool { get }
var switchColor: UIColor { get }
func onSwitchToggleOn(on: Bool)
}
这种情况下,比如需要一个图片框,只要一个iamgeProtocol就可以了,设计师要求改所有标签的颜色的话一行代码就可以搞定
class SwitchWithTextTableViewCell<T where T: TextPresentable, T: SwitchPresentable>: UITableViewCell {
private var delegate: T?
// T是视图模型
func configure(withDelegate delegate: T) {
// 在这里配置视图
}
}
在这种情况下,它没有实现这些协议,但是会期待某种实现这些协议的东西传递进去,因此我们使用了泛型,这个单元格期待了一个实现了TextPresentableProtocol 的委托。就我们而言,传递进去的将是一个实现了这些协议的东西就可以了,现在要基于这些信息在单元格当中配置所有的东西了,现在就可以基于浙西而信息在单元格中配置所有的东西了
extension MinionModeViewModel: TextPresentable {
var text: String { return "Minion Mode" }
var textColor: UIColor { return .blackColor() }
var font: UIFont { return .systemFontOfsize(17.0) }
}
我们的视图模型将会有一个TextPresentable代码,在其中可以配置文本、颜色、字体,并且由于所有的这些协议扩展中都已经有默认值了,甚至不需要视图模型去实现这些具体的内容
最后,视图模型当中的代码就只需要dequeue相应的单元格。然后通过视图模型对其进行配置,然后返回单元格即可
一种是针对所有 Cell 具有固定高度的情况,通过:self.tableView.rowHeight = 88;
指定了一个所有 cell 都是 88 高度的 UITableView,对于定高需求的表格,强烈建议使用这种(而非下面的)方式保证不必要的高度计算和调用。
另一种方式就是实现 UITableViewDelegate 中的:heightForRowAtIndexPath:需要注意的是,实现了这个方法后,rowHeight 的设置将无效。所以,这个方法适用于具有多种 cell 高度的 UITableView。
iOS7之后出了了estimatedRowHeight,面对不同高度的cell,只要给一个预估的值就可以了,先给一个预估值,然后边滑动边计算,但是缺点就是
iOS8 WWDC 中推出了 self-sizing cell 的概念,旨在让 cell 自己负责自己的高度计算,使用 frame layout 和 auto layout 都可以享受到:
相同的代码在 iOS7 和 iOS8 上滑动顺畅程度完全不同,iOS8 莫名奇妙的卡。很大一部分原因是 iOS8 上的算高机制大不相同,从 WWDC 也倒是能找到点解释,cell 被认为随时都可能改变高度(如从设置中调整动态字体大小),所以每次滑动出来后都要重新计算高度。
使用 UITableView+FDTemplateLayoutCell(百度知道负责人孙源) 无疑是解决算高问题的最佳实践之一,既有 iOS8 self-sizing 功能简单的 API,又可以达到 iOS7 流畅的滑动效果,还保持了最低支持 iOS6
FDTemplateLayoutCell 的高度预缓存是一个优化功能,利用RunLoop空闲时间执行预缓存任务计算,当用户正在滑动列表时显然不应该执行计算任务影响滑动体验。
Xcode插件
项目中常用的第三方库
不知为何每次面试官问这个问题我都感觉好low,虽然能吹牛逼吹好久。。
iOS7新特性
iOS8新特性
size classes是为了解决storyboard只能订制一种屏幕样式的问题,它不再是具体的尺寸,而是抽象尺寸通过宽/高 的compact、any、regular 组成了九种组合包含了所有苹果设备的尺寸。
iOS8中,字体是Helvetica,中文的字体有点类似于“华文细黑”。只是苹果手机自带渲染,所以看上去可能比普通的华文细黑要美观。iOS9中,中文系统字体变为了专为中国设计的“苹方” 有点类似于一种word字体“幼圆”。字体有轻微的加粗效果,并且最关键的是字体间隙变大了!
iOS9新特性
bitcode的理解应该是把程序编译成的一种过渡代码,然后苹果再把这个过渡代码编译成可执行的程序。bitcode也允许苹果在后期重新优化我们程序的二进制文件,有类似于App瘦身的思想。
stackView
Multasking:多任务特性,三种形式
UI Test:iOS9.0之前加入异步代码测设和性能测试,可以说Xcode自带的测试框架已经能满足绝大部分单元测试的需求了,但是这并不够,因为开发一个iOS app从来都是很注重UI和用户体验的,之前UI测试使用KIF,Automating,iOS9.0的Xcode给出了自带的XCUITest的一系列工具,和大多数UI测试工具类似,XCUI使用Accessbility标记来确定view,但因为是Apple自家的东西,可以自动记录操作流程,所以只要书写最后的验证部分就好了,比其他UI测试工具方便多了
iOS平台因为UIKit本身的特性,需要将所有的UI操作都放在主线程执行,所以有时候就习惯将一些线程安全性不确定的逻辑,以及它线程结束后的汇总工作等等放到了主线程,所以主线程包含大量计算、IO、绘制都有可能造成卡顿。
当检测到卡顿时,抓取堆栈信息,然后在客户端做一些过滤处理,便可以上报到服务器,通过收集一定量的卡顿数据后经过分析便能准确定位需要优化的逻辑
设置正确的 reuseidentifer 以重用 cell
尽量将 View 设置为不透明,包括 cell 本身(backgroundcolor默认是透明的),图层混合靠GPU去渲染,如果透明度设置为100%,那么GPU就会忽略下面所有的layer,节约了很多不必要的运算。模拟器上点击“Debug”菜单,然后选择“color Blended Layers”,会把所有区域分成绿色和红色,绿色的好,红色的性能差(经过混合渲染的),当然也有一些图片虽然是不透明的,但是也会显示红色,如果检查代码没错的话,一般就是图片自身的性质问题了,直接联系美工或后台解决就好了。除非必须要用GPU加载的,其他最好要用CPU加载,因为CPU一般不会百分百加载,可以通过CoreGraphics画出圆角
有时候美工失误,图片大小给错了,引起不必要的图片缩放(可以找美工去改,当然也可以异步去裁剪图片然后缓存下来),还是使用Instrument的Color Misaligned Images,黄色表示图片需要缩放,紫色表示没有像素对齐。当然一般情况下图片格式不会给错,有些图片格式是GPU不支持的,就还要劳烦CPU去进行格式转换。还有可以通过Color Offscreen-Rendered Yellow来检测离屏渲染(就是把渲染结果临时保存,等到用的时候再取出,这样相对于普通渲染更消耗内存,使用maskToBounds、设置shadow,重写drawRect方法都会导致离屏渲染)
避免渐变,cornerRadius在默认情况下,这个属性只会影响视图的背景颜色和 border,但是不会离屏绘制,不影响性能。不用clipsToBounds(过多调用GPU去离屏渲染),而是让后台加载图片并处理圆角,并将处理过的图片赋值给UIImageView。UIImageView 的圆角通过直接截取图片实现,圆角路径直接用贝塞尔曲线UIBezierPath绘制(人为指定路径之后就不会触发离屏渲染),UIGraphicsBeginImageContextWithOptions。UIView的圆角可以使用CoreGraphics画出圆角矩形,核心是CGContextAddArcToPoint 函数。它中间的四个参数表示曲线的起点和终点坐标,最后一个参数表示半径。调用了四次函数后,就可以画出圆角矩形。最后再从当前的绘图上下文中获取图片并返回,最后把这个图片插入到视图层级的底部。
“Flash updated Regions”用于标记发生重绘的区域
重用问题:比如UITableViewCell、UICollectionViewCell、UITableViewHeaderFooterViews等设置正确的reuseIdentifier,充分重用
懒加载控件、页面:对于不是立刻使用的数据,都应该使用延迟加载的方式,比如网络连接失败的提示界面,可能一直都用不到
使用Autorelease Pool:在某些循环创建临时变量处理数据时,自动释放池以保证能及时释放内存
不要使用太多的xib/storyboard:载入时会将其内部的图片在内的所有资源载入内存,即使未来很久才会需要使用,相对于纯代码写的延迟加载,在性能和内存上就差了很多
数据缓存:对于cell的行高要缓存起来,使用reloadData效率也极高,对于网络数据,不需要每次都请求的,应该缓存起来,可以写入数据库,也可以通过plist文件存储
选择正确的数据结构:针对不同的业务场景选择最合适的数据结构是写出高效代码的基础
gzip/zip压缩:当从服务器下载相关附件时,可以通过 zip压缩后再下载,使得内存更小,下载速度也更快
重大开销对象:一些objects的初始化很慢,比如NSDateFormatter和 NSCalendar,但是又无可避免的需要使用,通常作为属性存储起来,避免反复使用
避免反复处理数据:需要应用需要从服务器加载数据,常为JSON或者XML格式的数据,在服务器端或者客户端使用相同的数据结构很重要
选择图片时,要对图片进行压缩处理,根据不同的情况选择不同的图片加载方式,-imageNamed:读取到内存后会缓存下来,适合图片资源较小,使用很频繁的图片;-initWithContentsOfFiles:仅加载图片而不缓存,适合较大的图片。若是collectionView中使用大量图片的时候,可以用UIVIew.layer.contents=(__bridge id _Nullable)(model.clipedImage.CGImage);这样就更轻量级一些
当然有时候也会用到一些第三方,比如在使用UICollectionView和UITableView的时候,Facebook有一个框架叫AsyncDisplayKit,这个库就可以很好地提升滚动时流畅性以及图片异步下载功能(不支持sb和autoLayout,需要手动进行约束设置),AsyncDisplayKit用相关node类,替换了UIView和它的子类,而且是线程安全的。它可以异步解码图片,调整图片大小以及对图片和文本进行渲染,把这些操作都放到子线程,滑动的时候就流畅许多。我认为这个库最方便的就是实现图片异步解码。UIImage显示之前必须要先解码完成,而且解码还是同步的。尤其是在UICollectionView/UITableView 中使用 prototype cell显示大图,UIImage的同步解码在滚动的时候会有明显的卡顿。另外一个很吸引人的点是AsyncDisplayKit可以把view层次结构转成layer。因为复杂的view层次结构开销很大,如果不需要view特有的功能(例如点击事件),就可以使用AsyncDisplayKit 的layer backing特性从而获得一些额外的提升。当然这个库还处于开发阶段,还有一些地方地方有待完善,比如不支持缓存,我要使用这个库的时候一般是结合Alamofire和AlamofireImage实现图片的缓存
千万别打赏!俺不缺钱,缺赞!