[关闭]
@qidiandasheng 2022-08-29T01:23:54.000000Z 字数 7734 阅读 2170

深入理解Category(😁)

iOS运行时


前言

CategoryObjective-C中是个非常重要的概念。我们在写一个APP的时候会经常用到,很多第三方库都会有Category的存在。Category对于如何写一个好的框架会有很大的帮助。
在开发中如何扩展原有类的功能是一个永远无法回避的问题,一般会想到继承,但是继承也有它的局限性,继承会很大的增强整个程序的耦合性,改动会比较大。而Category就可以解决这些问题。


Category的优缺点:

优点:改动小,耦合性小,仅对本category有效,不会影响其他类与原有类的关系。

缺点:分类里的方法跟原有类的方法相同,同一个类的不同分类里面有方法冲突,这些都会发生一些奇怪的问题,互相覆盖之类的。类别里的方法优先级高于原有类的方法。

Category的结构

我们在Runtime源码地址里下载最新的Runtime源码objc4-680.tar.gz
然后我们在objc-runtime-new.h文件中看到如下定义:

  1. struct category_t {
  2. const char *name;
  3. classref_t cls;
  4. struct method_list_t *instanceMethods;
  5. struct method_list_t *classMethods;
  6. struct protocol_list_t *protocols;
  7. struct property_list_t *instanceProperties;
  8. method_list_t *methodsForMeta(bool isMeta) {
  9. if (isMeta) return classMethods;
  10. else return instanceMethods;
  11. }
  12. property_list_t *propertiesForMeta(bool isMeta) {
  13. if (isMeta) return nil; // classProperties;
  14. else return instanceProperties;
  15. }
  16. };

这里有一篇讲Category原理的文章可以看看深入理解Objective-C:Category
我这里简单说一下整个过程:

编译的时候系统应该是把类对应的所有category方法都找到并前序添加到method list中,也就是说后编译的category的方法在method list的最前面。比如先编译的category1的方法列表为d,后编译的方法列表为c。那么插入之后的方法列表将会是c,d。

最后把这个分类的method list前序添加到类的method list中,如果原来类的方法列表是a,b,Category的方法列表是c,d。那么插入之后的方法列表将会是c,d,a,b。所有说覆盖方法的优先级是:后编译的Category的方法>先编译的Category方法>类的方法。

注意:+(void)load;方法的执行顺序是先类,然后是先编译的Category,最后是后编译的Category

Category中调回主类的同名原方法

Category方法调用的先后顺序

编译顺序

Category有同名方法时,会按照编译链接的一个顺序加入到methodlist里,后编译链接的方法在methodlist数组的前面。主工程的category相对于Pod里的category先链接,所有pod里的category在methodlist的前面,而pod的链接顺序就是根据编译顺序来,后编译的后链接。

添加Category方法到methodlist里

  1. attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
  2. int flags)
  3. {
  4. constexpr uint32_t ATTACH_BUFSIZ = 64;
  5. method_list_t *mlists[ATTACH_BUFSIZ];
  6. property_list_t *proplists[ATTACH_BUFSIZ];
  7. protocol_list_t *protolists[ATTACH_BUFSIZ];
  8. uint32_t mcount = 0;
  9. uint32_t propcount = 0;
  10. uint32_t protocount = 0;
  11. bool fromBundle = NO;
  12. bool isMeta = (flags & ATTACH_METACLASS);
  13. auto rwe = cls->data()->extAllocIfNeeded();
  14. for (uint32_t i = 0; i < cats_count; i++) {
  15. auto& entry = cats_list[i];
  16. // 读取Category的方法
  17. method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
  18. if (mlist) {
  19. if (mcount == ATTACH_BUFSIZ) {
  20. prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
  21. rwe->methods.attachLists(mlists, mcount);
  22. mcount = 0;
  23. }
  24. // 从后往前添加mlist
  25. mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
  26. fromBundle |= entry.hi->isBundle();
  27. }
  28. property_list_t *proplist =
  29. entry.cat->propertiesForMeta(isMeta, entry.hi);
  30. if (proplist) {
  31. if (propcount == ATTACH_BUFSIZ) {
  32. rwe->properties.attachLists(proplists, propcount);
  33. propcount = 0;
  34. }
  35. proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
  36. }
  37. protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
  38. if (protolist) {
  39. if (protocount == ATTACH_BUFSIZ) {
  40. rwe->protocols.attachLists(protolists, protocount);
  41. protocount = 0;
  42. }
  43. protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
  44. }
  45. }
  46. if (mcount > 0) {
  47. prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
  48. NO, fromBundle, __func__);
  49. // 把mlists方法列表加入到原类的methods列表前面
  50. rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
  51. if (flags & ATTACH_EXISTING) {
  52. flushCaches(cls, __func__, [](Class c){
  53. // constant caches have been dealt with in prepareMethodLists
  54. // if the class still is constant here, it's fine to keep
  55. return !c->cache.isConstantOptimizedCache();
  56. });
  57. }
  58. }
  59. rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
  60. rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
  61. }
  62. void attachLists(List* const * addedLists, uint32_t addedCount) {
  63. if (addedCount == 0) return;
  64. if (hasArray()) {
  65. // many lists -> many lists
  66. uint32_t oldCount = array()->count;
  67. uint32_t newCount = oldCount + addedCount;
  68. array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
  69. newArray->count = newCount;
  70. array()->count = newCount;
  71. for (int i = oldCount - 1; i >= 0; i--)
  72. newArray->lists[i + addedCount] = array()->lists[i];
  73. for (unsigned i = 0; i < addedCount; i++)
  74. newArray->lists[i] = addedLists[i];
  75. free(array());
  76. setArray(newArray);
  77. validate();
  78. }
  79. else if (!list && addedCount == 1) {
  80. // 0 lists -> 1 list
  81. list = addedLists[0];
  82. validate();
  83. }
  84. else {
  85. // 1 list -> many lists
  86. Ptr<List> oldList = list;
  87. uint32_t oldCount = oldList ? 1 : 0;
  88. uint32_t newCount = oldCount + addedCount;
  89. setArray((array_t *)malloc(array_t::byteSize(newCount)));
  90. array()->count = newCount;
  91. if (oldList) array()->lists[addedCount] = oldList;
  92. for (unsigned i = 0; i < addedCount; i++)
  93. array()->lists[i] = addedLists[i];
  94. validate();
  95. }
  96. }

读取methodlist

  1. search_method_list_inline(const method_list_t *mlist, SEL sel)
  2. {
  3. int methodListIsFixedUp = mlist->isFixedUp();
  4. int methodListHasExpectedSize = mlist->isExpectedSize();
  5. if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
  6. // 二分法查找
  7. return findMethodInSortedMethodList(sel, mlist);
  8. } else {
  9. // 线性查找
  10. // Linear search of unsorted method list
  11. if (auto *m = findMethodInUnsortedMethodList(sel, mlist))
  12. return m;
  13. }
  14. #if DEBUG
  15. // sanity-check negative results
  16. if (mlist->isFixedUp()) {
  17. for (auto& meth : *mlist) {
  18. if (meth.name() == sel) {
  19. _objc_fatal("linear search worked when binary search did not");
  20. }
  21. }
  22. }
  23. #endif
  24. return nil;
  25. }
  26. findMethodInUnsortedMethodList(SEL sel, const method_list_t *list, const getNameFunc &getName)
  27. {
  28. for (auto& meth : *list) {
  29. if (getName(meth) == sel) return &meth;
  30. }
  31. return nil;
  32. }

给Category添加属性

Category可以给一个现有的类添加属性,但是不能添加实例变量。

我们现在来想一下什么是属性,属性在Objective-C中也是一个重要的概念,我们在声明属性的时候,其实系统默认会帮我们生成gettersetter方法,并生产对应的实例变量(一般为_propertyName)。

而在Category中是不会自动生成gettersetter方法和实例变量的。gettersetter可以自己写,然后用关联对象(Associated Objects)来实现实例变量。

Runtime Associate

Runtime Associate其实就算用来Category关联对象用的。它有如下几个对应的方法:

objc_setAssociatedObject
objc_getAssociatedObject
objc_removeAssociatedObjects

  1. OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)
  2. __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);
  3. OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
  4. __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);

我们发现上面这两个方法都需要一个key,这个key其实就是一个唯一常量,用来标识对应的关联对象用的。

我们在Category中添加关联对象使用Key一般有如下几种:

  1. static char studentNameKey;
  2. @implementation NSObject (Student)
  3. - (NSString *)name{
  4. return objc_getAssociatedObject(self, &studentNameKey);
  5. }
  6. - (void)setName:(NSString *)name{
  7. objc_setAssociatedObject(self, &studentNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
  8. }
  1. static const void *studentNameKey = &studentNameKey;
  2. @implementation NSObject (Student)
  3. - (NSString *)name{
  4. return objc_getAssociatedObject(self, studentNameKey);
  5. }
  6. - (void)setName:(NSString *)name{
  7. objc_setAssociatedObject(self, studentNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
  8. }
  1. @implementation NSObject (Student)
  2. - (NSString *)name{
  3. return objc_getAssociatedObject(self, @selector(name));
  4. }
  5. - (void)setName:(NSString *)name{
  6. objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
  7. }

说完了objc_setAssociatedObjectobjc_getAssociatedObject方法,那objc_removeAssociatedObjects又是用来干嘛的呢?其实我们一看名字就知道是用来移除关联对象用的。这个方法一般我们不会直接去使用它,都是系统在对象释放的时候调用的。

那关联对象是什么时候被释放的呢?

我们先来看看对象的销毁流程:

runtime源码的objc-runtime-new.mm文件中我们看到objc_destructInstance的定义如下,可以看到里面有个_object_remove_assocations执行了移除关联对象的操作:

  1. void *objc_destructInstance(id obj)
  2. {
  3. if (obj) {
  4. // Read all of the flags at once for performance.
  5. bool cxx = obj->hasCxxDtor();
  6. bool assoc = !UseGC && obj->hasAssociatedObjects();
  7. bool dealloc = !UseGC;
  8. // This order is important.
  9. if (cxx) object_cxxDestruct(obj);
  10. if (assoc) _object_remove_assocations(obj);
  11. if (dealloc) obj->clearDeallocating();
  12. }
  13. return obj;
  14. }

个人认为这个关联对象有点像类的weak对象,他们都会有一个专门的hash表来维护,当对象释放的时候,系统通过对应的方法找到hash表并一一清除。

Category的应用

DSCategories放了一些自己常用的Category

SDWebImage:

SDWebImage中的主要类就是使用了Category:UIImageView+WebCache.hUIButton+WebCache.h。这两个分类扩展了UIImageViewUIButton设置网络图片的方法,并在里面做了一些缓存啊下载之类的操作。

FDFullscreenPopGesture:

FDFullscreenPopGesture主要使用了UINavigationController的分类和UIViewController的分类,实现了全局基本不用加一行代码的全屏右划返回实现。

参考

深入理解Objective-C:Category

关于iOS 类扩展Extension的进一步理解

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注