@qidiandasheng
2022-08-29T09:23:54.000000Z
字数 7734
阅读 2712
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;
}
// 从后往前添加mlist
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, __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 keep
return !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 lists
uint32_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 list
list = addedLists[0];
validate();
}
else {
// 1 list -> many lists
Ptr<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 list
if (auto *m = findMethodInUnsortedMethodList(sel, mlist))
return m;
}
#if DEBUG
// sanity-check negative results
if (mlist->isFixedUp()) {
for (auto& meth : *mlist) {
if (meth.name() == sel) {
_objc_fatal("linear search worked when binary search did not");
}
}
}
#endif
return 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
的分类,实现了全局基本不用加一行代码的全屏右划返回实现。