iOS 类拓展分析、load_iamges分析、initalize分析

1,027 阅读12分钟

前言

  通过之前几篇对read_iamges的分析,我们知道了程序在启动运行时的流程,知道了什么是 懒加载类非懒加载类 和其加载过程,以及搭配 非懒加载分类懒加载分类 时的几种加载情况。

那么 类拓展 又是在怎么加载的呢?接下来,我们分析一下。

1.  类拓展分析

类拓展: 又叫匿名的分类。可以给当前类添加成员变量属性方法,在日常我们开发时有两种类拓展编写的形式,一种是直接写在主类里面。

如下面的一段代码,我们创建一个LGPerson类,LGPerson有一个name属性,同时在主类里面添加一个拓展,并且声明了一个mName的属性和extM_method方法。

@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end



@interface LGPerson ()
@property (nonatomic, copy) NSString *mName;

- (void)extM_method;
@end

@implementation LGPerson

+ (void)load{
    NSLog(@"%s",__func__);
}

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

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

类拓展的另一种形式就是以一个单独的.h文件的形式存在,如下:

@interface LGPerson ()
@property (nonatomic, copy) NSString *ext_name;
@property (nonatomic, copy) NSString *ext_subject;

- (void)extH_method;
@end

那么类拓展在程序中是什么时候加载的呢?

我们知道在read_images方法中,会读取所有的类,将其加入到表中,然后对类进行重映射,将类名和指针对应起来,然后进行一些初始化操作。

接下我们通过lldb,在重映射的时候,打印class中的ro,看看此时,上面的定义的类拓展方法和属性是否存在。存在,则说明类拓展的加载是在编译期进行。

接下来我们验证一下,首先在read_images方法中,添加下面代码,来判断LGPerson,然后断点调试

打印ro,

打印baseMethodList

从上面打印中,可以看到baseMethodList中有类拓展的方法,以及属性的settergetter。说明此事类拓展已经加载到ro中了,而ro是在编译期直接从数据段中读取的。

由此得知,我们得知类拓展是作为类的一部分编译到相应的数据段中,在读取的时候直接读取到ro中的。

那么上面我们也有提到类拓展有两种形式,一种是在主类中,另一种是一个单独的头文件的形式,这两种形式有什么区别呢?

通过验证:当我们不对类拓展的头文件引用时,通过上述方法验证,发现ro中并没有类拓展中的属性方法。当没有引用到主类时,系统默认没有用的,就不会加重这个类拓展

补充面试:

问:我们知道`类拓展`可以添加属性,那么分类不能添加属性吗?为什么?

答:
拓展的加载是在编译时,直接在相应的ro中编译添加
分类的加载是在运行时,只是添加了相应的rw,无法直接添加,

那么分类真的无法添加属性吗?当然不是,分类可以通过运行时为添加属性,接下来,我们分析一下原理。

2.  关联对象原理

我们可以通过下面代码,运行Runtim分类添加属性:

@interface LGPerson (LG)
@property (nonatomic, copy) NSString *cate_name;
@end

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

}


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

查看objc_getAssociatedObject方法和objc_setAssociatedObject方法源码时,发现在调用底层API时,系统都会添加一个中介层,对底层API进行一个封装,那么样的好处是什么呢?

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


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

添加中间隔层,防止直接操作私有API,方便API的调用和依赖,当下层的变了,不影响上层API的使用。

接下来,对我们分析一下分类添加属性的底层原理,看下面_object_set_associative_reference源码 和 _object_get_associative_reference

void _object_set_associative_reference(id object, 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;
    
    assert(object);
    
    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));
    
    
    // retain the new value (if any) outside the lock.
    // 在锁之外保留新值(如果有)。
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        // ✅关联对象的管理类  
        AssociationsManager manager;
        // ✅获取关联的 HashMap -> 存储当前关联对象
        AssociationsHashMap &associations(manager.associations());
        // ✅对当前的对象的地址做按位去反操作 - 就是 HashMap 的key (哈希函数)
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // ✅获取 AssociationsHashMap 的迭代器 - (对象的) 进行遍历
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                ObjectAssociationMap *refs = i->second;
                // ✅根据key去获取关联属性的迭代器
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    // ✅替换设置新值
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    // ✅到最后了 - 直接设置新值
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // create the new association (first time).
                // ✅如果AssociationsHashMap从没有对象的关联信息表,
                // ✅那么就创建一个map并通过传入的key把value存进去
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
            // setting the association to nil breaks the association.
            // ✅如果传入的value是nil,并且之前使用相同的key存储过关联对象,
            // ✅那么就把这个关联的value移除(这也是为什么传入nil对象能够把对象的关联value移除)
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i !=  associations.end()) {
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    // ✅最后把之前使用传入的这个key存储的关联的value释放(OBJC_ASSOCIATION_SETTER_RETAIN策略存储的)
    if (old_association.hasValue()) ReleaseValue()(old_association);
}

这其中的主要步骤就是,

  • 先获取所有对象的关联表AssociationsHashMap,因为可能有很多关联对象的类。
  • 通过迭代器找到我们要关联的对象表ObjectAssociationMap,比如这里是LGPerson的表
  • 关联存储ObjcAssociation(policy, new_value)
id _object_get_associative_reference(id object, void *key) {
    id value = nil;
    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
    {
        // ✅关联对象的管理类
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        // ✅生成伪装地址。处理参数 object 地址
        disguised_ptr_t disguised_object = DISGUISE(object);
        // ✅所有对象的额迭代器
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            ObjectAssociationMap *refs = i->second;
            // ✅内部对象的迭代器
            ObjectAssociationMap::iterator j = refs->find(key);
            if (j != refs->end()) {
                // ✅找到 - 把值和策略读取出来
                ObjcAssociation &entry = j->second;
                value = entry.value();
                policy = entry.policy();
                // ✅OBJC_ASSOCIATION_GETTER_RETAIN - 就会持有一下
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
                    objc_retain(value);
                }
            }
        }
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        objc_autorelease(value);
    }
    return value;
}

这其中的主要步骤就是,

  • 先获取所有对象的关联表AssociationsHashMap,因为可能有很多关联对象的类。
  • 通过迭代器找到我们要关联的对象表ObjectAssociationMap,比如这里是LGPerson的表
  • 根据key去除value 和关联策略。 value = entry.value(), policy = entry.policy()

关联对象的意义,将分类的属性进行存储,是这个属性的值的存储过程,将value其根据key存储到哈希表中,和从哈希表中取出的过程。

那么这个关联对象的哈希表什么时候释放呢?

dealloc中释放,查看dealloc 源码,最终会进入的下面的源码中,然后释放关联对象的哈希表

3.   load_images 分析

我们知道load方法在main之前调用,那么laod方法具体在什么时候调用的呢?

而且我们在分析objc_init方法时,只分析了_dyld_objc_notify_register(&map_images, load_images, unmap_image)方法中的map_images方法,那么load_images是做什么的,load是否在其中调用呢?

接下来我们分析一下。

首先我们创建LGPerson类,和LGPerson (LG) LGPerson (KC)两个分类,分别实现load方法并添加断点,并且在load_images方法中添加断点,然后断点调试。

load_images源码:

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
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}

通过断点调试,我们发现,程序先进入load_images方法,而尚未进入几个类中的load方法。

然后我们对上面load_images源码分析一下:

其实,load_images的源码很简单,除去一些判断条件,就剩下面两个主要步骤

  • prepare_load_methods
  • call_load_methods

prepare_load_methods 源码:

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

    runtimeLock.assertLocked();
    // ✅读取非懒加载类,只有非懒加载类实现了load方法
    classref_t *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        // ✅遍历
        schedule_class_load(remapClass(classlist[i]));
    }

    category_t **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);
        assert(cls->ISA()->isRealized());
        // ✅添加分类的load到list中
        add_category_to_loadable_list(cat);
    }
}

prepare_load_methods中,

  • 先读取非加载类(实现了load方法),然后遍历,进入schedule_class_load
  • 读取非懒加载分类,添加分类的loadlist中。add_category_to_loadable_list

查看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
    // ✅递归父类
    schedule_class_load(cls->superclass);
    // ✅把这个类的load添加到list中
    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();
    // ✅获取load方法的IMP
    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));
    }
    // ✅将cls和IMP赋值非模型
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}

add_class_to_loadable_list方法中,将cls和对应load方法的IMP赋值给模型。 add_category_to_loadable_list方法和add_class_to_loadable_list,基本一致,都是将cls和对应load方法的IMP赋值给模型。

到此,通过prepare_load_methods方法,将所有类的load方法已经准备完毕,接下来开始调用call_load_methods,调用load方法:

查看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
        // ✅调用主类load方法
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        // ✅调用分类load方法
        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;
}

分别查看call_class_loadsmore_categories源码如下:

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;
        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);
}

call_class_loads方法中,直接调用(*load_method)(cls, SEL_load)发送消息。

call_category_loads源码:

static bool call_category_loads(void)
{
    int i, shift;
    bool new_categories_added = NO;
    
    // Detach current loadable list.
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    int allocated = loadable_categories_allocated;
    loadable_categories = nil;
    loadable_categories_allocated = 0;
    loadable_categories_used = 0;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Category cat = cats[i].cat;
        load_method_t load_method = (load_method_t)cats[i].method;
        Class cls;
        if (!cat) continue;

        cls = _category_getClass(cat);
        if (cls  &&  cls->isLoadable()) {
            if (PrintLoading) {
                _objc_inform("LOAD: +[%s(%s) load]\n", 
                             cls->nameForLogging(), 
                             _category_getName(cat));
            }
            (*load_method)(cls, SEL_load);
            cats[i].cat = nil;
        }
    }

    // Compact detached list (order-preserving)
    shift = 0;
    for (i = 0; i < used; i++) {
        if (cats[i].cat) {
            cats[i-shift] = cats[i];
        } else {
            shift++;
        }
    }
    used -= shift;

    // Copy any new +load candidates from the new list to the detached list.
    new_categories_added = (loadable_categories_used > 0);
    for (i = 0; i < loadable_categories_used; i++) {
        if (used == allocated) {
            allocated = allocated*2 + 16;
            cats = (struct loadable_category *)
                realloc(cats, allocated *
                                  sizeof(struct loadable_category));
        }
        cats[used++] = loadable_categories[i];
    }

    // Destroy the new list.
    if (loadable_categories) free(loadable_categories);

    // Reattach the (now augmented) detached list. 
    // But if there's nothing left to load, destroy the list.
    if (used) {
        loadable_categories = cats;
        loadable_categories_used = used;
        loadable_categories_allocated = allocated;
    } else {
        if (cats) free(cats);
        loadable_categories = nil;
        loadable_categories_used = 0;
        loadable_categories_allocated = 0;
    }

    if (PrintLoading) {
        if (loadable_categories_used != 0) {
            _objc_inform("LOAD: %d categories still waiting for +load\n",
                         loadable_categories_used);
        }
    }

    return new_categories_added;
}

call_category_loads方法中也是调用(*load_method)(cls, SEL_load)发送消息,和 call_class_loads方法不同的是,当分类的load调用完毕后,会将其从loadable_categories移除。

当主类和分类的load方法调用完毕后,会调用objc_autoreleasePoolPop(pool)出栈。

至此,load_iamgesload方法的调用已经分析完毕,那么当主类分类有相同的方法,怎么调用?

1.通过上面 call_load_methods 方法得知,load 方法的调用顺序是先调用主类load 方法, 
  再调用分类的load 方法
2. 一般方法的调用,是先调用分类的同名方法,因为分类方法是通过 attachlist 插在方法列表的
  前面,所以先分类,因为方法查找流程(先调用分类同名方法,在调用同名方法时,走缓存查找)后
  主类,造成了分类方法覆盖主类的方法的假象。

4.   initalize 分析

前面我们分析了load的方法的调用,那么initalize方法在什么时候调用的呢?

我们在上面的类和分类中都添加initialize方法。

+ (void)initialize{
    NSLog(@"%s",__func__);
}

然后进行断点调试,发现最终进入lookUpImpOrForward方法的下面部分。

上面方法判断是否实现initialize方法,实现,则就会进入initializeAndLeaveLocked方法,然后调用initializeAndMaybeRelock方法,源码如下:

static Class initializeAndMaybeRelock(Class cls, id inst,
                                      mutex_t& lock, bool leaveLocked)
{
    lock.assertLocked();
    assert(cls->isRealized());

    if (cls->isInitialized()) {
        if (!leaveLocked) lock.unlock();
        return cls;
    }

    // Find the non-meta class for cls, if it is not already one.
    // The +initialize message is sent to the non-meta class object.
    Class nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);

    // Realize the non-meta class if necessary.
    if (nonmeta->isRealized()) {
        // nonmeta is cls, which was already realized
        // OR nonmeta is distinct, but is already realized
        // - nothing else to do
        lock.unlock();
    } else {
        nonmeta = realizeClassMaybeSwiftAndUnlock(nonmeta, lock);
        // runtimeLock is now unlocked
        // fixme Swift can't relocate the class today,
        // but someday it will:
        cls = object_getClass(nonmeta);
    }

    // runtimeLock is now unlocked, for +initialize dispatch
    assert(nonmeta->isRealized());
    // ✅关键步骤
    initializeNonMetaClass(nonmeta);

    if (leaveLocked) runtimeLock.lock();
    return cls;
}

initializeNonMetaClass方法中的下面代码,发送消息,调用initalize方法。

callInitialize源码:

void callInitialize(Class cls)
{
    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
    asm("");
}
  • 小结:initalize方法调用是在lookUpImpOrForward方法中,先判断是否是实现了initalize方法,然后调用initializeAndLeaveLocked,进行调用initializeAndMaybeRelock, 然后调用initializeNonMetaClass方法,最终调用callInitialize方法发送消息,调用initalize方法。

  • 当主类和多个分类都实现了initalize方法,那么最终会调用那么类里面的initalize方法呢?

    调用最后编译的分类的方法

总结

本篇主要对类拓展的加载load_iamgesinitalize的调用进行了分析。

  • 类拓展: 在编译时,作为类的一部分直接编译,在读取的时候直接读取到ro中。类拓展以独立文件存在时,不引用,则默认不会加载。

  • 关联对象,可以为分类添加属性,这是个对分类属性的值存储到关联哈希表的一个过程。

  • load_iamges

    • prepare_load_methods准备所有的load方法,遍历类和分类,将这个类的load方法的IMP 添加到list中。
    • call_load_methods调用load方法,先调用主类,在调用分类,分类的load方法调用完后,会将其从loadable_categories移除。
  • initalize

    lookUpImpOrForward方法中,先判断是否是实现了initalize方法,然后调用initializeAndLeaveLocked,进行调用initializeAndMaybeRelock, 然后调用initializeNonMetaClass方法,最终调用callInitialize方法发送消息,调用initalize方法。

个人见解,如有其他意见,请指点。