一、移动应用的性能
性能指标
- 内存
运行所需最小 RAM,内存平均值和峰值,内存泄漏
电量消耗
初始化时间
惰性初始化所需的对象
- 执行速度
并行处理技术或将复杂任务分发到服务器
响应速度
本地存储
互操作性
-
网络环境
- 高带宽稳定环境
- 低带宽稳定环境
- 高带宽不稳定环境
- 低带宽不稳定环境
- 无网络
带宽
数据刷新
多用户支持
单点登录
如果用户登录了一个应用,则只需要点击一次,就可以登录到其他应用
安全
崩溃
应用性能分析
- 采样
- 埋点
- 日志(debug、verbose、info、warning、rrror)
二、内存管理
iOS 的虚拟内存模型不包含交换内存,意味着磁盘不能被用来分页内存。iOS 应用的内存消耗分为两部分:栈大小和堆大小。
每个进程的所有线程共享同一个堆,只有操作系统能管理堆。
循环引用常见场景
- 委托
在委托中建立对操作的强引用,在操作中建立对委托的弱引用。
块
线程与计时器
NSTimer 创建后会持有创建者,同时被 runtime 持有,可以调用 NSTimer 的 invalidate 方法解除资源和持有。
推荐方案:将持有关系分散到多个类中,任务类执行具体动作,所有者类调用任务。从而通过间接层实现明确的销毁过程。间接层使用弱引用持有所有者类,以保证所拥有的对象能够在停止使用后执行销毁动作。
- 观察者
键值观察与通知中心均不会维持观察对象、被观察对象以及上下文的强引用,如有必要,需要自行维护。
返回错误
对象指针本身不会对对象的引用计数造成影响,对象指针与对象本身的内存管理修饰符应当一样。
{
NSError *error = nil;
@autoreleasepool {
NSError * __weak errorWeak = error;
[self performOperationWithError:&errorWeak];
}
NSLog(@"%@", error);
}
- (BOOL)performOperationWithError:(NSError *__weak*)error
{
// 方法执行发生错误
*error = [NSError errorWithDomain:@"MyAppDomain" code:123 userInfo:nil];
return NO;
}
NSError** 作为参数时应该使用 __autoreleasing
修饰符。虽然赋值对象指针时,所有权修饰符必须一致,但是这里传参时编译器会进行优化
NSError *error = nil;
NSError __autoreleasing *tmp = error;
BOOL result = [obj performOperationWithError:&tmp];
error = tmp;
单例
单例使用场景
- 队列操作(如日志或埋点)
- 访问共享资源(如缓存)
- 资源池(如线程池或连接池)
三、能耗
计算量的能耗取决于不同因素
- 对数据的处理
- 待处理的数据大小 —— 更大的显示器意味着更多的信息和数据需要处理
- 处理数据的算法和数据结构
- 执行更新的次数
最佳实践
- 针对不同情况选择优化算法
小于 43 个实例则插入排序优于归并排序,多于 286 个则快速排序最优,优先使用双枢轴快排。
- 尽量将数据处理放在服务器端
- 优化静态编译处理(AOT)
- 分析电量消耗
网络
- 在进行网络操作前检查合适的网络连接是否可用。
- 持续监视网络可用性,并在连接状态发生变化时给予适当的反馈。
推荐使用 Reachability Pod 检测网络状态。
使用基于队列的网络请求可以避免服务器遭到多个同时发起的请求轰炸。至少使用两个队列:一个用于通常不关键的大量图片下载,一个用于关键数据请求。
定位管理器
使用 GPS 坐标需要两点信息
- 时间锁——每个 GPS 卫星每毫秒广播唯一一个 1023 位随机数,GPS 接收芯片必须正确与时间锁对齐
- 频率锁——GPS 接收器计算由接收器与卫星的相对运动导致的多普勒偏移带来的信号误差
通常情况下锁定一颗卫星需要至少 30 秒,确定卫星越多,取得的坐标越精确。
只在必要时使用网络
每当应用建立网络连接时,硬件都会在连接完成后多维持几秒的活动时间,集中的网络通信都会消耗大量的电量。所以应该定期集中短暂地使用网络。
(可是埋点、网络监测模块应该怎么取舍呢?)
屏幕
- 动画在应用进入后台后应该暂停
- 视频播放时保持屏幕常亮
最佳实践
- 最小化硬件使用,尽可能晚地与硬件打交道,一旦完成任务立即结束使用
- 进行密集型任务前检查电池电量和充电状态
- 电量低时提示用户是否确定要执行任务,并在用户同意后再执行
- 提供设置选项,允许用户定义电量的阈值,以便在执行密集型操作前提示用户
四、并发编程
每个线程大约消耗 1KB 的内核内存空间,这块内存用于存储与线程相关的数据结构和属性,属于联动内存,无法分页。
主线程的栈空间大小为 1M,并且无法修改,其他二级线程默认分配 512KB 栈空间,但是并不会一次性创建完整大小,而是随着使用逐渐增长。
线程创建的时间消耗为 4~5 毫秒,启动线程的时间消耗为 29 毫秒,主要是上下文切换带来的时间开销。
GCD最大线程个数为 64 个。
并行读取、互斥写入
- 创建一个并行队列
- 对于读操作使用 dispatch_sync
- 对于写操作使用 dispatch_barrier_sync
dispatch_barrier_sync 可以在并行队列上创建一个同步点,dispatch_barrier_sync 中的代码块会在之前所有提交代码块执行结束后单独执行。
生成器模式
对于数据实体的初始化,一般有两种方案:
- 使用自定义初始化器
- 使用生成器模式
自定义初始化器需要众多的参数,和很长的方法名,并且会带来向下兼容的问题,加入新的属性将导致调用初始化器的代码不能使用。
因此建议使用生成器模式。
交叉引用
实体之间的交叉引用,尤其是不可变实体的交叉引用,会在初始化时陷入循环初始化陷阱,A 需要 B 来初始化,B 又需要 A 来初始化。此时可以利用冰棒不变性,用一个标志位和一个设置标志位的方法来进行一次性初始化操作,之后再对属性赋值时需要检测此属性来决定是否可以修改。
- (void)freeze
{
self.frozen = YES;
}
- (void)setUserId:(NSString *)userId
{
if (!self.frozen) {
_userId = userId;
}
}