iOS-底层原理 18:类的加载(下)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: iOS-底层原理 18:类的加载(下)

在上一篇文章iOS-底层原理 17:类的加载(上)中,理解了类是如何从Mach-O加载到内存中,这次我们来解释下分类是如何加载中的,以及分类和类搭配使用的情况


分类的本质


前提:在main中定义LGperson的分类LG


image.png

探索分类的本质,有以下三种方式


  • 【方式一】通过clang
  • 【方式二】通过Xcode文档搜索Category
  • 【方式三】通过objc源码搜索 category_t


方式一:通过clang


  • 【方式一】clang -rewrite-objc main.m -o main.cpp 查看底层编译,即 main.cpp,


  • 其中分类的 类型是_category_t
  • 分类的倒数第二个0,表示的是没有协议,所以赋值为0

image.png

搜索struct _category_t,如下所示


  • 其中有两个method_list_t,分别表示实例方法类方法
    image.png

搜索_CATEGORY_INSTANCE_METHODS_LGPerson_,找到其底层实现

image.png

其中有3个方法,格式为:sel+签名+地址,是method_t结构体的属性即key

image.png

搜索method_t,其中对应关系如下


  • name 对应 sel
  • type 对应 方法签名
  • imp 对应 函数地址

image.png

同时,我们发现了一个问题:查看看_prop_list_t,明明分类中定义了属性,但是在底层编译中并没有看到属性,如下图所示,这是因为分类中定义的属性没有相应的set、get方法,我们可以通过关联对象来设置(关于如何设置关联对象,我们将在后续的扩展中进行说明)


image.png


方式二:通过Xcode文档搜索 Category


如果不会clang,可以通过Xcode文档搜索 Category

image.png


方式三:通过objc源码搜索 category_t


还可以通过objc源码搜索category_t类型

image.png


总结


综上所述,分类的本质 是一个_category_t类型


  • 有两个属性:name(类的名称)cls(类对象)
  • 有两个 method_list_t类型的方法列表,表示分类中实现的实例方法+类方法
  • 一个protocol_list_t类型的协议列表,表示分类中实现的协议
  • 一个prop_list_t类型的属性列表,表示分类中定义的属性,一般在分类中添加的属性都是通过关联对象来实现
  • 需要注意的是,分类中的属性是没有set、get方法


分类的加载


前提:创建LGPerson的两个分类:LGA、LGB

image.png在上一篇iOS-底层原理 17:类的加载(上)文章中的realizeClassWithoutSwift -> methodizeClass -> attachToClass -> load_categories_nolock -> extAlloc ->attachCategories中提及了rwe的加载,其中分析了分类的data数据 时如何 加载到中的,且分类的加载顺序是:LGA -> LGB的顺序加载到类中,即越晚加进来,越在前面


其中查看methodizeClass的源码实现,可以发现类的数据分类的数据是分开处理的,主要是因为在编译阶段,就已经确定好了方法的归属位置(即实例方法存储在中,类方法存储在元类中),而分类是后面才加进来的

image.png

其中分类需要通过attatchToClass添加到类,然后才能在外界进行使用,在此过程,我们已经知道了分类加载三步骤的后面两个步骤,分类的加载主要分为3步:


  • 分类数据加载时机:根据类和分类是否实现load方法来区分不同的时机
  • attachCategories准备分类数据
  • attachLists分类数据添加到主类


分类的加载时机


下面我们来探索分类数据的加载时机,以主类LGPerson + 分类LGA、LGB 均实现+load方法为例


通过第二步数据准备反推第一步的加载时机


  • 通过上一篇文章我们了解到,在走到attachCategories方法时,必然会有分类数据的加载,可以通过反推法查看 在什么时候调用attachCategories的,通过查找,有两个方法中调用


load_categories_nolock方法中

image.pngaddToClass方法中,这里经过调试发现,从来不会进到if流程中,除非加载两次,一般的类一般只会加载一次

image.png

不加任何断点,运行objc代码,可以得出以下打印日志,通过日志可以发现addToClass方法的下一步就是load_categories_nolock方法就是加载分类数据

image.png


全局搜索load_categories_nolock的调用,有两次调用


  • 一次在loadAllCategories方法中

image.png

一次在_read_images方法中

image.png

但是经过调试发现,是不会走_read_images方法中的if流程的,而是走的loadAllCategories方法中的

image.png

全局搜索查看loadAllCategories的调用,发现是在load_images时调用的

image.png


通过堆栈信息分析


  • attachCategories中加自定义逻辑的断点,bt查看堆栈信息
    image.png所以综上所述,该情况下的分类的数据加载时机的反推路径为:


  • attachCategories -> load_categories_nolock -> loadAllCategories -> load_images


而我们的分类加载正常的流程的路径为:realizeClassWithoutSwift -> methodizeClass -> attachToClass ->attachCategories


其中正向和反向的流程如下图所示:

image.png

我们再来看一种情况:主类+分类LGA实现+load,分类LGB不实现+load方法


  • 断点定在attachCategories中加自定义逻辑部分,一步步往下执行

image.png

继续往下执行,会再次来到 attachCategories方法中断住


  • p entry.cat
  • p *$0
    image.png


总结只要有一个分类是非懒加载分类,那么所有的分类都会被标记位非懒加载分类,意思就是加载一次 已经开辟了rwe,就不会再次懒加载,重新去处理 LGPerson


分类和类的搭配使用


通过上面的两个例子,我们可以大致将类 和 分类 是否实现+load的情况分为4种


image.png

  • 【情况1】非懒加载类 + 非懒加载分类
  • 【情况2】非懒加载类 + 懒加载分类
  • 【情况3】懒加载类 + 懒加载分类
  • 【情况4】懒加载类 + 非懒加载分类


非懒加载类 与 非懒加载分类


主类实现了+load方法,分类同样实现了+load方法,在前文分类的加载时机时,我们已经分析过这种情况,所以可以直接得出结论,这种情况下


  • 类的数据加载是通过_getObjc2NonlazyClassList加载,即ro、rw的操作,对rwe赋值初始化,是在extAlloc方法中
  • 分类的数据加载是通过load_images加载到类中的


其调用路径为:


  • map_images -> map_images_nolock -> _read_images -> readClass -> _getObjc2NonlazyClassList -> realizeClassWithoutSwift -> methodizeClass -> attachToClass ,此时的mlists是一维数组,然后走到load_images部分
  • load_images --> loadAllCategories -> load_categories_nolock -> load_categories_nolock -> attachCategories -> attachLists,此时的mlists二维数组


下面为源码中调试的打印日志

image.png


非懒加载类 与 懒加载分类


主类实现了+load方法,分类未实现+load方法


  • 打开realizeClassWithoutSwift中的自定义断点,看一下ro

查看kc_ro

image.png

p kc_ro->baseMethodLiimage.png

image.png

image.png

  • 从上面的打印输出可以看出,方法的顺序是 LGB—LGA-LGPerson类,此时分类已经 加载进来了,但是还没有排序,说明在没有进行非懒加载时,通过cls->data读取Mach-O数据时,数据就已经编译进来了,不需要运行时添加进去


  • 来到methodizeClass方法中断点部分


  • p list
  • p $0->get(0)- p $0->get(5)


image.png

来到prepareMethodLists的for循环部分


  • p addedLists[0]
  • p addedLists[1]
  • p *$1
  • p *$2

image.png

来到fixupMethodList方法中的if (sort) {部分


  • 其中SortBySELAddress的源码实现如下:根据名字的地址进行排序
    image.png

走到mlist->setFixedUp();,在读取list

  • p mlist
  • p $7->get(0) ~ p $7->get(3)

    image.png

p $7->get(4) ~ p $7->get(6)

image.png

通过打印发现,仅对同名方法进行了排序,而分类中的其他方法是不需要排序的,其你imp地址是有序的(从小到大) -- fixupMethodList中的排序只针对 name 地址进行排序

image.png

不加任何断点,运行程序,获取打印日志

image.png

总结非懒加载类 与 懒加载分类的数据加载,有如下结论:


  • 类 和 分类的加载是在read_images就加载数据了
  • 其中data数据编译时期就已经完成了


懒加载类 与 懒加载分类


主类和分类均未实现+load方法


  • 不加任何断点,运行程序,获取打印日志

image.png


其中realizeClassMaybeSwiftMaybeRelock是消息流程中慢速查找中有的函数,即在第一次调用消息时才有的函数


  • readClass断住,然后读取kc_ro,即读取整个data

image.png


此时的baseMethodListcount还是16,说明也是从data中读取出来的,所以不需要经过一层缓慢的load_images加载进来


总结懒加载类 与 懒加载分类数据加载是在消息第一次调用时记载


懒加载类 与 非懒加载分类


主类未实现+load方法, 分类实现了+load方法

  • 不加任何断点,运行程序,获取打印日志

image.png

在打印的日志中没有看到load_categories_nolock方法,查看attachCategories -- extAlloc -- attachToClass -- attachCategories,在attachToClass中加断点

image.png

readClass方法中断住,查看kc_ro

image.png


其中baseMethodList的count是8个,打印看看:对象方法3个+属性的setget方法共4个+1个cxx方法 ,即 现在只有主类的数据

image.png


查看kc_ro结构

image.png

image.png

为了调试分类的数据加载, 继续往下执行,bt查看堆栈:load_images -> loadAllCategories -> load_categories_nolock

image.png

总结懒加载类 + 非懒加载分类的数据加载,只要分类实现了load,会迫使主类提前加载,即 主类 强行转换为 非懒加载类样式


总结


类和分类搭配使用,其数据的加载时机总结如下:


  • 【情况1】非懒加载类 + 非懒加载分类,其数据的加载在load_images方法中,首先对类进行加载,然后把分类的信息贴到类中
  • 【情况2】非懒加载类 + 懒加载分类,其数据加载在read_image就加载数据,数据来自data,data在编译时期就已经完成,即data中除了类的数据,还有分类的数据,与类绑定在一起
  • 【情况3】懒加载类 + 懒加载分类 ,其数据加载推迟到 第一次消息时,数据同样来自data,data在编译时期就已经完成
  • 【情况4】懒加载类 + 非懒加载分类 ,只要分类实现了load,会迫使主类提前加载,即在_read_images中不会对类做实现操作,需要在load_images方法中触发类的数据加载,即rwe初始化,同时加载分类数据


如下图所示

image.png


补充:load_images原理分析


load_images方法的主要作用是加载镜像文件,其中最重要的有两个方法:prepare_load_methods(加载) 和 call_load_methods(调用)


  • 进入load_images源码实现

image.png进入prepare_load_methods源码


  • 进入_getObjc2NonlazyClassList -> schedule_class_load源码,这里主要是根据类的继承链递归调用获取load,直到cls不存在才结束递归,目的是为了确保父类的load优先加载

image.png

进入add_class_to_loadable_list,主要是将load方法和cls类名一起加到loadable_classes表中

image.png

进入getLoadMethod,主要是获取方法的sel为load的方法


image.png

_getObjc2NonlazyCategoryList -> realizeClassWithoutSwift -> add_category_to_loadable_list ,主要是将非懒加载分类的load方法加入表中


image.png

进入add_category_to_loadable_list实现,获取所有的非懒加载分类中的load方法,将分类名+load加入表loadable_categories

image.png

进入call_load_methods源码,主要有3部分操作


  • 反复调用类的+load,直到不再有
  • 调用一次分类的+load
  • 如果有类或更多未尝试的分类,则运行更多的+load

image.png

进入call_class_loads,主要是加载类的load方法

image.png


  • 其中load方法中有两个隐藏参数,第一个为id 即self,第二个为sel,即cmd
  • call_category_loads,主要是加载一次分类的load方法

image.png

综上所述,load_images方法整体调用过程原理图示如下


  • 调用过程图示

image.png

原理图示

image.png

  • 主要分为两步


  • 从所有的非懒加载类和分类中的+load分别添加到表
  • 调用类和分类的+load方法



相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
7月前
|
存储 运维 安全
iOS加固原理与常见措施:保护移动应用程序安全的利器
iOS加固原理与常见措施:保护移动应用程序安全的利器
95 0
|
7月前
|
存储 运维 安全
iOS加固原理与常见措施:保护移动应用程序安全的利器
iOS加固原理与常见措施:保护移动应用程序安全的利器
160 0
|
3月前
|
Swift iOS开发
6-7|IOS如何定义一个类
6-7|IOS如何定义一个类
|
4月前
|
存储 iOS开发
iOS 16 系统键盘修复问题之确定UIKeyboardTaskQueue类对_lock的加锁和解锁操作如何解决
iOS 16 系统键盘修复问题之确定UIKeyboardTaskQueue类对_lock的加锁和解锁操作如何解决
|
4月前
|
存储 安全 iOS开发
iOS 16 系统键盘修复问题之确定UIKeyboardTaskQueue类中对_lock的使用是否正确如何解决
iOS 16 系统键盘修复问题之确定UIKeyboardTaskQueue类中对_lock的使用是否正确如何解决
|
7月前
|
安全 前端开发 数据安全/隐私保护
【教程】 iOS混淆加固原理篇
本文介绍了iOS应用程序混淆加固的缘由,编译过程以及常见的加固类型和逆向工具。详细讨论了字符串混淆、类名、方法名混淆、程序结构混淆加密等加固类型,并介绍了常见的逆向工具和代码虚拟化技术。
|
7月前
|
安全 算法 前端开发
【完整版教程】iOS混淆加固原理篇
在iOS开发中,应用程序的安全性和保护显得尤为重要。由于iOS系统的开放性,一些逆向工具可以轻松地对应用程序进行反编译和分析,从而导致应用程序源代码、算法和敏感信息的泄露。为了保护应用程序的安全性,我们需要对应用程序进行混淆加固。本文将介绍iOS混淆加固的原理和常见的加固类型。
|
7月前
|
JSON 安全 数据安全/隐私保护
​iOS Class Guard github用法、工作原理和安装详解及使用经验总结
​iOS Class Guard github用法、工作原理和安装详解及使用经验总结
109 0
|
7月前
|
安全 数据安全/隐私保护 iOS开发
【iOS开发】iOS App的加固保护原理:使用ipaguard混淆加固
【iOS开发】iOS App的加固保护原理:使用ipaguard混淆加固
100 0
|
JSON 安全 数据安全/隐私保护
​iOS Class Guard github用法、工作原理和安装详解及使用经验总结
iOS Class Guard是一个用于OC类、协议、属性和方法名混淆的命令行工具。它是class-dump的扩展。这个工具会生成一个symbol table,这个table在编译期间会包含进工程中。iOS-Class-Guard能有效的隐藏绝大多数的类、协议、方法、属性和 实例变量 名。iOS-Class-Guard不是应用安全的最终解决方案,但是它绝对能让攻击者更难读懂你的程序。iOS-Class-Guard会加大代码分析和runtime检查的难度,这个工具可以认为是一个简单基础的混淆方法。由于OC的架构决定了iOS应用程序的剖析相当简单,check out一下链接就知晓了: