iOS性能优化方案
2023-09-20 » 自我成长量化,监控,治理,提升
启动优化
数据上报埋点节点
- 进程开始时间
- main函数开始
- didFinishLaunch
- firstViewDidAppear
优化变化:
优化前,postmain的时间,大概是 平均在1.5 premain 1.2s 优化后,postmain 平均 700ms 90分位 1.3s premain 700ms
问题:
1、大仓变成全组件化接入,动态库使用过多(动态库的加载 rebase、bind都很耗时) 2、启动任务杂乱添加,无规范,部分业务初始化耗时大 3、load、静态函数的乱用,pageIn的耗时累计
premain前的优化操作:
1、动态库转静态库 减少400ms左右 1.2 -> 800 (剩余的动态库:FB的一堆库,twitter的库、libpag、下载库、zego库、音视频的一些库) 2、+load、静态优化,无用代码资源清理,减少70ms左右 (扫Mach-o的__DATA __objc_selrefs和__TEXT __objc_methname) 3、后续尝试二进制重排和 page in重命名
postmain部分的优化操作:
1、搭建自注册启动机制,按时机节点注册启动项(startPushManager、loginSucss回调处理) 2、能滞后的SDK初始化都滞后(特别是部分依赖配置项接口的启动逻辑),能减少的IO操作、UI操作都减少(userdefault、plist读取、db、IO,读取大张启动图改成小切图读取等)
首屏业务展示部分的优化操作:
1、闪屏和rootVC的同时加载、首页资源下载后置,减少无效页面的初始化 2、首页缓存单独管理,启动时异步线程优先读取,减少缺省情况 3、启动速度快于数据读取速度,减少首次启动接口诉求,优先展示核心数据
工具、防止劣化:
1、hook obj_sendMsg 获取系统测试阶段的打点统计所有方法耗时,输出火焰图数据,脚本读懂火焰图,自动完成版本变化对比 2、流水线监听无用资源、代码的自动扫描, +load方法的统计(静态代码扫描) 3、增加准入限制
- 新增动态库
- 新增 +load 和静态初始化
- 新增启动任务 Code Review
本地测试工具:
- Static Initializer:分析 C++ 静态初始化
- App Launch:Xcode 11 后新出的模板,可以认为是 Time Profiler + System Trace
低端机启动、长尾数据的处理
- 精简化启动任务、首屏UI
- 对低内存部分机器的 比如_Text 读取解密耗时的处理
- 主题功能的后置,主动改被动(减少初次展示内容)
- push等openURL方式进场的启动逻辑(后置一些非必要场景的创建,比如我就是来歌房空降的,首页的内容就无需太早构建)
流畅度优化
什么是卡顿
1、VSync 信号间隔固定为 16.67ms,物理驱动,不会改变(非高刷屏幕)
2、CADisplayLink 是由 VSync 信号驱动 source1信号 然后驱动 runloop执行的。没有卡顿的情况下,VSync 信号和 RunLoop 的唤醒 & CADisplayLink 回调的触发严格一一对应
3、RunLoop 卡顿,无法处理 Source 1 信号,CADisplayLink 回调被延迟到卡顿结束时,所以不是每次CADisplayLink回调都是16.67ms
一般的FPS的统计规则
1秒内的CADisplayLink回调次数
不足: 平均后的FPS有时反馈不了真实的卡顿,比如我10S的测算区间,前9S满帧,最后一秒掉得多,平均出来的FPS一点都不卡 (比如 1S回来54帧,其中 52次是16.67ms,1次是 16.67 * 3 一次是 16.67 * 5)
诉求: 希望能看到严重掉帧的比例更方便评估卡顿
卡顿率得分计算规则(基于CADisplayLink回调间隔时间处理)
- 卡顿率 = 卡顿帧次数 / 总帧数 (不同掉帧程度可以区分统计)
- 特卡率 掉5帧以上的次数 / 总帧数
- 流畅率 不掉帧的次数 / 总帧数
VSync 可变情况
iPhone高刷屏幕:低刷数据场景会自动降低VSync信号频率,高刷数据场景会拉高VSync信号频率,CADisplayLink 默认不设置会跟随VSync的回调
无高刷诉求不兼容的处理方式
- preferredFramesPerSecond 设置固定60即可,即使屏幕高刷,我们统计到的一帧还是小于16.67ms 的 不存在掉帧
- 本身业务也不存在高刷素材,所以,并无业务影响
优化
1、耗时原因分析
- 过量UI的构建、复杂UI列表,比如富文本列表、Feed播放卡片列表(包含多种类型cell)动态高度计算等
- 过量离屏渲染诉求的列表元素,比如圆角卡片、带阴影卡片、透明图层
- 歌词组件刷新列表耗时,积少成多的UI调用(UI过度渲染)cell中view过多
- 其他主线程事务卡顿影响,比如图片的编解码
2、线上统计
- 微信Matrix卡顿监控抽样上报
- bugly FPS统计,加权算卡顿率
3、线下工具
- 火焰图 + hook objc_msgsend(加规则,脚本自动扫火焰图,推算一些高耗时、高重复UI问题)
- instrument
4、优化的方式
- 预处理
- 比如高度预计算,不使用动态高度布局
- 图片预处理,比如动态图片先首帧返回,滑动期间减少解码下载操作,大图下采样处理等
- 减少离屏渲染
- 比如圆角处理方式换成贝塞尔曲线,减少透明图层混合场景(比如蒙层、渐变等)
- 减少图层
- 异步绘制的方式绘制到一个图层,Texture等方案(在IM场景、评论列表等场景)
- 分级处理
- 低端机去除如头像框动图、阴影底等
- 其他方式
- 利用RunLoop空闲时间执行预缓存任务
5、防止劣化
- hook objc_msgsend 输出火焰图
- 添加规则,让代码读懂火焰图
- 调用了耗时接口
- 大量调用同时发生
- 自动UI测试,自动生成报告
包大小优化
优化变化
原包大小 ipa 130M ,优化后64M
优化操作
1、armv7架构移除 30M
2、无效图片清理、无用代码清理、未使用的多语言资源
fengniao + 流水线提示 + 手动二次检查后删除
3、图片都用XCAsset管理、大图转webp,非必须资源后置下载,比如歌房礼物特效、特殊玩法的舞台特效
4、图片无损压缩
5、转静态库
6、本地hippy兜底包处理
后续尝试:
1、isInitialized 的结果会保存到元类的 class_rw_t 结构体的 flags 信息里
2、__TEXT 段迁移 使用 __TEXT 段迁移技术,在链接阶段使用 -rename_section 选项将 __TEXT,__text 迁移到 __BD_TEXT,__text,减少苹果对可执行文件的加密范围,提升可执行文件的压缩效率,从而减少 Download Size。
防止劣化
- 流水线扫描防止劣化,扫描每个子库的大小变更 并通知提示 10KB
内存管理优化
内存水位指标
50分位: 270M 90分位: 700M 99分位: 1.5G
OOM率
主要内存问题
1、图片缓存 VM imageIO
UIImage imageName 的方式获取图片释放不及时 (系统本身有bitmap数据的缓存机制,即使UIImageView置空了,也不会马上销毁) imageWithContentsOfFile 方式不会有缓存,但是有频繁IO、解码操作
2、内存泄漏
- block、音视频流处理开的buffer释放不及时、大量动画内存占用
3、大图渲染,一下子加载进内存
大图渲染: 苹果的CATiledLayer去加载。原理是分片渲染,滑动时通过指定目标位置,通过映射原图指定位置的部分图片数据解码渲染。这里不再累述,有兴趣的小伙伴可以自行了解下官方API。
https://wetest.qq.com/labs/367
- 处理方式
1、网图用SDWebImage处理,本地图片大而用的少的图用 imageWithContentsOfFile
2、设定内存监控,进出房间或定时触发内存监控,按内存水位判断,给系统发内存告警通知
dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationDidReceiveMemoryWarningNotification object:[UIApplication sharedApplication]]; });
3、图片渲染
- 超大图参考SDWebImageView的处理,可以在自动释放池内 切片后小片读入然后绘制到小比例图层上,循环处理,避免内存暴涨
- 图片读入用ImageIO 更省内存
- 图片缩放本质就是多个像素点 变一个像素点(下采样、上采样)
- UIGraphicsBeginImageContext (相比imageNamed 只消耗需要展示的视图大小的内存,而不是图片本身占用的内存)
- wwdc18,苹果基于iOS10,建议使用UIGraphicsImageRenderer
内存检测指标 FOOM
内存快照方案
内存快照方案:收到内存水位达到上限7成通知,单周期最多生成一次 https://cloud.tencent.com/developer/news/716328
- 获取最新的所有被注册到 runtime 中的对象 objc_copyClassList
- malloc_get_all_zones 获取所有已分配内存 遍历每个zone中管理的内存节点 获取 libmalloc 管理的存活的所有内存节点的指针和大小 分析引用关系
- 获取到的内存节点,逐个对象信息获取、分析
- 写入本地文件,下次启动后上报
采集过程 CFRunLoopObserverCreateWithHandler 监听 kCFRunLoopBeforeWaiting 在空闲时间处理 1、挂起所有非采集线程。 2、获取所有的内存节点,内存对象引用关系以及相应的辅助信息。 3、写入文件。 4、恢复线程状态。
线程挂起
kern_return_t kreturn;
const task_t thisTask = mach_task_self();
const thread_t thisThread = [self currentThreadPort];
// 获取当前task_self的所有thread
if ((kreturn = task_threads(thisTask, suspendedThreadsP, suspendedThreadsNumP)) != KERN_SUCCESS) {
return;
}
// 遍历所有 thread
for (mach_msg_type_number_t i = 0; i < *suspendedThreadsNumP; i++) {
thread_t thread = (*suspendedThreadsP)[i];
if (thread == thisThread) {
// 如果是当前thread
continue;
}
if ((kreturn = thread_suspend(thread)) != KERN_SUCCESS) {
}
}
线程恢复
kern_return_t kreturn;
const task_t thisTask = mach_task_self();
const thread_t thisThread = [self currentThreadPort];
if (threads == NULL || threadsNum == 0) {
return;
}
for (mach_msg_type_number_t i = 0; i < threadsNum; i++) {
thread_t thread = threads[i];
if (thread == thisThread) {
// 如果是当前thread
continue;
}
if ((kreturn = thread_resume(thread)) != KERN_SUCCESS) {
}
}
for (mach_msg_type_number_t i = 0; i < threadsNum; i++) {
mach_port_deallocate(thisTask, threads[i]);
}
vm_deallocate(thisTask, (vm_address_t)threads, sizeof(thread_t) * threadsNum);
/// 获取当前线程
+ (thread_t)currentThreadPort {
return pthread_mach_thread_np(pthread_self());
}
8P App 占用 1G 内存时,采集用时 1.5-2 秒,采集时额外内存消耗 10-20MB,生成的文件 zip 后大小在 5-20MB。
- 可配置频控,单个用户每天多少次,每次间隔时间
- uid + 设备id投放
2、VC泄漏 MLeaksFinder NSSet存放 objectPtrs,在dealloc后10秒内还没销毁的,就算泄露
3、大内存分配
1、通过hook malloc_logger函数来分析内存分配情况 // malloc_logger本身是个函数指针,只需要指向自己的malloc_logger实现 就能实现hook,注意要实现原有默认实现,避免覆盖 malloc_logger = (malloc_logger_t *)qmapm_stack_logger;
2、检查分配
- 内存分配按阈值检查,超出阈值尝试记录,默认为20M
- 记录频率限制,10s内不重复记录
https://juejin.cn/post/6844904056863850504
4、内存触顶
大于90% 可用内存上限的时候算内存触顶,触顶率 = 触顶次数 / 设备数
已用内存、可用内存上限
iOS 13之前
- memoryUsed 用 resident_size
- 上限采用预估值:设备物理内存上限的55%
iOS 13之后
- memoryUsed: phys_footprint memoryCanBeUse: os_proc_available_memory
- 上限采用 memoryUsed + memoryCanBeUse
1、bugly监控的原理
2、FBAllocationTracker 原理
WSMemoryDetector 拆解
MLeaksFinder 原理
1、本质是导航栈后退或者VC dismiss的时候,等两秒再检查退掉的VC和对应VC内部的VC、subviews是否释放 2、判断释放的标准就是给他发消息,看是否能接受,还接受的就是未释放内存对象 3、如何遍历所有内容,其实是在各类UI分类里面主动向下调用的,
内存泄漏检测 OOMDetector https://mp.weixin.qq.com/s/r0Q7um7P1p2gIb0aHldyNw
内存泄漏 https://www.cnblogs.com/kenshincui/p/13153681.html
大内存分配检测 bugly 分配场景、分配堆栈,内存大小 取的是vmInfo的phys_footprint,理论上说是准确的。
- WebPAllocateDecBuffer
- KSKitAudioPCMDecoder readFile:ioData:error
- PNGReadPlugin / ImageIO啊
//内存原理思考
Instruments Allocation
MetricKit
组件化
IO 文件管理
1、歌房动画资源的播放,选mp4的原因,是否有明显IO消耗
crash防护
-
数组、字典等多类族的越界问题,hook原调用方法,增加安全判断规则(分类中hook,注意用MRC 然后自动释放池包裹一下,构建的局部变量快速释放)
-
KVO 重复释放监听等,手动管理添加的监听,防止重复释放或者未释放
- 野指针,僵尸对象防护 hook C的 free函数,在safe_free方法中对已经释放变量的内存,填充0x55 (xcode僵尸对象填充的是这个),避免野指针
- 需要注意僵尸对象释放问题,有内存水位告警释放和定时释放策略
- 方法找不到,forwardingTargetForSelector 返回一个我们封装好的统一对象接收消息,resolveInstanceMethod 中转发到自定义IMP
3、上报
4、安全模式
IO优化
图片处理和性能优化
http://labmain.com/2019/12/18/iOS图片内存管理和性能优化/