iOS经典面试题之深入解析分类Category的本质以及如何被加载
【摘要】
一、分类的本质
① Category 与 extension
Category 是 Objective-C 2.0 之后添加的语言特性,Category 的主要作用是为已经存在的类添加方法。exten...
一、分类的本质
① Category 与 extension
- Category 是 Objective-C 2.0 之后添加的语言特性,Category 的主要作用是为已经存在的类添加方法。
- extension 看起来很像一个匿名的 Category,但是 extension 和有名字的 Category 几乎完全是两个东西。extension 在编译期决议,它就是类的一部分,在编译期和头文件里的 @interface 以及实现文件里的 @implement 一起形成一个完整的类,它伴随类的产生而产生亦随之一起消亡。extension 一般用来隐藏类的私有信息,必须有一个类的源码才能为一个类添加 extension,所以无法为系统的类比如 NSString 添加 extension。
- 但是 Category 则完全不一样,它是在运行期决议的。就 Category 和 extension 的区别来看,可以推导出一个明显的事实,extension 可以添加实例变量,而 Category 是无法添加实例变量的(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)。
② Category 分析
- 新建一个 YDWPerson 类,并添加一个分类 YDWPerson (Test),并在分类里面添加 ydw_test 方法并实现, 然后 Clang;
- 在 Clang 之前,先看一下分类的数据结构,在 Apple 官方文档中说明如下:
- 我们都知道:不管是对象还是类,存储在内存空间都是结构体的形式,id -> objc-object,Class -> objc_class,同样 Category 对应的结构体如下:
-
- 过时的分类定义如下:
-
- 现在的分类定义如下:
- category_t 结构体的具体定义如下:
struct category_t {
// 类的名称
const char *name;
// cls 要扩展的类对象,编译期间这个值不会存在,在 APP 被 runtime 加载时才会根据 name 对应的类对象
classref_t cls;
// 实例方法和类方法
WrappedPtr<method_list_t, PtrauthStrip> instanceMethods;
WrappedPtr<method_list_t, PtrauthStrip> classMethods;
// 协议
struct protocol_list_t *protocols;
// 实例属性
struct property_list_t *instanceProperties;
// Fields below this point are not always present on disk.
// 类属性
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
protocol_list_t *protocolsForMeta(bool isMeta) {
if (isMeta) return nullptr;
else return protocols;
}
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 然后回到通过 Clang 编译之后的文件,看看编译时做了什么内容:
static struct _category_t _OBJC_$_CATEGORY_YDWPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = // 存放在 _DATA 数据段中的 __objc__const 字段中
{
// 类名
"YDWPerson",
0, // &OBJC_CLASS_$_YDWPerson, // 编译期间不会有
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_YDWPerson_$_Test, // 实例方法
0,
0,
0,
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 搜索一下 CATEGORY_INSTANCE_METHODS_YDWPerson$_Test:
static struct /*_method_list_t*/ {
// 方法的定义
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_YDWPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
// struct objc_selector *)"ydw_test":Selector
// "v16@0:8":方法签名
// (void *)_I_YDWPerson_Test_ydw_test:函数指针地址
{{(struct objc_selector *)"ydw_test", "v16@0:8", (void *)_I_YDWPerson_Test_ydw_test}}
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 搜索 _I_YDWPerson_Test_ydw_test 完成之后,可以看到定义的方法:
// @implementation YDWPerson (Test)
static void _I_YDWPerson_Test_ydw_test(YDWPerson * self, SEL _cmd) {
}
// @end
- 1
- 2
- 3
- 4
- 5
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_YDWPerson_$_Test,
};
// 最后,这个类的 category 生成了一个数组,存在了 __DATA 字段下的 __objc_catlistsection 里
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
- 1
- 2
- 3
- 4
- 5
二、Category 如何被加载?
- 通过 Clang 知道了系统在编译时期对分类做了什么事情,那么什么时候被加载进来呢?程序的入口点在 iOS 中被称之为 main 函数:所写的所有代码,它的执行的第一步,均是由 main 函数开始的。但其实,在程序进入 main 函数之前,内核已经为程序加载和运行做了许多的事情。
- 当设置符号断点 objc _init,可以看到如下调用堆栈信息,这些函数都是在 main 函数调用前,被系统调用的:
- _objc_init 是 0bject-C runtime 的入口函数,在这里面主要功能是读取 Mach-0 文件 0C 对应的 Segment seciton,并根据其中的数据代码信息,完成为 0C 的内存布局,以及初始化 runtime 相关的数据结构。
- 可以看到,_objc_init 是被 _dyld_start 所调用起来的,_dyld_start 是 dyld 的 bootstrap 方法,最终调用到 _objc_init。
- 当程序启动时,系统内核首先会加载 dyld,而 dyld 会将 APP 所依赖的各种库加载到内存空间中,其中就包括 libobjc 库(OC 和runtime),这些工作是在 APP 的 main 函数执行前完成的。
void _objc_init(void) {
static bool initialized = false;
if (initialized) return;
initialized = true;
// 读取影响运行时的环境变量,如果需要,还可以打开环境变量帮助 export OBJC_HELP = 1
environ_init();
// 关于线程key的绑定,例如线程数据的析构函数
tls_init();
// 运行C++静态构造函数,在dyld调用我们的静态析构函数之前,libc会调用_objc_init(),因此必须自己做
static_init();
// runtime运行时环境初始化,里面主要是unattachedCategories、allocatedClasses,分类初始化
runtime_init();
// 初始化libobjc的异常处理系统
exception_init();
#if __OBJC2__
// 缓存条件初始化
cache_t::init();
#endif
// 启动回调机制,通常这不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,会迫不及待地加载trampolines dylib
_imp_implementationWithBlock_init();
/*
_dyld_objc_notify_register -- dyld 注册的地方
- 仅供objc运行时使用
- 注册处理程序,以便在映射、取消映射 和初始化objc镜像文件时使用,dyld将使用包含objc_image_info的镜像文件数组,回调 mapped 函数
map_images:dyld将image镜像文件加载进内存时,会触发该函数
load_images:dyld初始化image会触发该函数
unmap_image:dyld将image移除时会触发该函数
*/
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 可以看到,_objc_init 是被 _dyld_start 所调用起来的,_dyld_start 是 dyld 的 bootstrap 方法,最终调用了 _objc_init,dyld 是苹果的动态加载器,用来加载 image(Mach-O 的二进制文件)。
//
// Note: only for use by objc runtime
// Register handlers to be called when objc images are mapped, unmapped, and initialized.
// Dyld will call back the "mapped" function with an array of images that contain an objc-image-info section.
// Those images that are dylibs will have the ref-counts automatically bumped, so objc will no longer need to
// call dlopen() on them to keep them from being unloaded. During the call to _dyld_objc_notify_register(),
// dyld will call the "mapped" function with already loaded objc images. During any later dlopen() call,
// dyld will also call the "mapped" function. Dyld will call the "init" function when dyld would be called
// initializers in that image. This is when objc calls any +load methods in that image.
//
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 说明:
-
- _dyld_objc_notify_mapped 对应 map_image 回调:当 dyld 已将 images 加入内存时;
-
- _dyld_objc_notify_init 对应 load_image 回调:当 dyld 初始化 image 时,OC 调用类的 +load 方法,就是在这个时候进行的;
-
- _dyld_objc_notify_unmapped 对应 unmap_image 回调,当 dyld 将 images 移除内存时。
- 而 category 写入 target class 的方法列表,则是在 _dyld_objc_notify_mapped,即将 Mach-0 相关 sections 都加载到内存之后所发生的。
- 当 dyld 将 images 加载加入内存时,会触发以下这个回调函数:
/***********************************************************************
* map_images
* Process the given images which are being mapped in by dyld.
* Calls ABI-agnostic code after taking ABI-specific locks.
*
* Locking: write-locks runtimeLock
**********************************************************************/
void
map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{
mutex_locker_t lock(runtimeLock);
return map_images_nolock(count, paths, mhdrs);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 最终会调用 _read_images 方法读取 OC 相关 Section,并以此来初始化 OC 环境:
if (hCount > 0) {
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
- 1
- 2
- 3
- runtime 调用_getObjc2XXX 格式的方法,依次来读取对应的 section 内容,并根据其结果初始化其自身结构:
for (EACH_HEADER) {
if (! mustReadClasses(hi, hasDyldRoots)) {
// Image is sufficiently optimized that we need not call readClass()
continue;
}
classref_t const *classlist = _getObjc2ClassList(hi, &count);
bool headerIsBundle = hi->isBundle();
bool headerIsPreoptimized = hi->hasPreoptimizedClasses();
for (i = 0; i < count; i++) {
Class cls = (Class)classlist[i];
Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
if (newCls != cls && newCls) {
// Class was moved but not deleted. Currently this occurs
// only when the new class resolved a future class.
// Non-lazily realize the class below.
resolvedFutureClasses = (Class *)
realloc(resolvedFutureClasses,
(resolvedFutureClassCount+1) * sizeof(Class));
resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- _getObjc2XXX 方法有如下几种,可以看到它们都一一对应 Mach-O 中相关的 OC seciton:
// function name content type section name
GETSECT(_getObjc2SelectorRefs, SEL, "__objc_selrefs");
GETSECT(_getObjc2MessageRefs, message_ref_t, "__objc_msgrefs");
GETSECT(_getObjc2ClassRefs, Class, "__objc_classrefs");
GETSECT(_getObjc2SuperRefs, Class, "__objc_superrefs");
GETSECT(_getObjc2ClassList, classref_t const, "__objc_classlist");
GETSECT(_getObjc2NonlazyClassList, classref_t const, "__objc_nlclslist");
GETSECT(_getObjc2CategoryList, category_t * const, "__objc_catlist");
GETSECT(_getObjc2CategoryList2, category_t * const, "__objc_catlist2");
GETSECT(_getObjc2NonlazyCategoryList, category_t * const, "__objc_nlcatlist");
GETSECT(_getObjc2ProtocolList, protocol_t * const, "__objc_protolist");
GETSECT(_getObjc2ProtocolRefs, protocol_t *, "__objc_protorefs");
GETSECT(getLibobjcInitializers, UnsignedInitializer, "__objc_init_func");
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 可以看到,所使用的类,协议和 category 都是在 _read_images 方法中读取出来的;在读取 cateogry 的方法 get0bjc2CategoryList(hi, &count) 中,读取的是 Mach-0 文件的 _objc_catlist 段:
if (cls->isStubClass()) {
// Stub classes are never realized. Stub classes
// don't know their metaclass until they're
// initialized, so we have to add categories with
// class methods or properties to the stub itself.
// methodizeClass() will find them and add them to
// the metaclass as appropriate.
if (cat->instanceMethods ||
cat->protocols ||
cat->instanceProperties ||
cat->classMethods ||
cat->protocols ||
(hasClassProperties && cat->_classProperties))
{
// 如果分类中有实例方法、协议、实例属性,就会改写 target class 的结构
objc::unattachedCategories.addForClass(lc, cls);
}
} else {
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
// 如果分类中有类方法、协议或类属性,会改写 target class 的元类结构
if (cls->isRealized()) {
attachCategories(cls, &lc, 1, ATTACH_EXISTING);
} else {
objc::unattachedCategories.addForClass(lc, cls);
}
}
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
if (cls->ISA()->isRealized()) {
attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);
} else {
objc::unattachedCategories.addForClass(lc, cls->ISA());
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 总结:
-
- 先调用 _get0bjc2CategoryList 读取 _objc_catlist seciton 下所记录的所有 category,并存放到 category _t * 数组中;
-
- 依次读取数组中的 category_t *cat;
-
- 对每一个 cat,先调用 remapClass(cat->cls),并返回一个 objc_ class* 对象 cls,这一步的目的在于找到 category 对应的类对象 cls;
-
- 找到 category 对应的类对象 cls 后,就开始进行对 cls 的修改操作:
-
-
- 如果 category 中有实例方法、协议和实例属性之一的话,则直接对 cls 进行操作;
-
-
-
- 如果 category 中包含了类方法、协议、类属性(不支持)之一的话,还要对 (cls 所对应的元类 (cls- ->ISA()) 进行操作;
-
-
- 不管是对 cls 还是 cls 的元类进行操作,都是调用的方法 addUnattachedCategoryForClass,但这个方法并不是 category 实现的关键,其内部逻辑只是将 class 和其对应的 category 做一个映射,这样以 class 为 key 就可以取到所其对应的所有的 category;
-
- 做好 class 和 category 的映射后,会调用 remethodizeClass 方法来修改 class 的 method list 结构,这才是 runtime 实现 category 的关键所在。
文章来源: blog.csdn.net,作者:╰つ栺尖篴夢ゞ,版权归原作者所有,如需转载,请联系作者。
原文链接:blog.csdn.net/Forever_wj/article/details/126530555
【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)