iOS经典面试题之深入解析分类Category的本质以及如何被加载
Posted ╰つ栺尖篴夢ゞ
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS经典面试题之深入解析分类Category的本质以及如何被加载相关的知识,希望对你有一定的参考价值。
一、分类的本质
① 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;
;
- 然后回到通过 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,
;
- 搜索一下 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
;
- 搜索 _I_YDWPerson_Test_ydw_test 完成之后,可以看到定义的方法:
// @implementation YDWPerson (Test)
static void _I_YDWPerson_Test_ydw_test(YDWPerson * self, SEL _cmd)
// @end
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 ;
二、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
- 可以看到,_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);
- 说明:
-
- _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);
- 最终会调用 _read_images 方法读取 OC 相关 Section,并以此来初始化 OC 环境:
if (hCount > 0)
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
- 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;
- _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");
- 可以看到,所使用的类,协议和 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());
- 总结:
-
- 先调用 _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 的关键所在。
以上是关于iOS经典面试题之深入解析分类Category的本质以及如何被加载的主要内容,如果未能解决你的问题,请参考以下文章
iOS经典面试题之深入解析Runtime如何通过selector找到对应的IMP地址
iOS经典面试题之深入解析Runtime如何通过selector寻找对应的IMP地址
iOS经典面试题之深入解析objc对象的内存空间数据结构以及isa指针的理解