AI智能
改变未来

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

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

  • 前言
  • 1.  类拓展分析
  • 2.  关联对象原理
  • 3.   load_images 分析
  • 4.   initalize 分析
  • 总结

前言

  通过之前几篇对

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

中有类拓展的方法,以及属性的

setter

getter

。说明此事类拓展已经加载到

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/44094390if (!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 existsObjectAssociationMap *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

源码:

voidload_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 classif (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

  • 读取非懒加载分类,添加分类的
    load

    list

    中。

    add_category_to_loadable_list

查看

schedule_class_load

源码:

static void schedule_class_load(Class cls){if (!cls) return;assert(cls->isRealized());  // _read_images should realizeif (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方法的IMPmethod = cls->getLoadMethod();if (!method) return;  // Don\'t bother if cls has no +load methodif (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 untilthere 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_loads

more_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_iamges

load

方法的调用已经分析完毕,那么当

主类

分类

有相同的方法,怎么调用?

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 dolock.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 dispatchassert(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_iamges

initalize

的调用进行了分析。

  • 类拓展: 在编译时,作为类的一部分直接编译,在读取的时候直接读取到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

    方法。

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

    • 点赞1
    • 收藏
    • 分享
    • 文章举报

    亮亮不想说话发布了18 篇原创文章 · 获赞 10 · 访问量 261私信关注

    赞(0) 打赏
    未经允许不得转载:爱站程序员基地 » iOS 类拓展分析、load_iamges分析、initalize分析