优化

作者: 生产八哥 | 来源:发表于2021-02-03 12:01 被阅读0次

系统作的内存方面优化

NSString

NSString的类型分为三种:

  • __NSCFConstantString:字符串常量,存储在字符串常量区。引用计数很大。
  • __NSCFString:运行时类型,存储在堆上。引用计数+1.
  • NSTaggedPointerString:小对象,存储在常量区,既包含指针,也包含值。小对象指针不再是简单的地址,而是地址 + 值.优点是占用空间小 节省内存.

taggedPoint小地址是值和指针地址放在了一起,存储在常量区,不进行retain,release管理,能够直接释放回收,效率更高,创建更快,将近100倍。iOS14之后还对taggedPointer进行了混淆。最高位是taggedPointer的类型。

NSString * te = @“12345678912111”;这种方式不论多长,都是__NSCFConstantString类型,存储在常量区。

通过stringWithFormatalloc并且长度小于一定值时才为taggedPoint。

Nonpointer_isa

SideTables即散列表,散列表中主要有引用计数表弱引用表
Nonpointer_isa为非指针类型的isa,可以存储更多类的信息,包括引用计数。当引用计数存储到一定值时,并不会再存储到Nonpointer_isa的位域的extra_rc中,而是会存储到引用计数表中。

id objc_retain(id obj)
{
    if (obj->isTaggedPointerOrNil()) return obj;
    return obj->retain();
}

ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant) 

通过对retain源码的分析可以得知:

当引用计数存储到一定值时,并不会再存储到Nonpointer_isa的位域的extra_rc中,而是除以二(RC_HALF)的部分存储到SideTables 散列表中。

SideTables是一个真机长度8个元素,其他64个长度的hash数组,本质是一个哈希表,集合了数组和链表的长处,增删改查都比较方便,里面存储了SideTable。SideTables的hash键值就是一个对象obj的address。为什么sidetables最多有8张呢,而不是一张呢? 因为所有对象的引用技术全放在一张散列表中不安全,假如只访问一个而可以拿到全部的,并且每次访问都要加锁解锁,对于性能和安全性都不高,所以分了8张表。

retain做了什么:简单回答是计数值加1,更加底层应该这么回答

  1. 首先判断是不是taggedpointer,如果是,就立马返回。因为taggedpointer不需要内存管理,taggedPointer存储在常量区。
  2. 判断如果 不是nonpointerisa直接操作散列表sidetable的引用计数,每次操作散列表会开锁等耗时耗性能,所以表会有多张,为的是更安全。
  3. 如果 是nonpointerisa,表示isa联合体开启了指针优化,isa可以存储更多信息,那么就操作isa.bits中的extra_rc的引用计数+1。
  4. 如果extra_rc满了,就和散列表对半开,各存一半,以提高性能。//这么操作的目的在于提高性能,因为如果都存在散列表中,当需要release-1时,需要去访问散列表,每次都需要开解锁,比较消耗性能。extra_rc存储一半的话,可以直接操作extra_rc即可,不需要操作散列表,性能会提高很多。
  5. alloc创建的nopointerisa对象引用计数为0,包括sideTable, uintptr_t rc = 1 + bits.extra_rc;所以对于alloc来说,是 0+1=1,这也是为什么通过retaincount获取的引用计数为1的原因。alloc创建的对象实际的引用计数为0,其引用计数打印结果为1,是因为在底层rootRetainCount方法中,引用计数默认+1了,但是这里只有对引用计数的读取操作,是没有写入操作的,简单来说就是:为了防止alloc创建的对象被释放(引用计数为0会被释放),所以在编译阶段,程序底层默认进行了+1操作。实际上在extra_rc中的引用计数仍然为0。
  6. 在dealloc流程汇总会判断是否有isa、cxx、关联对象、弱引用表、引用计数表。

引用计数分别保存在isa.extra_rcsidetable中,当isa.extra_rc溢出时,将一半计数转移至sidetable中,而当其下溢时,又会将计数转回。当二者都为空时,会执行释放流程 。


项目可以进行的优化

界面优化:

  • cell数据预加载,数据请求下来后提前计算高度存储在model中,tableview代理中直接赋值高度。如果cell布局用的是autolayout,可以开启tableView的预估高度机制Self-Sizing 。
  • 按需加载:scrollviewwillEndDragging里判断加载的是否是前后三行。
  • 图片渲染:图片最终能展示的都是bitmap位图。不管是 JPEG 还是 PNG 图片,都是一种压缩的位图图形格式。只不过 PNG 图片是无损压缩,并且支持 alpha 通道,而 JPEG 图片则是有损压缩,可以指定 0-100% 的压缩比。解压缩后的图片大小 = 图片的像素宽 * 图片的像素高 * 每个像素所占的字节数 4。将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作,并且它默认是在主线程中执行的。imageView.image = image这一步会做解码,并且是主线程上。在将磁盘中的图片渲染到屏幕之前,必须先要得到图片的原始像素数据,才能执行后续的绘制操作,这就是为什么需要对图片解压缩的原因。所以可以用SDWebImage预解码,异步在子线程强制解压缩。强制解压缩的原理就是对图片进行重新绘制,得到一张新的解压缩后的位图。其中,用到的最核心的函数是 CGBitmapContextCreate。当头像要切圆角时,下载下来后,会在后台线程将头像预先渲染为圆形并单独保存到一个 ImageCache 中去。
  • 复杂界面的渲染可以合成一张图后然后再进行渲染,具体可以参考美团的Graver框架。
  • main函数前后阶段优化

冷启动:系统里没有任何进程的缓存信息,典型的是重启手机后直接启动 App
热启动:如果把 App 进程杀了,然后立刻重新启动,这次启动就是热启动,因为进程缓存还在。

main函数之前的阶段pre-main阶段的启动时间其实就是dyld加载过程的时间。针对main函数之前的启动时间,苹果提供了内建的测量方法,在Edit Scheme -> Run -> Arguments ->Environment Variables添加环境变量 DYLD_PRINT_STATISTICS 设为 1),然后运行就能看到打印耗时的日志了。

Total pre-main time: 2.7 seconds (100.0%)
         dylib loading time: 450.07 milliseconds (15.6%)   主要是`加载`动态库
        rebase/binding time: 210.8 milliseconds (8.5%)  (偏移修正/符号绑定耗时) ALSR rebase /binding符号 (偏移地址+ALSR = 运行时执行地址)   外部`绑定`的动态库越多越耗时
            ObjC setup time:  921.22 milliseconds (33.5%)   (OC类注册的耗时):OC类越多,越耗时。Swift耗时就会少很多。
           initializer time:  1113.94 milliseconds (45.3%)    执行load和构造函数的耗时

pre-main阶段优化建议
苹果官方建议自定义的动态库最好不要超过6个
二进制重排:主要是大项目 二进制重排优化都适用,主要目的是将启动时刻需要调用的方法排列在一起。启动时刻会出现大量的缺页异常PageFault,当进程访问一个虚拟内存Page而对应的物理内存却不存在时,会触发一次缺页中断(Page Fault),分配物理内存,有需要的话会从磁盘mmap读人数据。发生Page Fault的时候线程是被blocked。一般项目会有0.5-1s的page fault。导致Page Fault次数过多的根本原因是启动时刻需要调用的方法,处于不同的Page导致的。因此,我们的优化思路就是:将所有启动时刻需要调用的方法,排列在一起,即放在一个页中,这样就从多个Page Fault变成了一个Page Fault。这就是二进制重排的核心原理。

PageFault.png
利用xcode自带工具Instrument中的SystemTrace就能看到项目的PageFault次数,即图中的File Backed Page In
  • 工程Build Setting打开link map,记录了二进制文件的布局,可以看到方法的执行顺序,路径在.app上一级同级的文件再依次往下找。

  • 打开xcode的Instrument中的system trace,用来展示启动pageFault次数。

  • 工程路径中创建一个.order文件,在这个order文件里排列你想加载的顺序,然后在build setting里搜order file,把.order路径加进去就可以了。

那么那么多方法,哪个方法才算是启动期间调用的呢。在方法的启动耗时中,需要去 Hook objc_msgSend 来达到监控所有 ObjC 方法的目的。
hook objc_msgsend:

  1. 绝大部分Objective C的方法在编译后会走objc_msgSend,所以通过[fishhook](https://github.com/facebook/fishhook) hook这一个C函数即可获得Objective C符号。由于objc_msgSend是变长参数,所以hook代码需要用汇编来实现,对开发人员要求较高。而且也只能拿到OC 和 swift中@objc 后的方法.
  2. 所以用clang插桩来拿到所有方法,做到100%覆盖符号。进入clang官网,会有示例代码.主要是trace_pc_guard追踪各种方法、函数、block等的调用。需要在Build Settings中的Other C Flags,输入-fsanitize-coverage=trace-pc-guard,则Clang就在读代码时候生成中间代码IR时插入一行调用自己函数方法的代码,在xcode中实现对应的函数方法就可以拿到了。这属于汇编插桩
  3. dlfcn.h里的方法dladdr可以根据上面clang的函数方法返回的函数地址来获取到对应的方法名sname

细节可以参考抖音 字节

main阶段之后的优化建议

  • 多线程加载业务逻辑,充分利用多线程。能放后台初始化的放后台,尽量不要占用主线程的启动时间。减少启动初始化的流程,能懒加载的懒加载,能延迟的延迟。
  • 不用的代码和类去除(2w个类大约800毫秒)
  • 尽量主页面不要用xib等。
  • swift尽量使用struct结构体,因为结构体是值类型,保存在栈中,效率比堆上更高。栈空间地址分配的过程中是从高到低的(先分配0x0010再分配0x0001)

瘦身

官方 App Thinning
  • App Thinning 会专门针对不同的设备来选择只适用于当前设备的内容以供下载。大部分工作都是由 Xcode 和 App Store 来帮你完成的,你只需要通过 Xcode 添加 xcassets 目录,然后将图片添加进来即可。
  • Xcode 默认会开启DEAD_CODE_STRIP选项,C/C++/Swift 等静态语言编译器会在 link 的时候移除未使用的代码,但是对于 Objective-C 等动态语言是无效的,因为动态语言是建立在运行时上面的。
  • 编译期优化参数:GCC_OPTIMIZATION_LEVEL定义了 clang 用什么优化等级进行编译优化。 Xcode 默认的 Debug 使用 -O0, Release 使用 -Os。更为激进的-Oz减小相同代码存在多份问题,但是也会使得的函数调用存在更深的调用栈,会影响性能。Oz 的核心原理是对重复的连续机器指令外联成函数进行复用,和“内联函数”的原理正好相反。因此,开启 Oz,能减小二进制的大小,但同时理论上会带来执行效率的额外消耗。对性能(CPU)敏感的代码使用需要评估。
  • 链接期优化参数: LLVM 提供链接期编译优化,通过设置工程中的 Link-Time Optimization 进行控制,其本质是开启生成LTO等优化格式。调试期不建议开启,会增加编译时间。开启 LTO 之后对于 Objc Runtime 需要的一些结构 比如方法签名的 literal string, protocol的结构等有比较大的优化。
己方
  1. 使用频率高且小的图片放到 Asset.car 中,Asset.car 能保证其加载和渲染的速度最优。而大的图片比如背景图之类的,长宽尺寸就有上千个像素,而这种放到 Asset.car 中会大大的增加安装包的大小。
  2. 无用图片资源删除,推荐库LSUnusedResources
  3. 有用图片瘦身:图片大小超过了 100KB,你可以考虑使用 WebP;将图片转成 WebP格式,推荐iSparta.在显示图片时使用 libwebp 进行解析。WebP 在 CPU 消耗和解码时间上会比 PNG 高两倍。所以需要在性能和体积上做取舍。
  4. 对PNG图片无损压缩来优化包大小没有效果的,因为Xcode 会通过自己的压缩算法重新对图片进行处理,只能压缩其尺寸大小。Xcode 中,构建 Asset Catalog 的工具 actool 会首先对 Asset Catalog 中的 png 图片进行解码,得到 Bitmap 数据,然后再运用 actool 的编码压缩算法进行编码压缩处理。无损压缩通过变换图片的编码压缩算法减少大小,但是不会改变 Bitmap 数据。对于 actool 来说,它接收的输入没有改变,所以无损压缩无法优化 Assets.car 的大小。对于放入 Asset.car 中的图片如果图片没有半透明效果,使用 70% 的有损压缩JPEG是一个不错的方式,既能保证图片清晰度的同时获得更小的大小。
  5. 代码瘦身:LinkMap 来获得所有的代码类和方法的信息。Mach-O 文件的 __objc_selrefs、__objc_classrefs 和 __objc_superrefs可以获取用过的方法,类,父类。但是Objective-C 是门动态语言,方法调用可以写成在运行时动态调用,这样就无法收集全所有调用的方法和类,还要二次确认。例如+load方法会被系统调用,但也能检查为未使用类。推荐Appcode工具。最简单的静态分析:基于 otool dump 最终产物中的 __objc_class_list & __objc_class_refs 做差集找到未使用的 Objc 类。
  6. Assets.carMach-O是占用空间最大的两个文件。目前市场上最低支持的 iOS 系统版本一般为 iOS 9。然而,大部分 Pod 库的 Podspec 文件中指定的deployment_target(最低支持版本)由于未及时修改,依然还是 iOS 8,这就导致了这些 Pod 库中指定的 resource_bundles 在构建出 Assets.car 时,是以 iOS 8 为最低支持版本的。统一改成iOS 9这样会多出一些优化空间。
  7. 符号裁剪符号解释
  8. 减少 Block 的使用
    我们知道 Block 是一个特殊的 OC 对象。需要占用部分二进制空间来表征一个 Block 对象。所以在非必要使用 Block 的场景。去掉 Block 实现可以优化不少包大小,常见的比如 Masonry 通过 Block 实现的链式调用。由此可见越是方便开发工作量,对性能就越是一个考验。大部分问题都能转化为空间和时间的取舍问题。
实际用到了但被扫描成无用类:

* 一个类确实没有被其他地方使用, 但是本身逻辑依赖 +load 、+initialize、__attribute__((constructor)) 在启动时调用
* 通过 string 动态调用
* 抽象基类、基类等会被认为是无用类
* 通过运行时动态生成的代码引用了某个类
* 一个类专门作为通知处理类
* MTLModel 等,通过运行时消息机制 assign value 的无法通过 classref 统计
* 典型的 DI 场景。如果一个类声明遵循了某个 Protocol,外部使用的时候使用了这个 Protocol 进行方法调用

实际没用到但被认为有用到:
* 某个对象被另外一个对象引用,但是另外一个对象本身未被使用到。这时候会遗漏掉这个对象所属 Class 的检查

电量

  • CPU:要避免让 CPU 做多余的事情。对于大量数据的复杂计算,应该把数据传到服务器去处理,如果必须要在 App 内处理复杂数据计算,可以通过 GCD 的 dispatch_block_create_with_qos_class 方法指定队列的 Qos 为 QOS_CLASS_UTILITY,将计算工作放到这个队列的 block 里。在 QOS_CLASS_UTILITY 这种 Qos 模式下,系统针对大量数据的计算,以及复杂数据处理专门做了电量优化。
  • I/O:将碎片化的数据磁盘存储操作延后,先在内存中聚合,然后再进行磁盘存储。碎片化的数据进行聚合,在内存中进行存储的机制,可以使用系统自带的 NSCache 来完成。NSCache 是线程安全的,NSCache 会在到达预设缓存空间值时清理缓存,这时会触发 cache:willEvictObject: 方法的回调,在这个回调里就可以对数据进行 I/O 操作,达到将聚合的数据 I/O 延后的目的。I/O 操作的次数减少了,对电量的消耗也就减少了。SDWebImage 图片加载框架,在图片的读取缓存处理时没有直接使用 I/O,而是使用了 NSCache。SDWebImage 将获取的图片数据都放到了 NSCache 里,利用 NSCache 缓存策略进行图片缓存内存的管理。每次读取图片时,会检查 NSCache 是否已经存在图片数据:如果有,就直接从 NSCache 里读取;如果没有,才会通过 I/O 读取磁盘缓存图片。
  • 苹果专门维护了一个电量优化指南,分别从 CPU、设备唤醒、网络、图形、动画、视频、定位、加速度计、陀螺仪、磁力计、蓝牙等多方面因素提出了电量优化方面的建议。大家可以瞅瞅。

卡顿检测

卡顿检测原理是通过子线程对主runloop添加runloopObserver监控即将休眠唤醒两个状态间的时间间隔大于2s左右则认为卡顿,这里确切的说是执行souce和进入休眠两个状态更为精确。这里通过开启一个子线程,用while代码循环持续loop,用定义一个超时2s左右的信号量,超过这个时间往下执行的时候判断信号量若不等于0则认为卡顿,然后将 BeforeSourcesAfterWaiting这两个状态区间上传调用栈,并像微信卡顿监听方案matrix那样利用退火算法,保证重复的卡顿调用栈信息不会被上传。线程数超出64 个时会导致主线程卡顿,如果卡顿是由于线程多造成的,那么就没必要通过获取主线程堆栈去找卡顿原因了,根据 matrix-iOS 的实测,每隔 50 毫秒获取主线程堆栈会增加 3% 的 CPU 占用,可以忽略不计。 卡顿的类型有线程过多、CPU满负荷、绘制过度、IO操作、抢锁

文件 dump:如果内存 dump 的堆栈跟上次捕捉到的不一样,则 dump 到文件中;否则按照斐波那契数列将检查时间递增(1,1,2,3,5,8…)直到没有遇到卡顿或卡顿堆栈不一样。这样能够避免同一个卡顿写入多个文件的情况,也能避免检测线程围着同一个卡顿空转的情况。


离屏渲染

只要裁剪(透明度/阴影)的内容需要画家算法未完成之前的内容参与就会触发offscreenrendering

正常的显示是从帧缓存区FrameBuffer去取,而当我们设置了 cornerRadius 以及 masksToBounds 进行圆角 + 裁剪+阴影+高斯模糊时,如前文所述,masksToBounds 裁剪属性会应用到所有的 sublayer 上。这也就意味着所有的 sublayer 必须要重新被应用一次圆角+裁剪,这也就意味着所有的 sublayer 在第一次被绘制完之后,并不能立刻被丢弃,而必须要被保存在 Offscreen buffer 中等待下一轮圆角+裁剪,注意这时候并没有显示到屏幕上,多个图层都在离屏缓存区等待被一一裁剪圆角(这里是每个图层都要被裁剪,并不只是某个),这也就诱发了离屏渲染。帧缓存区是展示完了就丢弃的。离屏渲染并不都是坏的,因为对于频繁显示的复杂的,离屏会提高性能效率。

相关文章

  • 内存优化

    内存优化、UI优化(布局优化、会只优化)、速度优化(线程优化、网络优化)、启动优化、电量优化 内存优化 内存抖动:...

  • Android进阶之性能优化

    一、性能优化分类 布局优化 绘制优化 内存泄漏优化 响应速度优化 ListView优化 Bitmap优化 线程优化...

  • 性能优化

    内容优化 服务器优化 Cookie优化 CSS优化 javascript优化 图像优化

  • Android开发艺术探索之性能优化笔记

    Android性能优化 一,优化内容 布局优化、绘制优化、内存泄漏优化、响应速度优化、ListView优化、Bit...

  • Android性能优化

    Android性能优化包括布局优化、绘制优化、内存优化、线程优化、响应速度优化、Bitmap优化和ListView...

  • 对于手游的优化

    给手游做优化,无非就CPU性能优化、内存性能优化、资源优化、GPU优化、IO优化、网络优化、耗电优化这些,为此汇总...

  • 网站内部优化的流程介绍

    站内优化:网站本身内部的优化,其中包括代码优化、标签优化、内容优化、url优化等; 站内优化的重要性: 站内优化是...

  • 「性能优化系列」APP内存优化理论与实践

    性能优化系列: 启动优化 内存优化 布局优化 卡顿优化 apk瘦身优化 电量优化项目地址: fuusy/F...

  • Android 性能优化

    app性能优化 android优化分为: 内存优化 UI优化 电量优化 apk瘦身优化 启动优化 下面通过各种百度...

  • 21.性能优化

    关于iOS 性能优化梳理: 基本工具、业务优化、内存优化、卡顿优化、布局优化、电量优化、 安装包瘦身、启动优化、网...

网友评论

    本文标题:优化

    本文链接:https://www.haomeiwen.com/subject/mlmytltx.html