@qidiandasheng
2022-08-29T01:23:54.000000Z
字数 7734
阅读 3122
iOS运行时
Category在Objective-C中是个非常重要的概念。我们在写一个APP的时候会经常用到,很多第三方库都会有Category的存在。Category对于如何写一个好的框架会有很大的帮助。
在开发中如何扩展原有类的功能是一个永远无法回避的问题,一般会想到继承,但是继承也有它的局限性,继承会很大的增强整个程序的耦合性,改动会比较大。而Category就可以解决这些问题。
Category的优缺点:
优点:改动小,耦合性小,仅对本category有效,不会影响其他类与原有类的关系。
缺点:分类里的方法跟原有类的方法相同,同一个类的不同分类里面有方法冲突,这些都会发生一些奇怪的问题,互相覆盖之类的。类别里的方法优先级高于原有类的方法。
我们在Runtime源码地址里下载最新的Runtime源码objc4-680.tar.gz。
然后我们在objc-runtime-new.h文件中看到如下定义:
struct category_t {const char *name;classref_t cls;struct method_list_t *instanceMethods;struct method_list_t *classMethods;struct protocol_list_t *protocols;struct property_list_t *instanceProperties;method_list_t *methodsForMeta(bool isMeta) {if (isMeta) return classMethods;else return instanceMethods;}property_list_t *propertiesForMeta(bool isMeta) {if (isMeta) return nil; // classProperties;else return instanceProperties;}};
这里有一篇讲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有同名方法时,会按照编译链接的一个顺序加入到methodlist里,后编译链接的方法在methodlist数组的前面。主工程的category相对于Pod里的category先链接,所有pod里的category在methodlist的前面,而pod的链接顺序就是根据编译顺序来,后编译的后链接。
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,int flags){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();for (uint32_t i = 0; i < cats_count; i++) {auto& entry = cats_list[i];// 读取Category的方法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;}// 从后往前添加mlistmlists[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, __func__);// 把mlists方法列表加入到原类的methods列表前面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 keepreturn !c->cache.isConstantOptimizedCache();});}}rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);}void attachLists(List* const * addedLists, uint32_t addedCount) {if (addedCount == 0) return;if (hasArray()) {// many lists -> many listsuint32_t oldCount = array()->count;uint32_t newCount = oldCount + addedCount;array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));newArray->count = newCount;array()->count = newCount;for (int i = oldCount - 1; i >= 0; i--)newArray->lists[i + addedCount] = array()->lists[i];for (unsigned i = 0; i < addedCount; i++)newArray->lists[i] = addedLists[i];free(array());setArray(newArray);validate();}else if (!list && addedCount == 1) {// 0 lists -> 1 listlist = addedLists[0];validate();}else {// 1 list -> many listsPtr<List> oldList = list;uint32_t oldCount = oldList ? 1 : 0;uint32_t newCount = oldCount + addedCount;setArray((array_t *)malloc(array_t::byteSize(newCount)));array()->count = newCount;if (oldList) array()->lists[addedCount] = oldList;for (unsigned i = 0; i < addedCount; i++)array()->lists[i] = addedLists[i];validate();}}
search_method_list_inline(const method_list_t *mlist, SEL sel){int methodListIsFixedUp = mlist->isFixedUp();int methodListHasExpectedSize = mlist->isExpectedSize();if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {// 二分法查找return findMethodInSortedMethodList(sel, mlist);} else {// 线性查找// Linear search of unsorted method listif (auto *m = findMethodInUnsortedMethodList(sel, mlist))return m;}#if DEBUG// sanity-check negative resultsif (mlist->isFixedUp()) {for (auto& meth : *mlist) {if (meth.name() == sel) {_objc_fatal("linear search worked when binary search did not");}}}#endifreturn nil;}findMethodInUnsortedMethodList(SEL sel, const method_list_t *list, const getNameFunc &getName){for (auto& meth : *list) {if (getName(meth) == sel) return &meth;}return nil;}
Category可以给一个现有的类添加属性,但是不能添加实例变量。
我们现在来想一下什么是属性,属性在Objective-C中也是一个重要的概念,我们在声明属性的时候,其实系统默认会帮我们生成getter和setter方法,并生产对应的实例变量(一般为_propertyName)。
而在Category中是不会自动生成getter和setter方法和实例变量的。getter和setter可以自己写,然后用关联对象(Associated Objects)来实现实例变量。
Runtime Associate其实就算用来Category关联对象用的。它有如下几个对应的方法:
objc_setAssociatedObject
objc_getAssociatedObject
objc_removeAssociatedObjects
OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);
我们发现上面这两个方法都需要一个key,这个key其实就是一个唯一常量,用来标识对应的关联对象用的。
我们在Category中添加关联对象使用Key一般有如下几种:
static char studentNameKey;@implementation NSObject (Student)- (NSString *)name{return objc_getAssociatedObject(self, &studentNameKey);}- (void)setName:(NSString *)name{objc_setAssociatedObject(self, &studentNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);}
static const void *studentNameKey = &studentNameKey;@implementation NSObject (Student)- (NSString *)name{return objc_getAssociatedObject(self, studentNameKey);}- (void)setName:(NSString *)name{objc_setAssociatedObject(self, studentNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);}
@implementation NSObject (Student)- (NSString *)name{return objc_getAssociatedObject(self, @selector(name));}- (void)setName:(NSString *)name{objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);}
说完了objc_setAssociatedObject和objc_getAssociatedObject方法,那objc_removeAssociatedObjects又是用来干嘛的呢?其实我们一看名字就知道是用来移除关联对象用的。这个方法一般我们不会直接去使用它,都是系统在对象释放的时候调用的。
那关联对象是什么时候被释放的呢?
我们先来看看对象的销毁流程:
在runtime源码的objc-runtime-new.mm文件中我们看到objc_destructInstance的定义如下,可以看到里面有个_object_remove_assocations执行了移除关联对象的操作:
void *objc_destructInstance(id obj){if (obj) {// Read all of the flags at once for performance.bool cxx = obj->hasCxxDtor();bool assoc = !UseGC && obj->hasAssociatedObjects();bool dealloc = !UseGC;// This order is important.if (cxx) object_cxxDestruct(obj);if (assoc) _object_remove_assocations(obj);if (dealloc) obj->clearDeallocating();}return obj;}
个人认为这个关联对象有点像类的weak对象,他们都会有一个专门的hash表来维护,当对象释放的时候,系统通过对应的方法找到hash表并一一清除。
SDWebImage中的主要类就是使用了
Category:UIImageView+WebCache.h和UIButton+WebCache.h。这两个分类扩展了UIImageView和UIButton设置网络图片的方法,并在里面做了一些缓存啊下载之类的操作。
FDFullscreenPopGesture主要使用了
UINavigationController的分类和UIViewController的分类,实现了全局基本不用加一行代码的全屏右划返回实现。