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图片内存管理和性能优化/