其实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. 特殊之处
-
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]
- category和主类中都有+initialize方法,执行顺序:
只会执行一次,因为该方法只在类第一次调用时触发一次;不管先执行的主类方法还是category中的方法,都只触发category的initialize方法;若存在多个category,触发的是最后个编译的方法
2018-04-07 16:11:21.670656+0800 iOSLearnigDemo[15813:1206425] +[CategoryObj(Test2) initialize]
一个小总结:
- 基类的load方法先执行,子类的category后执行
- initialize方法,基类和子类都会执行,子类category先执行
- 同名方法,执行顺序:
category中的方法会覆盖主类同名方法,所以执行category方法;若category有多个同名方法,最后编译的那个执行;
解释下:category的方法在跟主类关联后,主类的方法列表会放到category方法列表的后边,而方法执行时,会查找方法列表,先找到哪个,就执行哪个;
2018-04-07 16:14:02.214067+0800 iOSLearnigDemo[15920:1210767] -[CategoryObj(Test2) print]
- 如何执行主类的同名方法?
可以通过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方法
网友评论