iOS - Category
1、Category简介
Category是Objective-C 2.0之后添加的语言特性。
Category可以做什么:
- 给目标类添加方法。
- 将一个类的实现拆分成多个独立的源文件文件。
- 声明私有的方法; (模拟多继承,framework私有方法公开)
Category的优点:
可以减少单个文件的体积;
可以把不同功能的组织到不同的category中;
可以按需求加载想要的category;
Category的缺点:
Category 非常强大,所以一旦误用就很可能会造成非常严重的后果。
覆写系统类的方法,不管在任何情况下,切记一定不要这么做。
2、Category与Extension
Extension在编译期决议,它就是类的一部分,在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,它伴随类的产生而产生,亦随之一起消亡。Extension一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加Extension,所以你无法为系统的类比如NSString添加Extension。
Category则完全不一样,它是在运行期决议的。
就Category和Extension的区别来看,我们可以推导出一个明显的事实,Extension可以添加实例变量,而Category是无法添加实例变量的。
3、Category本质
在runtime层,category的结构体category_t(在objc-runtime-new.h中可以找到此定义):(源码网址)
struct category_t {
struct property_list_t *instanceProperties;
const char *name; // category的名字
classref_t cls;
struct method_list_t *instanceMethods; // category中所有给类添加的实例方法的列表
struct method_list_t *classMethods; // category中所有添加的类方法的列表
struct protocol_list_t *protocols; // category实现的所有协议的列表
struct property_list_t *instanceProperties; // category中添加的所有属性
// 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;
}
};
Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)。
那我们都知道一个类对象可以写多个分类,那这些分类是如何加载的呢?
1、Objective-C的运行是依赖OC的runtime的,而OC的runtime和其他系统库一样,是OS X和iOS通过dyld动态加载的。
对于OC运行时,入口方法如下(在objc-os.mm文件中):
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
runtime_init();
exception_init();
cache_init();
_imp_implementationWithBlock_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
在这个运行时的初始化方法中我们可以看到&map_images,这是一个函数的地址。我们进去看就会发现其实调用的是如下代码:
void
map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{
mutex_locker_t lock(runtimeLock);
return map_images_nolock(count, paths, mhdrs);
}
map_images 方法中其实是调用的map_images_nolock方法,而map_images_nolock这个方法中又会调用一个_read_images的方法,
_read_images会读取一些模块,比如一些类信息、分类信息这些东西。
// 这里加载分类,调用方法(跟之前的源码是不一样,这是写成了一个方法。)
// Discover categories. Only do this after the initial category
// attachment has been done. For categories present at startup,
// discovery is deferred until the first load_images call after
// the call to _dyld_objc_notify_register completes. rdar://problem/53119145
if (didInitialAttachCategories) {
for (EACH_HEADER) {
load_categories_nolock(hi);
}
}
在load_categories_nolock方法中,跟之前实现不一致,之前可能是所有类别都不加判断的添加,现在是有一些取舍,比如说类别的目标勒确实等等一些。
static void load_categories_nolock(header_info *hi) {
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
size_t count;
auto processCatlist = [&](category_t * const *catlist) {
for (unsigned i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
locstamped_category_t lc{cat, hi};
if (!cls) {
// Category's target class is missing (probably weak-linked).
// Ignore the category.
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}
// Process this category.
if (cls->isStubClass()) {
// Stub classes are never realized. Stub classes
// don't know their metaclass until they're
// initialized, so we have to add categories with
// class methods or properties to the stub itself.
// methodizeClass() will find them and add them to
// the metaclass as appropriate.
if (cat->instanceMethods ||
cat->protocols ||
cat->instanceProperties ||
cat->classMethods ||
cat->protocols ||
(hasClassProperties && cat->_classProperties))
{
objc::unattachedCategories.addForClass(lc, cls);
}
} else {
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
if (cls->isRealized()) {
attachCategories(cls, &lc, 1, ATTACH_EXISTING);
} else {
objc::unattachedCategories.addForClass(lc, cls);
}
}
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
if (cls->ISA()->isRealized()) {
attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);
} else {
objc::unattachedCategories.addForClass(lc, cls->ISA());
}
}
}
}
};
processCatlist(_getObjc2CategoryList(hi, &count));
processCatlist(_getObjc2CategoryList2(hi, &count));
}
我们重点关注 attachCategories方法,看一下这个方法的实现:
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
int flags)
{
if (slowpath(PrintReplacedMethods)) {
printReplacements(cls, cats_list, cats_count);
}
if (slowpath(PrintConnecting)) {
_objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
}
/*
只有少数类在启动期间有超过64个类别。这使用了一个小堆栈,并避免了malloc。
*
*类别必须按正确的顺序添加,即从后到前。为了完成分块操作,我们从前向后迭代cats_list,向后构建本地缓冲区,并在块上调用attachLists。attachLists将列表放在前面,因此最终结果将按照预期的顺序进行。
* Only a few classes have more than 64 categories during launch.
* This uses a little stack, and avoids malloc.
*
* Categories must be added in the proper order, which is back
* to front. To do that with the chunking, we iterate cats_list
* from front to back, build up the local buffers backwards,
* and call attachLists on the chunks. attachLists prepends the
* lists, so the final result is in the expected order.
*/
constexpr uint32_t ATTACH_BUFSIZ = 64;
// 方法数组
method_list_t *mlists[ATTACH_BUFSIZ];
// 属性数组
property_list_t *proplists[ATTACH_BUFSIZ];
// 协议
protocol_list_t *protolists[ATTACH_BUFSIZ];
uint32_t mcount = 0;
uint32_t propcount = 0;
uint32_t protocount = 0;
bool fromBundle = NO;
bool isMeta = (flags & ATTACH_METACLASS);
auto rwe = cls->data()->extAllocIfNeeded();
/*
这里之前是 while 循环,倒序的,现在是一个正序的
while (i--) {
}
*/
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);
// 将所有分类对象的方法,附加到类对象方法列表中。
rwe->methods.attachLists(mlists, mcount);
mcount = 0;
}
// 本次是从后往前添加 64 - 1
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
fromBundle |= entry.hi->isBundle();
}
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
if (propcount == ATTACH_BUFSIZ) {
rwe->properties.attachLists(proplists, propcount);
propcount = 0;
}
proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
}
protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
if (protolist) {
if (protocount == ATTACH_BUFSIZ) {
rwe->protocols.attachLists(protolists, protocount);
protocount = 0;
}
protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
}
}
if (mcount > 0) {
prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount, NO, fromBundle);
rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
if (flags & ATTACH_EXISTING) flushCaches(cls);
}
rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
attachCategory做的工作相对比较简单,把所有Category的方法、属性、协议数据,合并到一个大数组中后面参与编译的Category数据,会在数组的前面,然后转交给了attachLists方法:
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
// array()->lists 原来的方法列表,将array()->lists向后移动addedCount
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
// addedLists 所有分类的方法列表,将addedLists 拷贝到array()->lists的头位置
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
attachLists:将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面
所以我们可以得出结论:
1.category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA;
2.category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休,殊不知后面可能还有一样名字的方法。
4、Category + load + initialize
+load 方法
+load方法会在runtime加载类、分类时调用。每个类、分类的+load,在程序运行过程中只调用一次。
load方法的调用顺序:
1.先调用类的+load
按照编译先后顺序调用(先编译,先调用)
调用子类的+load之前会先调用父类的+load
2.再调用分类的+load
按照编译先后顺序调用(先编译,先调用)
大家思考一下,上面我们看了category 本质的时候发现,如果分类中方法名和类中方法名一样,不是只执行分类的方法吗?为什么load 方法不是这样?
同样看源码我们也能找到答案。这里就不看这个源码了。给大家列出查看源码的顺序:
objc-os.mm
1、初始化方法_objc_init,查看load_images
2、call_load_methods方法
do {
// 这里也可以看出先调用类的load 方法,再调用分类的load 方法
// 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);
3、call_class_loads、call_category_loads
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
// 直接找到load 方法的地址
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方法
(*load_method)(cls, @selector(load));
}
通过查看源码我们可以得出:
+load方法是根据函数地址直接调用,并不是经过objc_msgSend函数调用(消息机制调用,isa 找到类,然后找到方法列表去遍历)
+initialize 方法
+initialize方法会在类第一次接收到消息时调用。
调用顺序
先调用父类的+initialize,再调用子类的+initialize
(先初始化父类,再初始化子类,每个类只会初始化1次)
上源码调用顺序:
1、objc-runtime-new.mm中class_getInstanceMethod,寻找方法。
2、找到 lookUpImpOrForward方法,initializeAndLeaveLocked->initializeAndMaybeRelock->initializeNonMetaClass
判断是否初始化过,保证只有第一次执行。
3、initializeNonMetaClass方法,callInitialize,然后objc_msgSend。
* class_initialize. Send the '+initialize' message on demand to any
* uninitialized class. Force initialization of superclasses first
*父类优先初始化。
这也是一个递归实现。
对比:
+initialize和+load的很大区别是,+initialize是通过objc_msgSend进行调用的,所以有以下特点:
1、如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次)
子类的isa 指针找到元类对象没有+initialize方法,然后superClass 中去找。
2、如果分类实现了+initialize,就覆盖类本身的+initialize调用
+Load | +initialize | |
---|---|---|
调用时机 | Runtime加载类、分类的时候调用 | 收到第一条消息时,可能永远不调用 |
调用方式 | 根据函数地址直接调用 | 通过objc_msgSend调用 |
调用顺序 | 父类->子类->分类 | 父类->子类 |
是否需要显式调用父类实现 | 1次 | 可能会调用多次 |
分类中的实现 | 类和分类都执行 | 覆盖类中的方法,只执行分类的实现 |
5、Category 关联对象
默认情况下,因为分类底层结构的限制,不能添加成员变量到分类中。但可以通过关联对象来间接实现。
// 项目写法
- (BYLoadingView *)loadingView {
BYLoadingView *loadingView = objc_getAssociatedObject(self, &kLoadingView);
return loadingView;
}
- (void)setLoadingView:(BYLoadingView *)loadingView {
objc_setAssociatedObject(self, &kLoadingView, loadingView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
关联对象提供了以下API
添加关联对象
void objc_setAssociatedObject(id object, const void * key,
id value, objc_AssociationPolicy policy)
获得关联对象
id objc_getAssociatedObject(id object, const void * key)
移除所有的关联对象
void objc_removeAssociatedObjects(id object)
我们思考一下:
但是关联对象又是存在什么地方呢? 如何存储? 对象销毁时候如何处理关联对象呢?
在objc-references.mm文件中有个方法_object_set_associative_reference:
我们可以看到所有的关联对象都由AssociationsManager管理,而AssociationsManager定义如下:
class AssociationsManager {
using Storage = ExplicitInitDenseMap, ObjectAssociationMap>;
static Storage _mapStorage;
public:
AssociationsManager() { AssociationsManagerLock.lock(); }
~AssociationsManager() { AssociationsManagerLock.unlock(); }
AssociationsHashMap &get() {
return _mapStorage.get();
}
static void init() {
_mapStorage.init();
}
};
AssociationsManager中有一个AssociationsHashMap:
//AssociationsHashMap 中有一个 ObjectAssociationMap
typedef DenseMap, ObjectAssociationMap> AssociationsHashMap;
//ObjectAssociationMap 中 ObjcAssociation
typedef DenseMap ObjectAssociationMap;
// ObjcAssociation的定义如下
uintptr_t _policy;
id _value;
所以我们也可以得到:
关联对象并不是存储在被关联对象本身内存中。
关联对象存储在全局的统一的一个AssociationsManager中。
设置关联对象为nil,就相当于是移除关联对象。
if (value) {
auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
if (refs_result.second) {
/* it's the first association we make */
object->setHasAssociatedObjects();
}
/* 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);
}
}
}
}
如何销毁某一个:
objc-runtime-new.mm中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;
}