十一:底层探索-load_images,类拓展,关联对象

375 阅读15分钟

在上文 分类的加载 中我们探究了分类是如何加载的,并通过分类与类(懒加载类和非懒加载类)相结合的方式,探究了不同情况下的加载情况。通过之前的文章我们知道 +load 会使类的加载提前,那么+load方法又是什么时候加载的呢?本篇将结合_dyld_objc_notify_register(&map_images, load_images, unmap_image)中所遗留的 load_images来进行探究!

注意:本文使用的objc源码为 779.1版本

一:load_images

首先还是看下源码中对 load_images 的定义
从注释就知道是用于加载load方法的

/***********************************************************************
* load_images
* Process +load in the given images which are being mapped in by dyld.
*
* Locking: write-locks runtimeLock and loadMethodLock
**********************************************************************/
extern bool hasLoadMethods(const headerType *mhdr);
extern void prepare_load_methods(const headerType *mhdr);

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    // 🌹准备load方法调用所需要的数据
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    // 🌹调用load方法
    call_load_methods();
}

1.1 prepare_load_methods

void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertLocked();

    // 🌹获取到懒加载类
    classref_t const *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        // 🌹遍历懒加载类
        schedule_class_load(remapClass(classlist[i]));
    }
    // 🌹 获取到懒加载分类
    category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        // 🌹遍历懒加载分类
        category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class
        if (cls->isSwiftStable()) {
            _objc_fatal("Swift class extensions and categories on Swift "
                        "classes are not allowed to have +load methods");
        }
        // 🌹加载相关类
        realizeClassWithoutSwift(cls, nil);
        ASSERT(cls->ISA()->isRealized());
        // 🌹添加分类到非懒加载分类列表中
        add_category_to_loadable_list(cat);
    }
}

值得注意的是:在遍历懒加载类时,会调用schedule_class_load,内部会进行递归调用,用于实现父类的方法,并将方法进行添加到列表

static void schedule_class_load(Class cls)
{
    if (!cls) return;
    ASSERT(cls->isRealized());  // _read_images should realize

    if (cls->data()->flags & RW_LOADED) return;
    // Ensure superclass-first ordering
    // 🌹递归调用遍历父类的+load方法,确保父类的+load方法顺序排在子类的前面
    schedule_class_load(cls->superclass);
    // 🌹把类的+load方法存在loadable_classes里面
    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

add_class_to_loadable_list

void add_class_to_loadable_list(Class cls)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = cls->getLoadMethod();
    if (!method) return;  // Don't bother if cls has no +load method
    
    if (PrintLoading) {
        _objc_inform("LOAD: class '%s' scheduled for +load", 
                     cls->nameForLogging());
    }
    
    if (loadable_classes_used == loadable_classes_allocated) {
        🌹 申请空间进行扩容
        loadable_classes_allocated = loadable_classes_allocated*2 + 16;
        loadable_classes = (struct loadable_class *)
            realloc(loadable_classes,
                              loadable_classes_allocated *
                              sizeof(struct loadable_class));
    }
    🌹 存储到loadable_class中
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}

遍历懒加载分类的操作几乎和类一样,此处就不做展开,有兴趣的可以看下源码。

从源码分析,我们知道prepare_load_methods做了以下的操作:

  1. 通过_getObjc2NonlazyClassList获取非懒加载类列表
  2. 通过schedule_class_load遍历这些类
    1. 递归调用遍历父类的+load方法,确保父类的+load方法顺序排在子类的前面
    2. 调用add_class_to_loadable_list把类的+load方法存在loadable_classes里面
  3. 调用_getObjc2NonlazyCategoryList取出非懒加载分类列表
  4. 遍历分类列表
    1. 通过realizeClassWithoutSwift来防止类没有初始化(若已经初始化了则不影响)
    2. 调用add_category_to_loadable_list加载分类中的+load方法到loadable_categories

1.2 call_load_methods

void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;
    🌹压栈自动释放池
    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            🌹调用类的load方法
            call_class_loads();
        }
        🌹循环调用分类load方法,仅一次
        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);
    🌹出栈自动释放池
    objc_autoreleasePoolPop(pool);

    loading = NO;
}
  • 通过objc_autoreleasePoolPush压栈一个自动释放池
  • do-while循环开始
  • 循环调用类的+load方法直到找不到为止
  • 调用一次分类中的+load方法
  • 通过 objc_autoreleasePoolPop`出栈一个自动释放池

从这也能看出load是先调用类的,再调用分类的

1.2.1 call_class_loads

/***********************************************************************
* call_class_loads
* Call all pending class +load methods.
* If new classes become loadable, +load is NOT called for them.
*
* Called only by call_load_methods().
**********************************************************************/
static void call_class_loads(void)
{
    int i;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        // 🌹遍历列表,得到IMP并强转
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        // 🌹直接调用函数
        (*load_method)(cls, SEL_load);
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

遍历非懒加载类列表,直接获取到IMP,强转为load_method_t,并直接调用函数实现,没有走消息发送流程

分类的调用call_category_loads实现和类的类似,就不展开了,有兴趣的朋友可以看下源码

至此load_images分析完毕

1.3 面试题

类和分类的load方法谁先调用?

答案已经在上面分析出来了,先调用类call_class_loads,再调用分类call_category_loads,所以当类和分类都实现load方法,先调用类的,再调用分类的。

分类方法与方法同名调用的是哪个?(常常说的会覆盖)

调用的是分类的方法,并且不是覆盖,只是分类的方法被附加到了类的方法的前面,具体的逻辑在attachLists方法中,详细内容在这里

二:分类

我们连分类的加载之前都研究了,那这里我们就分析下与分类相关的内容。
Objective-C 2.0中,出现category这个语言特性,可以动态地为已有类添加新行为。如今category使用广泛,从Apple官方的framework到各个开源框架,从功能繁复的大型App到简单的应用,catagory无处不在。

1. 分类的作用

    1. 可以把类的实现分开在几个不同的文件里面。这样做有几个显而易见的好处,a)可以减少单个文件的体积 b)可以把不同的功能组织到不同的category里 c)可以由多个开发者共同完成一个类 d)可以按需加载想要的category 等等。
    1. 声明私有方法

2. 分类添加属性会出现什么问题?

测试代码,我们创建WYPerson+test分类,并添加category_Name属性,然后在main.m中调用

🌹分类
@interface WYPerson (test)

@property(nonatomic,copy) NSString *category_Name;

@end

🌹main.m
int main(int argc, const char * argv[]) {
    @autoreleasepool {
         WYPerson *p =[[WYPerson alloc]init];
        p.category_Name = @"测试";
        NSLog(@"Hello, World!");
        
    }
    return 0;
}

相信这个代码运行的结果大家已经知道了,直接崩溃,如下图所示:

下面我们看下分类在源码中的定义

typedef struct category_t {
    const char *name;  //类的名字
    classref_t cls;  //类
    struct method_list_t *instanceMethods;  //category中所有给类添加的实例方法的列表
    struct method_list_t *classMethods;  //category中所有添加的类方法的列表
    struct protocol_list_t *protocols;  //category实现的所有协议的列表
    struct property_list_t *instanceProperties;  //category中添加的所有属性
} category_t;

其实从category的定义也可以看出category的可为(可以添加实例方法,类方法,甚至可以实现协议,添加属性)和不可为(无法添加实例变量)

注意:category实际上允许添加属性的,同样可以使用@property,但是不会生成 _变量(带下划线的成员变量),也不会生成添加属性的gettersetter方法,所以,尽管添加了属性,也无法使用点语法调用gettersetter方法。但实际上可以使用runtime去实现category为已有的类添加新的属性并生成gettersetter方法。

为什么不能添加实例变量呢?

类的内存布局在编译时期就已经确定了,category是运行时才加载的,此时早已经确定了内存布局所以无法添加实例变量,如果添加实例变量就会破坏category的内部布局。

3. 关联对象

通过runtime关联对象方式来给Category的属性添加对应的settergetter方法。

#import "WYPerson+test.h"
#import <objc/runtime.h>
@implementation WYPerson (test)
-(void)setCategory_Name:(NSString *)category_Name
{
    /**
    🌹参数一:id object : 给哪个对象添加属性,这里要给自己添加属性,用self。
    🌹参数二:void * == id key : 属性名,根据key获取关联对象的属性的值,在objc_getAssociatedObject中通过次key获得属性的值并返回。
    🌹参数三:id value : 关联的值,也就是set方法传入的值给属性去保存。
    🌹参数四:objc_AssociationPolicy policy : 策略,属性以什么形式保存。
    */
    objc_setAssociatedObject(self, @"name", category_Name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

-(NSString *)category_Name
{
     /**
    🌹参数一:id object : 获取哪个对象里面的关联的属性。
    🌹参数二:void*==idkey:什么属性,与objc_setAssociatedObject中的key相对应,即通过key值取出value。
    */
    return objc_getAssociatedObject(self, @"name");
}

3.1 关联对象实现原理

我们还是从底层源码来看它的实现,首先看下objc_setAssociatedObject,

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
    SetAssocHook.get()(object, key, value, policy);
}

在这之后又会调用两个函数

static ChainedHookFunction<objc_hook_setAssociatedObject> SetAssocHook{_base_objc_setAssociatedObject};

static void _base_objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
  _object_set_associative_reference(object, key, value, policy);
}

最终来到_object_set_associative_reference,这个才是我们要关注的重点,代码中已经加上部分注释。

void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
    // This code used to work when nil was passed for object and key. Some code
    // probably relies on that to not crash. Check and handle it explicitly.
    // rdar://problem/44094390
    if (!object && !value) return;

    // 判断本类对象是否允许关联其他对象.如果允许则进入代码块
    if (object->getIsa()->forbidsAssociatedObjects())
        _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
    // 将被关联的对象封装成DisguisedPtr方便在后边hash表中的管理,它的作用就像是一个指针
    DisguisedPtr<objc_object> disguised{(objc_object *)object};
    // 将需要关联的对象,封装成ObjcAssociation,方便管理
    ObjcAssociation association{policy, value};

    // retain the new value (if any) outside the lock.
    // 处理policy为retain和copy的修饰情况
    association.acquireValue();

    {
        // 关联对象的管理类
        AssociationsManager manager;
        // 获取关联的 HashMap -> 存储当前的关联对象
        AssociationsHashMap &associations(manager.get());

        if (value) {
            // 如果这个disguised存在于ObjectAssociationMap()中,则替换,如果不存在则初始化后在插入
            // 这里说明一下,我们关联的对象关系存在于ObjectAssociationMap中,而
            //  ObjectAssociationMap有多个,所以,这一步是对ObjectAssociationMap的一个管理,下边才是对我们要关联的对象的操作
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            // 如果这是此对象第一次被关联
            if (refs_result.second) {
                /* it's the first association we make */
                // 修改isa_t中的has_assoc字段,标记其被关联状态
                object->setHasAssociatedObjects();
            }

            /* establish or replace the association */
            // 这里才是对我们要关联的对象操作
            auto &refs = refs_result.first->second;
            // 向map中插入key value对
            auto result = refs.try_emplace(key, std::move(association));
            // 没有第二个就要交换一下
            if (!result.second) {
                association.swap(result.first->second);
            }
        } else {
            // value为空, 并且在associations中有记录,则进行擦除操作 
            auto refs_it = associations.find(disguised);
            if (refs_it != associations.end()) {
                auto &refs = refs_it->second;
                auto it = refs.find(key);
                if (it != refs.end()) {
                    association.swap(it->second);
                    refs.erase(it);
                    if (refs.size() == 0) {
                        associations.erase(refs_it);

                    }
                }
            }
        }
    }

    // release the old value (outside of the lock).
    association.releaseHeldValue();
}

其实那么多代码总结起来就是以下几点:

  1. 获取到关联对象管理类
  2. 根据指针地址获取到当前类的关联对象哈希表
  3. 根据key寻找对应的对象,找到就替换,未找到则插入
  4. 如果传入value为nil。如果有值,则移除

下面我们再看下get方法的关联,最终调用_object_get_associative_reference来进行相应的操作。

id objc_getAssociatedObject(id object, const void *key)
{
    return _object_get_associative_reference(object, key);
}

id _object_get_associative_reference(id object, const void *key)
{
    ObjcAssociation association{};

    {
        // 关联对象管理类
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        // 所有对象的迭代器
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        
        if (i != associations.end()) {
            ObjectAssociationMap &refs = i->second;
            // 内部对象的迭代器
            ObjectAssociationMap::iterator j = refs.find(key);
            if (j != refs.end()) {
                // 找到进行读取
                association = j->second;
                // retainReturnedValue内部会读取值和关联策略
                association.retainReturnedValue();
            }
        }
    }
    return association.autoreleaseReturnedValue();
}

可以看到,所有的关联对象都由AssociationsManager管理,而AssociationsManager定义如下:

class AssociationsManager {
    using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
    static Storage _mapStorage;

public:
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }

    AssociationsHashMap &get() {
        return _mapStorage.get();
    }

    static void init() {
        _mapStorage.init();
    }
};

AssociationsManager里面是由一个静态AssociationsHashMap来存储所有的关联对象的。这相当于把所有对象的关联对象都存在一个全局map里面。而map的的key是这个对象的指针地址(任意两个不同对象的指针地址一定是不同的),而这个mapvalue又是另外一个AssociationsHashMap,里面保存了关联对象的keyvalue

图片来源

3.2 消除关联对象

消除的逻辑在 dealloc方法中

- (void)dealloc {
    _objc_rootDealloc(self);
}

最终会调用objc_destructInstance方法中判断这个对象有没有关联对象,如果有,会调用_object_remove_assocations做关联对象的清理工作。

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}

三:类扩展

类扩展常常拿来与分类作比较,大家在平时用的已经比较多了,通常被我们叫做匿名的分类

1. 作用

  • 为一个类添加额外的原来没有变量,方法和属性
  • 一般的类扩展写到.m文件中
  • 一般的私有属性写到.m文件中的类扩展中

2. 通过运行时分析

类扩展有两种存在的形式,一种是与类共用.m,一种是采用创建单独的.m
共用 .m的情况

@interface WYPerson : NSObject
@end

#import "WYPerson.h"
#import "WYPerson+WYExtension.h"
@interface WYPerson()
@property(nonatomic,copy) NSString *kNmae;
- (void)extMethodOne;
@end
@implementation WYPerson
+(void)load
{
    NSLog(@"%s",__func__);
}
- (void)extMethodOne
{
    NSLog(@"%s",__func__);
}

- (void)extMethodTwo
{
    NSLog(@"%s",__func__); 
}

单独存在.m的情况

我们知道在map_images最终会调用read_images 来读取所以的镜像,那我们就在如下位置加入我们自己的代码,看是否能读取到 Extension 中的内容,以下代码已经省略部分。

// Discover classes. Fix up unresolved future classes. Mark bundle classes.
    bool hasDyldRoots = dyld_shared_cache_some_image_overridden();

    for (EACH_HEADER) {
        ......

        for (i = 0; i < count; i++) {
            Class cls = (Class)classlist[i];
            Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
            
            
                // 🌹自己添加的代码🌹
                const class_ro_t *ro = (const class_ro_t *)cls->data();
                const char *cname = ro->name;
                const char *oname = "WYPerson";
                if (strcmp(cname, oname) == 0) {
                    printf("类名: %s \n",cname);  // 🌹断点
                }
                // 🌹以上是自己添加的代码🌹

            if (newCls != cls  &&  newCls) {
                ......
            }
        }
    }

当断点来到断点处,我们来打印类的信息

通过打印,我们发现在 ro当中存在类扩展的 extMethodOne方法。但是这并不足以说明问题。如果我们把类扩展中的extMethodOne注释,那extMethodOne也可能在这里读取。下面我们接着对类扩展中的 kNmae 属性进行打印。

通过上图的打印,发现属性kNamesettergetter方法以及存在于 robaseMethodList,而且同时我们打印出了extMethodTwo这足以说明类扩展在编译期就已经被编译进类的 ro 当中。

注意:当类扩展是以.m文件存在的时候,如果未通过import引入到类(WYPerson)的时候,它是不会被编译进去的,系统会认为这是个无用的文件

从以上的分析我们可以总结一下类扩展的特点:

  1. 存在方式:可以以单个文件的形式存在,但是多数情况下寄生与宿主类的.m中。伴随着类的产生而产生,也随着类的消失而消失。
  2. 在编译器决定,是类的一部分,会被编译到类的 ro当中
  3. 通常用于声明私有的属性和方法,你必须有一个类的源码才能添加一个类的Extension,所以对于系统一些类,如NSString,就无法添加类扩展
  4. 如果类扩展以文件单独存在,必须import进相关的类,否则编译时是不会链接的。

3. 类扩展与分类的区别

Extension

  • extension看起来很像一个匿名的category,但是extension和有名字的category几乎完全是两个东西。
  • extension在编译期决议,它就是类的一部分,在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,它伴随类的产生而产生,亦随之一起消亡。
  • extension一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加extension,所以你无法为系统的类比如NSString添加extension

Category

  • 它是在运行期决议的。
  • 就category和extension的区别来看,我们可以推导出一个明显的事实,extension可以添加实例变量,而category是无法添加实例变量的
  • 原因:因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的

四:初始化initalize

官方的解释

Initializes the class before it receives its first message.
在这个类接收第一条消息之前调用。
Discussion
The runtime sends initialize to each class in a program exactly one time just before the class, or any class that inherits from it, is sent its first message from within the program. (Thus the method may never be invoked if the class is not used.) The runtime sends the initialize message to classes in a thread-safe manner. Superclasses receive this message before their subclasses.
Runtime在一个程序中每一个类的一个程序中发送一个初始化一次,或是从它继承的任何类中,都是在程序中发送第一条消息。(因此,当该类不使用时,该方法可能永远不会被调用。)运行时发送一个线程安全的方式初始化消息。父类的调用一定在子类之前。

其实initalize存在于lookUpImpOrForward

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    ......
    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
        // runtimeLock may have been dropped but is now locked again

        // If sel == initialize, class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }
    ......
}

// Locking: caller must hold runtimeLock; this may drop and re-acquire it
static Class initializeAndLeaveLocked(Class cls, id obj, mutex_t& lock)
{
    return initializeAndMaybeRelock(cls, obj, lock, true);
}


static Class initializeAndMaybeRelock(Class cls, id inst,
                                      mutex_t& lock, bool leaveLocked)
{
    ......
    initializeNonMetaClass(nonmeta);
    ......
}

initializeNonMetaClass中会进行递归调用,然后调用callInitialize

/***********************************************************************
* class_initialize.  Send the '+initialize' message on demand to any
* uninitialized class. Force initialization of superclasses first.
**********************************************************************/
void initializeNonMetaClass(Class cls)
{
    ...
    supercls = cls->superclass;
    if (supercls  &&  !supercls->isInitialized()) {
        initializeNonMetaClass(supercls);
    }
    ...
    {
            callInitialize(cls);

            if (PrintInitializing) {
                _objc_inform("INITIALIZE: thread %p: finished +[%s initialize]",
                             pthread_self(), cls->nameForLogging());
            }
        }
    ...
}

callInitialize就比较简单了,只是做了一个简单的消息发送。

void callInitialize(Class cls)
{
    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
    asm("");
}

initalize相关:

  1. initialize在类或者其子类的第一个方法被调用前(发送消息前)调用
  2. 只在类中添加initialize但不使用的情况下,是不会调用initialize
  3. 父类的initialize方法会比子类先执行
  4. 当子类未实现initialize方法时,会调用父类initialize方法;子类实现initialize方法时,会覆盖父类initialize方法
  5. 当有多个分类都实现了initialize方法,会覆盖类中的方法,只执行一个(会执行最后被加载到内存中的分类的方法)

参考链接

深入理解Objective-C:Category