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指针的理解

猿创征文|iOS经典面试题之深入解析objc对象的内存空间数据结构以及isa指针的理解

C++经典面试题之深入解析const修饰指针的作用

C经典面试题之深入解析sprintfstrcpy和memcpy的使用与区别