写在前面:
这篇文章并非原创,是对iOS 保持界面流畅的技巧的学习总结。
讲述图像显示的原理;界面卡顿的原因;从CPU和GPU方面如何解决卡顿(让界面保持流畅)。
一.屏幕显示图像的原理
1. HSync、VSync
CRT电子枪从上到下一行行扫描,扫描完成后回到初始位置继续下一次扫描。每扫描完一行,显示器会发出一个水平同步信号HSync,让显示器的显示过程和视屏控制器进行同步。
绘制完一帧后,CRT电子枪回复到原位准备下一帧前,显示器会发出垂直同步信号VSync。显示器也按照VSync信号产生的频率进行刷新。

2. 图像显示原理

1)CPU计算好内容,提交给GPU进行渲染,2)GPU把渲染的结果放入帧缓冲区(FrameBuffer);
3)视频控制器收到VSync信号后逐行读取FrameBuffer的数据,数据转换后传递给显示器
2.1 CPU的工作

2.1 GPU的工作

二.卡顿、掉帧产生的原因:

每秒会有60帧的画面更新,所以60分之一秒就要产生一个画面,即60ps是流畅画面;相当于16.7ms产生一个画面;
16.7ms内会场上一个VSync信号。
图中第一段是正常显示的情况:CPU和GPU完成工作后,把渲染结果提交到帧缓冲区,等待VSync信号到来后显示到屏幕上;
图中第二段开始出现卡顿、掉帧:在VSync信号到来之前,CPU或者GPU没有完成任务,这一帧就被丢弃,等待下一次机会再显示,但是屏幕保留着之前未显示完的画面,出现卡顿和掉帧。
总结:
出现卡顿和掉帧的原因:在VSync信号到来之前(16.7ms),CPU和GPU没有完成下一帧画面的合成,就会造成卡顿。
三.卡顿尝试的原因和解决方案
CPU方面:

1.对象创建、调整、销毁
1.1对象创建
1)对象创建会分配内存、调整属性,比较小号CPU资源。所以用
轻量级对象代替重量级对象。
eg:
对于不需要响应事件的控件,用CALayer显示;
对象不涉及UI操作,放到后台线程创建;
性能敏感的界面,storyborad的资源消耗>代码创建;2)推迟对象创建的时间,对象放到多个任务中
eg:懒加载3)对象复用
如果对象可以复用,并且复用的代价比释放、创建新对象要小,那么这类对象应当尽量放到一个缓存池里复用。
1.2对象调整
- 避免视图层次调整、添加、移除;
视图层次调整时,UIView和CALayer之间会调用很多方法和通知;
CAlayer内部没有属性,当调整UIView 的关于显示相关的属性(比如 frame/bounds/transform)的时候,resolveInstanceMethod临时创建一个方法,把修改的属性值放到字典里面,创建动画等,非常消耗属性。
所以修改UIView的frame/bounds/transform属性消耗资源大于一般的属性;
1.3对象销毁
- 容器有大量对象,销毁时CPU耗时明显;可以把对象释放放到后台操作。
eg:把对象捕获到 block 中,然后扔到后台队列去随便发送个消息以避免编译器警告,就可以让对象在后台线程销毁了。
NSArray *tmp = self.array;
self.array = nil;
dispatch_async(queue, ^{
[tmp class];
});
2.预排版(布局计算、文本计算)
2.1布局计算
把布局放到后台计算,并进行缓存;
例如tableview的cell高度,可以在获取到数据之后,在后台线程里面进行计算好,避免cell里面和height获取的时候再次计算;
2.2文本计算
a. 是什么
文本计算 = 计算一段文字渲染后的宽、高、换行位置、每行的字符范围。
比如 Feed Cell 里面一大段微博正文,要知道这个 Label 最终多高,才能确定 cell 总高度,tableView 才能返回heightForRow。b.为什么消耗 CPU
1). 要处理字体、字号、字间距、行间距、换行、emoji、中英文混排、截断、附件图片,大量的数学计算。
2). UILabel 是在主线程内部实时计算:cell 复用时、赋值 text 的时候才现场算;滑动 tableView,大量 cell 快速进出屏幕,就会反复执行文本计算,挤占主线 16.67ms 时间片。
-
c. 怎么实现优化
1). 放到后台子线程提前做文本计算,不要在 cellForRowAtIndexPath 主线程计算
网络拿到 JSON 数据之后,子线程对每一条 Feed,直接用boundingRectWithSize或者 CoreText 做完整文本排版;
2). 把计算结果全部缓存到 Layout 对象(模型):保存文本高度、每行位置、整体 cell 高度,内存缓存;
3). 当 tableView 要 cell 高度、渲染 cell 的时候,直接读取已经算好的 Layout 对象,主线程不再做任何文本计算。
注意:
boundingRectWithSize可以子线程调用;但是 UIKit 控件对象(UILabel)不能放到子线程操作,只是计算尺寸是安全的。
3.预渲染(文本等异步绘制、图片编解码、图像的绘制等)
3.1文本渲染
-
1.是什么
计算出宽高只是第一步;
文本渲染 = 把文字真正绘制到一张位图 Bitmap 上,生成可以给 layer 显示的图像。
UILabel、UITextView 底层全部基于 CoreText,在主线程把文字绘制到 layer 的 contents 对应的位图上。
- 2.为什么耗 CPU
默认 UILabel:渲染绘制逻辑全部在主线程执行。
滑动列表有大量文字 cell,每个 cell 显示的时候主线程执行 CoreText 绘制,占用大量 CPU 时间,容易超过 16ms,造成掉帧。
文本计算:算 “占多大位置”;文本渲染:真正 “画成像素图片”,两件事。
- 3.怎么实现优化:异步绘制(YYAsyncLayer / ASDisplayKit 原理)
核心思想:把绘制 Bitmap 的操作丢到后台子线程,主线程只负责设置 layer.contents 展示最终画好的图片,不做绘制工作。
流程拆解:
1). 自定义 CALayer 子类(YYAsyncLayer),重写display方法;
2). 当 layer 需要刷新内容[layer setNeedsDisplay],不去主线程 drawRect;
3). 切后台队列,创建CGBitmapContext位图上下文;
4). 在子线程使用 CoreText 把文字绘制到这个 bitmap 上下文;
5). 绘制完成,从上下文生成 CGImage;切回主线程,赋值给layer.contents = image;
6). 增加任务取消判断 isCancelled:tableView 高速滑动,cell 快速滑出屏幕,绘制任务还没完成,直接终止绘制,避免无效 CPU 消耗。
- 简化伪代码
- (void)display {
dispatch_async(backgroundQueue, ^{
// 子线程创建位图上下文
CGBitmapContextRef ctx = CGBitmapContextCreate(...);
// CoreText绘制文字到上下文
CTFrameDraw(ctFrame, ctx);
CGImageRef img = CGBitmapContextCreateImage(ctx);
CFRelease(ctx);
dispatch_async(dispatch_get_main_queue(), ^{
// 主线程只赋值展示图片,不做绘制
self.contents = (__bridge id)img;
CGImageRelease(img);
});
});
}
重要限制:
- UIView 不能子线程操作,但是 CGContext、CoreText 绘制可以子线程执行;
- drawRect 是主线程调用,不要重写 drawRect,改用自定义 layer 异步 display;
- 必须处理任务取消:cell 滑走,未完成绘制任务直接放弃,不然大量后台线程空转 CPU 暴涨。
- 现实项目:YYLabel、ASDK 的 ASTextNode 就是这套逻辑。
3.2图片的解码
-
1.什么是图片解码
磁盘上图片是压缩格式:PNG/JPG;二进制是压缩数据。
解码:把压缩的 jpg/png,解压转换成 GPU 可以直接识别的位图 Bitmap(RGBA 像素矩阵)。
- 关键行为:
// 此时只是读压缩二进制,**并不会解码**
UIImage *img = [UIImage imageWithContentsOfFile:path];
// !!! 当把image设置给layer.contents / UIImageView.image,准备提交给GPU渲染前,系统会在【主线程】自动做解码!!
imageView.image = img;
解码是 CPU 繁重操作,默认发生在主线程。滑动 Feed 大量图片 cell,主线程疯狂做图片解码,直接卡顿。
- 2.怎么实现优化:后台预先解码
思路:不要等赋值 imageView 的时候主线程自动解码;子线程手动提前完成解码,拿到已经解码好的位图 UIImage。
实现原理:
子线程创建位图上下文,把原始压缩图片绘制进上下文,强制触发解码,拿到已经解压完成的 bitmap 图片;主线程直接给 imageView,不再做解码。
现在主流 SDWebImage、YYWebImage 内部都做了这个逻辑:下载图片后子线程解码,缓存解码后位图内存。
伪代码:
dispatch_async(global_queue, ^{
UIImage *originImage = [UIImage imageWithData:downloadData];
// 子线程强制解码
UIGraphicsBeginImageContextWithOptions(size, NO, scale);
[originImage drawAtPoint:CGPointZero];
UIImage *decodedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
imageView.image = decodedImage; // 已经解码完成,主线程无解码开销
});
});
注意:解码会占用内存,大图解码后内存占用很高,要做图片缩放、尺寸裁剪,不要解码超大原图。
3.3 图像的绘制
1.什么是图像绘制
就是在drawRect:里面,用 CoreGraphics (CG 开头 API),画线、画圆、画图片、渐变,生成视图内容。
drawRect:默认在主线程调用。2.性能问题
- 大量复杂绘制逻辑写在 drawRect,主线程执行,抢占时间片;
- 每一次 setNeedsDisplay,就会触发一次 drawRect 重新绘制。cell 复用时频繁刷新,开销放大。
- 3、优化实现:异步绘制,放到后台 CGContext 绘制
和文本异步绘制原理一致:尽量不要用 UIView 的 drawRect(主线程),改用自定义 CALayer,子线程 CGBitmapContext 完成绘制,生成图片,再给 layer.contents。
drawRect 底层会自动创建位图上下文,每次调用都有开销;能不用就尽量不用。
Quartz 2D绘制路径、文字 、Quartz 2D绘制路径实例里面,在[UIView drawRect:] 用CG开头的方法进行绘制,就是最简单的绘制方法。
GPU方面:

纹理渲染
- 所有图片、文本绘制出来的 bitmap,都要从内存上传到显存,变成 GPU 的纹理 Texture;
- 短时间大量 cell 图片出现,短时间上传大量纹理,GPU 压力大;
- 图片尺寸超过 GPU 最大纹理(老设备 4096*4096),CPU 还要预处理缩放,双重开销。
✅优化:
- 图片服务器下发适合手机的缩略图,客户端不要解码超大原图;
- 避免瞬间大量图片同时显示,做预加载、优先级控制;
- GPU 的最大纹理尺寸是 4096×4096,尽量不要让图片和视图的大小超过这个值。
- 减少在短时间内大量图片的显示,尽可能将多张图片合成为一张进行显示;
图层混合
当多个视图(或者说 CALayer)重叠在一起显示时,GPU 会首先把他们混合到一起,混合的过程也会消耗很多 GPU 资源。所以需要试图混合。
- 如果视图不透明
view.opaque = YES,GPU 知道下层被完全遮挡,可以跳过下层像素计算,大幅提速; - 大量半透明、重叠图层,GPU 混合开销指数上升。
✅优化:
- 不透明的 view,务必设置
opaque = YES,以避免无用的 Alpha 通道合成 - 尽量减少视图数量和层次:减少嵌套过多子 layer、子 view,合并视图层级;
- 多个不需要单独动画、不需要点击的子 view,预先绘制一张 bitmap 图片代替多层 view,减少图层数量。
图形的生成-离屏渲染 Off‑Screen Rendering
-
1.什么是离屏渲染
GPU 不在当前屏幕帧缓冲区直接渲染;需要开辟一块额外的离屏缓冲区,先把内容渲染到这个临时缓冲区,渲染完成,再拷贝回屏幕帧缓冲区。开辟缓冲区、上下文切换,开销巨大。
触发离屏渲染常见属性:
layer.cornerRadius + masksToBounds(圆角裁剪)、layer.shadow阴影、layer.mask遮罩、CAShapeLayer矢量绘制。
Feed 列表大量 cell 头像圆角,如果全部用 layer.cornerRadius+masksToBounds,快速滑动 GPU 满载,帧率暴跌,CPU 占用却不高,这就是典型离屏渲染现象。
- 2.
cornerRadius + masksToBounds优化实现方案(3 种)
1). 后台线程预渲染圆角图片(ibireme Demo 首选)
头像下载完成后,子线程 CG 绘制,直接生成已经裁剪好圆角的 bitmap 图片缓存;UIImageView 直接展示已经圆角处理完的图片;GPU 只做普通纹理渲染、没有额外离屏缓冲区,完全避开 layer 圆角带来的离屏渲染。
- 完整实现代码(子线程执行,重点:CGContext 绘制 + 裁剪):
/**
子线程把原图绘制为指定圆角的新图片
@param originImage 下载好的原图
@param size 头像最终尺寸(比如40*40)
@param radius 圆角大小
@return 圆角成品图
*/
- (UIImage *)roundImageWithOriginImage:(UIImage *)originImage size:(CGSize)size radius:(CGFloat)radius {
// 1. 开启位图上下文,scale匹配屏幕,保证高清不模糊
UIGraphicsBeginImageContextWithOptions(size, NO, [UIScreen mainScreen].scale);
CGContextRef ctx = UIGraphicsGetCurrentContext();
// 2. 创建圆角路径,在上下文里设置裁剪区域
CGRect rect = CGRectMake(0, 0, size.width, size.height);
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius];
// 裁剪:后续所有绘制内容,只有path内部才保留,外部直接丢弃
[path addClip];
// 3. 在裁剪后的区域绘制原图
[originImage drawInRect:rect];
// 4. 从上下文取出已经裁好圆角的图片
UIImage *roundImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return roundImage;
}
调用时机:图片下载完成之后,子线程执行这个方法,不要在主线程!
// 伪代码,和YYWebImage/SDWebImage的transform逻辑对齐
dispatch_async(dispatch_get_global_queue(0, 0), ^{
UIImage *downloadImg = ...; // 下载+解码完成的原图
UIImage *roundImg = [self roundImageWithOriginImage:downloadImg size:CGSizeMake(40,40) radius:20];
// 缓存这张圆角图(内存缓存+磁盘缓存),下次直接读缓存,不用重复绘制
[cache setObject:roundImg forKey:cacheKey];
dispatch_async(dispatch_get_main_queue(), ^{
imageView.image = roundImg;
});
});
2). shouldRasterize光栅化layer.shouldRasterize = YES; layer.rasterizationScale = [UIScreen mainScreen].scale;
作用:把这个 layer 以及所有子 layer,光栅化成一张位图缓存;后续直接复用这张缓存,不再重复离屏渲染。
⚠️坑:视图发生变化(动画、滚动缩放)缓存失效,会重新光栅化;内存占用上涨,适合静态不变内容,不适合频繁变化的 cell。
3). 用一张圆角遮罩图片盖在头像上层模拟圆角(简单场景)(中间透明,四周是和背景一样颜色的纯色,视觉上挡住四个角,看起来就是圆形 / 圆角头像)。
- 制作圆角图片作为背景;
- 需要显示的图形在后台线程绘制为图片
YYKit的微博demo里面是把头像下载后在后台线程渲染为圆型后,放到ImageCache缓存中。
补充:
上述实践:微博Demo性能优化技巧
使用第三方# AsyncDisplayKit
,可把控件房贷线程创建和修改、图层预合成、异步并发操作、runloop任务分发、滑动列表预加载;
- 3. layer.shadow 阴影 优化实现方案
- 为什么会离屏渲染
默认:
view.layer.shadowColor = [UIColor blackColor].CGColor;
view.layer.shadowOffset = CGSizeMake(0,2);
view.layer.shadowOpacity = 0.3;
view.layer.shadowRadius = 4;
只要设置 shadow,默认触发离屏渲染。
GPU 需要先把这个 layer 渲染到离屏 buffer,再计算阴影模糊,再合并到屏幕帧缓存。
✅1)方案 1:shadowPath(最重要、成本最低的优化,优先试这个)
给阴影指定路径 shadowPath,告诉 GPU 阴影的轮廓,GPU 可以不用扫描整个 layer 的 alpha 去计算阴影,很多场景可以消除离屏渲染,在屏幕缓冲区直接渲染阴影。
view.layer.shadowColor = UIColor.blackColor.CGColor;
view.layer.shadowOffset = CGSizeMake(0,2);
view.layer.shadowOpacity = 0.3;
view.layer.shadowRadius = 4;
// 关键:指定阴影轮廓路径,和view的bounds匹配
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:view.bounds cornerRadius:8];
view.layer.shadowPath = path.CGPath;
原理:不设置 shadowPath 时,GPU 要遍历 layer 每一个像素的 alpha 通道,自动生成阴影轮廓,这个过程必须离屏;指定 shadowPath 后,GPU 已知轮廓,不需要 alpha 扫描,可直接 on-screen 渲染。
✅ 2)方案 2:预绘制阴影 + 圆角到一张位图(列表大量 cell 场景,ibireme 推荐)
子线程 CG 上下文,一次性把「圆角 + 阴影」全部绘制到一张 Bitmap 图片,缓存图片,layer 直接展示成品图。
GPU 只渲染普通纹理,没有阴影计算、没有离屏。
适合 Feed 列表卡片,尺寸固定、不会频繁变动。
缺点:尺寸写死;卡片大小一变,就要重新生成图片。
✅ 3)方案 3:单独放一个下层 shadow 图层(不推荐列表)
放一个底层 view 专门做阴影,上层 view 做内容;底层 view 同样配合 shadowPath。
缺点:多一层 view 层级,管理麻烦,动态尺寸维护成本高。
-
4. layer.mask 遮罩 优化实现方案
为什么 mask 会离屏
layer.mask= 给当前 layer 绑定一个遮罩 layer,根据遮罩的 alpha 决定像素是否显示。
CALayer *maskLayer = [CALayer layer];
maskLayer.contents = (__bridge id)[UIImage imageNamed:@"mask.png"].CGImage;
view.layer.mask = maskLayer;
只要设置 .mask,一定触发离屏渲染。
GPU 规则:要做像素 alpha 遮罩裁剪,必须先渲染到离屏 buffer,再做遮罩融合,再拷贝回屏幕 buffer。
注意:mask 和 masksToBounds 不是一回事:
- masksToBounds:沿着 layer 自身 bounds 裁剪(圆角)
- layer.mask:外部传入任意 layer 作为遮罩,可以做异形、心形、渐变遮罩,能力更强,代价更大
优化方案
✅ 方案 1:预绘制,把遮罩效果提前在 CG 画进图片(首选,列表)
子线程 CGBitmapContext,把原图 + mask 遮罩逻辑一起绘制,输出一张已经被遮罩裁剪完成的成品 Bitmap。
最终 layer.contents 直接拿成品图,不再设置任何 .mask。
适合:遮罩形状固定、图片内容不会实时变化的场景,比如异形头像、渐变遮罩封面图。
✅ 方案 2:用 CAShapeLayer + 绘制路径替代 mask(有限场景)
如果 mask 只是简单几何形状(圆形、圆角矩形),不要用 mask,改用 CG 绘制路径,预生成图片。
如果是渐变遮罩(最常见,比如文字渐隐、图片底部渐隐):
👉 渐变 mask 很难用 shadowPath 这类属性优化,几乎只能预绘制。
❌ 没有 “一行属性开关” 可以让 mask 不触发离屏渲染!
这点和 shadow 不一样,mask 没有类似 shadowPath 的捷径,只要运行时用 layer.mask,必然离屏。
高频坑:很多同学用 mask 做渐变淡出,放在 collectionView cell 里面,滑动直接 GPU 爆满,这个是典型坑。
下面对上面进行总结:
卡顿优化 - CPU
尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用CALayer取代UIView
不要频繁地调用UIView的相关属性,比如frame、bounds、transform等属性,尽量减少不必要的修改
尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性
Autolayout会比直接设置frame消耗更多的CPU资源
图片的size最好刚好跟UIImageView的size保持一致(不然CPU会对图片大小进行跳转)
控制一下线程的最大并发数量
尽量把耗时的操作放到子线程
文本处理(尺寸计算、绘制)
图片处理(解码、绘制)(把图片放到上下文,然后再进行显示)
卡顿优化 - GPU
尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示
GPU能处理的最大纹理尺寸是4096x4096,一旦超过这个尺寸,就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸
尽量减少视图数量和层次
减少透明的视图(alpha<1),不透明的就设置opaque为YES
尽量避免出现离屏渲染
面试题
你在项目中是怎么优化内存的?
优化你是从哪几方面着手?
列表卡顿的原因可能有哪些?你平时是怎么优化的?
遇到tableView卡顿嘛?会造成卡顿的原因大致有哪些?
卡顿检测
平时所说的“卡顿”主要是因为在主线程执行了比较耗时的操作
可以添加Observer到主线程RunLoop中,通过监听RunLoop状态切换的耗时,以达到监控卡顿的目的
可以使用第三方LXDAppFluecyMonitor-master辅助
耗电优化
1. 耗电主要来源
- CPU处理,Processing
- 网络,Networking
- 定位,Location
- 图像,Graphics
2.耗电优化
- 尽可能降低CPU、GPU功耗
- 少用定时器
- 优化I/O操作
- 尽量不要频繁写入小数据,最好批量一次性写入
- 读写大量重要数据时,考虑用dispatch_io,其提供了基于GCD的异步操作文件I/O的API。用dispatch_io系统会优化磁盘访问
- 数据量比较大的,建议使用数据库(比如SQLite、CoreData)
- 网络优化
- 减少、压缩网络数据
- 如果多次请求的结果是相同的,尽量使用缓存
- 使用断点续传,否则网络不稳定时可能多次传输相同的内容
- 网络不可用时,不要尝试执行网络请求
- 让用户可以取消长时间运行或者速度很慢的网络操作,设置合适的超时时间
- 批量传输,比如,下载视频流时,不要传输很小的数据包,直接下载整个文件或者一大块一大块地下载。如果下载广告,一次性多下载一些,然后再慢慢展示。如果下载电子邮件,一次下载多封,不要一封一封地下载
- 定位优化
- 如果只是需要快速确定用户位置,最好用CLLocationManager的requestLocation方法。定位完成后,会自动让定位硬件断电
如果不是导航应用,尽量不要实时更新位置,定位完毕就关掉定位服务 - 尽量降低定位精度,比如尽量不要使用精度最高的kCLLocationAccuracyBest
- 需要后台定位时,尽量设置pausesLocationUpdatesAutomatically为YES,如果用户不太可能移动的时候系统会自动暂停位置更新
- 尽量不要使用startMonitoringSignificantLocationChanges(监控位置改变的,精细的),优先考虑startMonitoringForRegion(监控区域改变的)
- 硬件检测优化
用户移动、摇晃、倾斜设备时,会产生动作(motion)事件,这些事件由加速度计、陀螺仪、磁力计等硬件检测。在不需要检测的场合,应该及时关闭这些硬件
启动优化
1.APP的启动
- APP的启动可以分为2种
- 冷启动(Cold Launch):从零开始启动APP
- 热启动(Warm Launch):APP已经在内存中,在后台存活着,再次点击图标启动APP
- APP启动时间的优化,主要是针对冷启动进行优化
- 检测启动时间:
- 通过添加环境变量可以打印出APP的启动时间分析(Edit scheme -> Run -> Arguments)
- DYLD_PRINT_STATISTICS设置为1
- 如果需要更详细的信息,那就将DYLD_PRINT_STATISTICS_DETAILS设置为1(启动时间在400ms以内的都算可行)
2. APP冷启动三阶段
包含以下三个阶段:
- dyld
- runtime
- main

2.1. dyld阶段
1.什么是 dyld(dynamic link editor):Apple的动态链接器,可以用来装载Mach-O文件(可执行文件、动态库等)
- 启动APP时,dyld所做的事情有
- 装载APP的可执行文件,同时会递归加载所有依赖的动态库
- 当dyld把可执行文件、动态库都装载完毕后,会通知Runtime进行下一步的处理
2.2. runtime阶段
启动APP时,runtime所做的事情有:
- 调用map_images进行可执行文件内容的解析和处理
- 在load_images中调用call_load_methods,调用所有Class和Category的+load方法
- 进行各种objc结构的初始化(注册Objc类 、初始化类对象等等)
- 调用C++静态初始化器和attribute((constructor))修饰的函数
到此为止,可执行文件和动态库中所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被runtime 所管理
2.3.main阶段
总结一下
- APP的启动由dyld主导,将可执行文件加载到内存,顺便加载所有依赖的动态库
- 并由runtime负责加载成objc定义的结构
- 所有初始化工作结束后,dyld就会调用main函数
- 接下来就是UIApplicationMain函数,AppDelegate的application:didFinishLaunchingWithOptions:方法
APP的启动优化
按照不同的阶段
- dyld
- 减少动态库、合并一些动态库(定期清理不必要的动态库)
- 减少Objc类、分类的数量、减少Selector数量(定期清理不必要的类、分类)
- 减少C++虚函数数量
- Swift尽量使用struct
runtime
用+initialize方法和dispatch_once取代所有的attribute((constructor))、C++静态构造器、ObjC的+loadmain
在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在finishLaunching方法中
按需加载
安装包瘦身
- 安装包(IPA)主要由可执行文件、资源组成
- 资源(图片、音频、视频等)
- 采取无损压缩
- 去除没有用到的资源: https://github.com/tinymind/LSUnusedResources
- 可执行文件瘦身
编译器优化
Strip Linked Product、Make Strings Read-Only、Symbols Hidden by Default设置为YES去掉异常支持,Enable C++ Exceptions、Enable Objective-C Exceptions设置为NO, Other C Flags添加-fno-exceptions
利用AppCode(https://www.jetbrains.com/objc/)检测未使用的代码:菜单栏 -> Code -> Inspect Code
编写LLVM插件检测出重复代码、未被调用的代码
- 生成LinkMap文件,可以查看可执行文件的具体组成

可借助第三方工具解析LinkMap文件: https://github.com/huanxsd/LinkMap
还需要学习的地方:
1.简单的 FPS 指示器:FPSLabel
2.查看自己app的fps
3.使用CADisplayLink显示FPS
1.如何异步绘制?
这篇博客异步绘制通过VVeboTableViewDemo分享了如何进行一步绘制;
并讲述以下部分:
提前计算并缓存好高度;
滑动时按需加载,在大量图片展示时提高滑动速度。
2.如何使用CoreText 绘制文本:(图文混排的绘制)
4.如何使用[NSAttributedString drawWithRect:options:context:] 绘制文本;
1.在后台线程操作对图片削圆,然后放到ImageCache缓存中/YYKit把头像渲染为圆形后放到缓存,如何使用这个功能?
2.YYKit的学习:YYLayout、YYDispatchQueuePool
、YYKit里面对图片的加载和解码的使用