性能指标
- 内存使用量
- 电量的消耗
- 应用初始化的时间
- 执行速度
- 响应速度
- 本地存储的使用;如果你的应用使用了本地存储,那么请提供一个清除数据的选项。
- 适应网络环境
- 数据刷新
- 单点登录
- 安全
- 崩溃,崩溃报告、检测和分析
内存管理
- iOS中内存消耗指的是应用消耗的RAM;iOS的虚拟内存模型并不包含交换内存,与桌面应用不同,这意味这磁盘不会被用来分页,最终的结果就是应用只能使用有限的RAM。应用中的内存消耗分为两部分:栈大小和堆大小
- 栈空间,应用中每个线程都有专用的栈空间,该空间有保留的内存和初始提交的内存组成。栈可以在线程存在期间自由的使用。线程的栈空间很小,这就决定了一下的限制
- 可以被递归调用的最大方法数
- 一个方法中最多可以使用的变量个数
- 视图层级中可以嵌入的最大视图深度
- 自动释放池块(@autoreleasepool{})是允许你放弃对一个对象的持有关系,但可以避免它立即被回收的一个工具。当从方法返回对象的时候非常有用;块中收到过autorelease消息的所有对象都会在autoreleasepool块结束的时候收到realease消息;@autoreleasepool块可以嵌套,被调用的方法可以拥有自己的@autoreleasepool,以提前执行对象的回收;
- 在循环中需要创建很多历史的大对象用于执行某项操作,循环中可以使用@autoreleasepool块为每一次迭代释放内存,可以降低应用的最大内存的需要
- 对于任何的自定义线程,必须创建自己的@autoreleasepool
- 当使用NSTimer和NSThread的时候,总是应该使用间接的层(任务类进行封装)实现明确呃销毁过程,这个间接层应使用弱引用,从而保证所拥有的对象能够在停止使用后执行销毁动作
- 尽量避免不必要的使用单例,无法完全避免单里的使用,但是至少可以尽量避免直接使用单例,而是采用“依赖注入”的方式使用单例
- 出现低内存告警时,应该对应用进行埋点,包括内存的使用及统计信息,并在应用重新裕兴时将这些信息上报给服务器
- 最佳实践
- 避免大量的使用单例
- 对子对象使用__strong
- 对父对象使用__weak
- 对引用图闭合的对象使用__weak
- 对数值属性,使用assign限定符
- 对于块属性,使用copy限定符
能耗
- 应用计算越多,消耗的电量就越多。在完成相同的基本操作时,老一代的设备会消耗更多的电量。
- 蜂窝网络的电量消耗,要远大于wifi;由于蜂窝网络要定期扫描以发现网络信号更强的网络
- 将消耗带宽的非关键联网请求,缓存到队列中甚至磁盘中,观察网络状态,当网络状态处于wifi环境中才处理这些请求
并发编程
- 所有的UI元素都需要在主线程中创建和管理
- 与用户交互相关的中断都会在主线程中处理,IBAction方法的代码都会在主线程中进行
- 线程的开销
- 消耗约1KB的内核内存空间
- 主线程的栈空间大小为1M,而且无法修改,所有的二级线程默认分配512KB的栈空间;完整的栈空间并不会立即被创建出来,实际的栈空间的大小会随着使用而增长。因此,即使主线程有1MB的栈空间,在某个时间点的实际的栈空间很可能会小很多
- GCD线程池有线程个数的上限,限制为64个如果超过个数,则会引起崩溃
- 原子属性(atomic)不一定保证代码是线程安全的
- NSCondition,用于协调线程之间的执行。使用NSCondition可以解决标准的生产者消费者问题
- 使用线程屏障块(dispatch_barrier_sync/dispatch_barrier_async)实现读写锁
- 创建自定义的并发队列(DISPATCH_QUEUE_CONCURRENT)
- 将dispatch_sync/dispatch_async用于读取操作
- 将dispatch_barrier_sync/dispatch_barrier_async用于写操作
- 主线程的主队列(mainQuque)是串行队列,故此向在主线程中向主队列提交sync任务会造成死锁
- dispatch_sync添加任务到队列,不会创建新的线程都是在当前线程中处理的。无论添加到串行队列里或者并行队列里,都是串行效果,因为这个方法是等任务执行完成以后才会返回。
- dispatch_async添加任务到
- mainQueue不创建线程,在主线程中串行执行
- globalQueue 和 并行队列:根据任务系统决定开辟线程个数
- 串行队列:创建一个线程:串行执行。
- 使用生成器对多字段(并且有可能版本迭代)的模型进行初始化,好处是可支持版本向下兼容,避免采用构造器的方式造成参数膨胀式的增长,并且便于提供默认值
应用的生命周期
- 优化启动过程(缩减在didLaunch回调中在主线程执行的时间)
- 确定在展示UI前必须执行的任务
- 按顺序执行任务
- 将任务拆分为必须主线程中执行的任务和可以在其他线程中执行的任务,然后分别执行
- 其他任务在加载UI后执行或异步执行
用户界面
- 视图控制器(controller)的最佳实践
- 将业务逻辑独立编写,不存放在controller中,controller只是纽带
- 不要在controller中编写动画逻辑。动画应该在独立的动画类中实现
- 使用数据源和委托协议,controller只能用来选择正确的视图,并将他们连接到正确的供应源
- controller响应来自视图的时间
- controller响应来自操作系统的UI相关事件
- 不要编写自定义的init方法,由于如果controller被重新切换至xib或者storyboard,那么init方法将永远不会被调用
- 创建一个实现了公共设置的基类controller
- 尽量不要在controller中使用代码手工布局UI
- 通过继承基类享受公共方法,通过编写Category定义自定义方法
- 减少viewDidLoad中的主线程操作的耗时,任何delay都会造成卡顿
- viewWillAppear,在这个时刻,过渡动画还未开始,视图对终端用户也是不可见的。不要启动任何视图动画,因为没有任何作用
- viewDidAppear,视图在视图窗口展示出来,切过度动画完成后,此方法会被调用;启动或者恢复任何想要呈现给用户的视图动画
- viewWillDisappear,没有直接的办法能够判断这是由于当前controller要出栈还是其他controller入栈导致的;可以通过遍历当前controller的navigationController的viewControllers属性,查找是否含有当前controller的实例,来判断
- 当应用处于前台,后台切换的时候,与视图相关的生命周期方法(willAppear,willDisappear)是不会被调用的,而是通知UIAPPDelegate
- 高效实用controller生命周期的最佳实践
- 不要重写loadView
- 在viewDidAppear中开始动画
- 在viewWillDisappear中来暂停或者停止动画
- 避免在视图层次中多层嵌套。在层次结构的任何位置添加视图时,他的祖先树节点会执行值为YES的setNeedsLayout:方法,当时事件队列正在执行时,该设置会触发layoutSubviews:。这个调用代价较大,因为视图必须根据约束重新计算子视图的位置,而且在祖先树的每一层都会发生
- 要避免使用具有通用目的及功能丰富的组件而带来的消耗,可以使用那些直接实现绘制方法的视图来替代
- [UIImage imageNamed:],可以确保内容只被加载至内存一次
- UITableView的最佳实践
- 进行单元格的复用
- 避免动态高度的单元格
- 当单元格实在复杂,无法避免在视图重用和渲染的时延,可以采用界面外壳(相当于loading态cell),并且监控TableView的滑动速度,当高度滑动的时候显示外壳cell,当滚动速度降低,并且低于阈值的时候,刷新列表展示真实的cell
- 复合视图(父子视图的嵌套)和直接绘制(drawRect:中直接绘制额)的PK
- 直接绘制的性能远远高出复合视图,数十倍的差距
- 但是直接绘制维护和拓展难度较大
网络
- DNS查找时间
- 网络延迟 = 联网请求的往返时间 - 服务器时间(服务器返回)
- 在任何可以使用HTTP2的情况下,都应该使用HTTP2
- HTTP2支持HTTP请求的真正复用
- HTTP2还支持报头的压缩
- 联网数据格式最佳实践
- 使用数据压缩,NSURLRequest会自动给头部添加Accept-Encoding:gzip、deflate
- 选择正确的数据格式:Json、XML
测试及发布(后续通过《腾讯iOS测试实践》来补充)
- 单元测试、功能测试、性能测试
- 测试夹具,进行测试用例前所需的准备(setUp)和清理阶段(tearDown)
- 通过编辑scheme,打开gather coverage data,可以收集本次测试的测试覆盖率,十分精确,精确到哪行代码运行了,if-else那个分支运行了,很直观
- 使用XCTestExpectation对象测试异步方法
XCTestExpectation *e = [self expectationWithDescription:@"Test networkApi"]; [[fmNetworkHandler sharedInstance] getFundDetailChartData:@{@"type": @"oneMonth", @"fundCode": @"003003"} success:^(id responseObject) { NSLog(@"返回结果: %@", responseObject); [e fulfill]; } failure:^(NSString *errorCode, NSString *errorMsg) { XCTAssert(nil, @"联网失败"); }]; [self waitForExpectationsWithTimeout:60 handler:^(NSError * _Nullable error) { if (error) { XCTAssert(nil, @"请求超时"); } }];
- measureBlock ,测试代码段的性能
[self measureBlock:^{ [fmPopupHistoryHandler updatePopupHistoryRecord:@"Test" messageId:@"Test" forUser:nil]; XCTAssertTrue([fmPopupHistoryHandler ifRecordExist:@"Test" messageId:@"Test" forUser:nil]); }];
- 单元测试是针对单一方法、类的测试