今天来聊聊类的扩展。
首先来看看扩展和分类的区别
格式上,扩展是匿名的分类
我们在OC的.m文件中,经常会使用扩展对某类进行私有的属性或者成员变量的声明,如下:
然后我们再来看一下类目的写法:
比较一下扩展和类目的写法,我们会发现它们两个的不同点就在于:类后面的小括号里面是否有内容,这个内容就是类目的名字。
因此,在形式上,我们可以说,扩展是匿名的分类。
那么,类扩展的数据,是如何加载进内存的呢?答案是,类扩展中的内容会在编译时作为类的一部分进行编译,因此读取的时候可以直接在ro中获取到。
需要注意的是,我们可以在类的.m文件中创建一个扩展,用于声明私有的数据和变量;也可以创建一个专门的扩展文件,这样的话,一个类如果需要相应的扩展,那就引入相应的扩展文件即可(一定要注意,是需要引入的哦~),如果不引入的话,就不会在编译的时候加载进ro的哦~
扩展中可以正常添加属性,分类中添加的属性不会自动生成setter和getter
我之前在关于类目的几点探讨中详细比较过类目和扩展,也介绍过为什么类目中添加的属性不能自动生成setter和getter。今天,我会在一个更底层的维度去解释这个原因。
前面提到了,扩展中的内容和原类中的内容一样,他们都是在编译期就会被直接编译进内存,因此是可以直接在ro中获取到的。而分类在运行的时候才会被加载进rw。
而通过比较ro和rw的数据结构我们发现,存储成员变量的数组ivars只在ro中有,rw中是没有ivars的。因此,分类中是添加不了成员变量的,而属性自动生成的setter和getter是需要生成一个带有下划线的成员变量的,所以分类中声明的属性不会自动生成setter和getter。
类目中关联对象的原理
在Runtime——使用类目给某个类添加属性中,我们可以了解到如何在类目中给一个类添加属性,现在我们就来探究一下其底层原理。
我们在自定义的setter中会通过objc_setAssociatedObject函数来设置值:
objc_setAssociatedObject(<#id _Nonnull object#>, <#const void * _Nonnull key#>, <#id _Nullable value#>, <#objc_AssociationPolicy policy#>)
它需要传4个参数:
我们点进去看其源码:
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
_object_set_associative_reference(object, (void *)key, value, policy);
}
我们看到,objc_setAssociatedObject内部又调用了_object_set_associative_reference。这里为什么需要再封装一层呢?objc_setAssociatedObject供外界调用,而_object_set_associative_reference是内部实现,万一将来某一天进行了代码调整,_object_set_associative_reference这个函数的函数名变了,那么我在外界调用的objc_setAssociatedObject不会受到丝毫影响,只需要改objc_setAssociatedObject里面调用的_object_set_associative_reference即可。
接下来我们看_object_set_associative_reference的源码:
// 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) {
// break any existing association.
// 获取 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);
}
}
}
}
给一个对象设置关联值的步骤总结如下:
上面了解了如何给一个对象设置关联值,那么获取对象的关联值是如何进行的呢?
我们在自定义的getter方法中会通过objc_getAssociatedObject函数来获取值:
objc_getAssociatedObject(<#id _Nonnull object#>, <#const void * _Nonnull key#>)
它有两个参数:
objc_getAssociatedObject函数的实现如下:
id objc_getAssociatedObject(id object, const void *key) {
return _object_get_associative_reference(object, (void *)key);
}
_object_get_associative_reference的源码如下:
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;
}
据此,我们可以分析出获取一个对象的关联值的步骤:
到这里,你可能会有一个疑问,当对象释放的时候,其关联的属性会同步释放吗?答案是会的,下面来分析。
任何对象的释放都会调用到dealloc方法:
- (void)dealloc {
_objc_rootDealloc(self);
}
_objc_rootDealloc:
_objc_rootDealloc(id obj)
{
assert(obj);
obj->rootDealloc();
}
rootDealloc:
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
object_dispose:
id
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
objc_destructInstance:
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;
}
在上面?的第10行,就是释放关联对象的操作。
load_images
_dyld_objc_notify_register这个函数里面会调用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();
}
这里面主要是调用了prepare_load_methods和call_load_methods两个函数。
prepare_load_methods
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertLocked();
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());
add_category_to_loadable_list(cat);
}
}
上面的第7行,首先是获取到所有非懒加载的类列表classlist。也许你会有疑问,为什么这里必须是非懒加载类呢?我们知道,实现了load方法就是非懒加载类,而我们现在研究的是load方法的加载时机,所以研究的这个类势必是非懒加载类。
获取到所有非懒加载的类列表classlist之后,遍历它,然后在每一次遍历体内都执行schedule_class_load函数。
接下来我们看一下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);
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_classes[loadable_classes_used].cls = cls;
loadable_classes[loadable_classes_used].method = method;
loadable_classes_used++;
}
重点关注一下第23~25行,loadable_classes是一个数组,其元素类型是loadable_class:
struct loadable_class {
Class cls; // may be nil
IMP method;
};
23~25行的作用就是将cls的信息加入到loadable_classes数组中。
接下来我们回到prepare_load_methods函数的源码,看第13~25行:
首先,会通过_getObjc2NonlazyCategoryList函数获取到非懒加载类目列表categorylist,然后遍历这个列表,在每一次遍历中,通过add_category_to_loadable_list函数,将类目cat的信息加入到loadable_categories数组中,loadable_categories数组的元素类型是loadable_category:
struct loadable_category {
Category cat; // may be nil
IMP method;
};
call_load_methods
前面通过prepare_load_methods函数已经将非懒加载类和非懒加载分类的信息分别加进loadable_classes和loadable_categories数组中了,接下来我们就是调用它们。
首先看一下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) {
call_class_loads();
}
// 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;
}
上面源码中的第14~24行,是一个do-while循环遍历,遍历执行原类以及分类中的+load方法。
总结
load_images中调用load方法的步骤如下:
一定要注意哦,不管是主类还是分类中,每一个+load方法都会被调用哦~
这里也引申出来一个面试题:主类和分类中有相同的方法,如何调用?
initialize方法分析
截止到上面所有的load方法调用完毕,整个函数也就执行完毕了。那么initialize方法是在什么时候调用的呢?
关于+load和+initialize的比较,我之前也写过两篇文章,大家可以了解一下:
我们应该都知道如下结论:一个类的+initialize方法会在第一次初始化这个类之前被调用,并且只被调用一次。这说明,+initialize方法很有可能是在消息查找期间被调用的,所以,我们就去lookUpImpOrForward函数中找找,看看能不能发现一些蛛丝马迹:
你别说,还真找到了,?上图红框内。
我们看一下initializeAndLeaveLocked函数的源码:
// 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);
}
它调用了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;
}
initializeAndMaybeRelock函数里面又调用了callInitialize函数:
接着看callInitialize函数的源码:
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
asm("");
}
这里就是给cls发送SEL_initialize消息,也就是调用+initialize方法。
要注意哦,+initialize方法和一般的方法的调用是一样的哦~都是调用的是最后attach进rw中的那一个方法的实现哦~
以上。