美文网首页
iOS-Category与Extension

iOS-Category与Extension

作者: 树下敲代码的超人 | 来源:发表于2019-03-19 15:02 被阅读0次

    知 识 文 / 超 人


    Category

    Category是Objective-C 2.0之后添加的新语言特性,分类和类别都是指的Category。Category的主要作用是为已经存在的类添加方法和属性,其作用是在运行期决定的。
    通过clang命令行查看Category源码:

    struct _category_t {
        const char *name;//类名字
        struct _class_t *cls;//类
        const struct _method_list_t *instance_methods;////category中所有给类添加的实例方法的列表
        const struct _method_list_t *class_methods;//category中所有添加的类方法的列表
        const struct _protocol_list_t *protocols;//category实现的所有协议的列表
        const struct _prop_list_t *properties;//category中添加的所有属性列表
    };
    

    在程序启动时,系统会自动做好class与class对应的Category的映射,会调用remethodizeClass方法来修改class的_method_list_t的结构,从而实现为原类添加方法和协议。这才是runtime实现Category的关键

    例如Category文件名为Person+Clothes.m
    1.打开终端
    2.cd到Person+Clothes.m文件目录
    3.在终端中输入xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Clothes.m
    4.系统会自动生成Person+Clothes.cpp文件,打开Person+Clothes.cpp文件,搜索_category_t就可以看到Category的结构体了

    装饰者模式

    装饰者模式:Category 是Objective-C 中对装饰者模式的一种体现。在不改变原有类的前提下,动态的给原类添加一些方法和属性。
    继承是面向对象语言三大特性之一,一般开发中,继承使用得比较频繁。而过多的使用继承和多层次的使用继承会造成维护困难,灵活性变得较低。而采用装饰者模式则可以在不修改原代码的情况下,通过灵活的选择组合方式,去达到新增方法和属性的目的。

    举例说明装饰者模式:现在我们需要创建一个人的对象,给人穿上各种衣物。
    采用传统的方式是:

    传统方式

    我们需要给人创建一个Person基类,人中要有衣服对象属性,裤子对象属性,鞋子对象属性,给衣服创建一个clothes基类,裤子trousers基类,鞋子shoes基类。然后通过继承方式为裤子等添加每种类型裤子的独有特性,比如裤子的长短区分的7分、9分裤,裤子造型区分的哈伦裤、直筒裤等。然后在给人穿上各种衣物时需要创建各个衣物的子类。把子类对象赋予Person类。来达到穿衣物的目的。 传统的方式只能在固有情况下去搭配,当我们需要给人穿上一件连体衣物时,那么我们就不需要裤子与鞋子对象属性,但这两个属性已经是Person类固有的,不能减少。而衣服的对象属性在原本创建的时候是按照上半身定位的,并没有考虑连体衣物这一类。这个时候就需要重新去修改Person这个类的属性。或者用继承的方式去写一个专门穿连体衣的这一类人。但这样就显得不灵活。

    采用装饰者模式的方式是:

    装饰者模式
    我们只要创建一个Person的基类,为Person增加一些额外分类(Category)。当我们需要给人穿衣服的时候,我们给Person添加衣服相关的方法属性。而不需要穿衣服时,则不用添加衣服相关的Category,Person也没有衣物相关的对象属性与方法。这样Person类就显得简单且不需要因为其他原因去修改调整,基类不动。
    Category的创建使用方法

    1.New File的时候选择Object-C File

    选择Object-C File
    2.填入Category文件名,选择Category和Category原类
    选择
    3.创建成功后
    创建成功后
    4.Person+Clothes.h文件会报错,因为Person类找不到。所以需要在Person+Clothes.h文件中导入Person类#import "Person.h"
    image.png
    5.需要用Category时,导入Category类即可。#import "Person+Clothes.h"
    导入"Person+Clothes.h"
    什么是成员变量和属性
    @interface ViewController :UIViewController
    {
        UIButton *selectButton;//实例变量
        int *number;//成员变量
    }
    @property (nonatomic, retain) UIButton *button;//属性
    @end
    

    { }中声明的都是成员变量,所以selectButton与number是成员变量,而button则是属性。
    而selectButton为实例变量,因为selectButton的类型是UIButton,它是一个OC对象。需要实例化。所以称之为实例变量。

    在以前苹果的编译器都是用GCC,如果我们要声明一个属性,那么我们必须为这个属性,声明一个成员变量。

    @interface ViewController :UIViewController
    {
        UIButton *button;//实例变量
        int _number;//成员变量
    }
    @property (nonatomic, retain) UIButton *button;//属性
    @end
    

    在GCC时代,我们要添加一个button属性,那么就必须在{}中为button声明一个成员变量。

    后来因为苹果为OC新增了许多特性,GCC不能很好的支持,苹果就支助Chris Lattner创建了LLVM开源库。LLVM是使用GCC作为前端来对用户程序进行语义分析产生IF(Intermidiate Format),然后LLVM使用分析结果完成代码优化和生成。在图形处理上,LLVM运行时的编译是架在OpenGL栈上的,所以OpenGL栈能够在LLVM中产出更高效率的图形代码。如果显卡足够高级,这些代码会直接扔入GPU执行,而对于一些不支持全部OpenGL特性的显卡(比如Intel GMA卡),LLVM能够把这些指令优化成高效的CPU指令,使程序依然能够正常运行。

    而在LLVM时代里,我们只需要声明一个属性即可,因为LLVM库,如果发现属性没有匹配的实例变量,它将自动创建一个以下划线开头的实例变量。所以我们在代码中可以直接使用下划线开头去调用一个属性。LLVM时代不需要在对应.m文件的@implementation中声明@ synthesize,@property会告诉LLVM自动生成对应getter和setter方法。

    @interface ViewController :UIViewController
    {
        int _number;//成员变量
    }
    @property (nonatomic, retain) UIButton *button;//属性
    @end
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
       
       _button = [UIButton new];
      self->_number = 1;
    }
    

    {}中声明的成员变量不会自动生成getter和setter方法。所以成员变量不能通过点语法进行访问。成员变量可以直接调用也可以通过->语法进行访问。

    为什么会将Category的时候突然说到成员变量与属性呢?
    因为Category中不能添加成员变量,但可以通过关联对象的方式添加属性。

    为什么Category不能添加成员变量呢?
    从文章最开头的Category结构体中我们可以发现,结构体中有属性列表,有协议列表,有类方法列表,有对象方法列表,但却唯独没有成员变量列表,所以Category中不能声明成员变量。当然大家可能会想到,属性在LLVM编译器下不是会自动生成成员变量吗。但是在Category中声明的属性,不会自动生成成员变量,也不会自动生成getter和setter方法,所以在Category声明的属性需要自己实现getter和setter方法。而Category中为属性添加getter和setter方法需要通过关联对象的方式进行。

    关联对象

    为什么Category中为属性添加getter和setter方法需要用到关联对象呢,因为Category中不能声明成员变量,那么也就意味着不能通过下划线的方式访问变量。不能通过下划线的方式那么就只能通过点语法的方式访问,而 点语法 本身就是调用的getter与setter方法,这样就造成了死循环。为了避免死循环,我们就采用了关联对象的方式去保存与获取对象。

    #import "Person+Clothes.h"
    #import <objc/runtime.h>
    
    
    @implementation Person (Clothes)
    
    //衣服颜色属性所记录的关联对象key值
    static const char clothesColorKey = '\0';
    - (void)setClothesColor:(UIColor *)clothesColor
    {
        //通过关联对象方式保存对象
        objc_setAssociatedObject(self, &clothesColorKey, clothesColor, OBJC_ASSOCIATION_RETAIN);
    }
    
    - (UIColor *)clothesColor
    {
        //通过关联对象方式获取对应key值里的对象
        return objc_getAssociatedObject(self, &clothesColorKey);
    }
    
    
    @end
    

    关联方法说明:

    /** 
     * Sets an associated value for a given object using a given key and association policy.
     * 
     * @param object The source object for the association.
     * @param key The key for the association.
     * @param value The value to associate with the key key for object. Pass nil to clear an existing association.
     * @param policy The policy for the association. For possible values, see “Associative Object Behaviors.”
     * 
     * @see objc_setAssociatedObject
     * @see objc_removeAssociatedObjects
     */
    OBJC_EXPORT void
    objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                             id _Nullable value, objc_AssociationPolicy policy)
        OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);
    

    objc_setAssociatedObject方法说明:建立关联对象并保存
    参数说明:
    object和key对应objc_getAssociatedObject;
    object:value所在对象的类
    key:建立关联对象的储存key值,只要是一个指针就可以
    value:需要和object建立关联引用对象的value;
    policy:关联策略,相当于给@property添加关键字。

    typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
        OBJC_ASSOCIATION_ASSIGN,// 关联方式采用assign
        OBJC_ASSOCIATION_RETAIN_NONATOMIC,// 非原子的强引用
        OBJC_ASSOCIATION_COPY_NONATOMIC,// 非原子的copy引用
        OBJC_ASSOCIATION_RETAIN,// 原子性的强引用
        OBJC_ASSOCIATION_COPY, // 原子性的copy引用   
    };
    
    /** 
     * Returns the value associated with a given object for a given key.
     * 
     * @param object The source object for the association.
     * @param key The key for the association.
     * 
     * @return The value associated with the key \e key for \e object.
     * 
     * @see objc_setAssociatedObject
     */
    OBJC_EXPORT id _Nullable
    objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
        OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);
    

    objc_getAssociatedObject方法说明:这个函数先根据对象地址在 AssociationsHashMap 中查找其对应的 ObjectAssociationMap 对象,如果能找到则进一步根据 key 在 ObjectAssociationMap 对象中查找这个 key 所对应的关联结构 ObjcAssociation ,如果能找到则返回 ObjcAssociation 对象的 value 值,否则返回 nil;Map跟字典的键值对关系是一样的。采用哈希表方式存储。
    objc_getAssociatedObject有两个参数,第一个参数为从该object中获取关联对象,第二个参数为想要获取关联对象的key;

    /** 
     * Removes all associations for a given object.
     * 
     * @param object An object that maintains associated objects.
     * 
     * @note The main purpose of this function is to make it easy to return an object 
     *  to a "pristine state”. You should not use this function for general removal of
     *  associations from objects, since it also removes associations that other clients
     *  may have added to the object. Typically you should use \c objc_setAssociatedObject 
     *  with a nil value to clear an association.
     * 
     * @see objc_setAssociatedObject
     * @see objc_getAssociatedObject
     */
    OBJC_EXPORT void
    objc_removeAssociatedObjects(id _Nonnull object)
        OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);
    

    objc_removeAssociatedObjects:删除所有关联对象
    object:需要删除所关联对象的对象

    无论在 MRC 下还是 ARC 下关联对象都不需要 dealloc 的时候释放,被关联的对象在生命周期内要比对象本身释放的时间晚很多,它们会在被 NSObject -dealloc 调用的 object_dispose()方法中释放。


    Extension

    Extension称为扩展、延展、匿名分类。Extension和Category几乎完全是两个东西。Extension不但可以声明方法,还可以声明属性、成员变量。Extension一般用于声明私有方法,私有属性,私有成员变量。
    但是Extension只存在于一个.h文件中,并寄生于一个类的.m文件中。它就相当于把.m文件中@ interface 里的代码提取出来单独放在一个.h文件中,但是类文件外部不能访问Extension文件声明的属性、变量与方法。Extension是在编译期就已经决定好的部分,它就是类的一部分,在编译期与.h里的@interface和.m里的@implement一起形成一个完整的类。Extension一般用来隐藏类的私有信息,你必须先有一个类的源码才能添加这个类的Extension,所以我们无法给系统的类添加Extension。

    Extension的创建与使用

    1.New File的时候选择Object-C File


    选择Object-C File

    2.填入Extension文件名,选择Extension和需要扩展Extension的类


    选择Extension
    3.生成后只有一个.h文件
    生成的Extension的.h文件

    4.在文件中写入你想要私有的方法属性成员变量

    #import "Person.h"
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface Person ()
    {
        int chest_Circumference;//胸围
        int waist_Circumference;//腰围
        int hip_Circumference;//腿围
    }
    
    //年龄
    @property (nonatomic, assign) int age;
    //手机号
    @property (nonatomic, copy) NSString *phone;
    
    - (NSMutableDictionary *)getUserCircumference;
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    5.然后在需要的地方引入Extension的.h文件

    #import "Person+Private.h"
    
    @implementation Person
    
    @end
    

    Extension的内容相对较少,因为Extension用得非常少。可能个人对Extension的作用理解还不够深入,目前项目中非常少使用。

    由上面的Category与Extension的内容可知,前者是在运行期决定,后者是在编译期就决定了。注定了Category是无法添加实例变量,而Extension可以添加。看过我iOS-APP的启动流程和生命周期读者应该知道,在Category加载之前,类的内存布局已经初始化确定好,而在后面的运行期去添加实例变量就会破坏类的内存布局,这对编译型语言而言是不可取的。


    扩展知识点

    GCC(GNU Compiler Collection,GNU编译器套装):是一套由 GNU 开发的编程语言编译器。GCC 原名为 GNU C 语言编译器,因为它原本只能处理 C语言。GCC 很快地扩展,变得可处理 C++。之后也变得可处理 Fortran、Pascal、Objective-C、Java, 以及 Ada与其他语言。

    LLVM:是一个开源编译器框架,这个库提供了与编译器相关的支持,能够进行程序语言的编译期优化、链接优化、在线编译优化、代码生成。而Clang编译器就是基于LLVM的一个编译器。

    Clang编译器:是LLVM编译器工具集的一个用于编译C、C++、Objective-C的前端。它的编译速度较快、内存占用较小、设计更简单清晰、扩展性强。并兼容GCC编译器。是苹果为了取代GCC而产生的编译器。

    相关文章

      网友评论

          本文标题:iOS-Category与Extension

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