interview-优化

491 阅读13分钟

界面优化

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的优化

    1. 合理重用cell,避免重复创建和销毁
    1. rowHeight or tableView(_:heightForRowAt:) 避免动态计算单元格高度,尽量使用静态单元格高度,如果非要动态的,尽量缓存单元格高度
    1. 预估高度,使用 tableView.estimatedRowHeight = 44.0 来减少计算开销
    1. 异步加载数据和图片
    1. 避免复杂视图层次结构,如果非要实现,使用opaque = true跳过混合计算(blending calculations),直接渲染视图,减少绘制开销 和 rasterization = true光栅化技术缓存复杂视图的内容
    1. 使用 willDisplaydidEndDisplaying 优化单元格 在 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.使用工具检测内存泄露

  • InstrumentsLeaksAllocations 分析内存泄漏和内存占用情况
  • MLeaksFinder: 原理
    • 1.MLeaksFinder会在特定的时机(如视图控制器被从视图层次结构中移除时)给对象添加一个“标记”。这个标记通常是通过关联对象(associated objects)来实现的。
    • 2.检查标记,在对象的dealloc方法中,MLeaksFinder会检查该对象是否带有“标记”。如果对象带有标记但没有被释放,MLeaksFinder会记录这个内存泄漏。
  • FBRetainCycleDetector: 原理
    • 1 对象图遍历:FBRetainCycleDetector 通过遍历对象的引用图,收集对象之间的引用关系。这通常是通过对象的属性、实例变量以及关联对象(associated objects)来实现的。
    • 2 检查循环引用: 一旦对象图被构建,FBRetainCycleDetector 会检查对象图中的循环引用路径。如果检测到一个对象从自己出发,通过一系列引用最终又回到了自己,则认为存在循环引用

FBRetainCycleDetector 的使用存在两个问题:

    1. 需要找到候选的检测对象
    1. 检测循环引用比较耗时 所以一般先通过 MLeaksFinder 找到内存泄漏的对象,然后再过 FBRetainCycleDetector 检测该对象有没有循环引用即可。

包大小优化 今日头条iOS端安装包大小优化

资源优化

  1. 无用图片删除: 使用第三方MACAppLSUnusedResources
  2. 使用tint color精简单色图片
  3. 大图云端下载

代码优化

  1. 无用类删除 MachO文件中有__DATA.__objc_classrefs__DATA.__objc_selrefs段,分别近似于“被使用的类的集合”和“被使用的方法的集合”。通过取差集的方式可以筛选出未被使用的类和方法。
1. 使用`otool`命令可以拿到类在二进制文件中的位置地址
2. otool -o 二进制文件中的位置地址 转换成可读的类名

但是有些类是在第三方的库里的,删除不了

  1. 无用方法排查:这部分一般在工具类进行 所有已经被实现的方法可以通过linkmap来获取,对linkmap做grep操作即可获得结果:
grep 'xxxx'

而被使用的方法在上面__DATA.__objc_selrefs已经拿到,同样取差集即可

  1. 无用第三方库删除
  2. 编译优化: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就是利用了这个原理;

界面优化 上面有了,可以不看

  • 预处理
  1. 预排版:异步子线程提前计算好布局,在切回主程序更新布局。eg:提前计算cell高度
  2. 预渲染:异步子线程中提前将cell某个button的圆角渲染好。
  3. 预解码:图片加载过程中,将解码耗时操作放在异步子线程中.eg:像YYImage,SDWebImage这些都是通过异步子线程画出位图(bit map),然后切到主线程赋值。整个原理和预渲染差不多
  • 按需加载
  1. 只加载当前显示的cell+前后3个cell。减少cell计算处理数量。eg:微博加载首页,VVeboTableViewDemo
  2. Runloop分发:在滑动时,图片解码会占用大量的计算能力。使用runloop监听其将要休眠(kCFRunloopBeforeWaitng)的时候,我们再去做图片加载操作;
  • 减少图层层级: layout阶段和commit阶段需要进行多次递归操作。如果图层非常多,递归操作计算就很多,造成卡顿

  • 异步渲染:通过Core Graphics 合成一张位图bitMap 美团开源过一个框架Graver它通过异步绘制可以将多个图层合层一张位图

启动时间优化:二级制重排

二级制重排

  • 启动概念 1.- 启动分为冷启动(app第一次启动)和热启动,而优化的一般是冷启动
    • 冷启动又分为main函数之前和main函数之后
  • main函数之后 1.使用BLStopwatch打点 2.二进制重排

  • 虚拟内存和物理内存

  1. app进程在访问内存时并不直接访问,其拥有一份虚拟内存。
  2. 虚拟内存通过一种中间的页表(映射表)找到真正的物理内存。
  3. 虚拟内存地址是连续的,但是映射后真实的物理地址并不一定是连续的。
  • 内存分页管理
  1. 每个进程都有若干张虚拟页表(映射表),在进程运行的时候,会去访问页表
  2. 但是当某块物理内存没有对应的页表的时候,系统会去创建新的一张页表。此时系统就会阻塞当前进程(这个过程我们称为缺页中断)
  3. 如果物理内存有空的内存区就在空的内存区创建,没有空的内存区,系统就会找一页去覆盖某个不活跃的页。
  • 二进制重排原理
  1. 启动的时候,需要加载大量的代码,而这些代码的顺序是根据文件的顺序生成的。
  2. 这些代码对应方法可能分散在好几张页表中
  3. 二进制重排就是将启动时用到的方法放在前一两张页表中,并且按顺序排列
  • 二进制重排操作
  1. 通过查看Linkmap(二进制文件的布局),拿到方法调用的顺序
  2. 通过**.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函数BlockSwift的方法/函数,之后统称符号)。所以如何找到所有的方法呢。---> LLVM内置了一个简单的代码覆盖率检测工具(SanitizerCoverage),它能对所有符号进行hook.

操作

Clang13的文档 关于 Tracing PCs (跟踪CPU执行到的代码),通过Clang插桩我们可以跟踪到所有函数的执行,包括APP启动时刻所调用的

    1. 根据上文中的Tracing PSs,在Build Setting -> Other C Flags中,增加-fsanitize-coverage=trace-pc-guard的配置
    1. 编译之后会报错,说缺少两个函数,其实就是Tracing PCs中的例子代码,复制过来放到工程任何一个.m文件中即可,再运行还是报错,那就根据报错删除不存在的方法
    1. 在复制的文本中我们可以看到两个函数 __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),每调用一个地址,这个函数就会被调用一次
    1. 尝试打印每个方法,下面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; 这句方法被返回了

    1. 稍加修改获取符号方法打印
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文件
    1. 解决循环引发的天坑 SanitizerCoverage不但拦截方法函数Block,还会对循环进行HOOK。
      案例中while循环被HOOK,循环的执行会进入回调函数。回调函数中存入队列的还是touchesBegan的函数地址,这会导致队列中永远存在一个到两个touchesBegan,next永远获取不完。

解决办法:
Build Setting -> Other C Flags中,将配置修改为-fsanitize-coverage=func,trace-pc-guard对其增加func参数

  • 8.取反去重生成点.order文件,这样就完成了

参考 Clang 插桩

工具和第三方

  • Instruments
  1. System Trace:跟踪Page Fault 缺页中断数
  2. Time Profiler:监测运行时间
  3. Leaks:内存泄露
  4. Zombies:监测僵尸指针
  5. Color offscreen rendered yellow:离屏渲染监测
  • 第三方
  1. BLStopwatch :打点计时器
  2. MLeaksFinder:内存泄露
  3. YYFPSLabel:卡顿监测
  4. Matrix:微信自研卡顿分析工具

其他优化

  • 正确的选择使用imageNamed(耗时,但有缓存)和imageWithContentsOfFile
  • 避免过于庞大的xib
  • 正确的选择使用集合
      1. Array:使用index来lookup很快,使用valuelook很慢,插入/删除很慢
      1. Dictionary:使用Key查找快
      1. Set:无序组,用值来找很快,插入/删除很快
  • 选择正确的数据存储选项NSUserDefaults、Core Data、FMDB