Objc Runtime 再次实践

好久不用,再次使用runtime重写代码。就用高性能添加图片圆角来再一次实践一下runtime的基本用法。

runtime使用场景:

  • category添加关联属性
  • MethodSwizzle替换/交换系统方法

平常使用cornerRadius和maskToBounds组合设置圆角


girl

category添加关联属性


废话不多说,直接上代码
创建一个NSError的category,添加一个errorMsg的属性。因为category本身不能添加属性,这里是使用runtime动态添加关联属性

NSError+Msg_h

NSError+Msg_m

MethodSwizzle交换系统方法


创建一个UIImageView的category,在分类中交换(exchange,注意这里不是替换replace)系统的setImage: 方法.

我们要交换一个类的系统方法,那么首先想想在哪个方法中交换最合适呢?
我们知道app在启动之后,会进行类注册,调用类的+load()方法,且只调用一次。(注意与+initialize的区别)第51条:load与initialize的区别所以我们在分类中重写load方法中添加交换方法。

首先我们借用AFNetworking中AFURLSessionManager中定义的swizzle的内联函数

1
2
3
4
5
6
7
8
9
10
11
12
static inline void hl_swizzleSelector(Class theClass, SEL originalSelector, SEL swizzledSelector) {
Method originalMethod = class_getInstanceMethod(theClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(theClass, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
}

static inline BOOL hl_addMethod(Class theClass, SEL selector, Method method) {
/*
YES if the method was added successfully, otherwise NO (for example, the class already contains a method implementation with that name)
*/
return class_addMethod(theClass, selector, method_getImplementation(method), method_getTypeEncoding(method));
}

然后在+load方法中添加如下代码:
1
hl_swizzleSelector([self class], @selector(setImage:), @selector(hl_setImage:));

/* 这里参考的是AFURLSessionManager中的方式来添加方法,但是实际上在这个情况下是永远返回NO的

  • 原因:因为我们是在UIImageView的category中添加了hl_setImage:,所以在UIImageView中已经存在了该方法,再调用class_addMethod就会返回NO。
  • 因此下面这些代码如果写在category中是不需要的了。
    */
    1
    2
    3
    4
    5
    6
    Method hlSetImageMethod = class_getInstanceMethod([self class],@selector(hl_setImage:));

    BOOL result = hl_addMethod([self class],@selector(hl_setImage:), hlSetImageMethod);
    NSLog(@"%d",result);
    if (result) {
    }

测试一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
UIImageView * v = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 240, 240)];
v.center = self.view.center;
[self.view addSubview:v];

UIImage * image = [UIImage imageNamed:@"IMG_0730.jpg"];

//会产生混合图层
v.layer.cornerRadius = v.frame.size.width / 2;
v.layer.masksToBounds = YES;

//采用UIGraphicImageContext重绘后,解决混合图层问题
[v setImage:[image hl_imageByCroppingForSize:CGSizeMake(240, 240) fillColor:[UIColor whiteColor]]];

//使用Method swizzle交换setImage和hl_setImage:之后:
v.image = image;


girls

其中的性能问题


我们在UIImage+HLAdd分类中使用UIGraphicImageContext来重新绘图,这肯定是要耗时的操作。
1
2
3
4
5
CFTimeInterval start = CACurrentMediaTime();
...
//绘图
...
NSLog(@"%f",CACurrentMediaTime() - start);

1
2016-11-22 17:53:42.614 iOS-Kick-On[47630:2598373] 0.003525

大概耗时在0.003~0.006之间,以纳秒进行运算的cpu来说,还是一个稍微耗时的操作,因为我们可以把它放在后台线程来做,然后采用回调来获取返回的image

1
2
3
4
5
6
7
8
9
10
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

UIImage * result = [self hl_imageByCroppingCorner:radius forSize:targetSize fillColor:[UIColor whiteColor]];

dispatch_async(dispatch_get_main_queue(), ^{
if (completion) {
completion(result);
}
});
});

本篇文章代码