iOS 14 苹果对 Objective-C Runtime 的优化

概述

Objective-C 是一门古老的语言,诞生于 1984 年,跟随 Apple 一路浮沉,见证了乔布斯创建了 NeXT,也见证了乔布斯重回 Apple 重创辉煌,它用它特立独行的语法,堆砌了 UIKit,AppKit, Foundation 等一个个基石,时间来到 2020 年,面对汹涌的"后浪" Swift,"老前辈" Objective-C 也在发挥着自己的余热,即使面对越来越多阵地失守,唯有“老兵不死,只会慢慢凋亡"才能体现的悲壮。今年,Apple 给 Objective-C Runtime 带来了新的优化,接下来,让我们深入理解这些变化。

类数据结构变化

首先我们先来了解一下二进制类在磁盘中的表示

首先是类对象本身,包含最常访问的信息:指向元类,超类和方法缓存的指针,在类结构之中有指向包含更多数据的结构体class_ro_t的指针,包含了类的名称,方法,协议,实例变量等等编译期确定的信息。其中 ro 表示 read only 的意思。

当类被 Runtime 加载之后,类的结构会发生一些变化,在了解这些变化之前,我们需要知道2个概念:

Clean Memory:加载后不会发生更改的内存块,class_ro_t属于Clean Memory,因为它是只读的。

Dirty Memory:运行时会进行更改的内存块,类一旦被加载,就会变成Dirty Memory,例如,我们可以在 Runtime 给类动态的添加方法。

这里要明确,Dirty MemoryClean Memory要昂贵得多。因为它需要更多的内存信息,并且只要进程正在运行,就必须保留它。对于我们来说,越多的Clean Memory显然是更好的,因为它可以节约更多的内存。我们可以通过分离出永不更改的数据部分,将大多数类数据保留为Clean Memory,如何怎么做的呢?
在介绍优化方法之前,我们先来看一下,在类加载之后,类的结构会变成如何呢?

在类加载到 Runtime 中后会被分配用于读取/写入数据的结构体class_rw_t

Tips:class_ro_t是只读的,存放的是编译期间就确定的字段信息;而class_rw_t是在 runtime 时才创建的,它会先将class_ro_t的内容拷贝一份,再将类的分类的属性、方法、协议等信息添加进去,之所以要这么设计是因为 Objective-C 是动态语言,你可以在运行时更改它们方法,属性等,并且分类可以在不改变类设计的前提下,将新方法添加到类中。

事实证明,class_rw_t会占用比class_ro_t占用更多的内存,在 iPhone 中,我们在系统测量了大约 30MB 的这些class_rw_t结构。应该如何优化这些内存呢?通过测量实际设备上的使用情况,我们发现大约 10% 的类实际会存在动态的更改行为,如动态添加方法,使用 Category 方法等。因此,我们能可以把这部分动态的部分提取出来,我们称之为class_rw_ext_t,所以,结构会变成这个样子。

经过拆分,可以把 90% 的类优化为Clean Memory,在系统层面,取得效果是节省了大约 14MB 的内存,使内存可用于更有效的用途。

Tips:head xxxxx | egrep 'class_rw|COUNT’ 你可以使用此命令来查看 class_rw_t 消耗的内存。xxxx可以替换为需要测量的 App 名称。如:head Mail | egrep 'class_rw|COUNT’\'查看 Mail 应用的使用情况。

相对方法地址

现在,我们来看看 Runtime 的第二处的变化,方法地址的优化。

每个类都包含一个方法列表,以便 Runtime 可以查找和消息发送。结构大概如下图所示:

方法包含了3部分的内容:

  • Selector:方法名称或选择器。选择器是字符串,但是它们是唯一的
  • 方法类型编码:方法类型编码标识(详情可以查看参考链接)
  • IMP:方法实现的函数指针

在 64 位系统中,它们占用了 24 字节的空间

了解了方法的结构之后,我们来看下进程中内存的简化视图

这是一个 64 位的地址空间,其中各种块分别表示了栈,堆以及各种库。我们把焦点放在 AppKit 库中的init方法。

如图所示,图中的3个地址分别为方法的 3 个部分的表示的绝对地址,我们知道,库的地址取决于动态链接库加载之后的位置,ASLR(Address space layout randomization 地址空间布局随机化)的存在,动态链接器需要修正真实的指针地址,这也是一种代价。由于方法实现地址不会脱离当前库的地址范围的特性存在,所以实际上,方法列表并不需要使用 64 位的寻址范围空间。他们只需要能够在自己的库地址中查找引用函数地址即可,这些函数将始终在附近。所以我们可以使用 32 位相对偏移来代替绝对 64 位地址。

现在我们地址将变成这样

这么做有几个优点:

  1. 无论将库加载到内存中的任何位置,偏移量始终是相同的,因此从加载后不需要进行修正指针地址。
  2. 它们可以保存在只读存储器中,这会更加的安全。
  3. 使用 32 位偏移量在 64 位平台上所需的内存量减少了一半。在 iPhone 中我们可以节省约 40MB 的内存大小。

优化后,指针所需的内存占用量可以减少一半。

相对方法地址会引发另外一个问题,那就是在Method Swizzling如何处理呢?众所皆知,Method Swizzling替换的是 2 个方法函数指针指向,方法函数实现可以在任意地方实现,使用了相对偏移地址了之后,这样就无法工作了。
针对Method Swizzling我们使用全局映射表来解决这个问题,在映射表中维护Swizzles方法对应的实现函数指针地址。由于Method Swizzling的操作并不常见,所以这个表不会变得很大,新的Method Swizzling机制如下图。

Tagged Pointer 格式的变化

接下来我们会深入了解 Tagged Pointer 在 ARM CPU 下的格式变化
首先,让我们先来了解下 Tagged Pointer 是什么
Tagged Pointer:一种特殊标记的对象,Tagged Pointer 通过在其最后一个 bit 位设置为特殊标记位,并且把数据直接保存在指针本身中。Tagged Pointer 是一个"伪"对象,使用 Tagged Pointer 有 3 倍的访问速度提升,100 倍的创建、销毁速度提升。

Tips:Advances in Objective-C

在我们查看对象指针时,在 64 位系统中,我们会看到 16 进制地址如0x00000001003041e0,我们把它转换为二进制表示如下图

在 64 位系统中,我们有 64 位可以表示一个对象指针,但是我们通常没有真正使用到所有这些位,由于内存对齐要求的存在,低位始终为0,对象必须始终位于指针大小倍数的地址中。高位也始终为0。实际上我们只是用中间这一部分的位。

因此,我们可以把最低位设置为 1,表示这个对象是一个 Tagged Pointer 对象。设置为 0 则表示为正常的对象

在设置为 1 表示为 Tagged Pointer 对象之后,在最低位之后的 3 位,我们给他赋予类型意义,由于只有 3 位,所以它可以表示 7 种数据类型

OBJC_TAG_NSAtom            = 0,
OBJC_TAG_1                 = 1,
OBJC_TAG_NSString          = 2,
OBJC_TAG_NSNumber          = 3,
OBJC_TAG_NSIndexPath       = 4,
OBJC_TAG_NSManagedObjectID = 5,
OBJC_TAG_NSDate            = 6,
OBJC_TAG_7                 = 7

在剩余的字段中,我们可以赋予他所包含的数据。在 Intel 中,我们 Tagged Pointer 对象的表示如下

OBJC_TAG_7类型的 Tagged Pointer 是个例外,它可以将接下来后 8 位作为它的扩展类型字段,基于此我们可以多支持 256 种类型的 Tagged Pointer,如 UIColors 或 NSIndexSets 之类的对象。

上文中,我们介绍的是在 Intel 中 Tagged Pointer 的表示,在 ARM64 中,我们情况有些变化。

我们使用最高位代表 Tagged Pointer 标识位,最低位 3 位标识 Tagged Pointer 的类型,接下去的位来表示包含的数据(可能包含扩展类型字段),为什么我们使用高位指示 ARM上 的 Tagged Pointer,而不是像 Intel 一样使用低位标记?

它实际是对 objc_msgSend 的微小优化。我们希望 msgSend 中最常用的路径尽可能快。最常用的路径表示普通对象指针。我们有两种不常见的情况:Tagged Pointer 指针和 nil。事实证明,当我们使用最高位时,可以通过一次比较来检查两者。与分别检查 nil 和 Tagged Pointer 指针相比,这会为 msgSend 中的节省了条件分支。

总结

在 2020 年中,Apple 针对 Objective-C 做了三项优化

  • 类数据结构变化:节约了系统更多的内存。
  • 相对方法地址:节约了内存,并且提高了性能。
  • Tagged Pointer 格式的变化:提高了 msgSend 性能。

通过优化,希望大家可以享受 iPhone 更好,更快的使用体验。

Tips:
类结构的数据变更会在最新的 Runtime 版本中体现,实测 MacOS 10.5.5 中已经存在。
相对方法地址的优化在 Xcode developmentTarget > 14 时会自动进行处理。
Tagged Pointer 的变化则会在 iOS 14, MacOS Big Sur, iPadOS 14 上生效。


https://mp.weixin.qq.com/s/vSw98xbpEe4pjtBfqFNGAw

iOS中的内嵌汇编

写一篇在iOS上使用汇编的文章的想法在脑袋里面停留了很久了,但是迟迟没有动手。虽然早前在做启动耗时优化的工作中,也做过通过拦截objc_msgSend并插入汇编指令来统计方法调用耗时的工作,但也只仅此而已。刚好最近的时间项目在做安全加固,需要写更多的汇编来提高安全性(文章内汇编使用指令集为ARM64),也就有了本文

发布于:17天以前  |  72次阅读  |  详细内容 »

不会吧,这也行?iOS后台锁屏监听摇一摇

一般情况下,出于省电、权限、合理性等因素考虑,给人的感觉是很多奇怪的需求安卓可以实现,但是iOS就无法实现!今天要介绍的需求也有这种感觉,就是“当 APP 处于后台或锁屏状态时,依旧可以监听到摇一摇,进而触发某些功能,比如:语音播报”。

发布于:1月以前  |  148次阅读  |  详细内容 »

iOS 稳定性:App 被终止的原因

本次 session 主要内容如下: 介绍了后台应用终止的常见原因,并提供了一些优化建议 介绍了 MetricsKit 提供的在代码中获取诊断和性能数据的方法 介绍了 Xcode Metrics Ogranizer 提供的关于线上用户性能数据的可视化报告

发布于:1月以前  |  187次阅读  |  详细内容 »

Vue中Axios的封装和API接口的管理

在vue项目中,和后台交互获取数据这块,我们通常使用的是axios库,它是基于promise的http库,可运行在浏览器端和node.js中。他有很多优秀的特性,例如拦截请求和响应、取消请求、转换json、客户端防御XSRF等。所以我们的尤大大也是果断放弃了对其官方库vue-resource的维护,直接推荐我们使用axios库。如果还对axios不了解的,可以移步axios文档。

发布于:1月以前  |  172次阅读  |  详细内容 »

iOS 持续集成:更完备的 App Store Connect API

时隔两年 App Store Connect API 有了更新,WWDC 2018 推出了 App Store Connect API ,用于自动化一些 App Store Connect 后台操作。这次更新包含了 app 元数据相关的API,补上了原来缺失的重要一环, 使得几乎可以通过 App Store Connect API 完成 App Store Connect 上的所有操作。今后开发、证书配置、用户管理、测试、发布全流程都可以通过 API 完成。

发布于:1月以前  |  222次阅读  |  详细内容 »

iOS 性能优化:优化 App 启动速度

苹果是一家特别注重用户体验的公司,过去几年一直在优化 App 的启动时间,特别是去年的 WWDC 2019 keynote[1] 上提到,在过去一年苹果开发团队对启动时间提升了 200%

发布于:1月以前  |  195次阅读  |  详细内容 »

让你的应用远离越狱:iOS 14 App Attest 防护功能

当越狱在 iOS 设备第一次流行起来时,iOS 开发人员会尝试各种方法来保护自己的应用程序,以让应用免受盗版等不确定因素的困扰。有许多方法可以做到这一点,包括检查 Cydia 是否存在、检测应用程序是否可读取自身沙箱之外的文件、在检测到调试器时让应用程序崩溃等等。

发布于:1月以前  |  216次阅读  |  详细内容 »

探秘 iOS 14 的 WidgetKit

Widget Extension 提供了 small, medium, large 三个尺寸,不同尺寸可以展示不同的数据、不同的界面,开发者也可以锁定自己APP的 Widget 只有某类尺寸,相同的widget也能重复添加。作为添加在主屏幕上的控件,苹果用了 “At a glance” 来形容 widget ,所以 widget extension 是无法交互的,它能做的只有展示一些信息与点击两个作用,点击后就会引导至app,同时为了性能与耗电量的考虑,Widget extension 也不能展示视频和动态图像。

发布于:1月以前  |  238次阅读  |  详细内容 »

iOS14 Widget 万字指北,先人一步获得顶级流量

2020 年 6 月 22 日,苹果召开了第一次线上的开发者大会 - WWDC20。这次发布会上宣布了ARM架构Mac芯片(拳打Intel)、iOS 14 ATT(脚踢Facebook),可谓是一次载入史册(我是爸爸)的发布会了,当然还发布了被称为下一个顶级流量入口的Widget。踩着八月的尾巴,本次我们就来探究一下Widget。本文会从Widget初窥和Widget开发两个维度和章节来探究一下Widget, 其中初窥章节会带您简单的了解一下Widget,适合应用决策者阅读; 开发章节会带着您一步一步的完成设计开发Widget,适合程序员阅读。

发布于:1月以前  |  325次阅读  |  详细内容 »

OCRunner:完全体的iOS热修复方案

为了能够实现一篇文章的思路:Objective-C源码 -> 二进制补丁文件 ->热更新(具体是哪篇我忘了)。当时刚好开始了oc2mango翻译器的漫漫长路(顺带为了学习编译原理,嘻嘻),等基本完成以后,就开始肝OCRunner:完全兼容struct,enum,系统C函数调用,魔改libffi,生成补丁文件等,尽可能兼容Objective-C,为了做一个直接运行OC的快乐人。

发布于:1月以前  |  269次阅读  |  详细内容 »

iOS APP图标版本化

在我们的项目开发过程中,需要频繁打包给测试人员去测试,有时候我们都不知道测试机上安装的版本是否是最新的,这样会造成很多不必要的麻烦和成本。因此我们需要将buildNumber以水印的方式打在APPIcon上,可以很直观的知道当前是哪一个版本。

发布于:1月以前  |  234次阅读  |  详细内容 »

百度App iOS工程化实践: EasyBox破冰之旅

百度App从单一的搜索工具发展到今天以搜索和Feed流为双引擎的综合性内容消费服务平台,其复杂程度已然不可同日而语矣。 作为一个日活过亿的超级App,业务规模庞大,相关技术人员超过千人,客户端支持主流的移动技术,涉及近百业务方,技术形态复杂,各种组件近三百个,代码百万量级,由此带来的工程化问题是技术团队的一个极大挑战。

发布于:1月以前  |  272次阅读  |  详细内容 »

最多阅读

快速配置 Sign In with Apple 1年以前  |  3466次阅读
开篇 关于iOS越狱开发 1年以前  |  2470次阅读
给数组NSMutableArray排序 1年以前  |  2415次阅读
APP适配iOS11 1年以前  |  2399次阅读
在越狱的iPhone设置上使用lldb调试 1年以前  |  2344次阅读
使用 GPUImage 实现一个简单相机 1年以前  |  2255次阅读
UITableViewCell高亮效果实现 1年以前  |  2247次阅读
App Store 审核指南[2017年最新版本] 1年以前  |  2222次阅读
所有iPhone设备尺寸汇总 1年以前  |  2149次阅读
使用ssh访问越狱iPhone的两种方式 1年以前  |  2073次阅读
关于Xcode不能打印崩溃日志 1年以前  |  2028次阅读
使用ssh 访问越狱iPhone的两种方式 1年以前  |  1910次阅读
UIDevice的简单使用 1年以前  |  1752次阅读
为对象添加一个释放时触发的block 1年以前  |  1713次阅读
使用最高权限操作iPhone手机 1年以前  |  1681次阅读