探求系列已发布文章列表,有爱好的同学能够翻阅一下:
第一篇 | iOS 特点 @property 详细探求
第二篇 | iOS 深化了解 Block 运用及原理
第三篇 | iOS 类别 Category 和扩展 Extension 及相关目标详解
第四篇 | iOS 常用锁 NSLock ,@synchronized 等的底层完成详解
第五篇 | Equality 详细探求
——- 正文开端 ——-
引言
类别
category
答应你在没有源代码情况下,依然能够向已有的类中增加办法。它的功用很强大,答应你无需子类化而扩展现有类。运用类别,还能够将类的完成分发到多个文件中。类扩展extension
与此相似,但答应在主类@interface
块内以外的位置为类声明额定的 API。
-
代码示例
类别 Category:
#import "ClassName.h"
@interface ClassName (CategoryName)
// method declarations
@end
扩展 Extension:
@interface MyClass : NSObject
@property (nonatomic, copy, readonly) NSString *name;
@end
// Private extension, typically hidden in the main implementation file.
@interface MyClass ()
@property (nonatomic, copy, readwrite) NSString *name;
@end
-
实质
您能够经过在接口文件中,以类别称号声明它们,并在完成文件中以相同称号界说它们来将办法增加到类。类别称号表明这些办法是对在别处声明的类的增加,而不是一个新类。可是,不能经过类别增加实例变量到类中。
类别增加的办法成为类类型的一部分。例如,在一个类别中增加到 NSArray
类中的办法,是编译器希望 NSArray
实例在其装备表中包含的办法。然而,子类中增加到 NSArray
类中的办法并不包含在 NSArray
类型中。(这只对静态类型的目标有影响,由于静态类型是编译器知道目标类的仅有办法。)
-
常用介绍
类别:
Category
在经历过编译后里边的内容:目标办法、类办法、协议、特点都转化为类型为 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;
}
};
详细 category
都能做什么,常用的大致有如下几个场景:
- 在不修改原有类的基础上给原有类增加办法,由于
category
的结构体指针中没有特点列表,只要办法列表。所以原则来说只能给category
增加办法,不能增加特点,假如需求给category
增加相似特点功用,能够经过相关目标完成,下面会有详细介绍; -
Category
中的办法优先于原有类同名的办法,即会优先调用category
中的办法,疏忽原有类的办法。即category
与原有类同名办法调用的优先级为:category
> 本类 > 父类。开发中尽量不要掩盖本类的办法,假如掩盖会导致本类办法失效; - 假如给
category
增加特点@property
,只会生成setter/getter
办法的声明,并不会有详细的代码完成,详细解说可参阅历史文章:iOS 特点 @property 详细探求 -
Category
中能够拜访原有类中.h
中声明的成员变量;
类的扩展 Extension:
@interface Person ()
@end
类的 extension
看起来很像一个匿名的 category
。一般用来声明私有办法,私有特点和私有成员变量。
extension 在编译期抉择, category 在运行期抉择。
类扩展不能像类别 category
那样拥有独立的完成部分(@implementation
部分)。也便是说,类的扩展所声明的办法必须依托原类的完成代码部分来完成。
因而,咱们不能给体系类增加类扩展。即扩展的办法只能在原类中完成。例如咱们扩展 NSString
,那么只能在 NSString的.m
中完成,但咱们拿不到 NSString.m
的源码。因而,咱们不能给 NSString
增加扩展,只能给 NSString
增加 category
。
界说在 .m
文件中的类扩展办法为私有的,假如需求声明私有办法,这种办法特别适宜。界说在 .h 文件(头文件)中的类扩展办法为公有的。
类别 Category 与类扩展 Extension 的差异
- Category 有姓名,extension 没有姓名,像是一个匿名的 category;
- Category 是运行时抉择,而 extension 是编译时抉择。所以 category 中的办法没有完成不会正告,而 extension 声明的办法不完成则会呈现正告;
- Category 原则上能够增加特点,实例办法,类办法,并且外部类是能够拜访的。extension 能增加特点、办法、实例变量,且默许是私有的;
- Category 有自己的完成部分,extension 没有自己的完成部分,只能依靠类本身来完成;
- 能够为体系类增加 category,而不能为体系类增加 extension;
关于类的 + (void)load
与 + (void)initialize
的差异
+ (void)load
+ (void)initialize
- 两者的差异如下:
- 相同点:
- 两个函数都是体系主动调用,因而无需手动调用(假如手动调用则与一般函数调用相似);
- 两个函数都会山人调用各自父类对应的
+ (void)load
或+ (void)initialize
办法,即子类调用办法之前,会优先调用其父类对应的办法;- 两个函数内部都运用了锁,因而两个函数都是线程安全的;
- 不同点:
- 调用机遇不同:
+ (void)load
在main
函数之前履行,即objc_init
Runtime初始化时调用,且只会调用一次。+ (void)initialize
在类的办法首次被调用时履行,每个类只会调用一次,但父类可能会调用屡次;- 调用办法不同:
+ (void)load
是依据函数地址直接调用,+ (void)initialize
是经过音讯发送机制即objc_msgSend(id self, SEL _cmd, ...)
调用;- 子类父类调用联系不同:
- 假如子类没有完成
+ (void)load
,则不会调用其父类的+ (void)load
办法。- 假如子类没有完成
+ (void)initialize
,则会调用其父类的办法,因而父类的+ (void)initialize
可能会调用屡次;
- 类别
category
对调用的影响不同:
- 假如
category
中完成了+ (void)load
,则会优先调用原类的的+ (void)load
,再调用category
的,即优先级为:父类 > 原类 >category
- 没有承继联系的不同类中的
+ (void)load
的调用次序跟Compile Sources
次序有关,即在前面的优先编译的类或许category
先调用( 备注: 一切类的+ (void)load
优先级大于category
的优先级);- 同一个类的
category
的+ (void)load
的调用次序跟Compile Sources
次序有关,即在前面的优先编译的category
会先调用;- 同一镜像中主工程的
+ (void)load
办法优先调用,然后再调用静态库的+ (void)load
办法。有多个静态库时,静态库之间的履行次序与编译次序有关,即它们在Link Binary With Libraries
中的次序;- 不同镜像中,动态库的
+ (void)load
办法优先调用,然后再调用主工程的+ (void)load
,多个动态库的+ (void)load
办法的调用次序跟编译次序有关,即它们在Link Binary With Libraries
中的次序;
- 假如
category
中完成了+ (void)initialize
,则原类的+ (void)initialize
将不会再调用
- 多个
category
中一同完成了+ (void)initialize
办法时,Compile Sources中次序最下面的一个,即最终一个被编译 Category 的+ (void)initialize
会履行;
类别 Category
中增加相关目标
Category
中增加特点 @property
在之前文章已做过简单介绍,详细可检查 iOS 特点 @property 详细探求,这儿咱们重点说一下相关目标的完成原理:
操作相关目标有三个中心办法:
- 设置相关目标办法:
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)
- id _Nonnull object: 给哪个目标增加相关目标,一般是当前目标,即用
self
即可;
- const void * _Nonnull key: 相关目标的
key
,作为相关目标的仅有标识存在,它只要是一个非空指针即可;
- id _Nullable value: 相关目标的值,经过相关
key
进行设值及获取值,假如需求铲除一个已存在的相关目标,将其值设置为nil
即可;
- objc_AssociationPolicy policy: 相关战略,即相关目标的存储形式,其可选枚举值如下:
public enum objc_AssociationPolicy : UInt {
case OBJC_ASSOCIATION_ASSIGN = 0 // 指定对相关目标的弱引证
case OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1 // 指定对相关目标的强引证,非原子性
case OBJC_ASSOCIATION_COPY_NONATOMIC = 3 // 指定仿制相关的目标,非原子性
case OBJC_ASSOCIATION_RETAIN = 769 // 指定对相关目标的强引证,原子性
case OBJC_ASSOCIATION_COPY = 771 // 指定仿制相关的目标,原子性
}
依据源码,咱们能够知道 objc_setAssociatedObject
实践调用的是 _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;
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;
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 */
isFirstAssociation = true;
}
/* establish or replace the association */
auto &refs = refs_result.first->second;
auto result = refs.try_emplace(key, std::move(association));
if (!result.second) {
association.swap(result.first->second);
}
} else {
auto refs_it = associations.find(disguised);
if (refs_it != associations.end()) {
auto &refs = refs_it->second;
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();
}
依据上述源码能够发现,ObjcAssociation
依据传入的 value
及 policy
创立目标,并经过 acquireValue
函数处理生成新的 _value
。acquireValue
函数内部是经过对战略 policy
的判别进行相应处理,生成新值,其完成如下:
inline void acquireValue() {
if (_value) {
switch (_policy & 0xFF) {
case OBJC_ASSOCIATION_SETTER_RETAIN:
_value = objc_retain(_value);
break;
case OBJC_ASSOCIATION_SETTER_COPY:
_value = ((id(*)(id, SEL))objc_msgSend)(_value, @selector(copy));
break;
}
}
}
接下来咱们首要需求了解一下 AssociationsManager
和 AssociationsHashMap
class AssociationsManager {
using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
static Storage _mapStorage;
public:
AssociationsManager() { AssociationsManagerLock.lock(); }
~AssociationsManager() { AssociationsManagerLock.unlock(); }
AssociationsHashMap &get() {
return _mapStorage.get();
}
static void init() {
_mapStorage.init();
}
};
由源码能够知道,AssociationsManager
是以 DisguisedPtr<objc_object>
即一个指针地址作为 key
,以 ObjectAssociationMap
即一个相关表作为 value
的哈希表来运用的。其内部是运用一个大局静态变量 static Storage _mapStorage
来存储程序中一切的相关目标。
这儿重点介绍一下大局静态变量 static Storage _mapStorage
的初始化机遇。App 发动过程中,在 _objc_init
函数中会调用 void _dyld_objc_notify_register(...)
,详细如下:
void _objc_init(void)
{
//...
// 此处仅保留谈到的函数
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
//...
}
在 dyld
源码中能够看到,函数 _dyld_objc_notify_register
中的三个参数为三个回调函数的指针,如下图:
回调函数会在一切镜像文件初始化完成之后,回调 map_images(unsigned count, const char * const paths[], const struct mach_header * const mhdrs[])
函数。详细调用流程如下图:
备注:图中已对无关代码进行删减,仅用来展现调用流程
从上图咱们能够知道,在 App 发动过程中 AssociationsManager
中的静态变量 static Storage _mapStorage
的初始化机遇。在 App 发动之后,一切用到相关目标的当地,程序都是从这个大局静态变量 _mapStorage
中获取 AssociationsHashMap
来对相关目标进行进一步处理。
在 AssociationsManager
中,咱们能够看到是由一个 AssociationsManagerLock
叫做 spinlock_t
的互斥锁:
using spinlock_t = mutex_tt;
它是用来保证 AssociationsManager
中对 AssociationsHashMap
操作的线程安全。
AssociationsHashMap &get() {
return _mapStorage.get();
}
关于 AssociationsHashMap
这个哈希表,则是由大局静态变量 _mapStorage
获取而来,因而不管任何时分操作相关目标,程序一直都是在操作这个 AssociationsHashMap
大局仅有的哈希表。
再回到上面 _object_set_associative_reference
源码中,当咱们增加一个相关目标时,AssociationsHashMap
会调用如下函数:
auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
try_emplace
函数的源码如下:
template <typename... Ts>
std::pair<iterator, bool> try_emplace(const 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, Key, std::forward<Ts>(Args)...);
return std::make_pair(
makeIterator(TheBucket, getBucketsEnd(), true),
true);
}
首要依据传来的 key
即 disguised
在 AssociationsHashMap
中查找对应的 ObjectAssociationMap
是否已在映射表中,假如不在则将元素插入。假如键不在,则创立一个 BucketT
即一个空的桶。在第2次调用 try_emplace
时将 ObjcAssociation
(里边包含了 _policy
和 _value
)存储到这个 BucketT
空桶中。
当设置的相关 value
为空 nil
的时分会进入 if
判别的 else
里边:
auto refs_it = associations.find(disguised);
if (refs_it != associations.end()) {
auto &refs = refs_it->second;
auto it = refs.find(key);
if (it != refs.end()) {
association.swap(it->second);
refs.erase(it);
if (refs.size() == 0) {
associations.erase(refs_it);
}
}
}
先去 AssociationsHashMap
里边查找 disguised
,假如找到则依据 key
查找到指定的相关目标,然后进行铲除 erase
操作。之后判别当前 object
的相关目标是否为0,假如为0,则将当前相关目标从大局的 AssociationsHashMap
中移除。
- 获取相关目标办法:
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
- id _Nonnull object: 获取哪个目标里边的相关目标;
- const void * _Nonnull key: 相关目标的
key
,与objc_setAssociatedObject
中的key
相对应,经过key
值取出value
即相关目标;
其内部调用的是 _object_get_associative_reference
,内部详细完成如下:
假如咱们了解了设置相关目标的过程,上面的代码了解起来就比较简单了,从大局的 AssociationsHashMap
中取得 object
目标对应的 ObjectAssociationMap
,然后依据 key
从 ObjectAssociationMap
获取对应的 ObjcAssociation
,然后依据相关战略 _policy
判别是否需求对 _value
履行 retain
操作。最终依据相关战略 _policy
判别是否需求将 _value
增加到主动开释池,并返回 _value
。
- 移除相关目标:
上面已经提到,假如想要铲除某一个特定相关目标,设置相关目标的
value
为nil
即可。假如想要移除一切相关目标,则能够运用:
objc_removeAssociatedObjects(id _Nonnull object)
- id _Nonnull object: 移除指定目标的一切相关目标
其内部完成代码如下:
当调用移除相关目标操作时,会先判别 object
是否为空及是否有相关目标存在,假如存储则会调用 _object_remove_assocations
函数。
从上图其内部完成代码能够看到,程序会获取大局的 AssociationsHashMap
然后从中获取目标对应的 ObjectAssociationMap
,注释说假如不是 deallocating
,则体系的相关目标将会保留。而 objc_removeAssociatedObjects
函数传入的 deallocating
参数为 false
,因而咱们能够揣度,免除相关必定不是在调用 objc_removeAssociatedObjects
时。
于是,我查找了一下 _object_remove_assocations
,发现了真实的调用机遇,即在 objc_destructInstance
函数调用时,如上图。
那什么时分会调用 objc_destructInstance
函数呢?带着这个疑问,我查了一下源码,这儿简单说一下调用流程,后续会专门针对 dealloc
写相关文章,其大体流程如下:
图中函数调用流程十分清晰,此处不做过多解说。由此,咱们知道免除相关目标是在源目标 dealloc
时进行的。
拓展常识
- iOS 中变量修饰词
@public
、@protected
、@package
、@private
的效果:
@package // 常用于结构类的实例变量,运用 @private 太约束,运用 @protected 或许 @public 又太敞开,这时能够运用 @package
@private // 效果规模只能在本身类,即使子类也无法运用,但 category 及 extension 类中能够运用
@protected // 体系默许为 @protected,效果规模在本身类及子类
@public // 揭露类型,效果域大,只要能拿到所属实例目标就能够运用
实例变量规模图(@package
的规模图中未展现)
@interface Person : NSObject {
@package
NSString *_country; // 结构内拿到 Person 及其子类的实例变量都能够运用
@protected
NSString *_birthday; // 只能在本身类及子类中运用,包含 category 及 extension
@private
NSString *_weight; // 只能在本身类中运用,包含 category 及 extension
@public
NSString *_height; // 大局任意拿到 Person 及其子类实例变量的当地都能够运用
}
详细实例如下: Son
承继自 Person
:
@interface Son : Person
@end
@protected
从上图示例代码能够看到,在子类中是能够拜访父类的 @protected _birthday
成员变量,但不能拜访父类的 @private _weight
成员变量。
从上图示例代码能够看到,在其他类中是能够拜访父类的 @protected _birthday
成员变量,但不能拜访父类的 @private _weight
成员变量。
总结
类别 category
和扩展 extension
涉及到的东西仍是挺多的,这儿仅对其中心要关注的一些点进行了详细介绍。另外还有关于 category
装载的过程,有爱好的同学能够查阅一下。以上便是本文对类别 category
和 扩展 extension
相关常识点的介绍,感谢阅览。
参阅资料:
-
Categories and Extensions
-
Defining a Class
-
Associated Objects
关于技能组
iOS 技能组主要用来学习、分享日常开发中运用到的技能,一同坚持学习,坚持前进。文章库房在这儿:github.com/minhechen/i… 微信公众号:iOS技能组,欢迎联系进群学习沟通,感谢阅览。