背景介绍
有些特殊的时候会将APP设置成灰白的主题色,iOS不像在网页中有全局样式滤镜,那在iOS中应该如何便捷有效的实现该功能,接下来就分享我在项目中使用黑魔法(Method Swizzling)进行全局处理的一种实现方法。
Method Swizzling 原理
Object-C中每个类都维护着一个方法(Method)列表,Method 则包含 SEL 和其对应 IMP 的信息。在Objective-C中调用一个方法时,其实是向一个对象发送消息SEL,根据(Method)列表找到对应的 IMP 并执行。
黑魔法(Method Swizzling)要做的事情就是把 SEL 和 IMP 的对应关系断开,并和新 IMP 生成对应关系,进行交换,在运行时偷偷替换系统对应的实现方法,有点像Java中的AOP。
交换前:Asel->AImp Bsel->BImp
交换后:Asel->BImp Bsel->AImp
更多Method Swizzling的相关知识可以到网上查找。
具体实现
因为项目主要使用UIImageView来呈现图片,所以创建一个UIImageView的Category,并在该Category中实现方法交换。先来看看具体实现代码:
// UIImageView+xmtGrayImage.h
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIImageView (xmtGrayImage)
@end
NS_ASSUME_NONNULL_END
// UIImageView+xmtGrayImage.m
#import "UIImageView+xmtGrayImage.h"
#import <objc/runtime.h>
#import "UIImage+xmtImage.h"
#import "xmtThemeManager.h"
@implementation UIImageView (xmtGrayImage)
#pragma mark - Swizzling
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method setImage = class_getInstanceMethod(self,@selector(setImage:));
Method xmtSwizzledSetImage = class_getInstanceMethod(self,@selector(xmtSwizzledSetImage:));
///交换IMP
method_exchangeImplementations(setImage, xmtSwizzledSetImage);
});
}
- (void)xmtSwizzledSetImage:(UIImage *)image {
if (ThemeManager.isGrayImage) {
/// 图片使用灰白处理
image = [UIImage grayImage:image];
}
[self xmtSwizzledSetImage:image];
}
在 + (void)load 方法中实现交换代码,可以保证在系统加载该类文件时就执行代码。
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});
}
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});
而 dispatch_once(&onceToken 就是为了保证中括号内的代码只被执行一次,IMP 始终只被交换一次。
///获取 setImage: 方法的 IMP
Method setImage = class_getInstanceMethod(self,@selector(setImage:));
///获取 xmtSwizzledSetImage: 方法的 IMP
Method xmtSwizzledSetImage = class_getInstanceMethod(self,@selector(xmtSwizzledSetImage:));
///交换 IMP
method_exchangeImplementations(setImage, xmtSwizzledSetImage);
而这三句话是将UIImageView中的setImage:和xmtSwizzledSetImage:的IMP交换。
接下来再看看交换IMP后的这段代码。
UIImageView *imageView = [UIImageView new];
[imageView setImage:[UIImage imageNamed:@"xxxx.png"]];
[imageView xmtSwizzledSetImage:[UIImage imageNamed:@"xxxx.png"]];
因为对IMP进行交换,所以对 imageView 发送 [imageView setImage:[UIImage imageNamed:@"xxxx.png"]] 消息,系统执行的是 - (void)xmtSwizzledSetImage:(UIImage *)image 方法的实现代码;对 imageView 发送 [imageView xmtSwizzledSetImage:[UIImage imageNamed:@"xxxx.png"] 消息,系统执行的是 - (void)setImage:(UIImage *)image 的实现代码。
这也是为什么在 - (void)xmtSwizzledSetImage:(UIImage *)image 调用 - (void)xmtSwizzledSetImage:(UIImage *)image 不会发生循环调用的原因。
哈哈,是不是有点饶?啥发送消息,分明是调用方法?emmm...在OC中,调用方法就是对对象发送消息,等对象找到对应对IMP才是真正的执行方法,但为了通俗易懂,通常也会把发送消息说成调用方法。
到这,我们想将图片的改为灰白,在 - (void)xmtSwizzledSetImage:(UIImage *)image 中做相应的处理即可,因为在项目中所有对UIImageView控件设置图片都会经过该方法。同理,其他的图片控件也可以用这种方法进行处理。
顺道附一段对图片进行灰白处理对代码,有透明图层对也可以用。
+ (UIImage *)grayImage:(UIImage *)sourceImage {
int width = sourceImage.size.width;
int height = sourceImage.size.height;
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();
CGContextRef context = CGBitmapContextCreate(nil,width,height,8,0,colorSpace,kCGImageAlphaPremultipliedLast);
CGColorSpaceRelease(colorSpace);
if (context == NULL) {
return nil;
}
CGContextDrawImage(context,CGRectMake(0, 0, width, height), sourceImage.CGImage);
CGImageRef grayImageRef = CGBitmapContextCreateImage(context);
UIImage *grayImage = [UIImage imageWithCGImage:grayImageRef];
CGContextRelease(context);
CGImageRelease(grayImageRef);
return grayImage;
}
结语
通过黑魔法实现全局图片处理,也算有不小对收获吧。但自身对黑魔法只会些皮毛方法,有什么理解不对的地方还请赐教,及时指出。