前言
在项目中,直接面向用户的客户端往往是一个项目的门面。因此,在项目开发建设的过程中,为了交付用户体验较佳的客户端App,保障产品交付质量。往往需要我们开发者关注客户端软件的性能指标问题。因此,我们要对应用的性能优化专题有所研究!!
我们通常关注的性能指标有:
页面卡顿耗电、发热网络优化应用启动安装包瘦身等
我们在开发建设项目过程中,可以粗略划分为几个阶段:开发阶段、测试阶段、维护阶段:
- 在
开发阶段,我们要掌握性能调试、性能监测的手段,从而保障,在当前稳定版本的客户端软件,有一个比较合理的性能保障; - 在
测试阶段,测试团队等若干同事往往会给我们提出一些用户体验上的反馈和建议,因此,我们需要掌握性能调试的手段,从而改造出比较符合团队要求的产品; - 在上线
维护阶段,针对已经上线的应用,我们的开发团队要有线上性能监控的能力,从而及时收集不满足性能指标要求的业务交互场景和步骤,捕获具体问题进行分析,从而以此为依据作为有效迭代优化我们客户端的有力助力。
为此,我们本次将会用几篇文章,围绕一些常见的性能指标,去关注 如何调试、如何监测、如何改进处理问题:
- Instruments
- 其它性能指标的关注
一、概述
本文主要是针对 维护阶段 这两个线上场景,围绕常见的几个性能指标要点:页面卡顿、离屏渲染、耗电优化、内存泄露、App启动优化,展开来陈述如何利用代码插件、平台工具进行性能指标监测的。关于相关的同一主题的其它要点,待来日按需逐个补充完善。
二、卡顿的检测
平时所说的“卡顿”主要是因为在主线程执行了比较耗时的操作
1.FPS监控
FPS的监控,参照YYKit中的YYFPSLabel,主要是通过CADisplayLink实现。借助link的时间差,来计算一次刷新刷新所需的时间,然后通过刷新次数 / 时间差得到刷新频次,并判断是否其范围,通过显示不同的文字颜色来表示卡顿严重程度。代码实现如下
class HPFPSLabel: UILabel {
fileprivate var link: CADisplayLink = {
let link = CADisplayLink.init()
return link
}()
fileprivate var count: Int = 0
fileprivate var lastTime: TimeInterval = 0.0
fileprivate var fpsColor: UIColor = {
return UIColor.green
}()
fileprivate var fps: Double = 0.0
override init(frame: CGRect) {
var f = frame
if f.size == CGSize.zero {
f.size = CGSize(width: 80.0, height: 22.0)
}
super.init(frame: f)
self.textColor = UIColor.white
self.textAlignment = .center
self.font = UIFont.init(name: "Menlo", size: 12)
self.backgroundColor = UIColor.lightGray
//通过虚拟类
link = CADisplayLink.init(target: CJLWeakProxy(target:self), selector: #selector(tick(_:)))
link.add(to: RunLoop.current, forMode: RunLoop.Mode.common)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
link.invalidate()
}
@objc func tick(_ link: CADisplayLink){
guard lastTime != 0 else {
lastTime = link.timestamp
return
}
count += 1
//时间差
let detla = link.timestamp - lastTime
guard detla >= 1.0 else {
return
}
lastTime = link.timestamp
//刷新次数 / 时间差 = 刷新频次
fps = Double(count) / detla
let fpsText = "(String.init(format: "%.2f", fps)) FPS"
count = 0
let attrMStr = NSMutableAttributedString(attributedString: NSAttributedString(string: fpsText))
if fps > 55.0 {
//流畅
fpsColor = UIColor.green
}else if (fps >= 50.0 && fps <= 55.0){
//一般
fpsColor = UIColor.yellow
}else{
//卡顿
fpsColor = UIColor.red
}
attrMStr.setAttributes([NSAttributedString.Key.foregroundColor: fpsColor], range: NSMakeRange(0, attrMStr.length - 3))
attrMStr.setAttributes([NSAttributedString.Key.foregroundColor: UIColor.white], range: NSMakeRange(attrMStr.length - 3, 3))
DispatchQueue.main.async {
self.attributedText = attrMStr
}
}
}
2.主线程卡顿监控
我们可以添加Observer到主线程RunLoop中,通过监听RunLoop状态切换的耗时,以达到监控卡顿的目的
实现思路:
在整个运行循环中,需要监听的主要就是RunLoop结束休眠到处理Source0的这段时间,如果时间过长,就证明有耗时操作
检测主线程每次执行消息循环的时间,当这个时间大于规定的阈值时,就记为发生了一次卡顿。这个也是微信卡顿三方matrix的原理
以下是一个简易版RunLoop监控的实现
class HPBlockMonitor: NSObject {
static let share = LLBlockMonitor.init()
fileprivate var semaphore: DispatchSemaphore!
fileprivate var timeoutCount: Int!
fileprivate var activity: CFRunLoopActivity!
private override init() {
super.init()
}
public func start(){
//监控两个状态
registerObserver()
//启动监控
startMonitor()
}
}
fileprivate extension LLBlockMonitor{
func registerObserver(){
let controllerPointer = Unmanaged<LLBlockMonitor>.passUnretained(self).toOpaque()
var context: CFRunLoopObserverContext = CFRunLoopObserverContext(version: 0, info: controllerPointer, retain: nil, release: nil, copyDescription: nil)
let observer: CFRunLoopObserver = CFRunLoopObserverCreate(nil, CFRunLoopActivity.allActivities.rawValue, true, 0, { (observer, activity, info) in
guard info != nil else{
return
}
let monitor: LLBlockMonitor = Unmanaged<LLBlockMonitor>.fromOpaque(info!).takeUnretainedValue()
monitor.activity = activity
let sem: DispatchSemaphore = monitor.semaphore
sem.signal()
}, &context)
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, CFRunLoopMode.commonModes)
}
func startMonitor(){
//创建信号
semaphore = DispatchSemaphore(value: 0)
//在子线程监控时长
DispatchQueue.global().async {
while(true){
// 超时时间是 1 秒,没有等到信号量,st 就不等于 0, RunLoop 所有的任务
let st = self.semaphore.wait(timeout: DispatchTime.now()+1.0)
if st != DispatchTimeoutResult.success {
//监听两种状态kCFRunLoopBeforeSources 、kCFRunLoopAfterWaiting,
if self.activity == CFRunLoopActivity.beforeSources || self.activity == CFRunLoopActivity.afterWaiting {
self.timeoutCount += 1
if self.timeoutCount < 2 {
print("timeOutCount = (self.timeoutCount)")
continue
}
// 一秒左右的衡量尺度 很大可能性连续来 避免大规模打印!
print("检测到超过两次连续卡顿")
}
}
self.timeoutCount = 0
}
}
}
}
使用时,直接调用即可
HPBlockMonitor.share.start()
三、使用第三方库
我们开发过程中,也可以使用一些不错的第三方库来做性能指标的监测。比如:腾讯的matrix、滴滴DoraemonKit。都是很不错的框架
1.DoraaemonKit简单介绍
DoraaemonKit支持以下多种性能指标的监测:
【帧率】App 帧率信息提供波形图查看功能,让帧率监控的趋势更加明显;【CPU】App CPU 使用率信息提供波形图查看功能,让 CPU 监控的趋势更加形象;【内存】App 内存使用量信息提供波形图查看功能,让内存监控的趋势更加鲜明;【流量监控】拦截 App 内部流量信息,提供波形图展示、流量概要展示、流量列表展示、流量筛选、流量详情,对流量信息统一拦截,成为我们 App 中自带的 "Charles";【卡顿】锁定 App 出现卡顿的时刻,打印出对应的代码调用堆栈;【大图检测】通过流量监测,找出所有的大小超标的图片,避免下载大图造成的流量浪费和渲染大图带来的CPU消耗。【启动耗时】无侵入的统计出App启动过程的总共耗时;【UI层级检查】检查出每一个页面中层级最深的元素;【函数耗时】从函数级别分析app性能瓶颈;【Load】找出所有的Load方法,并给出耗时分析;(iOS独有)【内存泄漏】找出App中所有的内存泄漏的问题。
2.DoraaemonKit支持多个平台
一套框架能够支持一个项目组的多个平台,这是一件非常合乎团队管理者心意的事情。因为这保障了整个团队输出的质量!!!
3.DoraaemonKit底层实现
DoraaemonKit的作者真的很有心,还和广大开发者们分享了其底层实现的代码
总结
本文 简单介绍了 项目 性能调试工具 Instruments 的基本使用,目前只对Allocations调试这一块 做了简单介绍 。
接下来会用几篇文章,围绕几个常见的性能问题,展开对 性能调试工具 Instrument 的 其它模块的使用介绍:
- iOS 研发助手 DoKit 技术实现(一)
- iOS 研发助手 DoKit 技术实现(二)
- DoKit支持iOS本地crash查看功能
- 开源组件 DoKit 之 Android 版本技术实现(一)
- 开源组件 DoKit 之 Android 版本技术实现(二)
- DoKit支持Activity启动耗时统计方案
- DoKit 微信小程序SDK对外发布
- 滴滴DoKit2.0 - 泛前端开发者的百宝箱
- 滴滴正式发布开源客户端研发助手 DoKit 3.0,新特性解读
- 滴滴DoKit Android核心原理揭秘之函数耗时
- 滴滴DoKit Android核心原理揭秘之AOP字节码实现
相关系列文章
Instruments