iOS 类拓展分析、load_iamges分析、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/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
。 - 读取非懒加载分类,添加分类的
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 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_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 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_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
方法。
个人见解,如有其他意见,请指点。