category

作者: George_Luofz | 来源:发表于2018-04-07 16:15 被阅读2次

其实category我们都会用,原理大概知道得少一些,面试也常问load顺序、方法查找顺序等等

1. 是啥
  • 扩展,可以为已经存在的类添加方法和属性(需要trick)
  • 主要明白它与exetension的区别
    1>. exetension 是在编译期决议,是类本身的一部分,就是说它存在于object_class中;可以定义私有属性、私有方法;
    可以通过runtime获取所有的prop/method列表,一看便知;或者clang生成编译后代码(不推荐,那代码太长了)
@interface CategoryObj : NSObject
@property NSString *prop1;
@end

@interface CategoryObj()
@property NSString *prop2;
@property NSString *prop3;
@property NSString *prop4;

@end
生成的proplist,打印结果如下:
2018-04-07 14:49:09.738925+0800 iOSLearnigDemo[14452:1143039] 0:prop2
2018-04-07 14:49:09.739281+0800 iOSLearnigDemo[14452:1143039] 1:prop3
2018-04-07 14:49:09.739461+0800 iOSLearnigDemo[14452:1143039] 2:prop4
2018-04-07 14:49:09.739602+0800 iOSLearnigDemo[14452:1143039] 3:prop1
可以看到编译时,匿名扩展中的property是先于主类中的property加入类的propertyList中的

2>. category 是运行期决议(就是说category是在运行时才跟主类建立关联),是一个单独生成的部分,存在于category_t中;可以定义方法;
(不能在category中定义exetensions,会报symbols not found错误)
category_t的结构在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;
    // 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);
};

由该结构,可以看到category能干啥事,加对象方法、类方法、协议、属性、类属性(还加了下划线,不知有何用,估计是为元类对象准备的)
如何加载可参考:深入理解Objective-C:Category,这篇文章条理清晰,堪称category讲解最经典文章

2. 有啥用

1.可以把类的实现分开在几个不同的文件里面

  • 可以减少单个文件的体积
  • 可以把不同的功能组织到不同的category里
  • 可以按需加载想要的category
  • 声明私有方法
  • 模拟多继承
  • 把framework的私有方法公开
    对于最后一条,就是可以通过加category方式,让类中定义的私有方法暴露出来,举个栗子:
// 在主类中定义一个私有方法
- (void)privateMethod{
    NSLog(@"%s",__func__);
}
// 在扩展中定义方法声明
@interface CategoryObj (Test)
- (void)privateMethod;
@end
调用该方法,打印日志:
2018-04-07 15:19:27.539348+0800 iOSLearnigDemo[14922:1165090] -[CategoryObj privateMethod]
解释下:方法调用时,runtime会从完整的方法列表中找imp,直到找到为止,由于我们在主类私有方法中实现了该selector,所以可以成功调用

2.给类添加的实例方法 (instanceMethod)
3.给类添加类方法 (classMethod)
4.实现协议 (protocol)
5.添加属性 (instancePropertie)
参考:Category 你用好了吗?

3. 怎么用
  • 网上代码一大堆,我就不再贴代码了

  • 关于上边第5条,添加属性部分学习:
    关联对象是个什么东西,此处要借助于源码理解:
    在objc-references.mm中可以看到objc_setAssociatedObject、objc_getAssociatedObject、objc_removeAssociatedObject分别都做了啥

    set方法如下:

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    // retain the new value (if any) outside the lock.
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager; // 下边贴它的实现
        AssociationsHashMap &associations(manager.associations()); // manager中有个全局的hashMap,来存associations
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // break any existing association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // create the new association (first time).
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
            // setting the association to nil breaks the association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i !=  associations.end()) {
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
}

AssociationsManager的实现

class AssociationsManager {
    // associative references: object pointer -> PtrPtrHashMap.
    static AssociationsHashMap *_map;
public:
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }
    
    AssociationsHashMap &associations() { 
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return *_map;
    }
};

还是用伪代码理解:

这里边有个disguised_ptr_t类型的伪装对象,根据伪装对象去找整个hashMap,若没找到创建一个新的ObjectAssociationMap对象,并存入hashMap中,map的key是这个伪装对象,value是新建的ObjectAssociationMap对象;这会儿还没太看懂,主要是hashmap不太会。。
好吧,功力不够,还写不来;

get方法实现较简单

id _object_get_associative_reference(id object, void *key) {
    id value = nil;
    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            ObjectAssociationMap *refs = i->second;
            ObjectAssociationMap::iterator j = refs->find(key);
            if (j != refs->end()) {
                ObjcAssociation &entry = j->second;
                value = entry.value();
                policy = entry.policy();
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
                    objc_retain(value);
                }
            }
        }
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        objc_autorelease(value);
    }
    return value;
}
解释:
遍历整个hashMap,根据key来找对应的值,直到遍历结束;结束后,若值存在,根据不同的引用策略,返回autorelease的或者retain的对象
4. 特殊之处
  1. category和主类中都有load方法,执行顺序:
    都会执行,主类先执行,category中的顺序与build phases->Compile Sources中的文件有关,谁在上边,谁先load


    .m文件编译顺序
2018-04-07 16:09:46.139766+0800 iOSLearnigDemo[15813:1206425] +[CategoryObj load]
2018-04-07 16:09:46.141486+0800 iOSLearnigDemo[15813:1206425] +[CategoryObj(Test) load]
2018-04-07 16:09:46.141828+0800 iOSLearnigDemo[15813:1206425] +[CategoryObj(Test2) load]
  1. category和主类中都有+initialize方法,执行顺序:
    只会执行一次,因为该方法只在类第一次调用时触发一次;不管先执行的主类方法还是category中的方法,都只触发category的initialize方法;若存在多个category,触发的是最后个编译的方法
2018-04-07 16:11:21.670656+0800 iOSLearnigDemo[15813:1206425] +[CategoryObj(Test2) initialize]

一个小总结:

  1. 基类的load方法先执行,子类的category后执行
  2. initialize方法,基类和子类都会执行,子类category先执行
  1. 同名方法,执行顺序:
    category中的方法会覆盖主类同名方法,所以执行category方法;若category有多个同名方法,最后编译的那个执行;

解释下:category的方法在跟主类关联后,主类的方法列表会放到category方法列表的后边,而方法执行时,会查找方法列表,先找到哪个,就执行哪个;

2018-04-07 16:14:02.214067+0800 iOSLearnigDemo[15920:1210767] -[CategoryObj(Test2) print]
  1. 如何执行主类的同名方法?
    可以通过runtime获取整个方法列表,找到要执行的方法,执行它(会了runtime这些问题都异常简单啊)
- (void)_test_call_same_method{
  
    // 调用主类的print方法
    unsigned int count = 0;
    IMP mainIMP = NULL;
    // 获取方法列表
    Method *methodList = class_copyMethodList([CategoryObj class], &count);
    for(int i = 0 ;i < count;i++){
        Method method = methodList[i];
        SEL sel = method_getName(method);
        // 如果是print方法,则拿到其IMP
        if([NSStringFromSelector(sel) isEqualToString:NSStringFromSelector(@selector(print))]){
            mainIMP = method_getImplementation(method);
        }
    }
    // 执行该IMP
    if(mainIMP){
        mainIMP();
    }
   free(methodList);
}
输出结果:
2018-04-07 16:40:19.340521+0800 iOSLearnigDemo[16348:1229293] -[CategoryObj print] //成功输出了主类的print方法

相关文章

网友评论

      本文标题:category

      本文链接:https://www.haomeiwen.com/subject/pnkyhftx.html