iOS 中 Category 的一些事
前几天遇到了一个由于分类引起的 Bug, 折腾了好久才找到根源点,过程甚是纠结。于是对分类这块有一些想法啦,想想平常是否对这块遗漏了什么?特此总结下。
- 分类 和 继承 的区别
- 分类常用的写法
- 使用分类时遇到的BUG
一、分类 和 继承 的区别
昨天在写一个类的扩展的时候,我第一反应是用了继承,然后事后被小伙伴说此处已经有了写好的分类了,然后我就在想为什么我第一反应会用继承呢?为什么不是用分类的呢?也就自然想到了我们面试经典题: 分类 和 继承 的区别。
网上的答案:
- 分类:分类是对一个功能完备的类的一种补充,就像是一个东西的主要基本功能都完成了,可以用类别为这个类添加不同的组件,使得这个类能够适应不同情况的需求。比如animal这个类,具有 eat 和 run 等方法,想给这个类添加一个 bark 的方法,可以用分类。
- 继承:多个类具有相同的实例变量和方法时,考虑用继承。即子类可以继承父类的相同特性。如 animal 具有年龄和体重两个属性,dog 也具有年龄和体重两 个属性,dog 可以继承 animal的这两个属性,即为继承。
- 区别:
1、分类是对方法的扩展,不能添加成员变量。继承可以在原来父类的成员变量的基础上,添加新的成员变量
2、分类只能添加新的方法,不能修改和删除原来的方法。继承可以增加、修改和删除方法。
3、分类不提倡对原有的方法进行重载。继承可以通过使用super对原来方法进行重载。
4、分类可以被继承,如果一个父类中定义了分类,那么其子类中也会继承此分类。
而我个人感觉用时的简单感受就是: ** 继承修改老方法,分类添加新方法**。
#import <UIKit/UIKit.h>
@interface PQTextField : UITextField
@property (nonatomic, assign) CGFloat leftPadding; // 有左边View 离边框的距离
@property (nonatomic, assign) CGFloat rightPadding; // 有右边View 离边框的距离
@property (nonatomic, assign) CGFloat leftPlaceholderNormalPadding; //代表(没有View)离左边的距离
@end
#import "PQTextField.h"
@implementation PQTextField
- (instancetype)init {
if (self = [super init]) {
// 设置默认的一些情况
_leftPadding = 8;
_rightPadding = 8;
_leftPlaceholderNormalPadding = 10;
}
return self;
}
// 左边View 距离边界的距离
- (CGRect)leftViewRectForBounds:(CGRect)bounds {
CGRect leftRect = [super leftViewRectForBounds:bounds];
leftRect.origin.x += _leftPadding;
return leftRect;
}
// 右边View 距离边界的距离
- (CGRect)rightViewRectForBounds:(CGRect)bounds {
CGRect rightRect = [super rightViewRectForBounds:bounds];
rightRect.origin.x -= _rightPadding;
return rightRect;
}
//UITextField 文字与输入框的距离
- (CGRect)textRectForBounds:(CGRect)bounds{
if (self.leftView) {
return CGRectInset(bounds, _leftPadding * 2 + self.leftView.frame.size.width, 0);
}
return CGRectInset(bounds, _leftPlaceholderNormalPadding, 0);
}
//控制编辑文本的位置
- (CGRect)editingRectForBounds:(CGRect)bounds{
if (self.leftView) {
return CGRectInset(bounds, _leftPadding * 2 + self.leftView.frame.size.width, 0);
}
return CGRectInset(bounds, _leftPlaceholderNormalPadding, 0);
}
@end
上面这个就是我用到的一个继承的例子,对 UITextField 进行扩展,重写该类的方法。
二、分类常用的写法
- 例子一:对 UIButton 点击方法扩展 (直接增加方法)
#import <UIKit/UIKit.h>
typedef void (^PQClickButtonHandler)(UIButton *button);
@interface UIButton (PQHandler)
- (void)pq_addClickHandler:(PQClickButtonHandler)handler;
@end
#import "UIButton+PQHandler.h"
#import <objc/runtime.h>
NSString const *pq_button_handler = @"pq_button_handler";
@implementation UIButton (PQHandler)
- (void)pq_addClickHandler:(PQClickButtonHandler)handler {
objc_setAssociatedObject(self, &pq_button_handler, handler, OBJC_ASSOCIATION_COPY_NONATOMIC);
[self addTarget:self action:@selector(clickAction:) forControlEvents:UIControlEventTouchUpInside];
}
- (void)clickAction:(UIButton *)button {
PQClickButtonHandler handler = objc_getAssociatedObject(self, &pq_button_handler);
if (handler) {
handler(button);
}
}
@end
- 例子二:对 UIButton 点击热区域扩大 的扩展 (直接增加属性)
#import <UIKit/UIKit.h>
@interface UIButton (PQTouchExtraInsets)
@property (nonatomic, assign) UIEdgeInsets pq_touchExtraInsets;
@end
#import "UIButton+PQTouchExtraInsets.h"
#import <objc/runtime.h>
@implementation UIButton (PQTouchExtraInsets)
- (void)setPq_touchExtraInsets:(UIEdgeInsets)pq_touchExtraInsets {
objc_setAssociatedObject(self, @selector(pq_touchExtraInsets), [NSValue valueWithUIEdgeInsets:pq_touchExtraInsets], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UIEdgeInsets)pq_touchExtraInsets {
return [objc_getAssociatedObject(self, _cmd) UIEdgeInsetsValue];
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
UIEdgeInsets touchAreaInsets = self.pq_touchExtraInsets;
CGRect bounds = self.bounds;
bounds = CGRectMake(bounds.origin.x - touchAreaInsets.left,
bounds.origin.y - touchAreaInsets.top,
bounds.size.width + touchAreaInsets.left + touchAreaInsets.right,
bounds.size.height + touchAreaInsets.top + touchAreaInsets.bottom);
return CGRectContainsPoint(bounds, point);
}
@end
- 例子三:对 UIButton 背景颜色添加状态 的扩展 (扩展方法)
#import <UIKit/UIKit.h>
@interface UIButton (PQBackgroundColor)
- (void)pq_setBackgroundColor:(UIColor *)color state:(UIControlState)state;
@end
#import "UIButton+PQBackgroundColor.h"
@implementation UIButton (PQBackgroundColor)
- (void)pq_setBackgroundColor:(UIColor *)color state:(UIControlState)state {
[self setBackgroundImage:[self pq_imageWithColor:color] forState:state];
}
- (UIImage *)pq_imageWithColor:(UIColor *)color {
CGRect rect = CGRectMake(0.0f, 0.0f, 1.0f, 1.0f);
UIGraphicsBeginImageContext(rect.size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [color CGColor]);
CGContextFillRect(context, rect);
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
@end
- 例子四:对 UIView 父UIViewController 的扩展
#import "UIView+PQViewController.h"
@implementation UIView (PQViewController)
- (UIViewController *)viewController {
//通过响应者链,取得此视图所在的视图控制器
UIResponder *next = self.nextResponder;
do {//判断响应者对象是否是视图控制器类型
if ([next isKindOfClass:[UIViewController class]]) {
return (UIViewController *)next;
}
next = next.nextResponder;
}while(next != nil);
return nil;
}
@end
- 例子五:对 UIViewController dealloc 的扩展
#import "UIViewController+PQDealloc.h"
#import <objc/runtime.h>
@implementation UIViewController (PQDealloc)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
Method setTextMethod = class_getInstanceMethod(class, NSSelectorFromString(@"dealloc"));
Method replaceSetTextMethod = class_getInstanceMethod(class, NSSelectorFromString(@"pq_dealloc"));
method_exchangeImplementations(setTextMethod, replaceSetTextMethod);
});
}
- (void)pq_dealloc {
NSLog(@"%@--->>>>已经释放了",[self class]);
[self pq_dealloc];
}
@end
上述需要注意的确实 Runtime。
- 注意 例子一 和 例子二处 _cmd 和 const void *key 的区别
- OBJC_ASSOCIATION_RETAIN_NONATOMIC 和 OBJC_ASSOCIATION_COPY_NONATOMIC 的区别
- Method_exchangeImplementations 和 AssociatedObject 的运用场景
- 给自己的方法添加上特殊名字(pq_), 避免覆写系统类的方法
在此特别推荐一下 JKCategories,里面中的分类真的超多,上述有几个例子就是从此处学习的。另外,Objective-C 基础类的一些实用 Category 这里面推荐的也可以看看。
三、使用分类时遇到的BUG
-
使用 method_exchangeImplementations 时 replaceSetTextMethod 的设置不当
一般这种设置不当是由于某些特殊的场景导致的, replaceSetTextMethod 里面的参数和判断有变化而成的。 -
分类中 dealloc 设置错了
当然我遇到的这个是处于iOS8 某个版本中,这个 BUG:UITextField textInputView: message sent to deallocated instance, 就是 dealloc 方法中写的有问题,当初可也是莫名其妙的。
总的来说,一些莫名其妙的问题,例如根本不知道 崩 在什么地方情况下,就得考虑下是否是分类中有问题,或者说哪里运用到了一些 Runtime 的方案。
PS: Category 进一步理解
备注参考:
http://www.cnblogs.com/williamliuwen/p/5370155.html
https://github.com/shaojiankui/JKCategories
http://www.jianshu.com/p/1c7d34dbf671