iOS 底层探索系列html
- iOS 底层探索 - alloc & init
- iOS 底层探索 - calloc 和 isa
- iOS 底层探索 - 类
- iOS 底层探索 - cache_t
- iOS 底层探索 - 方法
- iOS 底层探索 - 消息查找
- iOS 底层探索 - 消息转发
- iOS 底层探索 - 应用加载
- iOS 底层探索 - 类的加载
- iOS 底层探索 - 分类的加载
- iOS 底层探索 - 类拓展和关联对象
- iOS 底层探索 - KVC
- iOS 底层探索 - KVO
iOS 查漏补缺系列objective-c
前面咱们探索了 iOS
中类和分类的加载,关于类这一块的内容,咱们还有一些坑没有填,好比类拓展和关联对象,今天让咱们一块儿填下这块的坑。bash
关于类拓展的具体定义,你们能够直接参考 Apple 对于类拓展的说明。markdown
A class extension bears some similarity to a category, but it can only be added to a class for which you have the source code at compile time (the class is compiled at the same time as the class extension).架构
类拓展和分类很类似,可是前提是你拥有原始类的源码,而且是在编译时被附加到类上的。(类和类扩展同时编译)app
类拓展的结构:ide
@interface ClassName ()
@end
复制代码
Because no name is given in the parentheses, class extensions are often referred to as anonymous categories. 由于括号中没有填写任何内容,因此类扩展也被称为匿名的分类。oop
咱们在 Xcode
中建立 Objective
类型的文件的时候,能够选择空文件、分类、协议以及类扩展。布局
若是咱们选择 Extension
选项,Xcode
会帮咱们生成一个 NSObject + 扩展名
的头文件出来,也就是说类扩展的命名方式为 类名_扩展名.h
post
而这样的操做其实咱们不多作,咱们通常都是在 .m
文件中声明一下当前类的拓展,基本上咱们都会在类扩展去声明一些私有的属性、方法。好比在 .h
文件中声明一个只读的属性,而后在 .m
文件的类拓展中去重写这个属性为可读可写。
咱们不妨使用 LLDB
打印看一下类拓展到底是不是在编译时就被附加到了类上面了呢?
咱们在 objc-756
源码中的 objc-debug
项目下新建一个类 Person
,而后给这个类添加一个属性 name
,而后在 .m
文件中的类拓展中添加一个属性 mName
和方法 extM_method
,接着再建立一个 Person
的类拓展 Person+Extension.h
文件:
// Person.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end
NS_ASSUME_NONNULL_END
// Person.m
#import "Person.h"
#import "Person+Extension.h"
@interface Person ()
@property (nonatomic, copy) NSString *mName;
- (void)extM_method;
@end
@implementation Person
+ (void)load{
NSLog(@"%s",__func__);
}
- (void)extM_method{
NSLog(@"%s",__func__);
}
- (void)extH_method{
NSLog(@"%s",__func__);
}
@end
// Person+Extension.h
#import <AppKit/AppKit.h>
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Person ()
@property (nonatomic, copy) NSString *ext_name;
@property (nonatomic, copy) NSString *ext_subject;
- (void)extH_method;
@end
NS_ASSUME_NONNULL_END
复制代码
接着咱们在 main.m
中来测试一下:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [Person alloc];
NSLog(@"%@ - %p", p, p);
}
return 0;
}
复制代码
咱们在 Person
实例化对象 p
这一行打上断点,而后运行项目。接着在控制台进行 LLDB
打印:
由于对象的属性以及方法都存储在类对象上面,而因为类结构里面的
ro
是编译时就肯定了其内容,因此咱们只须要打印出类对象的ro
结构中 是否有类拓展中的mName
属性和extM_method
方法 是否有类扩展中的ext_name
和ext_subject
属性以及extH_method
方法
x/4gx
命令打印出 LGPerson
类对象的内存地址,以 16 进制方式打印,打印 4 段isa
,紧接着是 superclass
,而后是 cache_t
。咱们前面已经分析过,在默认的 arm64
处理器架构下,isa
占 8 个字节,superclass
占 8 个字节,而 cache_t
的三个属性加起来是 8 + 4 + 4 = 16 个字节,因此要想拿到 bits
须要进行 8 + 8 + 16 = 32 字节的内存平移,可是这里是 16 进制,因此须要移动 0x20 个内存地址,也就是 0x100002420 + 0x20 = 0x100002440
data()
属性会返回 bits.data()
,因此这里直接打印刚才取到的 bits
的 data()
属性,而 bits
的 data()
属性其实返回的是 rw
。struct objc_class : objc_object { class_rw_t *data() { return bits.data(); } } struct class_data_bits_t { class_rw_t* data() { return (class_rw_t *)(bits & FAST_DATA_MASK); } } 复制代码
rw
的属性 ro
,而后咱们先尝试读取 baseMethodList
属性,该属性存储的是编译时肯定的类的全部的方法。baseMethodList
属性是一个 List
类型的容器,咱们直接使用 get(index)
来获取其 index
处的值,结果咱们所要寻找的 extH_method
和 extM_method
出现了,不过还没结束,咱们还没验证类拓展中声明的两个属性,让咱们打印一下 ro
的 baseProperties
mName
,ext_name
和 ext_subject
都被找到了,那么是否是就是说类拓展就是编译时肯定的了呢?咱们还漏掉了这三个属性的 getter
和 setter
了,让咱们回过头再去 baseMethodList
中查找一下getter
和 setter
方法也生成了,至此,咱们就彻底肯定了类拓展在编译时就会被加载到类的 ro
中。这里有个注意点,就是若是咱们没有在类的头文件或者源文件中引入单独的类拓展头文件,那么这个单独的类拓展的头文件里面的属性和方法将不会被加载到类上面来。
研究对象 | 加载时机 | 操做对象 | 可否经过@property声明属性生成 getter 和 setter |
---|---|---|---|
分类(实现了load方法) | 运行时 | rw | 不能,须要借助关联对象来实现 |
分类(没有实现load方法) | 编译时 | ro | 不能,须要借助关联对象来实现 |
类拓展 | 编译时 | ro | 能够 |
上一节咱们探索了类拓展以及类拓展与分类的区别,咱们知道,类拓展中能够声明属性,编译器会帮助咱们生成属性对应的 getter
和 setter
方法,可是分类经过 @property
的方式来声明属性却不能生成 getter
和 setter
方法。而其实 iOS
中有一种方式能够为分为增长具备 getter
和 setter
的属性,那就是 - 关联对象 Associated Objects
。
关联对象的官方定义能够在 苹果官方文档 上找到。
Associative references, available starting in OS X v10.6, simulate the addition of object instance variables to an existing class. Using associative references, you can add storage to an object without modifying the class declaration. This may be useful if you do not have access to the source code for the class, or if for binary-compatibility reasons you cannot alter the layout of the object.
关联引用,是从
OS X 10.6
开始启用的,模拟了将对象实例变量添加到已经存在的类中。经过使用关联引用,你能够在不修改类声明的前提下为对象添加内容。若是你无权访问该类的源代码,或者因为二进制兼容性缘由而没法更改该对象的布局,则这可能颇有用。
Associations are based on a key. For any object you can add as many associations as you want, each using a different key. An association can also ensure that the associated object remains valid for at least the lifetime of the source object.
关联引用机制基于
key
。对于任何对象,你均可以根据须要添加任意数量的关联引用,每一个关联都使用不一样的key
。关联引用还能够确保关联的对象至少在源对象的声明周期内保持有效。
而关于关联对象的最佳实践能够参考 NSHipster - Associated Objects 一文。
从苹果官方文档能够看到,关联引用其实不是只能在分类中使用,只不过对于咱们平常开发来讲,分类中使用关联引用仍是更经常使用的场景。相信大多数开发者都知道怎么使用关联引用,的确,关联引用使用起来很简单,不外乎两个方法:
// 设置关联对象
objc_setAssociatedObject()
// 获取关联对象
objc_getAssociatedObject()
复制代码
咱们若是要给一个分类中的属性设置关联对象,须要重写属性的 setter
方法,而后使用 objc_setAssociatedObject
:
- (void)setXXX:(关联值数据类型)关联值
objc_setAssociatedObject(self, 关联的key, 关联值, 关联对象内存管理策略);
}
复制代码
而后还须要重写 getter
方法,而后使用 objc_getAssociatedObject
:
- (关联值数据类型)关联值{
return objc_getAssociatedObject(self, 关联的key);
}
复制代码
这其中的关联对象内存管理策略以下表所示:
关联策略 | 等同的 @property | 描述 |
---|---|---|
OBJC_ASSOCIATION_ASSIGN | @property (assign) 或 @property (unsafe_unretained) 指定一个关联对象的弱引用。 | 指定一个关联对象的弱引用。 |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | @property (nonatomic, strong) | 指定一个关联对象的强引用,不能被原子化使用。 |
OBJC_ASSOCIATION_COPY_NONATOMIC | @property (nonatomic, copy) | 指定一个关联对象的copy引用,不能被原子化使用。 |
OBJC_ASSOCIATION_RETAIN | @property (atomic, strong) | 指定一个关联对象的强引用,能被原子化使用。 |
OBJC_ASSOCIATION_COPY | @property (atomic, copy) | 指定一个关联对象的copy引用,能被原子化使用。 |
关于关联对象的底层原理,这里有一篇灯塔
draveness
的博文 关联对象 AssociatedObject 彻底解析 十分值得一读。
固然,若是也能够跟随笔者一块儿探索下关联对象的底层原理。咱们不妨从最直观的 objc_setAssociatedObject
方法开始切入:
// objc-runtime.mm
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
而 _object_set_associative_reference
方法的实现很是长,这里就分段来进行探索吧。
// 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; 复制代码
根据注释咱们能够知道,当传入的
object
和key
同时为nil
的时候,直接返回。这样的处理是为了不传入空值时而致使崩溃。
// objc-references.mm 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)); // objc-runtime-new.h bool forbidsAssociatedObjects() { return (data()->flags & RW_FORBIDS_ASSOCIATED_OBJECTS); } 复制代码
判断要进行关联的对象是否禁用掉了关联引用,这里是经过对象的
isa
的rw
的flags
属性与上一个宏RW_FORBIDS_ASSOCIATED_OBJECTS
来判断的。
// retain the new value (if any) outside the lock. ObjcAssociation old_association(0, nil); 复制代码
初始化一个
ObjcAssociation
对象,用于持有原有的关联对象
id new_value = value ? acquireValue(value, policy) : nil;
复制代码
判断传入的关联对象值是否存在,若是存在就调用
acquireValue
方法来获取值,咱们能够进入acquireValue
方法看一下:
static id acquireValue(id value, uintptr_t policy) { switch (policy & 0xFF) { case OBJC_ASSOCIATION_SETTER_RETAIN: return objc_retain(value); case OBJC_ASSOCIATION_SETTER_COPY: return ((id(*)(id, SEL))objc_msgSend)(value, SEL_copy); } return value; } 复制代码
能够看到
acquireValue
会根据关联策略来进行retain
或copy
消息的发送
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
复制代码
初始化一个
AssociationsManager
对象,而后获取一个AssociationsHashMap
哈希表,而后经过DISGUISE
方法做为去哈希表查找的key
。这里的DISGUISE
其实进行了按位取反的操做。
inline disguised_ptr_t DISGUISE(id value) { return ~uintptr_t(value); } 复制代码
若是传入的关联对象值存在,说明是进行赋值操做;若是传入的关联对象值不存在,说明是进行置空操做。这里咱们先看一下赋值操做的流程:
if (new_value) { // break any existing association. AssociationsHashMap::iterator i = associations.find(disguised_object); if (i != associations.end()) { // secondary table exists ObjectAssociationMap *refs = i->second; 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). ObjectAssociationMap *refs = new ObjectAssociationMap; associations[disguised_object] = refs; (*refs)[key] = ObjcAssociation(policy, new_value); object->setHasAssociatedObjects(); } } 复制代码
1.经过上一步按位取反以后的结果,在
AssociationsHashMap
哈希表中查询,这里是经过迭代器的方式进行查询,查询的结果是ObjcAssociation
对象,这个结构也是一个哈希表,其内部存储的是_object_set_associative_reference
方法传入的key
为键,ObjcAssociation
对象为值的键值对 2.若是没有查询到,说明以前在当前类上没有设置过关联对象。则须要初始化一个ObjectAssociationMap
出来,而后经过setHasAssociatedObjects
设置当前对象的isa
的has_assoc
属性为true
3.若是查询到了,说明以前在当前类上设置过关联对象,接着须要看key
是否存在,若是key
存在,那么就须要更新原有的关联对象;若是key
不存在,则须要新增一个关联对象
// setting the association to nil breaks the association. 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); } } 复制代码
由于来到这里的条件是
new_value
为nil
,也就表明着要删除关联对象,内部的逻辑和上面的流程大同小异,不过最后多了一步在ObjectAssociationMap
擦除key
对应的节点
// release the old value (outside of the lock). if (old_association.hasValue()) ReleaseValue()(old_association); 复制代码
最后会判断
old_association
是否有值,若是有的话就释放掉,固然前提是旧的关联对象的策略是OBJC_ASSOCIATION_SETTER_RETAIN
struct ReleaseValue { 复制代码
void operator() (ObjcAssociation &association) {
releaseValue(association.value(), association.policy());
}
复制代码
}; static void releaseValue(id value, uintptr_t policy) { if (policy & OBJC_ASSOCIATION_SETTER_RETAIN) { return objc_release(value); } }
复制代码
objc_setAssociatedObject
方法分析完了,咱们接着看另一个重要的方法 objc_getAssociatedObject
:
id objc_getAssociatedObject(id object, const void *key) { return _object_get_associative_reference(object, (void *)key); } 复制代码
能够看到,跟
objc_setAssociatedObject
同样,objc_getAssociatedObject
这里又包裹了一层,其实现为_object_get_associative_reference
,而这个方法相比于上一节的_object_set_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());
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();
if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
objc_retain(value);
}
}
}
}
if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
objc_autorelease(value);
}
return value;
}
复制代码
1.先初始化一个空的
value
,以及一个策略为OBJC_ASSOCIATION_ASSIGN
的policy
2.初始化一个AssociationsManager
关联对象管理类,接着拿到AssociationsHashMap
对象,这个对象在AssociationsManager
底层是静态的 3.而后以DISGUISE(object)
按位取反以后的结果为键去查询AssociationsHashMap
4.若是在AssociationsHashMap
中扎到了,接着以key
为键去ObjectAssociationMap
中查询ObjcAssociation
若是在ObjectAssociationMap
中查询到了ObjcAssociation
,则把值和策略赋值给方法入口声明的两个临时变量,而后判断获取到的关联对象的策略是否为OBJC_ASSOCIATION_GETTER_RETAIN
,若是是的话,须要对关联值进行retain
操做 5.最后判断若是关联值是否存在且策略为OBJC_ASSOCIATION_GETTER_AUTORELEASE
,是的话就须要调用objc_autorelease
来释放关联值 6.最后返回关联值
objc_removeAssociatedObjects
方法咱们平时可能用的很少,从字面含义来看,这个方法应该是用来删除关联对象。咱们来到它的定义处:
void objc_removeAssociatedObjects(id object) { if (object && object->hasAssociatedObjects()) { _object_remove_assocations(object); } } 复制代码
这里会判断
object
存在且有关联对象才会进入真正的实现_object_remove_assocations
,该实现也不是很复杂,咱们仍是直接贴出代码
void _object_remove_assocations(id object) {
vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
if (associations.size() == 0) return;
disguised_ptr_t disguised_object = DISGUISE(object);
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// copy all of the associations that need to be removed.
ObjectAssociationMap *refs = i->second;
for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
elements.push_back(j->second);
}
// remove the secondary table.
delete refs;
associations.erase(i);
}
}
// the calls to releaseValue() happen outside of the lock.
for_each(elements.begin(), elements.end(), ReleaseValue());
}
复制代码
这里会将对象包含的全部关联对象加入到一个
vector
中,而后对全部的ObjcAssociation
对象调用ReleaseValue()
方法,释放再也不被须要的值。
getter
和 setter
,并且分类不能声明实例变量ObjcAssociation
对象的结构AssociationsManager
管理类存储了一个静态的哈希表 AssociationsHashMap
,这个哈希表存储的是以对象指针为键,以该对象全部的关联对象为值,而对象全部的关联对象又是以 ObjectAssociationMap
来存储的ObjectAssociationMap
存储结构为 key
为键,ObjcAssociation
为值isa
的 has_assoc