界面优化
1.异步加载数据和图片
- 使用GCD 或者 NSOperation异步加载一些耗时的数据和图片,处理好后,再切换回主线程刷新UI
- 使用缓存(eg:NSCache)已经加载的图片,减少重复加载
2.减少离屏渲染
- 尽量减少不必要的UI属性(圆角、阴影、遮罩),这些会导致离屏渲染,可以使用图片代替这些复杂的图形效果
- 使用光栅化(
shouldRasterize)来缓存复杂的图层内容,但要注意内存消耗。
layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale
3.减少视图创建的开销
- 在列表视图中,合理使用UITableViewCell/UICollectionViewCell的重用机制,减少视图创建的开销
- 仅加载和渲染可见的部分的数据,减少视图创建的开销
- 合理使用lazyload(一般是View)
- 减少图层数量
- 对于复杂的页面,尽量不要用xib画UI
4.tableview的优化
-
- 合理重用cell,避免重复创建和销毁
-
rowHeightortableView(_:heightForRowAt:)避免动态计算单元格高度,尽量使用静态单元格高度,如果非要动态的,尽量缓存单元格高度
-
- 预估高度,使用
tableView.estimatedRowHeight = 44.0来减少计算开销
- 预估高度,使用
-
- 异步加载数据和图片
-
- 避免复杂视图层次结构,如果非要实现,使用
opaque = true跳过混合计算(blending calculations),直接渲染视图,减少绘制开销 和rasterization = true光栅化技术缓存复杂视图的内容
- 避免复杂视图层次结构,如果非要实现,使用
-
- 使用
willDisplay和didEndDisplaying优化单元格 在tableView(_:willDisplay:forRowAt:)和tableView(_:didEndDisplaying:forRowAt:)方法中进行资源管理,如启动和停止动画、加载和卸载图片等。
- 使用
swift
复制代码
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// 启动动画或加载数据
}
override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// 停止动画或释放资源
}
- 7.数据量大的时候,使用分页加载 在大量数据时,采用分页加载(Pagination)策略,仅加载当前屏幕可见的数据,减少内存占用和加载时间。
swift
复制代码
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
if offsetY > contentHeight - scrollView.frame.height * 4 {
// 加载更多数据
}
}
内存优化
1.正确使用weakself,防止循环引用,避免内存泄露
2.使用工具检测内存泄露
- 如
Instruments的Leaks和Allocations分析内存泄漏和内存占用情况 - MLeaksFinder: 原理
- 1.MLeaksFinder会在特定的时机(如视图控制器被从视图层次结构中移除时)给对象添加一个“标记”。这个标记通常是通过关联对象(associated objects)来实现的。
- 2.检查标记,在对象的
dealloc方法中,MLeaksFinder会检查该对象是否带有“标记”。如果对象带有标记但没有被释放,MLeaksFinder会记录这个内存泄漏。
- FBRetainCycleDetector: 原理
- 1 对象图遍历:
FBRetainCycleDetector通过遍历对象的引用图,收集对象之间的引用关系。这通常是通过对象的属性、实例变量以及关联对象(associated objects)来实现的。 - 2 检查循环引用: 一旦对象图被构建,
FBRetainCycleDetector会检查对象图中的循环引用路径。如果检测到一个对象从自己出发,通过一系列引用最终又回到了自己,则认为存在循环引用
- 1 对象图遍历:
FBRetainCycleDetector 的使用存在两个问题:
-
- 需要找到候选的检测对象
-
- 检测循环引用比较耗时 所以一般先通过 MLeaksFinder 找到内存泄漏的对象,然后再过 FBRetainCycleDetector 检测该对象有没有循环引用即可。
包大小优化 今日头条iOS端安装包大小优化
资源优化
- 无用图片删除: 使用第三方MACAppLSUnusedResources
- 使用tint color精简单色图片
- 大图云端下载
代码优化
- 无用类删除
MachO文件中有
__DATA.__objc_classrefs和__DATA.__objc_selrefs段,分别近似于“被使用的类的集合”和“被使用的方法的集合”。通过取差集的方式可以筛选出未被使用的类和方法。
1. 使用`otool`命令可以拿到类在二进制文件中的位置地址
2. otool -o 二进制文件中的位置地址 转换成可读的类名
但是有些类是在第三方的库里的,删除不了
- 无用方法排查:这部分一般在工具类进行 所有已经被实现的方法可以通过linkmap来获取,对linkmap做grep操作即可获得结果:
grep 'xxxx'
而被使用的方法在上面__DATA.__objc_selrefs已经拿到,同样取差集即可
- 无用第三方库删除
- 编译优化:Link Time Optimization。直接在项目的build setting中设置,带来的效果是不到500KB
(1)将一些函数內联化
(2)去除了一些无用代码
(3)对程序有全局的优化作用
卡顿优化
-
FPS:YYFPSLabel,利用CADisplayLink(类似定时器和屏幕刷新频率相同),将其加入到runloop.commom中。
-
基于Runloop - Matrix微信自研卡顿分析工具 在上面的内容中我们说了UI操作的具体实现,其核心就是由Runloop去处理UI显示或者其他事务,那么我们可以这么认为从Runloop任务开始(beforeSource)--任务结束(beforewating)这段时间就是任务处理的时间t,如果这个t过长我们就可以认为产生了卡顿。Matrix就是利用了这个原理;
界面优化 上面有了,可以不看
- 预处理
- 预排版:异步子线程提前计算好布局,在切回主程序更新布局。eg:提前计算cell高度
- 预渲染:异步子线程中提前将cell某个button的圆角渲染好。
- 预解码:图片加载过程中,将解码耗时操作放在异步子线程中.eg:像YYImage,SDWebImage这些都是通过异步子线程画出位图(bit map),然后切到主线程赋值。整个原理和预渲染差不多
- 按需加载
- 只加载当前显示的cell+前后3个cell。减少cell计算处理数量。eg:微博加载首页,VVeboTableViewDemo
- Runloop分发:在滑动时,图片解码会占用大量的计算能力。使用runloop监听其将要休眠(kCFRunloopBeforeWaitng)的时候,我们再去做图片加载操作;
-
减少图层层级: layout阶段和commit阶段需要进行多次递归操作。如果图层非常多,递归操作计算就很多,造成卡顿
-
异步渲染:通过Core Graphics 合成一张位图bitMap 美团开源过一个框架Graver它通过异步绘制可以将多个图层合层一张位图
启动时间优化:二级制重排
二级制重排
- 启动概念 1.- 启动分为冷启动(app第一次启动)和热启动,而优化的一般是冷启动。
-
- 冷启动又分为main函数之前和main函数之后
-
main函数之后 1.使用BLStopwatch打点 2.二进制重排
-
虚拟内存和物理内存
- app进程在访问内存时并不直接访问,其拥有一份虚拟内存。
- 虚拟内存通过一种中间的页表(映射表)找到真正的物理内存。
- 虚拟内存地址是连续的,但是映射后真实的物理地址并不一定是连续的。
- 内存分页管理
- 每个进程都有若干张虚拟页表(映射表),在进程运行的时候,会去访问页表
- 但是当某块物理内存没有对应的页表的时候,系统会去创建新的一张页表。此时系统就会阻塞当前进程(这个过程我们称为缺页中断)
- 如果物理内存有空的内存区就在空的内存区创建,没有空的内存区,系统就会找一页去覆盖某个不活跃的页。
- 二进制重排原理
- 启动的时候,需要加载大量的代码,而这些代码的顺序是根据文件的顺序生成的。
- 这些代码对应方法可能分散在好几张页表中
- 二进制重排就是将启动时用到的方法放在前一两张页表中,并且按顺序排列
- 二进制重排操作
- 通过查看Linkmap(二进制文件的布局),拿到方法调用的顺序
- 通过**.order**文件指定符号(方法)的放到虚拟内存页表的前几页
Linkmap
Linkmap是iOS编译过程的中间产物,记录了二进制文件的布局,需要在Xcode的Build Settings里开启Write Link Map File - YES:
开启后,run一次demo,可以找到XXX.linkMap-normal-arm64.txt文件
product - show in finder - intermediates.noindex - XXX build - bebug - XXX.build - XXX.linkMap-normal-arm64.txt
下面是XXX.linkMap-normal-arm64.txt文件的大致内容
# Path: /Users/Library/Developer/Xcode/DerivedData/Demo-aomzumcaofovkzbrotetwdamhtrc/Build/Products/Debug-iphoneos/Demo.app/Demo
# Arch: arm64
# Object files:
[ 0] linker synthesized
[ 1] /Users/Library/Developer/Xcode/DerivedData/Demo-aomzumcaofovkzbrotetwdamhtrc/Build/Intermediates.noindex/Demo.build/Debug-iphoneos/Demo.build/Objects-normal/arm64/AppDelegate.o
[ 2] /Users/Library/Developer/Xcode/DerivedData/Demo-aomzumcaofovkzbrotetwdamhtrc/Build/Intermediates.noindex/Demo.build/Debug-iphoneos/Demo.build/Objects-normal/arm64/ViewController.o
[ 3] /Users/Library/Developer/Xcode/DerivedData/Demo-aomzumcaofovkzbrotetwdamhtrc/Build/Intermediates.noindex/Demo.build/Debug-iphoneos/Demo.build/Objects-normal/arm64/main.o
...
# Sections:
# Address Size Segment Section
0x100005EC0 0x00000664 __TEXT __text
0x100006524 0x00000090 __TEXT __stubs
0x1000065B4 0x000000A8 __TEXT __stub_helper
...
0x100008000 0x00000008 __DATA __got
0x100008008 0x00000060 __DATA __la_symbol_ptr
0x100008068 0x00000060 __DATA __cfstring
...
# Symbols:
# Address Size File Name
0x100005EC0 0x000000A8 [ 3] _main
0x100005F68 0x0000002C [ 1] +[AppDelegate load]
0x100005F94 0x0000002C [ 2] +[ViewController load]
0x100005FC0 0x00000064 [ 2] -[ViewController viewDidLoad]
0x100006024 0x0000006C [ 1] -[AppDelegate application:didFinishLaunchingWithOptions:]
0x100006090 0x00000114 [ 1] -[AppDelegate application:configurationForConnectingSceneSession:options:]
0x1000061A4 0x00000064 [ 1] -[AppDelegate application:didDiscardSceneSessions:]
0x100006208 0x00000014 [ 2] -[ViewController test2]
0x10000621C 0x00000014 [ 2] -[ViewController test1]
0x100006230 0x00000070 [ 2] -[ViewController viewDidAppear:]
0x1000062A0 0x00000088 [ 4] -[SceneDelegate scene:willConnectToSession:options:]
0x100006328 0x00000040 [ 4] -[SceneDelegate sceneDidDisconnect:]
...
linkmap主要包括三大部分:
- Object Files 生成二进制用到的link单元的路径和文件编号,其顺序就是工程文件在
build phase - compile sources中的顺序 - Sections 记录Mach-O每个Segment/section的地址范围.
__TEXT是代码段,只读;__DATA是数据段,可读可写。 - Symbols 按顺序记录每个符号的地址范围。1.以地址为区分的 2.file指的是占用多少空间,在方法中写的代码越多,size越大
.order文件
.order文件可以指定符号(符号)的放到虚拟内存页表的前几页。按照这个思路,我们可以将app启动时需要调用的符号(方法)整理到.order文件文件中,这样缺页中断数就会减少,启动也就是更快了。
.order文件配置
- 在主工程路径下新建一个.order文件,使用vim编辑它
~ cd /Users/Demo
~ Demo touch fun.order
~ Demo vim fun.order
- vim编辑内容
_main
+[AppDelegate load]
+[ViewController load]
-[ViewController viewDidLoad]
-[dljljl dage]//工程中的无效的方法会被丢弃
-[oooo v587]//工程中的无效的方法会被丢弃
- 指定.order文件路径- Build Settings - order file。
这样二进制文件重排就OK了,当然这只是个demo,二进制重排启动优化效果不明显,但是如果是大的工程,那绝对是有很大的进步。
启动优化进阶 Clang 插桩
上面通过linkMap其实是无法覆盖所有方法的(方法包括:OC方法、C函数、Block、Swift的方法/函数,之后统称符号)。所以如何找到所有的方法呢。---> LLVM内置了一个简单的代码覆盖率检测工具(SanitizerCoverage),它能对所有符号进行hook.
操作
Clang13的文档 关于 Tracing PCs (跟踪CPU执行到的代码),通过Clang插桩我们可以跟踪到所有函数的执行,包括APP启动时刻所调用的。
-
- 根据上文中的Tracing PSs,在
Build Setting -> Other C Flags中,增加-fsanitize-coverage=trace-pc-guard的配置
- 根据上文中的Tracing PSs,在
-
- 编译之后会报错,说缺少两个函数,其实就是Tracing PCs中的例子代码,复制过来放到工程任何一个.m文件中即可,再运行还是报错,那就根据报错删除不存在的方法
-
- 在复制的文本中我们可以看到两个函数
__sanitizer_cov_trace_pc_guard_init与__sanitizer_cov_trace_pc_guard
__sanitizer_cov_trace_pc_guard_init: 这个函数记录当前程序中有多少个符号的__sanitizer_cov_trace_pc_guard: 这个函数式能记录当前hook的符号的返回地址(__builtin_return_address),每调用一个地址,这个函数就会被调用一次
- 在复制的文本中我们可以看到两个函数
-
- 尝试打印每个方法,下面
info.dli_sname就是每个符号的名字
- 尝试打印每个方法,下面
#include <dlfcn.h> //需要加入这个库
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return;
void *PC = __builtin_return_address(0);
Dl_info info;
dladdr(PC, &info);
NSLog(@"%s", info.dli_fname);
NSLog(@"%p", info.dli_fbase);
NSLog(@"%s", info.dli_sname);
NSLog(@"%p", info.dli_saddr);
}
Dl_info是个结构体
typedef struct dl_info {
const char *dli_fname; /* 当前MachO路径(文件的名字) Pathname of shared object */
void *dli_fbase; /* 当前MachO起始地址(文件的地址) Base address of shared object */
const char *dli_sname; /* 函数名称 Name of nearest symbol */
void *dli_saddr; /* 函数地址 Address of nearest symbol */
} Dl_info;
需要注意Load()方法也是能被hook的,只是因为
if (!*guard) return;这句方法被返回了
-
- 稍加修改获取符号方法打印
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
void *PC = __builtin_return_address(0);
Dl_info info;
dladdr(PC, &info);
NSLog(@"%s", info.dli_sname);
}
@end
// 控制台打印
+[ViewController load]
main
-[AppDelegate application:didFinishLaunchingWithOptions:]
-[SceneDelegate window]
-[SceneDelegate setWindow:]
-[SceneDelegate window]
-[SceneDelegate window]
-[SceneDelegate scene:willConnectToSession:options:]
-[SceneDelegate window]
-[SceneDelegate window]
-[SceneDelegate window]
-[ViewController viewDidLoad]
-[SceneDelegate sceneWillEnterForeground:]
-[SceneDelegate sceneDidBecomeActive:]
...
- 6.
__sanitizer_cov_trace_pc_guard的回调是多线程的。所以当我们通过回调收集函数名称时也要需要通过原子队列OSAtomicDequeue保证当前线程安全,从而生成.order文件 -
- 解决循环引发的天坑
SanitizerCoverage不但拦截方法、函数、Block,还会对循环进行HOOK。
案例中while循环被HOOK,循环的执行会进入回调函数。回调函数中存入队列的还是touchesBegan的函数地址,这会导致队列中永远存在一个到两个touchesBegan,next永远获取不完。
- 解决循环引发的天坑
解决办法:
Build Setting -> Other C Flags中,将配置修改为-fsanitize-coverage=func,trace-pc-guard对其增加func参数
- 8.取反去重生成点.order文件,这样就完成了
参考 Clang 插桩
工具和第三方
- Instruments
- System Trace:跟踪
Page Fault 缺页中断数 - Time Profiler:监测运行时间
- Leaks:内存泄露
- Zombies:监测僵尸指针
- Color offscreen rendered yellow:离屏渲染监测
- 第三方
- BLStopwatch :打点计时器
- MLeaksFinder:内存泄露
- YYFPSLabel:卡顿监测
- Matrix:微信自研卡顿分析工具
其他优化
- 正确的选择使用
imageNamed(耗时,但有缓存)和imageWithContentsOfFile - 避免过于庞大的xib
- 正确的选择使用集合
-
- Array:使用index来lookup很快,使用valuelook很慢,插入/删除很慢
-
- Dictionary:使用Key查找快
-
- Set:无序组,用值来找很快,插入/删除很快
-
- 选择正确的数据存储选项NSUserDefaults、Core Data、FMDB