0%

利用category定义一段宏代码

近期在对 iOS 页面生命周期进行测速跟踪时,需要在 UIVIewController 的生命周期方法里加入打点代码,同时还需要持有打点任务对象,对 VC 类的侵入较大,且冗余代码较多。本来计划采用 hook 方法,考虑到风险并未实行,最终决定采用宏定义代码的方式实现。

首先实现一个 UIViewController 的分类,在分类里通过关联对象将追踪任务对象 task 加入给 VC 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@implementation UIViewController (Track)

static const NSString *speedTask = @"pageSpeedTask";

- (void)trackDidLoad
{
NSLog(@"Yasic track Did Load");
}

- (NSObject *)metricsSpeedTask
{
NSObject *task = objc_getAssociatedObject(self, &speedTask);
if (!task) {
task = [[NSObject alloc] init];
objc_setAssociatedObject(self, &speedTask, task, OBJC_ASSOCIATION_RETAIN);
}
return task;
}

@end

这里仅仅用 NSObject 作为举例,打点方法也只打印一句日志用作标识。

objectivec 的所有关联对象都存在于一个全局 map 里面,map 的 key 是这个对象的指针地址,而这个 map 的 value 又是另外一个 map,里面保存了此对象对应关联对象的 kv 对。

category 会在运行时执行时将方法注册到对应的类对象上,即使不引入 category 头文件也可以通过反射调用对应方法,因此可以直接在某个 VC 中执行下面的方法。

1
2
3
if ([self respondsToSelector:NSSelectorFromString(@"trackDidLoad")]) {
[self performSelector:NSSelectorFromString(@"trackDidLoad")];
}

但是这样会带来一个编译期警告

1
PerformSelector may cause a leak because its selector is unknown

原因是 ARC 在编译期需要知道所有方法的参数和返回值,根据方法返回值来加入管理内存引用计数的代码,具体来说

  • 基本类型直接忽略
  • 常用对象先 retain,等到用不到的时候再 release
  • 初始化方法返回的对象不 retain,等到用不到的时候直接 release
  • 什么也不做,默认返回值在返回前后是始终有效的(一直到最近的 release pool 结束为止,用于标注ns_returns_autoreleased的方法)

然而 NSSelectorFromString 不能确定这些,因而无法加入合适的 ARC 代码。

消除警告的方式是临时忽略 clang 中对于 perform 的警告

1
2
3
4
5
6
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([self respondsToSelector:NSSelectorFromString(@"trackDidLoad")]) {
[self performSelector:NSSelectorFromString(@"trackDidLoad")];
}
#pragma clang diagnostic pop

但是这样仍然会有风险,如果调用了类似 init 的方法,会因为没有执行 release 操作的代码而不能释放这个对象。

这段代码也很冗余,所以接下来在某个全局的宏定义文件里定义一个宏用于调用这段代码

1
2
3
4
5
6
7
#define VC_TRACK_VIEW_DID_LOAD \
_Pragma("clang diagnostic push")\
_Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"")\
if ([self respondsToSelector:NSSelectorFromString(@"sak_pageTrackViewDidLoad")]) {\
[self performSelector:NSSelectorFromString(@"sak_pageTrackViewDidLoad")];\
}\
_Pragma("clang diagnostic pop")

使用 _Pragma 格式是为了在宏定义中实现换行效果

最终需要调用时直接调用 VC_TRACK_VIEW_DID_LOAD 即可。

更推荐的 perform 方式如下

1
2
3
4
SEL selector = NSSelectorFromString(@"trackDidLoad");
IMP imp = [self methodForSelector:selector];
void (*func)(id, SEL) = (void *)imp;
func(self, selector);

关于 category

category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA

category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休^_^,殊不知后面可能还有一样名字的方法。

参考链接