iOS八股文(十)分类和关联对象源码解析
我们平时在开发的时候经常会使用分类来添加方法、协议、属性,但在添加属性的时候属性是不会自动生成成员变量的,这时候我们就需要关联对象来动态存储属性值。
@interface NSObject (Study)
@property (nonatomic, strong) NSObject *obj1;
@property (nonatomic, strong) NSObject *obj2;
- (void)instanceMethod;
+ (void)classMethod;
@end
static const void *NSObjectObj1Name = "NSOBJECT_OBJ1";
@implementation NSObject (Study)
@dynamic obj2;
- (void)setObj1:(NSObject *)obj1 {
objc_setAssociatedObject(self, &NSObjectObj1Name, obj1, OBJC_ASSOCIATION_RETAIN);
}
- (NSObject *)obj1 {
return objc_getAssociatedObject(self, &NSObjectObj1Name);
}
- (void)instanceMethod {
NSLog(@"-类名:%@,方法名:%s,行数:%d",NSStringFromClass(self.class),__func__,__LINE__);
}
+ (void)classMethod {
NSLog(@"+类名:%@,方法名:%s,行数:%d",NSStringFromClass(self.class),__func__,__LINE__);
}
@end
这段代码包括Object-C
的两个知识点,分别是分类
和关联对象
,本文主要围绕这两个知识点来进行探究。
分类
可以使用clang
重写将上面的代码成c++代码。
重写后的关键代码:
static struct _category_t _OBJC_$_CATEGORY_NSObject_$_Study __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"NSObject",
0, // &OBJC_CLASS_$_NSObject,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_NSObject_$_Study,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_NSObject_$_Study,
0,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_NSObject_$_Study,
};
可以看到其根本的实现是_category_t
这个结构,那么我们可以借助objc4(828.2)源码来查找关于category_t
的定义:
struct category_t {
const char *name;
classref_t cls;
WrappedPtr<method_list_t, PtrauthStrip> instanceMethods;
WrappedPtr<method_list_t, PtrauthStrip> classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
protocol_list_t *protocolsForMeta(bool isMeta) {
if (isMeta) return nullptr;
else return protocols;
}
};
根据源码的定义,我们可以总结:
- 分类里面即有实例方法列表又有类方法列表
- 分类没有成员变量列表
分类的加载
分类的加载是在objc
中实现的。
在源码attachCategories
的实现中:
// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order,
// oldest categories first.
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
int flags)
{
//。。。缩简//
bool fromBundle = NO;
bool isMeta = (flags & ATTACH_METACLASS);
//新建rwe
auto rwe = cls->data()->extAllocIfNeeded();
//debug代码可以放这里
//遍历每个分类
for (uint32_t i = 0; i < cats_count; i++) {
auto& entry = cats_list[i];
//获取分类里面的方法
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
if (mcount == ATTACH_BUFSIZ) {
prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
rwe->methods.attachLists(mlists, mcount);
mcount = 0;
}
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
fromBundle |= entry.hi->isBundle();
}
//。。缩简了协议和属性的内容
if (mcount > 0) {
prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
NO, fromBundle, __func__);
//添加分类的方法到rwe中
rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
if (flags & ATTACH_EXISTING) {
flushCaches(cls, __func__, [](Class c){
// constant caches have been dealt with in prepareMethodLists
// if the class still is constant here, it's fine to keep
return !c->cache.isConstantOptimizedCache();
});
}
}
rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
const char *mangledName = cls->nonlazyMangledName();
//你添加分类的类名
const char *className = "OSObject";
if (strcmp(mangledName, className) == 0 && !isMeta) {
printf("debug find it");
}
注意:这里面,分类和本类都需要实现+load
方法才可以。
我们先看断点的堆栈信息
可以看到是
load_images
中调用的。前面的文章已经讲解过load_images
的调用时机。
这里可以再复习一遍。
接下来我们通过lldb
来调试。在使用lldb
的时候,要多看源码,在源码中寻找可以使用的方法。如果是地址就用*
来去处地址里面的内容。如果是内容看源码中的定义,使用其方法获取有效信息。
这里需要注意⚠️,使用了2种获取列表数量的方式,其中一种是不准确的,但是在加载完分类的时候,不准确的方式就正确了,暂时没找到原因。我的本类中有一个实例方法,分类里面也有一个实例方法,在没有加载分类的时候,我的方法列表里面的数量是1(第2中方式查看得到)。我们继续过断点,再设置完分类后,我们同样方式再来看效果:
可以看到加载完分类之后,方法列表的数量是2。
纠正:
这里的lldb指令有些过于复杂,在我们获取到method_array_t
的时候,该结构体有一个count()
方法的,该方法可直接获取数量。
使用count()方法的调试如下:
在没有加载分类的时候count为1,在看看加载完分类之后的:
可以看到count为2,得到了和上面同样的结论。
关联对象
回到我们一开始的代码,还有一个关联对象。我们先在objc源码中找到关联对象api的实现部分:
void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
_object_set_associative_reference(object, key, value, policy);
}
void objc_removeAssociatedObjects(id object)
{
if (object && object->hasAssociatedObjects()) {
_object_remove_assocations(object, /*deallocating*/false);
}
}
可以看到是调用了内部函数_object_set_associative_reference
,解析注解如下:
void
_object_set_associative_reference(id object, const 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;
//isa有一位信息为禁止关联对象,如果设置了,直接报错
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));
//包装对象,转换类型
DisguisedPtr<objc_object> disguised{(objc_object *)object};
//包装值和属性信息
ObjcAssociation association{policy, value};
// retain the new value (if any) outside the lock.
//设置属性信息
association.acquireValue();
bool isFirstAssociation = false;
{
//调用构造函数,构造函数内加锁操作
AssociationsManager manager;
//获取全局的HasMap
AssociationsHashMap &associations(manager.get());
//如果值不为空
if (value) {
//去关联对象表中找对象对应的二级表,如果没有内部会重新生成一个
auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
//如果没有找到
if (refs_result.second) {
/* it's the first association we make */
//说明是第一次设置关联对象,把是否关联对象设置为YES
isFirstAssociation = true;
}
/* establish or replace the association */
auto &refs = refs_result.first->second;
//在二级表中找key对应的内容,
auto result = refs.try_emplace(key, std::move(association));
//如果已经有内容了,没有内容上面根据association已经插入了值,所以啥也不用干
if (!result.second) {
//替换掉
association.swap(result.first->second);
}
//如果value为空
} else {
//通过object找对应的二级表
auto refs_it = associations.find(disguised);
// 如果有
if (refs_it != associations.end()) {
auto &refs = refs_it->second;
//通过key再在二级表里面找对应的内容
auto it = refs.find(key);
//如果有
if (it != refs.end()) {
//删除掉
association.swap(it->second);
refs.erase(it);
if (refs.size() == 0) {
associations.erase(refs_it);
}
}
}
}
}
// Call setHasAssociatedObjects outside the lock, since this
// will call the object's _noteAssociatedObjects method if it
// has one, and this may trigger +initialize which might do
// arbitrary stuff, including setting more associated objects.
if (isFirstAssociation)
object->setHasAssociatedObjects();
// release the old value (outside of the lock).
association.releaseHeldValue();
}
其中需要注意try_emplace
这个方法。
// Inserts key,value pair into the map if the key isn't already in the map.
// The value is constructed in-place if the key is not in the map, otherwise
// it is not moved.
template <typename... Ts>
std::pair<iterator, bool> try_emplace(KeyT &&Key, Ts &&... Args) {
BucketT *TheBucket;
//如果已经存在了
if (LookupBucketFor(Key, TheBucket))
return std::make_pair(
makeIterator(TheBucket, getBucketsEnd(), true),
false); // Already in map.
// Otherwise, insert the new element.
//不存在就插入一个新的对象
TheBucket =
InsertIntoBucket(TheBucket, std::move(Key), std::forward<Ts>(Args)...);
return std::make_pair(
makeIterator(TheBucket, getBucketsEnd(), true),
true);
}
这里返回的是一个迭代器,如果有内容返回对应的迭代器,如果没有的话,添加一个,并返回迭代器。
可以看到使用了两次try_emplace
方法,可以得知他是嵌套两层的HashMap结构,根据上面代码的理解,可以得到以下结构图:
下面我们在看看get_associtiond的源码:
id
_object_get_associative_reference(id object, const void *key)
{
ObjcAssociation association{};
{ //加锁
AssociationsManager manager;
//全局的表
AssociationsHashMap &associations(manager.get());
//通过object找对应的二级表
AssociationsHashMap::iterator i = associations.find((objc_object *)object);
if (i != associations.end()) {
ObjectAssociationMap &refs = i->second;
//在二级表内通过key在找对应的值
ObjectAssociationMap::iterator j = refs.find(key);
if (j != refs.end()) {
association = j->second;
association.retainReturnedValue();
}
}
}
//取值并返回然后放到自动释放池中
return association.autoreleaseReturnedValue();
}
除了get
、set
方法,在对象被销毁的时候还会调用remove
方法,从全局的关联对象表中把对象对应的关联表删除。后面在整理对象销毁流程的时候会涉及。
参考链接
以上便是我们经常在分类里面编码对应源码的解析。希望各位看官有什么不同见解可以一起沟通学习。
到这里,已经通过十篇文章对Object-C的动态性,从多角度进行了分析。虽然起名为八股文系列,但个人认为除了面试装X之外,认证探究后,对底层也有更深一步的理解,能为以后编码提供一定的帮助,尤其是通过对源码的分析,可以效仿优秀代码的逻辑构思和编码风格。这些博客的目的,首先是希望自己能通过写博客,增强记忆,在以后面试复习的时候有一份不错的复习资料,其二是记录学习探究过程,能对外传播自己微薄的能量,能和优秀的从业者沟通交流,从而进一步提升自己。在写博客的过程中查看借鉴了很多优秀的博客,大多已经注明了参考链接,也有部分遗漏的,如有雷同,可联系作者,作者会第一时间补全出处。写博客不易,转载注明出处。
转载自:https://juejin.cn/post/7099767067221426190