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私信关注