此为高性能iOS应用开发的笔记。
此书很不错,看了多次决定记一下笔记。
有些我觉得是废话的,我就懒得记下了。
卡顿优化 或 内存优化,简单的优化入手方面有:
页面重叠部分、圆角部分、对象释放部分、static使用部分(如单列对象)、UI刷新前的数据处理部分、页面刷新次数、通知和监听的移除、数据的不合理读取(如cell里面访问数据库就不行)。
代码优化:
各页面的接口请求都封装到一个类中,所有的类放到统一的文件夹中。
返回对象数据全部用Model。
建立一个所有ViewController都继承的基层控制器。
View里谁的任务交给谁去处理,Model负责存储数据,Controller负责处理与传递数据。
不要太死板。
程序猿皆知的DRY原则。
工程与代码:
好的工程与代码应符合三点:
1.能轻松地构建和发布应用
2.可测试性。确保代码能同时在模拟数据和真实数据上工作,其中包括模拟的环境
3.可跟踪性。能够快速找到问题所在并处理
注意收集崩溃报告信息
可使用Flurry,或者腾讯Bugly
应用埋点的三个时期
当应用进入前台、当应用进入后台、当应用受到低内存警告
日志与埋点:埋点应用于特定阶段、分析特定情况的时候,日志则用于整个APP的生命周期。日志可用CocoaLumberjack
内存消耗
RAM的消耗主要分为两部分:栈大小和堆大小。
栈大小
- 应用中新创建的每个线程都有专用的栈空间,该空间由保留的内存和初始提交的内存组成。
- 栈可以在线程存在期间自由使用。
线程的最大栈空间很小,所以会有一些限制。如:
1.可被递归调用的最大方法数
每个方法都有其自己的栈帧,并会消耗整体的栈空间。
2.一个方法中最多可以使用的变量个数
所有的变量都会载入方法的栈帧中,并消耗一定的栈空间
3.视图层级中可以嵌入的最大视图深度
渲染复合视图将在整个视图层级树中递归的调用layoutSubViews和drawRect方法。如果层级过深,可能会导致栈溢出。
堆大小
- 每个进程的所有线程共享同一个堆。一个应用可以使用的堆大小通常远远小于设备的RAM值。
- 应用不能控制分配给它的堆,只有操作系统才能管理堆。
- 使用NSString、载入图片、创建或使用JSON/XML数据、使用视图等都会消耗大量的堆内存。
与通过类创建的对象相关的所有数据都存放在堆中。
类可能包含属性或值类型的实例变量(iVars),如 int、char或struct。但因为对象是在堆内创建的,所以它们只消耗堆内存。
当对象被创建并被赋值时,数据可能会从栈复制到堆。类似地,当值仅在方法内部使用时,它们也可能会被从堆复制到栈。
数据从栈复制到堆,是将一个局部变量(如方法所需的参数)赋值给一个对象的属性时,必须被复制到堆中(如NSInteger)。但若属性是Copy类型时,则值是被复制或克隆到堆中(也就是深拷贝与浅拷贝)。
而数据从堆复制到栈中,则就是将对象的属性赋值给局部属性。
每个线程都会被分配一个栈,而栈则是通过操作栈帧运行各个程序的方法,栈帧中保存着变量和其他调用方法的引用,还有操作数栈。
而直接定义的局部变量都是放在栈中的,对象的存储放在堆中,全局和静态的对象是放在数据段的静态存储区。
内存管理模型
如果一个对象正处于被持有的状态,那它占用的内存就不能被回收。
当一个对象创建于某个方法的内部时,那该方法就持有这个对象了。
如果这个对象从方法返回,则调用者声称建立了持有关系。
这个值可以赋值给其他变量,对应的变量同样会声称建立了持有关系。
如果一个对象没有被引用,就会被释放。内存被回收。
自动释放对象
自动释放对象让你能够放弃对一个对象的持有关系,但延后对它的销毁。
当在方法中创建一个对象并需要将其返回时,自动释放就显得非常有用。
可用于MRC中管理对象的生命周期。
自动释放池块:@autoreleasepool
自动释放池块允许你放弃对一个对象的持有关系、但可避免它立即被回收。
在块内创建的对象会在块完成时被回收。
常用于从方法返回对象时。
可用来尽早地释放其中的对象,从而使内存用量保持在较低的水平。
autorelease可以嵌套使用,块中收到过autorelease消息的所有对象都会在autorelease块结束时收到release消息。
而且是每个autorelease调用都会发送一个release消息。所以一个对象收到多次autorelease消息,也会收到多次release消息。
整个应用都是在一个autorelease块中。
常用于:
1.创建了很多的临时对象的循环。
创建了很多的临时对象的循环时用自动释放池,可避免一次性过多的持有对象。
2.创建一个线程时。
创建一个线程时,每个线程都将有它自己的autoreleasepool块栈。主线程用自己的autoreleasepool启动,因为他来自统一生成的代码。然而,对于任何自定义的线程,必须创建自己的autoreleasepool。
也就是线程调用的方法内部,放一个autoreleasepool。
ARC的规则:开发人员不能直接进行内存管理。如不遵守,在编译时期报错而不是运行时崩溃。
1.不能实现或调用retain、release、autorelease或retainCount方法。对象、选择器都不能用。所以,[obj release]或@selector(retain)是编译时的错误。
2.dealloc方法可以实现,但不能调用它们。包括父类的也不能调用。
但是CoreFoundation类型的对象可以调用CFRetain、CFRelease等相关方法。
3.不能调用NSAllocateObject 和 NSDeallocateObject 方法。应使用alloc方法创建对象,运行时负责回收对象。
4.不能再C语言的结构体内使用对象指针。
5.不能在id类型和void * 类型之间自动转换。如果需要,必须做显示转换。
6.不能使用NSAutoreleasePool,要替换使用autoreleasepool块
7.不能使用NSZone内存区域。
8.属性的访问器名称不能以new开头,以确保与MRC的互操作性。
9.MRC和ARC可以混合使用。
ARC新增引用类型:弱引用
变量限定符
_strong、_weak、__unsafe_unretained、_autoreleasing
属性限定符
strong、weak、assign(ARC之前,其是默认的持有关系限定符。ARC之后,表示__unsafe_unretained)、copy、retain (指定了__strong关系)、unsafe_unretained(制定了__unsafe_unretained)
assign和unsafe_unretained只进行值复制而没有实质性的检查,所以它们只应该用于值类型(Bool、NSInteger、NSUInteger,等等)。应避免用于引用类型,尤其是指针类型。如果NSString * 和 UIView *
僵尸对象:用于捕捉内存错误的调试功能。
但是开启僵尸对象会占用大量的内存,所以应只在需要排查的时刻去开启。
开启:Product -> Scheme -> Edit Scheme 。选择左侧的Run, 然后在右侧选取Diagnostics标签页。选中Enable Zombie Objects选项。
内存管理规则
1.你拥有所有自己创建的对象,如new、alloc、copy或mutableCopy
2.你可以用MRC中的Retain或者ARC中__strong 引用来拥有任何对象的而持有关系。
3.在MRC中,当不需要某个对象时须release该对象。而ARC无需特殊操作。持有关系会在对象失去最后的引用时被抛弃,如方法中的最后一行代码。
4.一定不能抛弃原本并不存在持有关系的对象。
循环引用
循环引用就是A引用B,B引用A,进而导致内存泄露。
避免循环引用的方式:
1.对象不应该持有它的父对象,而是用weak引用指向它的父对象。
简单说,a包含b,b属于a,所以a中有属性指向b,b中有属性指向a。那么b指向 a应是weak引用。
2.作为必然的结果,一个层级体系中的子对象应该保留祖先对象。
3.连接对象不应该持有它们的目标对象。目标对象的角色是持有者。连接对象包括:
(1)使用委托的对象。委托应该被当作目标对象,即持有者。
(2)包含目标和action的对象,这是由上一条规则推理得到的。列如,UIButton会调用它的目标对象上的action方法。按钮不应该保留它的目标。
(3)观察者模式中被观察的对象。观察者就是持有者,并会观察发生在被观察对象上的变化。
像是delegate对象是weak,避免A持有B,B持有C,C持有A
4.使用专用的销毁方法中断引用
常见的循环引用
代理、Block、线程、计时器
我发现计时器方面的常被忽视,所以单独说下计时器常见泄露情况
self.timer = [NSTimer scheduledTimerWithTimeInterval:120
target:self selector:@selector(updateFeed:) userInfo:nil repeats:YES];
}
self 持有timer , timer持有self 所以需要[self.timer invalidate]断开循环
但是self不会被释放,所以dealloc中销毁是不会成功的,应该在跳转其他页面或运行停止时调用
键 - 值观察
键 - 值观察方法 addObserver:forKeyPath:options:context: 不会维持观察对象、
被观察对象及上下文对象的强引用。如有必要,你需要自行维护对它们的强引用。
简单来说,键值观察不会影响到对象的引用计数。
返回错误
当用某个方法接收NSError *参数,并在发生错误时填充错误变量,则必须使用 __autoreleasing 限定符:
NSError __autoreleasing *error;
接收参数: error:(NSError * __autoreleasing *) error
//处理
//如果发生错误
*error = [[NSError alloc] initWithDomain:@"transpose" code:123 userInfo:nil];
弱类型:id
应避免使用id,尽量用具体的类型取而代之。
单列
创建之后,会存在于整个程序的运行期间存活。所以若无必要,尽量不要用。有句俗话:占着茅坑不拉x
常用于日志、埋点、某些缓存操作、线程池或连接池等。
获取一个对象的所有引用
通过关闭ARC,计算retain来获取。
先禁用ARC,而后在自定义类中添加代码:
#if !_has_feature(objc_arc) -(id) retain
{
DDLogInfo(@"%s %@", _PRETTY_FUNCTION, [NSThread callStackSymbols]); return [super retain];
} #endif
这段代码记录retain方法的调用情况,将调用栈打印出来。因此会获得调用次数 与 精确的调用明细。
但是就目前的个人接触来说,可以直接使用僵尸对象检测。而且Block等内存泄露的情况下,XCode也会有警告提示,只要注意一下、不是故意的,通常会避免。
实践总结
1.避免大量的单列。
2.对子对象使用__strong
3.对父对象使用__weak
4.对使引用图闭合的对象(如委托、Block)使用__weak
5.对数值属性(NSInteger、SEL、CGFloat等)而言,使用assign限定符
6.对于块属性,使用copy限定符
7.当声明使用NSError ** 参数的方法时,使用__autoreleasing,并要注意用正确的语法:NSError * __autoreleasing *
8.避免在块内直接引用外部的变量。在外部对self使用weak ,内部用self
9.注意以下清理:销毁计时器、移除观察者、解除回调(强引用的委托置为nil)
生产环境的内存使用
要想在不同的情境中分析应用,可以使用埋点。
尤其是内存超出阈值时(如规定数值,或收到内存警告时)配合一些辅助定位信息(如内存的使用及统计信息)。
或者是一定间隔后在本地记录,上报给服务器。
能耗
除 CPU 外,耗电量高、值得关注的硬件模块还包括:网络硬件、蓝牙、GPS、麦克风、加 速计、摄像头、扬声器和屏幕。
应用计算得越多,消耗的电量就越多。在完成相同的基本操作时,老一代的设备会消耗更多的电量。
计算量的消耗取决于不同的因素:
1.对数据的处理(如:文本格式化)。
2.待处理的数据大小 — 更大的显示屏允许软件在单个视图中展示更多的信息,但这也意味着要处理更多的数据。
3.处理数据的算法和数据结构。
4.执行更新的次数,尤其是在数据更新后,触发应用的状态或UI进行更新(应用收到的推送通知也会导致数据更新,如果此时用户正在使用应用,你还需要更新UI)。
实践建议:
1.针对不同的情况选择优化的算法。(我的天。。)
2.如果应用从服务器接收数据,尽量减少需要在客户端进行的处理。
3.优化静态编译(AOT)处理。
(动态编译处理的缺点在于它会强制用户等待操作完成。但是激进的AOT处理则会导致计算资源的浪费。需要根据应用和设备选择精确定量的AOT处理。)
4.分析电量消耗。测量用户设备上的电量消耗,而后找到高消耗想法降低。
网络
蜂窝无线系统(LTE、4G、3G等)对电量的消耗远大于WiFi信号。
在进行网络操作之前,先检查合适的网络连接是否可用。
持续监视网络的可用性,并在链接状态发生变化时给予适当的反馈。
苹果公司提供了示例代码(http://apple.co/1Q3gRKL),以检查和监听网络状态的变化
若使用CocoaPods,可使用Reachabilitypod
NSOperationQueue 不会暂停或挂起任何执行中的操作。一个挂起的队列仅仅意味着 后续操作在其恢复之前不会被执行。
操作只有完成后才会从队列中移除,所以要先启动操作用于完成执行。而队列被挂起不会启动任何新的操作,所以也不会移除任何正在排队且未被执行的操作(包括那些已经取消的操作)。
使用基于队列的网络请求以避免服务器被多个同时发起的请求所轰炸。至少使用两个队列:一个用于通常不是很关键的大量图片下载。 一个用于关键数据的请求。
定位管理器和GPS
定位服务需要大量的电量。
使用GPS计算坐标需要确定两点信息:
1.时间锁
每个GPS卫星每毫秒广播唯一一个1023位随机数,因而数据传播速率是1.024Mbits/s。GPS的接收芯片必须正确的与卫星的时间锁槽对齐。
2.频率锁
GPS接收器必须计算由接收器与卫星的相对运动导致的多普勒偏移带来的信号误差。
通常情况下,锁定一颗卫星至少需要30秒。必须锁定接受范围内的所有卫星。确定的卫星越多,取得的定位坐标就越精确。
计算坐标会不断地使用CPU 和 GPS的硬件资源,因此会迅速的消耗电池电量。
最佳初始化:
1.distanceFilter,设备的移动超过了最小距离,距离过滤器就会导致管理器对委托对象的locationManager: didUpdateLocations: 事件通知发生变化。该距离使用公知单位(米)。
它并不会有助于减少GPS接收器的使用,但会影响应用的处理速度,从而直接减少CPU的使用
2.desiredAccuracy
精度参数的使用直接影响了使用天线的个数,进而影响了对电池的消耗。精度级别的选取取决于应用的具体用途。按降序排列,精度由以下常量定义。
- kCLLocationAccuracyBestForNavigation:用于导航的最佳精度级别
- kCLLocationAccuracyBest:设备可能达到的最佳精度级别
- kCLLocationAccuracyNearestTenMeters:精度接近10米。如果对用户所走的每一米并不感兴趣,不妨使用这个值。
- kCLLocationAccuracyHudredMeters:精度接近100米(计算距离时,值需要乘以100米)
- kCLLocationAccuracyKilometer:精度在千米范围。这在粗略测量两个距离数百千米的兴趣点时非常有用。(列如从中国的北京到日本的东京)
- KCLLocationAccuracyThreeKilometers:精度在三千米范围内。在距离真的很远时使用这个值(好比 北极到南极)
距离过滤器只是软件层面的过滤器(但会影响数据的处理,进而直接影响到CPU使用),而精度级别会影响物理天线的使用。
当委托的回调方法locationManager:didUpdateLocations:被调用时,使用距离范围更广的过滤器只会影响间隔。另一方面,更高的精度级别意味着更多的活动天线,这会消耗更多的能量。
关闭无关紧要的特性
判断何时需要跟踪位置的变化。在需要跟踪时调用startUpdatingLocation方法,无需跟踪时调用stopUpdatingLocation方法。
假设用户需要用一个消息类的应用与朋友分享位置。如果该应用只是发送城市的名称,则只需要一次性的获取地理位置信息,然后就可以通过调用stopUpdatingLocation关闭位置跟踪。在一定的时间间隔后可以再次开启定位。你可以设置固定的间隔(如30S),也可以动态的计算时间间隔(如 根据之前获取的坐标和速度,估算穿过城市的时间上限)。
当应用在后台运行或用户没有与别人聊天时,也应该关闭位置跟踪。
向终端用户提供关闭非必要功能的选项是一个更好的解决方案。
只在必要时使用网络
为了提高电量的使用效率,iOS总是尽可能地保持无线网络关闭。当应用需要建立网络连接时,iOS会利用这个机会向后台应用分享网络会话,以便一些优先级的事件能够被处理,如推送,收取电子邮件等。
关键在于每当应用建立网络连接时,网络硬件都会在连接完成后多维持几秒的活动时间。每次集中的网络通信都会消耗大量的电量。
所以为减轻这个危害,应该定期集中短暂的使用网络,而不是持续着保持活动的数据流。
后台定位服务
CLLocationManager提供了一个替代的方法来监听位置的更新。startMonitoringSignificantLocationChanges可以帮助你在更远的距离跟踪运动。精确的值由内部决定,且与distanceFilter无关。
使用这一模式可以在应用进入后台后继续跟踪运动。
典型的做法是在应用进入后台时执行startMonitoringSignificantLocaitonChanges方法,而当应用回到前台时执行startUpdatingLocation。
NSTimer、NSThread和定位服务
当应用位于后台时,任何定时器或线程都会挂起。但如果你在应用位于后台状态时申请了定位,那么应用会在每次收到更新后被短暂唤醒。在此期间,线程和计时器都会被唤醒。
若在此期间做了任何网络操作,则会启动所有相关的天线。想控制这种情况,最佳选择使用NSURLSession类。
在应用关闭后重启
在其他应用需要更多资源时,后台应用可能会被关闭。
在此情况下,一旦发生位置变化,应用会被重启,因而需重新初始化监听过程。
若发生这种情况,application:didFinishLaunchingWithOptions:方法会收到键值为UIApplicationLaunchOptionsLocationKey的条目。(launchOptions[UIApplicationLaunchOptionsLocationKey]有值)
屏幕
屏幕越大越费电(废话。)
动画
明智的使用动画,列如前台时使用动画,后台暂停。
视频播放
视屏播放时强制保持屏幕常亮。使用UIApplication 对象的 idleTimerDisabled属性来实现这个目的。一旦设置为 YES,它会阻止屏幕休眠,从而实现常亮。也可以通过响应应用的通知来释放和获取锁。
多屏幕
设备连接外部显示设备,除了系统默认行为外(显示设备投影) 还可以做别的更多操作。
如电影播放或运行动画,从设备屏幕挪到外部屏幕。
处理方式:
1.启动器件检测屏幕数量,若大于1,则进行切换。
2.监听屏幕在连接和断开时的通知,若有新屏幕加入则切换,若外部屏幕都移除则恢复到默认显示。
http://apple.co/1jauUnu 苹果公司提供的外部屏幕使用示列
在一个屏幕上显示却在另一个屏幕上控制,感觉挺笨拙但实际也不错。尤其对于播放、暂停、恢复这种标准操作。
其他硬件:
当应用进入后台时,应释放对这些硬件的锁定:
蓝牙、相机、扬声器(除非是音乐类)、麦克风
别太死板。
电池电量与代码感知
可以通过UIDevice实例获取batteryLevel 和 batteryState(充电状态)
使用电量级别和充电状态进行条件处理,参数是电量百分比
-(BOOL)shouldProceedWithMinLevel:(NSUInteger)minLevel {
UIDevice *device = [UIDevice currentDevice];
device.batteryMonitoringEnabled = YES;
UIDeviceBatteryState state = device.batteryState; if(state == UIDeviceBatteryStateCharging ||
state == UIDeviceBatteryStateFull) { //充电或电池已充满
return YES; }
NSUInteger batteryLevel = (NSUInteger) (device.batteryLevel * 100); //获取当前电量,范围是0.00~1.00
if(batteryLevel >= minLevel) {
return YES; }
return NO; }
类似的,还可以获取到CPU使用情况
应用对 CPU 的使用率
-(float)appCPUUsage { kern_return_t kr;
task_info_data_t info;
mach_msg_type_number_t infoCount = TASK_INFO_MAX;
kr = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)info, &infoCount);
if (kr != KERN_SUCCESS) { return -1;
}
thread_array_t thread_list; mach_msg_type_number_t thread_count; thread_info_data_t thinfo; mach_msg_type_number_t thread_info_count; thread_basic_info_t basic_info_th;
kr = task_threads(mach_task_self(), &thread_list, &thread_count); if (kr != KERN_SUCCESS) {
}
return -1; float tot_cpu = 0;
}
int j;
for (j = 0; j < thread_count; j++) {
thread_info_count = THREAD_INFO_MAX;
kr = thread_info(thread_list[j], THREAD_BASIC_INFO,
(thread_info_t)thinfo, &thread_info_count); if (kr != KERN_SUCCESS) {
return -1;
basic_info_th = (thread_basic_info_t)thinfo;
if (!(basic_info_th->flags & TH_FLAGS_IDLE)) { tot_cpu += basic_info_th->cpu_usage /
(float)TH_USAGE_SCALE * 100.0;
} }
vm_deallocate(mach_task_self(), (vm_offset_t)thread_list, thread_count * sizeof(thread_t));
return tot_cpu;
}
分析电量使用
使用专业点的设备, Monsoon Solutions 的电源监控器
使用方法:
(1) 拆开 iOS 设备的外壳,找到电池后面的电源针脚。 (2) 连接电源监控器的设备针脚。
(3) 运行应用。
(4) 测量电量消耗。
最佳实践
1.尽可能晚的使用硬件,并在任务完成时立即结束使用。
2.进项密集型任务前,检查电池电量和充电状态
3.电量低时,提示用户是否确定要执行任务,并在用户同意后再执行。
4.可以提供让用户定义电量的阈值,以便某些操作前(如执行密集型操作)提示用户
小结
低电量也是挺重要的,毕竟用户不是每次都携带移动电源的。
任务复杂性不能降低(处理图片或画图等),不妨提供对电池电量保持敏感的方案并在适当的时机提示用户。