美文网首页其他一丢丢#iOS#HeminWon
iOS 中 Category 的一些事

iOS 中 Category 的一些事

作者: 天空中的球 | 来源:发表于2017-02-16 11:55 被阅读179次

    前几天遇到了一个由于分类引起的 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

    相关文章

      网友评论

      本文标题:iOS 中 Category 的一些事

      本文链接:https://www.haomeiwen.com/subject/mufqwttx.html