Improve app size and runtime performance
前语
WWDC2022 上苹果更新了Xcode14,里面提到了一些相关的优化。其中讲了通过对Swift和Objective-C运转时做了一些优化,达到了包体积变得更小、运转速度更快,发动速度更快的目的。假如你是用Xcode14来构建App,那么会有其中三点优化
- 高效的协议查看(针对Swift protocol check)
- 更快的音讯发送机制(message send)
- release 和return调用优化(release & retain)
- Autorelease elision的优化(主动开释省掉)
当你用Swift或Objective-C编写代码时,其实是会阅历三个个过程。
- 编码,通过Xcode编写代码
- 编译,运用了Swift和Clang编译器
- 运转,通过Swift和Objective-C运转时中完结
此次的这些要害优化其实便是在第三过程运转时来完结的,运转时嵌入在咱们一切平台的操作体系中。编译器在构建时不能做的事情,运转时能够做。而此次一切的修正其实关于开发者来说是无感透明的,所以任何代码都不必改动,只需你运用Xcode14来进行打包编译,便会享受的这些优化点。
Swift协议查看(Protocol checks)
先来看一个比方!
// 界说一个协议
protocol CustomLoggable {
// 协议中界说一个特点 customString,只读特点
var customLogString: String { get }
}
// 界说一个log函数,参数为Any类型
func log(value: Any) {
//假如value遵从CustomLoggable协议,就输出字符串
if let value = value as? CustomLoggable {
debugPrint(value.customLogString)
} else {
...
}
}
// 界说一个Event类型 遵从协议,并完结customLogString
struct Event: CustomLoggable {
var name: String
var date: String
var customLogString: String {
return "(self.name), on (self.date)"
}
}
看上面代码,因为log函数的参数需求输出字符串,所以在输出前要先判别这个value是否遵从CustomLoggable协议,Swift是静态言语,所以一般来说这样的查看都是发生在编译时期。可是编译器纷歧定能拿到满足的协议元数据信息来完结查看。比方这儿并不知道每次传入的 Any 类型是哪个确定类型,也就无法确定是否遵从 CustomLoggable
协议。所以这种查看常常发生运转时,体系借助计算好的协议查看元数据(protocol check metadata),运转库知道这个特别目标是否符合协议。
这些元数据的构建尽管大部分在编译期间,可是仍是有一部分是要在运转时完结,比方上面的比方,并且一个项目中必定不止有一个协议,所以跟着协议越多运转时的功率就越低,关于用户来说这个时刻大部分是发动时刻,所以用户感知为发动时刻变长。而Xcode14新推的的Swift Runtime处理了这个问题,只需你是用Xcode14编译且运转在iOS16及以上版即可。
依照苹果的说法,他们会把是否遵从协议
的这个判别前置到build时期,也便是把协议元数据计算
的过程前置到build中,详细便是他把这些操作放在App可履行文件和发动时任何动态库的dyld 闭包的一部分
为什么这样做能够节约发动时刻,需求先了解下app发动流程,需求一个知识布景从iOS11开端dyld3被加入,iOS13第三方库也开端运用dyld3加载。
所以咱们要看下dyld3的加载流程
dyld 3 主要包含了两个过程 进程外(发动前)和进程内(发动后),咱们来看发动前做了那些事情
- 进程外 Mach-O 分析器和编译器 (out-of-process mach-o parser) dyld 3 中将采用提早写入把成果数据缓存成文件的办法构成一个 lauch closure(能够理解为缓存文件)
- 分析依靠库
- 履行符号查找
- Write closure 缓存服务 (launch closure cache ) 体系程序的 closure 直接内置在 shared cache 中,而关于第三方APP,将在APP装置或更新时生成,这样就能确保 closure 总是在 APP 翻开之前预备好。说白了便是把上面做的成果全都缓存起来
综上看来曾经需求在in-process中做的事情,现在在out-of-process就能够完结,发动时或许运转时直接读取缓存数据即可,加快了发动速度和运转时的功能。其实在笔者看来当咱们下载或许更新App 的时分App上的进度条其实是分两部分正在下载
和正在装置
,此次的优化或许稍微进步装置的时长来下降发动速度,进步运转时功能。
on apps that rely heavily in Swift, this could add up to half the launch time
假如有条件的同学能够试下是否能够进步这么多的发动耗时。
音讯发送优化(Message send)
直接抛成果,苹果这边给到的数据是运用Xcode14编译打包的数据能够让ARM64上发送音讯耗费从12字节下降到8字节,二进制巨细也有2%的下降,也便是苹果对包巨细和功能都做了优化,默认是一起敞开的,由苹果来平衡两者的关系,当然也能够运用objc_stubs_small
来只是优化包巨细。
下面咱们看下是怎样优化的,相同运用官方代码举例
// 声明一个日历目标
NScalendar *cal = [self makeCalendar];
// 声明一个日期目标并赋值
NSDateComponents *dateComponents = [[NSDateComponents alloc] init];
dateComponents.year = 2022;
dateComponents.month = 2022;
dateComponents.day = 2022;
S
// 把日期转换为date
NSDate *theDate = [cal dateFromComponents: dateComponents];
// 回来date
return theDate;
大家知道OC调用办法终究会走到_objc_msgSend
,所以上面代码不算终究的return,会走7个 _objc_msgSend
,其中每一个都需求一条指令来调用便是bl 如下图
该函数界说为Id objc_msgSend(id self, SEL _cmd, ...)
,参数界说为 self是函数的调用方,SEL为详细调用哪个函数,详细的办法查找流程就不在这儿赘述。
咱们拿其中详细的一个函数调用来分析
NSDate *theDate = [cal dateFromComponents: dateComponents];
比方这个函数调用,转化为mesagesend的时分就变成这样
objc_msgSend(cal, @selector(dateFromComponents))
为了告知运转时调用哪个办法,咱们有必要传递一个Selector给这些objc_msgSend调用,就如上图的@selector(dateFromComponents)
咱们再来看Id objc_msgSend(id self, SEL _cmd, ...)
履行后他是怎样履行汇编指令的。
// 运用adrp找到该办法的地址 耗费4字节
adrp x1, [selector "dateFromComponents"]
// 将 地址加载到X1寄存器中 耗费4字节
ldr x1, [x1, selector "dateFromComponents"]
// 履行bl指令跳转到该办法并履行 耗费4字节
bl _objc_msgSend
从上面的代码看出每次履行办法调用都会 走以上三个过程,每个过程耗费4字节 一共耗费12字节,而前两步是预备selector,任何一次办法调用都会履行他,现在的策略是每调一个办法都会生成上面三步,那么此刻优化空间就来了。
因为这儿存在相同的代码(前两步),咱们能够考虑共享它,并且只在每个 selector 中触发它一次,而不是每次发送音讯时都生成这段指令代码
。所以咱们能够把这部分相同代码提取出来,放到一个小助手函数中(helper function), 并调用该函数。通过运用同一 selector 进行多次调用(通过传递参数不同,内部指令是相同的,现在封装成一个存根函数,曾经是散落在各个 _objc_msgSend 调用处),咱们能够保存一切这些指令字节。所以能够理解为把前两步封装一下
所以原来的调用就变成了
bl _objc_msgSend$dateFromComponents 4字节
bl _objc_msgSend 4字节
这也便是苹果说的从12字节优化到8字节,其中_objc_msgSend$dateFromComponents
也被称为selector stub 存根函数
相同_objc_msgSend
本身也有一个存根函数写法
这样一来咱们现在就有两个存根函数
- _objc_msgSend$dateFromComponents:
- _objc_msgSend:
这两个函数封装了一些通用的东西,共享了最多的代码,使代码尽或许的小,可是这样带来的缺乏是我需求连着两个bl跳转,这对操作体系来说开支较大。所以为了平衡包体积和功能,咱们能够运用下面这种办法来提高这一点。咱们能够把前面调用的两个存根函数封装成一个(都封装成_objc_msgSend$dateFromComponents),这样,咱们能够使代码更紧凑,不需求那么多调用。如下图这样
这就回到了之前的问题,你能够通过_objc_stubs_small
符号了只下降包巨细,或许采用默认的办法让体系主动平衡,两者的区别在汇编层面就体现在如下图
综上:这便是Meesage send 占用从 12 bytes 下降到 8 bytes和二进制巨细下降12%的原因
Retain and release
这个优化是苹果这边使Retain and release的开支更小,苹果的说法是Retain and release的调用开支从8字节下降到4字节,一起包体积也会有2%的优化
咱们知道ARC相比于MRC是开发者不需求再写retain、release这些代码,其实并不是不需求,而是编译器帮咱们主动在需求的位置刺进了这些代码,所以换句话说他们仍是存在的,只是你看不到也不必在关心他们。
仍是拿之前的比方来说
// Retain/release calls inserted by ARC
NScalendar *cal = [self makeCalendar]; // bl _objc_retain
NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; // bl _objc_retain
dateComponents.year = 2022;
dateComponents.month = 2022;
dateComponents.day = 2022;
NSDate *theDate = [cal dateFromComponents: dateComponents]; // bl _objc_retain
return theDate;
// bl _objc_release
// bl _objc_release
// bl _objc_release
在变量创立的时分咱们运用retain来添加的他的引用计数不被毁掉,在办法完毕后咱们运用release来毁掉不需求的变量,这也是iOS的内存管理机制。在ARC下这些都是编译器咱们刺进的代码,咱们无需关心。
retain和release都是C言语的函数,他们带着一个参数便是被操作的目标,一起他遵从C言语的ABI,所以当你调用这些办法的时分体系还会为你做一些额定的事情,比方下图中的mov操作,而这些操正是咱们优化的用武之地,通过自界说调用重新约好 retain/release 接口,咱们能够依据目标指针的位置,适当的运用正确的变量,这样就能够不必移动它。简单的说,便是修正了底层 ABI
。
咱们是怎样做的优化呢?看下之前的流程,咱们用下面这行代码举例
objc_release(dateComponents);
// mov x0, x20 耗费4 字节
// bl _objc_release 耗费4字节
流程为
- 先履行 mov 把副本地址(X20,也便是目标的地址)存到寄存器 x0
- 然后bl跳转到
_objc_release
函数进行开释
依据之前讲的每个指令耗费4字节,所以这儿耗费8字节
咱们修正ABI之后其省掉调用mov指令 然后原本跳转到_objc_release函数 改为跳转到_objc_release_x20
函数,而mov的指令放到C言语更底层的ABI里面去做,你能够理解为咱们封装了一个新的retain、release函数,你只需传入一个寄存器地址我就去更底层的当地完结mov操作,所以功率更高了
。现在因为只用履行一条指令,所以内存耗费为4字节。现在的流程看起来为
这么看来咱们代码里很多的release和retain 都通过这样的样的优化所以整体的二进制包下降2% 一起调用内存耗费游8字节变为4字节,一起ABI 接口修正,去除冗余 mov 指令调用,下沉到 ABI。因为 ABI 是内嵌体系
,这儿新增 mov 指令占用能够忽略不计。
Apple 果然是坚持用户体会优先,为了更好体会不惜修正 c 的 ABI
Autorelease elision(主动开释省掉优化)
iOS中除了运用release之外还有另一个 便是autorelease主动开释机制,相同在这个当地苹果也做了主动开释省掉的优化让主动开释机制功率更高。咱们来看下面这个比方
// Return Value Autoreleases
theWWDCDate = [event getWWDCDate];
-(NSDate*)getWWDCDate {
...
return theDate;
}
创立一个暂时目标(theDate),并将其回来给调用方(event)。getWWDCDate()
办法中回来暂时的 theDate,然后调用完结(回来 theDate 之后,getWWDCDate 就调用完结)。这时调用方(event)将其保存到自己的变量中(theWWDCDate 中)。
依据体系刺进retain和release的机制来说应该是这样的,可是显着retain处不能进行release,因为我需求吧theDate回来回去,假如这儿开释了我就没办法呢回来了。
因此,为了处理上述问题,需求运用一个特别的约好用来回来这个暂时回来值。这就引入了Autorelease,这样调用者能够 retain 它。autorelease 在这儿确保在调用方能够正常回来该值,而不被提早开释,延长开释生命周期。你之前或许看到过 autorelease 和 autoreleasePools:其实这是一种将 release 操作推迟到稍后某个时刻的办法。所以上面的代码改为Autorelease
// Return Value Autoreleases
theWWDCDate = [[event getWWDCDate] retain];
-(NSDate*)getWWDCDate {
...
return [theDate autorelease];
}
体系并不知道他在什么时分会被开释,横竖只需不在retain的时分开释就行,所以我在retain的时分先打个符号,符号他之后或许会被开释。可是这样的操作现在会带来一些开支,其实便是我尽管打了release符号,可是我分明一会还要retain,没必要多此一举
,所以基于此咱们之前引入了Autorelease elision
来削减这部分开支(假如Autorelease后紧接一个retain我就都不做了
)。咱们先从汇编层面看下Autorelease elision做了什么
提炼出以下代码
// What the compiler emits
bl _getWWDCDate
mov x29, x29
bl _objc_retainAutoreleasedReturnValue
b _objc_autoreleaseReturnValue // autorelease -> runtime -> _objc_autoreleaseReturnValue
其实便是以下过程
- 当咱们回来值调用Autorelease时分体系会调用
_objc_autoreleaseReturnValue
来回来一个autoreleased value
- 履行Autorelease后编译器会添加个符号
mov x29, x29
而这句指令在实际运转中这个指令会变为二进制的方式变为0xAA1D03FD
- 后续的操作就运转时会先判别是否有对应的符号
0xAA1D03FD
,假如有,这意味着编译器告知runtime, 咱们将回来一个已经被符号,可是将立即被持有(retain) 的暂时变量,后面就不需求再retain操作了
static ALWAYS_INLINE bool
callerAcceptsOptimizedReturn(const void *ra)
{
// fd 03 1d aa mov fp, fp
// arm64 instructions are well-aligned
if (*(uint32_t *)ra == 0xaa1d03fd) {
return true;
// 回来true 需求优化 把release、rentain删掉
}
return false;
}
说白了便是在回来值身上调用objc_autoreleaseReturnValue
办法时,runtime将这个回来值object符号(储存在TLS中),然后直接回来这个object(不调用autorelease);一起,在外部接收这个回来值的objc_retainAutoreleasedReturnValue
里,发现有之前的符号(TLS中正好存了这个目标),那么直接回来这个object(清楚之前的符号且不再调用retain)。
注意:TLS相关的含义能够参考这儿
可是这儿有一个问题,以二进制的方式来加载代码并不是很常见,并且咱们不但要加载它还要比较他尤其在CPU上并不是最优策略,所以这儿仍是有开支的,因此咱们看下怎么优化。
相同履行流程,当履行完_objc_autoreleaseReturnValue
函数时分咱们会获得一个回来地址,这个地址是一个指针,指向了被符号为Autorelease的目标。然后代码持续履行到_objc_retainAutoreleasedReturnValue
这儿要进行reatin,而被reatain的变量地址咱们也能够拿到,所以只需比较这两个指针即可,这样一来咱们也不再需求mov操作
优化点
- 把原来的比较二进制数据改为比较指针。速度更快功率更高
- 削减mov指令 削减4字节,二进制巨细估计下降2%
总结
这便是Xcode14+iOS16的编译期间优化,能够看出苹果也在帮咱们完结OKR削减包体积,进步发动速度,添加代码履行功率,一起也能看出苹果在追求极致用户体会道路上所做的事情。本文部分翻译自Improve app size and runtime performance,一起也添加了自己的考虑。
参考资料
2021年十大漏洞使用 – 网安 (wangan.com)
ARM指令浅析
EarlGrey 源码阅览(一) | SeanChense TLS
Autorelease 的回来值优化 | SeanChense
Improve app size and runtime performance – WWDC22 – Videos – Apple Developer
WWDC22 笔记 (ming1016.github.io)
重学OC第十篇:dyld加载
知道 dyld :动态链接器