iOS-Performance Tips

在研发过程中,处于性能考虑,需要考虑耗电、内存、网络和存储等因素。

Reduce Your App’s Power Consumption

iOS的电量管理系统,通过关闭没被使用的硬件来节省电量,在开发时,可以通过考虑下面罗列的硬件使用来省电。

  • CPU
  • Wi-Fi, Bluetooth, baseband(EDGE,3G) radios(基带无线电)
  • Core Location framework
  • Accelerometers
  • Disk

优化的目标应该是最高效率地在最短时间内完成需要的任务,你可以通过优化算法来达到目标,但是即使最优的算法,仍然会对设备的电量造成影响,因此,需要遵循以下的准则:

  • 避免使用轮询。轮询会阻止CPU进入休眠,用NSRunLoopNSTimer替代;
  • 不要修改UIApplicationidleTimerDisabled属性为NO,这一计时器会在一段时间没有任何活动时,让设备自动熄灭屏幕。如果App在熄屏后,会出现副作用,最好是通过代码去排除副作用,而不是通过改变这一属性;
  • 尽可能将计算集中在一起,避免多次唤醒CPU;
  • 避免过于频繁地访问硬盘,如果需要保存状态信息,在状态发生变化时,再保存;
  • 只在需要的时候绘制到屏幕,绘制是一个非常昂贵的操作,不要依赖硬件来控制帧率,帧率只保持在需要的水平;
  • 如果通过UIAccelerometer来获取加速事件,在不需要的时候,设置事件的分发为NO,其他事件同理。

对于网络,越多数据通过网络传输,则越多的电量将被用于维持无线电。事实上,网络是一个电量非常敏感,并且你可以控制的操作,因此,需要遵循以下的准则:

  • 只在必要时,连接远程服务器,并且不要轮询;
  • 当需要连接网络时,尽可能只传输最小数量的数据,使用轻量级数据协议,并且不要包含一些额外的内容;
  • 集中传输数据,会比通过长时间分包传输更省电,系统会在检测到没有活动时,将Wi-Fi以及无线电关闭,尽量使用NSURLSession来创建上传和下载任务,系统会自动对其进行管理;
  • 尽可能使用Wi-Fi,比蜂窝网络更省电;
  • 如果使用Core Location framework来获取定位,要设置好距离和精度,以保证更新不要太频繁,并且在不需要时,要立即关闭定位更新。

Use Memory Efficiently

Apps应该尽可能减小占用的内存,当内存过少时,可能会导致系统在处理请求时出现异常。

Observe Low-Memory Warnings

当接受到系统的低内存警告呢,需要立即响应。低内存警告是一个释放不需要对象的引用的时机,如果App没有做出处理,将非常有可能被系统直接终止。系统通过以下API释放低内存警告:

1
2
3
4
applicationDidReceiveMemoryWarning:(AppDelegate)
didReceiveMemoryWarning(UIViewController)
UIApplicationDidReceiveMemoryWarningNotification notification
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE(唯一一个指标可以查看内存压力的严重性)

监测内存的情况的代码如下:

1
2
3
4
5
6
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_MEMORYPRESSURE, 0, DISPATCH_MEMORYPRESSURE_NORMAL|DISPATCH_MEMORYPRESSURE_WARN|DISPATCH_MEMORYPRESSURE_CRITICAL, dispatch_get_main_queue());
dispatch_source_set_event_handler(source, ^{
dispatch_source_memorypressure_flags_t pressureLevel = dispatch_source_get_data(source);
NSLog(@"%lu", pressureLevel);
});
dispatch_resume(source);

在接收到低内存警告时,应该立即释放不必要的内存,清空缓存,以及释放图片。如果需要保存,写入到硬盘。

Reduce Your App’s Memory Footprint

下面是一些可以降低内存占用的技巧:

  • 排除内存泄漏:内存是非常敏感的资源,对于内存泄漏,应该立即排除;
  • 保持资源文件尽可能小:对于PNG图片,用pngcrush工具进行压缩,对于plist文件,可以通过NSPropertyListSerialization将其写成二进制文件;通过区分设备来加载不同分辨率的图片,避免加载大的资源文件,一定要加载的时候,可以只加载文件使用到的那一部分,使用mmapmunmap函数;
  • 使用CoreData或者Sqlite来处理大数据集:CoreData或者Sqlite都对大数据集进行更有效地处理,而不需要一次性加载进内存;
  • 懒加载:在需要时,再加载资源;
  • 避免无限的问题集:无限的问题集可能需要非常大的内存来支持计算,当内存不足时,app可能将无法完成计算。

Tune Your Networking Code

iOS的网络栈包含了一些通过无线电硬件通信的接口,主要的程序接口是CFNetworkCFNetwork是基于BSD Sockets(伯克利套接字)以及Core Foundation中的一些数据类型,来与网络进行交互的库。你也可以选择使用Core Foundation中的NSStream和更底层的BSD Sockets

Wi-Fi和蜂窝无线电在没有活动时,将会被休眠,但是基于无线电本身的性质,进入休眠需要几秒钟。如果App每几秒钟传输一点数据,无线电可能一直处于工作状态,并持续耗电,即使他们可能并没有真正的工作。因此,一次性集中地传输数据会更省电。

当通过网络通信时,包可能随时会丢失。因此,当编写程序时,需要确保其对于异常进行处理。最好是能对网络状态的变化进行监听,并进行相应的处理,但是这些处理不一定都会被正确的调用。例如,在网络丢失时,Bonjour服务的网络回调不一定会被调用。Bonjour服务会在接收到一个服务断开的通知时,立刻调用回调,但是网络服务可能会在丢失时,没有发出通知。这种情况可能是,当设备提供的网络服务意外地断开连接或者通知丢失。

当App使用Wi-Fi时,应该在App的Info.plist中加入UIRequiresPersistentWiFi字段。该字段会通知系统,在检测到任何活跃的Wi-Fi热点时,显示网络选择的弹窗,同时,系统也不会在App运行期间,关闭Wi-Fi硬件。iOS系统会启动一个计时器,如果30分钟完全没有运行中的App使用网络,Wi-Fi硬件将会被关闭以省电,而UIRequiresPersistentWiFi字段会让iOS系统关闭该计时器,当App退出或者被挂起时,才会重新启动该计时器。注意,在锁屏状态下,UIRequiresPersistentWiFi字段不起作用。

如果App在飞行模式下启动,系统在满足以下条件时,会弹出一个提示弹窗:

  • App的Info.plist中包含UIRequiresPersistentWiFi字段,且值为true;
  • App在飞行模式下启动;
  • 设备的Wi-Fi开关没有在飞行模式下被手动打开。

下面是一些更有效使用网络的技巧:

  • 对于自定义的协议,尽可能地更精简地定义好数据格式(例如直接使用二进制);
  • 避免使用聊天协议;
  • 尽可能集中地传输数据,不要分开传输。

Improve Your File Management

对于硬盘写入,最小化需要写入的数量。文件操作是比较慢,并且涉及到写入闪存,闪存寿命是有限的。下面是一些最小化文件操作的技巧:

  • 只写入文件变化的部分,不要对整个文件进行重写,只为了修改一小部分;
  • 在定义文件格式的时候,把会经常变化的部分集中在一起,从而最小化每次需要写入硬盘的块数量;
  • 如果数据包含随机访问的结构化内容,将其存储在Core Data或者SQLite中,特别是需要操作的数据的大小超过MB的。

如果在App退出时,这时就需要把需要存储的信息都写入到硬盘中,以保证下次启动的用户体验。

Make App Backups More Efficient

在用户通过iCloud或者iTunes进行数据备份时,设备上的文件将会被传输到用户的计算机或者iCloud账户中,文件保存在App沙盒中的路径,决定了该文件是否会被备份。如果App创建了多个非常大的文件,并且将其放置在会被备份的路径下,将导致备份的时候,非常慢。因此,在编写文件管理的代码时,需要注意这一点。

对于文件的备份,App并不需要进行额外的备份或者储存操作。iCloud和iTunes会在必要的时候,进行备份,除了以下的目录:

1
2
3
<Application_Home>/AppName.app
<Application_Data>/Library/Caches
<Application_Data>/tmp

注意:iTunes会备份app文件,但是并不是每次同步的时候,都会备份,只有在设备上购买了App,然后下一次连接上电脑的iTunes时,才会进行备份,除非App文件本身发生了变化,例如更新了。

当用户更新一个App时,iTunes会在一个新的App目录下安装更新,然后将旧目录下的用户数据文件移到新的目录下,然后删除旧版本文件。文件在以下目录下会在更新过程中会被保留下来:

1
2
<Application_Data>/Documents
<Application_Data>/Library

注意,尽管其他的目录下的文件也可能会被移动过去,但是在更新过程中无法得到确保。

下面是数据文件存储的一些技巧:

  • 用户敏感数据应该保存在<Application_Data>/Documents目录下,用户敏感数据指的是一些无法被App重建的数据,例如用户的文件,以及用户生成的内容;
  • 对于App可以重复下载的文件,存放路径取决于系统版本,iOS 5.1及以上,应该存放在<Application_Data>/Library/Application Support目录下,并且对该NSURL对象添加NSURLIsExcludedFromBackupKey属性,使用setResourceValue:forKey:error:方法,确保文件不会被备份,如果有非常多的该类型文件,也可以自定义一个目录存放,并设置其NSURLIsExcludedFromBackupKey属性;iOS 5.0及以下,应该存放在<Application_Data>/Library/Caches目录下,文件将不会被备份;
  • 缓存数据应该存放在<Application_Data>/Library/Caches目录下,例如数据库缓存文件,下载的内容文件(杂志,新闻以及地图数据等)。缓存数据可能被删除,App已经对这种情况有处理策略;
  • 临时数据应该存放在<Application_Data>/tmp目录下,临时数据如果不需要了,需要将其删除以释放硬盘空间。

Move Work off the Main Thread

App的主线程是用于处理触摸事件以及其他用户输入的,需要限制主线程中的工作类型,保证App可以响应用户的输入,不要在主线程中执行耗时或者无边界的任务,例如网络访问。将这些任务放到后台线程中,使用GCD或者NSOperation对象来管理。

将任务切换到后台线程,将会让主线程有更多的时间来持续处理用户的输入,这在App启动或者退出时,非常关键。如果主线程在启动时被挂起,系统可能会在完成启动前就直接终止App。如果主线程在退出时被挂起,系统也同样可能会直接终止App,不会给机会保存一些用户敏感的数据。