iOS面试题:@protocol 和 category 中如何使
-
在 protocol 中使用 property 只会生成 setter 和 getter 方法声明,我们使用属性的目的,是希望遵守我协议的对象能实现该属性。在实现 protocol 的类中如果要使用 property 对应的实例变量,则需要做一下 @synthesize var = _var;。
-
在 category 中增加属性的目的主要为了解耦,在很多第三方框架中会使用。在 category 中使用 @property 只会生成 setter 和 getter 方法的声明,并不会自动生成实例变量以及存取方法,Xcode 会警告需要手动实现 setter 和 getter 方法。这是因为 category 它是在运行时决定的。在编译时,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的。所以一般使用 runtime 中的关联对象为已经存在的类添加属性。关联对象类似于成员变量,不过是在运行时添加的。在 runtime 中所有的关联对象都由 AssociationsManager 管理。AssociationsManager 里面是由一个静态 AssociationsHashMap 来存储所有的关联对象的。这相当于把所有对象的关联对象都存在一个全局 map 里面。而 map 的 key 是这个对象的指针地址(任意两个不同对象的指针地址一定是不同的),而这个 map 的 value 又是另外一个 AssociationsHashMap,里面保存了关联对象的 KV 对。runtime 的销毁对象函数 objc_destructInstance里面会判断这个对象有没有关联对象,如果有,会调用 _object_remove_assocations 做关联对象的清理工作。
如果我们真的需要给 category 增加属性的实现,需要借助于运行时的两个函数:
设置关联对象
objc_setAssociatedObject(<#id object#>, <#const void *key#>, <#id value#>, <#objc_AssociationPolicy policy#>)
1.第一个参数: id object : 需要传入的是 : 对象的主分支
2.第二个参数: const void *key : 是一个 static 类型的 关键字,这里根据开发者自身来定义就行(尽量写的有根据一点,避免以后忘记是干啥用的)
3.第三个参数: id value : 传入的是: 对象的子分支
4.第四个参数: objc_AssociationPolicy policy :是当前关联对象的类型 strong,weak,copy (枚举类型:开发者可以点进去看)
①主分支:如果将一个label控件和控制器关联上,而且放在控制器上面,那么这个控制器对象 self 或者 self.view 就充当主分支
②子分支 : 那么这个label 就充当 子分支
③如果 控制器上有两个控件, 一个Label, 一个 View, 那么想这两个控件 弄上关联关系, 这两个控件随意一个做主分支,一个做子分支(这个根据开发场景而定)
获取关联对象
objc_getAssociatedObject(<#id object#>, <#const void *key#>)就相对来说容易理解一点了
1.第一个参数 : 主分支
2.第二个参数 : 关键字
示例:
// MyView+MyCategory.h
#import "MyView.h"
@interface MyView (MyCategory)
// 在 Category 中定义属性:
@property (assign, nonatomic) int32_t viewIndex;
@end
// MyView+MyCategory.m
#import "MyView+MyCategory.h"
#import <objc/runtime.h>
// 标记属性的 Key:
static const void *ViewIndexKey = &ViewIndexKey;
@implementation MyView (MyCategory)
@dynamic viewIndex;
- (void)setViewIndex:(int32_t)viewIndex {
objc_setAssociatedObject(self, ViewIndexKey, @(viewIndex), OBJC_ASSOCIATION_ASSIGN);
}
- (int32_t)viewIndex {
return [objc_getAssociatedObject(self, ViewIndexKey) intValue];
}
@end
另外
// 释放关联对象
// 第三个参数, 设为 nil, 则将 self 与 nil 关联... 也等同于 : 没关联任何对象
objc_setAssociatedObject(self, &overView, nil, OBJC_ASSOCIATION_ASSIGN);
// 移除 所有关联对象 : 这个方法 相当于 初始化 arr 对象一样(并不是初始化arr这个指针所指向的内存地址)
objc_removeAssociatedObjects(self);
扩展一下:到别人代码块里头会用到 _cmd 这个关键字
此关键字代表的是什么?
- (UILabel *)textLabel{
return objc_getAssociatedObject(self, _cmd);
}
上面代码 直接通过 objc_getAssociatedObject
方法获取到 textLabel 关联的关键字, 或者 对象本身 或者 Bool 等等
而关键在于下面一个句代码:
- (void)setTextLabel:(UILabel *)textLabel{
objc_setAssociatedObject(self, @selector(textLabel), textLabel, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
_cmd
:第一句返回的时候,相当于返回一个 id对象 ,而关键字则是 : textLabel ,它是根据当前方法,直接使用方法的名称,即 - (***)textLabel 方法,并且能保证改名称不重复.(第一句代码方法名 : textLabel ,因为一个类中不可能用相同的方法名)
第二句代码中出现 @selector(textLabel)
,这里它就调用第一句代码了, @selector
直接返回SEL,则获取到textLabel的方法名,则为第一句代码返回的值.
1:上面两个方法 写在一个 单独的View中,在这个类的 .h中,创建一个 weak(若引用)的textLabel,而在.m文件中 initWithFrame中直接创建self.textLabel = [[UILabel alloc] initWithFrame:self.bounds];并添加到这个view上
2:那么按照常理来说, 这里应该报警告, 但是 在控制器中打印 这个类的 textLabel , 你会发现是有内存的, 如果把第一句和第二句代码注释掉, 打印则没有内存.