iOS - 冷启动优化

标签: ios 冷启动 优化 | 发表时间:2022-03-25 06:30 | 作者:PolarBear
出处:https://juejin.cn/tag/%E6%9E%B6%E6%9E%84

随着App不断迭代,使的业务模块增加,逻辑变得复杂,集成了更多的第三方库,App 启动也会越来越慢,因此我们希望能在业务扩张的同时,保持较好的启动速度,给用户带来良好的体验。

一、名词概念理论

为了更准确地了解 App 冷启动的流程,我们需要掌握一些基本的概念

1.1.Mach-O

Mach-O(Mach Object File Format)是一种用于记录可执行文件、对象代码、共享库、动态加载代码和内存转储的文件格式。App 编译生成的二进制 可执行文件就是 Mach-O 格式的,iOS 工程所有的类编译后会生成对应的目标文件 .o 文件,而这个可执行文件就是这些 .o 文件的集合。

在 Xcode 的控制台输入以下命令,可以打印出运行时所有加载进应用程序的 Mach-O 文件。

  image list -o -f

Mach-O 文件主要由三部分组成:

  • Mach header:描述 Mach-O 的 CPU 架构、文件类型以及加载命令等;
  • Load commands:描述了文件中数据的具体组织结构,不同的数据类型使用不同的加载命令;
  • Data:Data 中的每个段(segment)的数据都保存在这里,每个段都有一个或多个 Section,它们存放了具体的数据与代码,主要包含这三种类型:

    • __TEXT 包含 Mach header,被执行的代码和只读常量(如C 字符串)。只读可执行(r-x)。
  • __DATA 包含全局变量,静态变量等。可读写(rw....)。
  • __LINKEDIT 包含了加载程序的 元数据,比如函数的名称和地址。只读(r...)。

1.2.dylib

dylib 也是一种 Mach-O 格式的文件,后缀名为 .dylib 的文件就是动态库(也叫动态链接库)。动态库是运行时加载的,可以被多个 App 的进程共用。

如果想知道 TestDemo 中依赖的所有动态库,可以通过下面的指令实现:

  otool -L /TestDemo.app/TestDemo

动态链接库分为 系统 dylib内嵌 dylib(embed dylib,即开发者手动引入的动态库)。系统 dylib 有:

  • iOS 中用到的所有系统 framework,比如 UIKit、Foundation;
  • 系统级别的 libSystem(如 libdispatch(GCD) 和 libsystem_blocks(Block));
  • 加载 OC runtime 方法的 libobjc;

1.2.1.dyld

dyld(Dynamic Link Editor):动态链接器,其本质也是 Mach-O 文件,一个专门用来加载 dylib 文件的库。 dyld 位于 /usr/lib/dyld,可以在 mac 和越狱机中找到。dyld 会将 App 依赖的动态库和 App 文件加载到内存后执行。

1.2.2.dyld shared cache

dyld shared cache 就是动态库共享缓存。当需要加载的动态库非常多时,相互依赖的符号也更多了,为了节省解析处理符号的时间,OS X 和 iOS 上的动态链接器使用了共享缓存。OS X 的共享缓存位于 /private/var/db/dyld/,iOS 的则在 /System/Library/Caches/com.apple.dyld/

当加载一个 Mach-O 文件时,dyld 首先会检查是否存在于共享缓存,存在就直接取出使用。每一个进程都会把这个共享缓存映射到了自己的地址空间中。这种方法大大优化了 OS X 和 iOS 上程序的启动时间。

1.2.3.dyld shared cache

dyld shared cache 就是动态库共享缓存。当需要加载的动态库非常多时,相互依赖的符号也更多了,为了节省解析处理符号的时间,OS X 和 iOS 上的动态链接器使用了共享缓存。OS X 的共享缓存位于 /private/var/db/dyld/,iOS 的则在 /System/Library/Caches/com.apple.dyld/

当加载一个 Mach-O 文件时,dyld 首先会检查是否存在于共享缓存,存在就直接取出使用。每一个进程都会把这个共享缓存映射到了自己的地址空间中。这种方法大大优化了 OS X 和 iOS 上程序的启动时间。

1.2.4.images

images 在这里不是指图片,而是 镜像。每个 App 都是以 images 为单位进行加载的。images 类型包括:

  • executable:应用的二进制可执行文件;
  • dylib:动态链接库;
  • bundle:资源文件,属于不能被链接的 dylib,只能在运行时通过  dlopen() 加载。

1.2.5.framework

framework 可以是动态库,也是静态库,是一个包含 dylib、bundle 和头文件的文件夹。

二、冷启动相关(首页为原生)

当用户按下 home 键,iOS App 不会立刻被 kill,而是存活一段时间,这段时间里用户再打开 App,App 基本上不需要做什么,就能还原到退到后台前的状态。我们把 App 进程还在系统中,无需开启新进程的启动过程称为 热启动

冷启动则是指 App 不在系统进程中,比如设备重启后,或是手动杀死 App 进程,又或是 App 长时间未打开过,用户再点击启动 App 的过程,这时需要创建一个新进程分配给 App。我们可以将冷启动看作一次完整的 App 启动过程,本文讨论的就是冷启动的优化。

1.冷启动:

1.1冷启动的出处

WWDC 2016 中首次出现了 App 启动优化的话题,其中提到:

  • App 启动最佳速度是400ms以内,因为从点击 App 图标启动,然后 Launch Screen 出现再消失的时间就是400ms;
  • App 启动最慢不得大于20s,否则进程会被系统杀死;(启动时间最好以 App 所支持的最低配置设备为准。)

1.1.1关于冷启动的两种说法:

说法一:

冷启动的整个过程是指从用户唤起 App 开始到 AppDelegate 中的 didFinishLaunchingWithOptions 方法执行完毕为止,并以执行 main() 函数的时机为分界点,分为 pre-mainmain() 两个阶段。

说法二:

也有一种说法是将整个冷启动阶段以主 UI 框架的 viewDidAppear 函数执行完毕才算结束。这两种说法都可以,前者的界定范围是 App 启动和初始化完毕,后者的界定范围是用户视角的启动完毕,也就是首屏已经被加载出来。

注意:这里很多文章都会把第二个阶段描述为 main 函数之后,个人认为这种说法不是很好,容易让人误解。要知道 main 函数在 App 运行过程中是不会退出的,无论是 AppDelegate 中的 didFinishLaunchingWithOptions 方法还是 ViewController 中的 viewDidAppear 方法,都还是在 main 函数内部执行的。

1.2.pre-main 阶段

pre-main 阶段指的是从用户唤起 App 到 main() 函数执行之前的过程。

1.2.1查看阶段耗时(以xcode13为‘分水岭’)

1.2.1.1. Xcode13之前

1.我们可以在 Xcode 中配置环境变量

Product -> Edit Scheme -> Run -> Arguments ->Environment Variables -> +

DYLD_PRINT_STATISTICS 设置为 1

image.png

image.png 这时在 iOS 10 以上系统中运行这个 Demo, pre-main 阶段的启动时间会在控制台中打印出来(备注:本人x-code 已经升级到13.3,无法打印出日志)

如果要更详细的信息,就设置 DYLD_PRINT_STATISTICS_DETAILS 为 1。

1.2.1.2在Xcode13之后上面的方法就失效了

可以采用下面的方法

代码贴出来如下

  #import <Foundation/Foundation.h>



NS_ASSUME_NONNULL_BEGIN



 @interface AppLaunchTime : NSObject



+ (void)mark;



 @end
  #import "AppLaunchTime.h"

#import <sys/sysctl.h>

#import <mach/mach.h>

 @implementation AppLaunchTime



double __t1; // 创建进程时间
double __t2; // before main
double __t3; // didfinsh

/// 获取进程创建时间

+ (CFAbsoluteTime)processStartTime 
{
  if (__t1 == 0) 
  {
    struct kinfo_proc procInfo;
    int pid = [[NSProcessInfo processInfo] processIdentifier];
    int cmd[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
    size_t size = sizeof(procInfo);
    if (sysctl(cmd, sizeof(cmd)/sizeof(*cmd), &procInfo, &size, NULL, 0) == 0) {
      __t1 = procInfo.kp_proc.p_un.__p_starttime.tv_sec * 1000.0 + procInfo.kp_proc.p_un.__p_starttime.tv_usec / 1000.0;
    }
  }
  return __t1;
}

/// 开始记录:在DidFinish中调用
+ (void)mark 
{
  double __t1 = [AppLaunchTime processStartTime];
  dispatch_async(dispatch_get_main_queue(), ^{ // 确保didFihish代码执行后调用
    if (__t3 == 0) 
    {
      __t3 = CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970;
    }

    double pret = __t2 - __t1 / 1000;
    double didfinish = __t3 - __t2;
    double total = __t3 - __t1 / 1000;

    NSLog(@"----------App启动---------耗时:pre-main:%f",pret);
    NSLog(@"----------App启动---------耗时:didfinish:%f",didfinish);
    NSLog(@"----------App启动---------耗时:total:%f",total);
  });
}
// 构造方法在main调用前调用

// 获取pre-main()阶段的结束时间点相对容易,可以直接取main()主函数的开始执行时间点.推荐使用__attribute__((constructor)) 构建器函数的被调用时间点作为pre-main()阶段结束时间点:__t2能最大程度实现解耦:

void static __attribute__ ((constructor)) before_main() 
{
  if (__t2 == 0) 
  {
    __t2 = CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970;
  }
}

代用运行打印

  - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

  [AppLaunchTime mark];

  return YES;

}

日志打印

  冷启动优化[3454:116391] ----------App启动---------耗时:pre-main:0.718716
冷启动优化[3454:116391] ----------App启动---------耗时:didfinish:0.028895
冷启动优化[3454:116391] ----------App启动---------耗时:total:0.747611

1.2.2.启动过程分析与优化

1.2.2.1.总体启动过程分析

启动一个应用时,系统会通过 fork() 方法来新创建一个进程,然后执行镜像通过 exec() 来替换为另一个可执行程序,然后执行如下操作:

  1. 把可执行文件加载到内存空间,从可执行文件中能够分析出 dyld 的路径;
  2. 把 dyld 加载到内存;
  3. dyld 从可执行文件的依赖开始,递归加载所有的依赖动态链接库 dylib 并进行相应的初始化操作。

结合上面 pre-main 打印的结果,我们可以大致了解整个启动过程如下图所示:

image.png

1.2.2.2.Load Dylibs

这一步,指的是 动态库加载。在此阶段,dyld 会:

  1. 分析 App 依赖的所有 dylib;
  2. 找到 dylib 对应的 Mach-O 文件;
  3. 打开、读取这些 Mach-O 文件,并验证其有效性;
  4. 在系统内核中注册代码签名;
  5. 对 dylib 的每一个 segment 调用  mmap()

一般情况下,iOS App 需要加载 100-400 个 dylibs。这些动态库包括系统的,也包括开发者手动引入的。其中大部分 dylib 都是系统库,系统已经做了优化,因此开发者更应关心自己手动集成的内嵌 dylib,加载它们时性能开销较大。

App 中依赖的 dylib 越少越好,Apple 官方建议尽量将内嵌 dylib 的个数维持在6个以内。

优化方案

  • 尽量不使用内嵌 dylib;
  • 合并已有内嵌 dylib;
  • 检查 framework 的  optional 和  required 设置,如果 framework 在当前的 App 支持的 iOS 系统版本中都存在,就设为  required,因为设为  optional 会有额外的检查;
  • 使用静态库作为代替;(不过静态库会在编译期被打进可执行文件,造成可执行文件体积增大,两者各有利弊,开发者自行权衡。)
  • 懒加载 dylib。(但使用  dlopen() 对性能会产生影响,因为 App 启动时是原本是单线程运行,系统会取消加锁,但  dlopen() 开启了多线程,系统不得不加锁,这样不仅会使性能降低,可能还会造成死锁及未知的后果,不是很推荐这种做法。)
1.2.2.3.Rebase/Binding

这一步,做的是 指针重定位

在 dylib 的加载过程中,系统为了安全考虑,引入了 ASLR(Address Space Layout Randomization)技术和代码签名。由于 ASLR 的存在,镜像会在新的随机地址(actual_address)上加载,和之前指针指向的地址(preferred_address)会有一个偏差(slide,slide=actual_address-preferred_address),因此 dyld 需要修正这个偏差,指向正确的地址。具体通过这两步实现:

第一步: Rebase,在 image 内部调整指针的指向。将 image 读入内存,并以 page 为单位进行加密验证,保证不会被篡改,性能消耗主要在 IO。

第二步: Binding,符号绑定。将指针指向 image 外部的内容。查询符号表,设置指向镜像外部的指针,性能消耗主要在 CPU 计算。

通过以下命令可以查看 rebase 和 bind 等信息:

  xcrun dyldinfo -rebase -bind -lazy_bind TestDemo.app/TestDemo

通过 LC_DYLD_INFO_ONLY 可以查看各种信息的偏移量和大小。如果想要更方便直观地查看,推荐使用 MachOView 工具。

指针数量越少,指针修复的耗时也就越少。所以,优化该阶段的关键就是减少 __DATA 段中的指针数量。

优化方案

  • 减少 ObjC 类(class)、方法(selector)、分类(category)的数量,比如合并一些功能,删除无效的类、方法和分类等(可以借助 AppCode 的 Inspect Code 功能进行代码瘦身);
  • 减少 C++ 虚函数;(虚函数会创建 vtable,这也会在  __DATA 段中创建结构。)
  • 多用 Swift Structs。(因为 Swift Structs 是静态分发的,它的结构内部做了优化,符号数量更少。)
1.2.2.4.ObjC Setup

完成 Rebase 和 Bind 之后,通知 runtime 去做一些代码运行时需要做的事情:

  • dyld 会注册所有声明过的 ObjC 类;
  • 将分类插入到类的方法列表中;
  • 检查每个 selector 的唯一性。

优化方案

Rebase/Binding 阶段优化好了,这一步的耗时也会相应减少。

1.2.2.5.Initializers

Rebase 和 Binding 属于静态调整(fix-up),修改的是 __DATA 段中的内容,而这里则开始动态调整,往堆和栈中写入内容。具体工作有:

  • 调用每个 Objc 类和分类中的  +load 方法;
  • 调用 C/C++ 中的构造器函数(用  attribute((constructor)) 修饰的函数);
  • 创建非基本类型的 C++ 静态全局变量。

优化方案

  • 尽量避免在类的  +load 方法中初始化,可以推迟到  +initiailize 中进行;(因为在一个  +load 方法中进行运行时方法替换操作会带来 4ms 的消耗)
  • 避免使用  __atribute__((constructor)) 将方法显式标记为初始化器,而是让初始化方法调用时再执行。比如用  dispatch_once()pthread_once() 或  std::once(),相当于在第一次使用时才初始化,推迟了一部分工作耗时。:
  • 减少非基本类型的 C++ 静态全局变量的个数。(因为这类全局变量通常是类或者结构体,如果在构造函数中有繁重的工作,就会拖慢启动速度)

总结一下 pre-main 阶段可行的优化方案:

  • 重新梳理架构,减少不必要的内置动态库数量
  • 进行代码瘦身,合并或删除无效的ObjC类、Category、方法、C++ 静态全局变量等
  • 将不必须在  +load 方法中执行的任务延迟到  +initialize 中
  • 减少 C++ 虚函数

1.3.main() 阶段

对于 main() 阶段,主要测量的就是从 main() 函数开始执行到 didFinishLaunchingWithOptions 方法执行结束的耗时。

1.3.1.查看阶段耗时

这里介绍两种查看 main() 阶段耗时的方法。

方法一:手动插入代码,进行耗时计算。

第一步:在 main() 函数里用变量 MainStartTime 记录当前时间

  #import <UIKit/UIKit.h>

#import "AppDelegate.h"



CFAbsoluteTime MainStartTime;



int main(int argc, char * argv[]) 

{

  NSString * appDelegateClassName;

  MainStartTime = CFAbsoluteTimeGetCurrent();

  

  @autoreleasepool {

    appDelegateClassName = NSStringFromClass([AppDelegate class]);

  }

  return UIApplicationMain(argc, argv, nil, appDelegateClassName);

}

第二步:在 AppDelegate.m 文件中用 extern 声明全局变量

第三步:在 didFinishLaunchingWithOptions 方法结束前,再获取一下当前时间,与 MainStartTime 的差值就是 main() 函数阶段的耗时

  #import "AppDelegate.h"

#import "AppLaunchTime.h"

extern CFAbsoluteTime MainStartTime;

 @interface AppDelegate ()

 @end



 @implementation AppDelegate



- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

  

  [AppLaunchTime mark];

   

  double mainLaunchTime = (CFAbsoluteTimeGetCurrent() - MainStartTime);

  NSLog(@"main() 阶段耗时:%.2fms", mainLaunchTime * 1000);

   

  return YES;

}



//日志:

 //冷启动优化[3616:126842] main() 阶段耗时:21.92ms
方法二:借助 Instruments 的 Time Profiler 工具查看耗时。

打开方式为: Xcode → Open Developer Tool → Instruments → Time Profiler

image.png 操作步骤:

  1. 配置 Scheme。点击  Edit Scheme 找到  Profile 下的  Build Configuration,设置为  Debug

image.png 2. 配置 PROJECT。点击 PROJECT,在  Build Settings 中找到  Build Options 选项里的  Debug Information Format,把  Debug 对应的值改为  DWARF with dSYM File

image.png 3. 启动 Time Profiler,点击左上角红色圆形按钮开始检测,然后就可以看到执行代码的完整路径和对应的耗时。

为了方面查看应用程序中实际代码的执行耗时和代码路径实际所在的位置,可以勾选上 Call Tree 中的 Separate ThreadHide System Libraries

image.png

1.3.2.启动优化

main() 被调用之后, didFinishLaunchingWithOptions 阶段,App 会进行必要的初始化操作,而 viewDidAppear执行结束之前则是做了首页内容的加载和显示。

关于 App 的初始化,除了统计、日志这种须要在 App 一启动就配置的事件,有一些配置也可以考虑延迟加载。如果你在 didFinishLaunchingWithOptions 中同时也涉及到了 首屏的加载,那么可以考虑从这些角度优化:

  • 用纯代码的方式,而不是 xib/Storyboard,来加载首页视图
  • 延迟暂时不需要的二方/三方库加载;
  • 延迟执行部分业务逻辑和 UI 配置;
  • 延迟加载/懒加载部分视图;
  • 避免首屏加载时大量的本地/网络数据读取;
  • 在 release 包中移除 NSLog 打印;
  • 在视觉可接受的范围内,压缩页面中的图片大小;
  • ……

三.首页为H5 页面优化

可参考 VasSonic 的原理 Sonic是腾讯团队研发的一个轻量级的高性能的Hybrid框架,专注于提升页面首屏加载速度

  • 终端耗时

    • webView 预加载:在 App 启动时期预先加载了一次 webView,通过创建空的 webView,预先启动 Web 线程,完成一些全局性的初始化工作,对二次创建 webView 能有数百毫秒的提升。
  • 页面耗时(静态页面)

    • 静态直出:服务端拉取数据后通过 Node.js 进行渲染,生成包含首屏数据的 HTML 文件,发布到 CDN 上,webView 直接从 CDN 上获取;
  • 离线预推:使用离线包。
  • 页面耗时(经常需要动态更新的页面)

    • 并行加载:WebView 的打开和资源的请求并行;
  • 动态缓存:动态页面缓存在客户端,用户下次打开的时候先打开缓存页面,然后再刷新;
  • 动静分离:将页面分为静态模板和动态数据,根据不同的启动场景进行不同的刷新方案;
  • 预加载:提前拉取需要的增量更新数据。

四.总结

冷启动本就是一个比较复杂的流程,它的优化没有固定的方式,我们需要结合业务,配合一些性能分析工具和线上监控日志,灵活应用

相关 [ios 冷启动 优化] 推荐:

iOS - 冷启动优化

- - 掘金 架构
随着App不断迭代,使的业务模块增加,逻辑变得复杂,集成了更多的第三方库,App 启动也会越来越慢,因此我们希望能在业务扩张的同时,保持较好的启动速度,给用户带来良好的体验. 为了更准确地了解 App 冷启动的流程,我们需要掌握一些基本的概念. Mach-O(Mach Object File Format)是一种用于记录可执行文件、对象代码、共享库、动态加载代码和内存转储的文件格式.

[IOS]iOS App性能优化

- - 操作系统 - ITeye博客
iOS App的性能关注点. 虽然iPhone的机能越来越好,但是app的功能也越来越复杂,性能从来都是移动开发的核心关注点之一. 我们说一个app性能好,不是简单指感觉运行速度快,而应该是指应用启动快速、UI反馈响应及时、列表滚动操作流畅、内存使用合理,当然更不能随随便便Crash啦. 工程师开发应用时除了在设计上要避免性能“坑”的出现,在实际遇到“坑”时也要能很快定位原因所在.

最新进展:Serverless 冷启动优化技术

- - IT瘾-dev
作者 | 华为云 Serverless 团队策划 | 褚杏娟问题背景 Serverless 计算也称服务器无感知计算或函数计算,是近年来一种新兴的云计算编程模式. 其致力于大幅简化云业务开发流程,使得应用开发者从繁杂的服务器运维工作中解放出来(例如自动伸缩、日志和监控等). 借助 Serverless 计算,开发者仅需上传业务代码并进行简单的资源配置便可实现服务的快速构建部署,云服务商则按照函数服务调用量和实际资源使用收费,从而帮助用户实现业务的快速交付 (fast built & Relia.

中国大学 MOOC Android 性能优化:冷启动优化总结

- - 有道技术沙龙博客
联系我们: 有道技术团队助手:ydtech01 / 邮箱:[email protected]. 本文的重点在于如何定量的排查冷启动过程中的耗时操作,并提供对应的优化思路和实践方法总结. 同时本文涉及到的冷启动优化主要涵盖两个方面:Application 的性能优化和 Launcher Activity 的性能优化.

iOS性能优化十三招

- - CSDN博客推荐文章
iOS应用是非常注重用户体验的,不光是要求界面设计合理美观,也要求各种UI的反应灵敏,我相信大家对那种一拖就卡卡卡的 TableView 应用没什么好印象. 还记得12306么,那个速度,相信大家都受不了. 为了提高 iOS 的运行速度,下面我将抛砖引玉介绍一些我实践过的用来提供iOS程序运行效率的方法,与大家分享,希望能得到更多的反馈和建议.

iOS性能优化过程浅析

- - 极客521 | 极客521
这一系列文章是我的读书笔记,整理一下,也算是温故而知新. 在以上的四个步骤中循环反复,直到问题解决. Profile!不要猜!. 不要做无用功:不要在启动时花几百ms来做logging,不要为同样的数据做多次查询. 试图重用:对于创建过程昂贵的对象,要重用而不是重新创建. Table View的cell.

Music Beta by Google 推出针对 iOS 设备优化的界面

- Wynnsyt - 谷奥——探寻谷歌的奥秘
尽管之前有人发现过让使用Flash的Music Beta by Google在iOS的Safari Mobile浏览器里播放音乐的办法,但毕竟是野路子. 今天Google正式推出了针对iOS设备优化的界面,iPhone和iPad都可以利用浏览器享受到这个优雅的网页应用了. Music Beta by Google的iOS界面功能也很全,在第一次访问的时候会要求你开辟25MB空间留作缓存之用,然后你就可以访问自己的曲库了,播放、搜索、随机播放等都没问题,还可以左右滑动屏幕来切换界面,动画也很流畅.

iOS图片加载速度极限优化—FastImageCache解析

- - bang's blog
FastImageCache是Path团队开发的一个开源库,用于提升图片的加载和渲染速度,让基于图片的列表滑动起来更顺畅,来看看它是怎么做的. iOS从磁盘加载一张图片,使用UIImageVIew显示在屏幕上,需要经过以下步骤:. 从内核缓冲区复制数据到用户空间. 生成UIImageView,把图像数据赋值给UIImageView.

iOS开发那些事--性能优化–内存泄露问题的解决

- - ITeye博客
内存泄漏(Memory Leaks)是当一个对象或变量在使用完成后没有释放掉,这个对象一直占有着这块内存,直到应用停止. 如果这种对象过多内存就会耗尽,其它的应用就无法运行. 这个问题在C++、C和Objective-C的MRR中是比较普遍的问题. 在Objective-C中释放对象的内存是发送release和autorelease消息,它们都是可以将引用计数减1,当为引用计数为0时候,release消息会使对象立刻释放,autorelease消息会使对象放入内存释放池中延迟释放.

「原创译文」iOS 性能优化:Instruments 工具的救命三招

- - SegmentFault 最新的文章
你的 iOS 应用,运行速度靠谱吗. 中枪的同学莫要愁,性能优化咱有妙招. 用 Xcode 自家的调试工具 Instruments,揪出那些堵线程、占内存、耗资源的问题代码,彻底破掉迷局,让应用扬眉吐气. 对于每位 iOS 开发者来说,代码性能是个避不开的话题. 随着项目的扩大和功能的增多,没经过认真调试和优化的代码,要么任性地卡顿运行,要么低调地崩溃了之……结果呢,大家用着不高兴,开发者也不开心.