iOS 分类(Category)部分一

主要讲解 category 的基础知识;

isa和 superclass 的指向关系, 请先看这篇文章, 不然下面很多知识可能会无法理解;
分类(Category)部分一
分类(Category)部分二
分类(Category)部分三

1:日常开发中用分类做哪些工作?

  • 1.1声明调用Framework或者类的私有方法;
    例如Cat类有一个私有方法
    -(void)privateEat:(NSString *)somthing;
    viewDidLoad方法中想要使用Cat的这个方法;
    Cat* d = [[Cat alloc] init];
    [d privateEat:@"Fish"];
    直接调用不能编译, 但是我们给Cat添加一个分类后, 并且在分类.h中声明这个方法, 就可以正常调用这个方法了;示例代码
///Cat的实现
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Cat : NSObject  {
///Cat的.h文件中并没有-(void)privateEat:(NSString *)somthing的声明;
@end
NS_ASSUME_NONNULL_END


#import "Cat.h"
@implementation Cat
///私有方法, 在ViewController中不能直接调用;
- (void)privateEat:(NSString *)somthing {
    NSLog(@"\n私有方法,Cat类外部没有接口,但是通过Category方式可以调用到;  \nCat 在偷吃%@", somthing);
}
@end

///Cat的分类实现
@interface Cat (Add)   
///如果能知道私有方法名字和参数通过这种方式可以调用;
- (void)privateEat:(NSString *)thing;
@end

#import "Cat+Add.h"
@implementation Cat (Add)

@end
  • 1.2 为体积庞大的类减负;
    例如我们的工程中在LaunchVC中有很多逻辑处理(不讨论合理不合理), 其中路由部分有上千行代码, 这时采用Category方式将路由部分剥离开来很好的增加可读性;
Category的.h文件

Category的.m文件

在LaunchVC的合适地方调用

2: 分类的特点?

  • 2.1 运行时生效: 编写好分类后并没有立即将分类添加到相应的类中, 在运行时通过runtime将分类内容绑定到相应的类中;
  • 2.2 为系统类添加分类; 例如对UIView添加分类用self.x直接获取到x坐标;
  • 2.3 多个分类有同名方法时, 最后编译的分类的方法会被实现(覆盖前面的);
  • 2.4 分类添加的方法可以"覆盖"宿主类方法;
  • 2.5 名字相同的分类会引起编译报错;

3: 分类中可以添加哪些内容?

  • 3.1 添加实例方法;
  • 3.2 添加类方法;
  • 3.3 添加(非正式)协议;
  • 3.4 添加属性; 实际上是只声明了settergetter, 并没有添加成员变量;

4: 能不能为分类添加成员变量?

不能直接添加(从下方的源码结构中也可以得到, 因为没有存放成员变量的列表);
但是可以通过关联对象的方式添加;通过关联对象方式关联的对象并没有存储在相关类的内存中; 所有不同的类添加的关联对象被放到了一个统一的全局容器中(AssociationsHashMap,通过AssociationsManager管理);

  • 在正常的类中添加一个属性编译后的结果为; 成员变量.h 文件中setter()getter() 的声明.m文件中setter()getter() 的实现;

类中: @property = ivar+ getter + setter

  • 在分类中添加一个属性编译后的结果为; 只有.h 文件中setter()getter() 的声明, 没有成员变量和相关方法的实现; 可以通过关联对象的方式来添加和实现;
关联对象所用到方法:
/*
  这里不能用 nil 而是用 &NameKey, 因为这个是相当于 NSDictionary 的 key 不能有重复的;
  用 nil 的话会发生未可知的 问题;加上 static 是限制NameKey当前类使用;
*/
static const void *NameKey = &NameKey;

- (void)setName:(NSString *)name {
  /**
     objc_setAssociatedObject(<#id  _Nonnull object#>, <#const void * _Nonnull key#>, <#id  _Nullable value#>, <#objc_AssociationPolicy policy#>)
     把value通过key, 用policy这种策略关联给object;
   */
    objc_setAssociatedObject(self, NameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name {
    /**
       objc_getAssociatedObject(<#id  _Nonnull object#>, <#const void * _Nonnull key#>)
       根据指定key从objectz中获取对应关联值;
     */
    return objc_getAssociatedObject(self, NameKey);
}

/**
   objc_removeAssociatedObjects(<#id  _Nonnull object#>)
   移除obeject的所有关联对象
 */

5: 附加: 成员变量, 属性, 实例变量区别

#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN

@interface Cat : NSObject  {
    ///成员变量
    NSString    *color;
    NSInteger   age;
}

///属性
@property (nonatomic, copy) NSString *kind;

@end
NS_ASSUME_NONNULL_END

声明在大括号内是成员变量, 下面的property修饰的是属性; 实例变量本质上就是成员变量, 实例变量是相对于类的概念;

成员变量 属性
不会自动生成setter和getter, 需要自己手动实现 自动生成setter和getter方法
外部不能用"."语法, 要用"->" 外部可以使用"."语法(本质上是settergetter)
默认是@protected修饰 默认是@protected修饰(外部并不能直接访问这个成员变量, 而是需要通过gettersetter)

成员变量外部不能用"."语法调用, 需要用"->"方式


成员变量的默认修饰为@protected, 如果外部想调用需要手动修改为@public; 如图:


示例代码下载



补充

1. 分类和扩展的区别?

分类: 运行时才会将数据合并到类信息中;
扩展: 编译的时候就已经将数据并到类信息中;

2. 为什么分类跟类的同名方法会降原先方法"覆盖"?

首先同名方法时, 分类的方法并不是将原先方法覆盖, 而是因为在方法列表中分类的排序靠前所以查找到可以调用的方法后就会结束查找; 方法的调用顺序
另外多个分类如果有同名方法, 后面编译的的分类覆盖前面编译的;

3. 分类(Category)的实现原理?
  • 首先分类编译后的底层结构变为struct category_t结构, 里面存储着分类的对象方法, 类方法, 属性, 协议等等;
///objc4源码中Category的实现
///从存储结构中也可以看到category 不能直接添加成员变量, 因为没有存储成员变量的列表;
struct category_t {
    ///分类名字
    const char *name;
    ///关联类
    classref_t cls;
    ///实例方法列表
    struct method_list_t *instanceMethods;
    ///类方法列表
    struct method_list_t *classMethods;
    ///协议列表
    struct protocol_list_t *protocols;
     ///属性列表
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
    
    protocol_list_t *protocolsForMeta(bool isMeta) {
        if (isMeta) return nullptr;
        else return protocols;
    }
};
  • 在程序运行时, runtimeCategory的数据合并到类信息中(类对象, 元类对象中);

objc4源码查看category附加到类的大致流程为
  • objc-os.mm文件
    _objc_init
    map_images
    map_images_nolock
  • objc-runtime-new.mm文件
    _read_images
    load_categories_nolock
    attachCategories
    attachLists
大致流程如下, 很多objc4的源码我也看不懂, 所以只记录下能大致看懂的部分, 
以下代码的注释部分, 英文为官方注释, 中文部分为个人理解
///1 首先是objc的入口  objc-os.mm
///objc的初始化
void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    runtime_init();
    exception_init();
    cache_init();
    _imp_implementationWithBlock_init();
  ///此处的image不是图片, 而是镜像/模块的意思; map_images则是匹配模块
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}
===>
void
map_images(unsigned count, const char * const paths[],
           const struct mach_header * const mhdrs[])
{
    mutex_locker_t lock(runtimeLock);
    return map_images_nolock(count, paths, mhdrs);
}
===>
void 
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
                  const struct mach_header * const mhdrs[])
{
...

    if (hCount > 0) {
        ///读取模块
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }
...
}
===>
///注意此方法的参数totalClasses, 即所有的类;
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
...
    // Discover categories. Only do this after the initial category
    // attachment has been done. For categories present at startup,
    // discovery is deferred until the first load_images call after
    // the call to _dyld_objc_notify_register completes. rdar://problem/53119145
///发现分类Category, 然后读取
    if (didInitialAttachCategories) {
        for (EACH_HEADER) {
            load_categories_nolock(hi);
        }
    }
...
}
===>
static void load_categories_nolock(header_info *hi) {
...

  // First, register the category with its target class.
  // Then, rebuild the class's method lists (etc) if
  // the class is realized.
  if (cat->instanceMethods ||  cat->protocols
      ||  cat->instanceProperties)
  {
    ///如果类已经实现了, 则调用attachCategories函数
      if (cls->isRealized()) {
          attachCategories(cls, &lc, 1, ATTACH_EXISTING);
      } else {
          objc::unattachedCategories.addForClass(lc, cls);
      }
  }
....
}
===>
// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order, 
// oldest categories first.
///这个函数的作用就是附加方法, 属性, 协议到相关类里; 
///假设所有的category已经被加载并且是排序过(越早的category在数组中越靠前) ; 放在数组cats_list中
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
    if (slowpath(PrintReplacedMethods)) {
      ///此方法的实现在下面
        printReplacements(cls, cats_list, cats_count);
    }
    if (slowpath(PrintConnecting)) {
        _objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
                     cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
                     cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
    }

    /*
     * Only a few classes have more than 64 categories during launch.
     * This uses a little stack, and avoids malloc.
     *
     * Categories must be added in the proper order, which is back
     * to front. To do that with the chunking, we iterate cats_list
     * from front to back, build up the local buffers backwards,
     * and call attachLists on the chunks. attachLists prepends the
     * lists, so the final result is in the expected order.
     */
    ///只有极少数的类的category会超过64个, 所以以64为界限
    constexpr uint32_t ATTACH_BUFSIZ = 64;
    /*
    二维数组, 所有的方法, 现在为空, 遍历完成后伪代码应该如下
    [
        [method_t1, method_t2], //categoryA
        [method_t3, method_t4], //categoryB
     ...
        [method_t5, method_t6] //class原先的方法
    ]
    */
    method_list_t   *mlists[ATTACH_BUFSIZ];
    /*
    二维数组, 所有的属性, 现在为空, 遍历完成后伪代码应该如下
    [
        [prop_t1, prop_t2], //categoryA
        [prop_t3, prop_t4], //categoryB
    ...
        [prop_t5, prop_t6] //class原先的属性
    ]
    */
    property_list_t *proplists[ATTACH_BUFSIZ];
    /*
    二维数组, 所有的协议, 现在为空, 遍历完成后伪代码应该如下
    [
        [protop_t1, proto_t2], //categoryA
        [proto_t3, proto_t4], //categoryB
     ...
        [proto_t5, proto_t6], //class原先的协议
    ]
    */
    protocol_list_t *protolists[ATTACH_BUFSIZ];
    ///记录所有categories中有方法的category数量
    uint32_t mcount = 0;
    ///记录所有categories中有属性的category数量
    uint32_t propcount = 0;
    ///记录所有categories中有协议的category数量
    uint32_t protocount = 0;
    ///往下一级函数传递用, 此处不考虑
    bool fromBundle = NO;
    ///判断是否是元类, 以下的注释默认为NO,不是元类,只考虑实例方法之类的附加到类对象的过程;  如果是YES, 附加到元类的逻辑类似;
    bool isMeta = (flags & ATTACH_METACLASS);
    ///编译后的一个类的数据
    auto rwe = cls->data()->extAllocIfNeeded();
    ///遍历category数组
    for (uint32_t i = 0; i < cats_count; i++) {
        auto& entry = cats_list[i];//简单认为entry是对category的封装, 从entry中可以取出category的数据
        /*
        取出一个category中的实例方法并放入数组mlist中, isMeta入参NO;此方法实现如下
            method_list_t *methodsForMeta(bool isMeta) {
                if (isMeta) return classMethods;//如果是元类则返回类方法
                else return instanceMethods;//如果不是元类则返回实例方法
              }
      */
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        ///如果整个category中的方法不为空
        if (mlist) {
            ///如果已经记录的有方法的category数量达到64个则开始执行附加操作
            if (mcount == ATTACH_BUFSIZ) {
                ///讲所有的方法准备号
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
                ///将二位数组mlists附加到类中
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
          ///将一个category中的方法数组mlist放在数组的后面, 倒序插入
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
            fromBundle |= entry.hi->isBundle();
        }
      ///属性和协议的附加过程跟方法类似,不再一一注释
        property_list_t *proplist =
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            if (propcount == ATTACH_BUFSIZ) {
                rwe->properties.attachLists(proplists, propcount);
                propcount = 0;
            }
            proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
        }

        protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
        if (protolist) {
            if (protocount == ATTACH_BUFSIZ) {
                rwe->protocols.attachLists(protolists, protocount);
                protocount = 0;
            }
            protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
        }
    }
    ///如果实际的有方法的category数量不够64, 或者超过64个后运行到这里, 执行下面逻辑
    if (mcount > 0) {
        /*
        这里的操作是将开辟的多余的空间去掉, 例如: ATTACH_BUFSIZ是固定每次64, 假设最终mcount只有10; 
        那么剩下的54如何处理; mlists是数组, 在C语言中数组的首地址可以用数组名代替; 因为之前已经注释过, 
        插入过程是倒序插入, 所以前面54数组位置是空的, 
        所以mlists + ATTACH_BUFSIZ - mcount可以理解为将数组的首地址向后移动54个位置;
        */
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount, NO, fromBundle);
        ///原先类的method调用附加方法
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) flushCaches(cls);
    }
    rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}

===>
///category的数据附加到类中的过程(附加方法为例)
  void attachLists(List* const * addedLists, uint32_t addedCount) {
        ///如果需要附加的数据为0, 直接返回
        if (addedCount == 0) return;
        ///如果原先的类的method数组不为空, 意思就是原先就有实例方法
        if (hasArray()) {
            // many lists -> many lists===原先有很多, 附加很多, 以这种情况为例进行注释
            ///记录原先的方法数量
            uint32_t oldCount = array()->count;
            ///附加后需要的空间(数组count)
            uint32_t newCount = oldCount + addedCount;
            ///realloc为数组重新开辟空间
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            /*
            用memmove操作将原先类中的method移动到后面, 
            具体是多少, 举例:oldCount=5;  需要新加的addedCount = 10 ;
            则数组realloc后的长度是15, 则将原先类中的method移动到下标10的位置; 
            */
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            ///然后将需要附加的category方法数组直接拷贝到0下标位置;
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        /*
          至此, 分类的方法附件到类中完毕, 最终类中的方法存放结构伪代码如下;
         [
        [method_t1, method_t2], //后编译的分类
         ...
        [method_t3, method_t4], //先编译的分类
        [method_t5, method_t6] //class原先的方法
          ]  
        */

        }
        
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list==原先没有, 附加1个
            list = addedLists[0];
        } 
        else {
            // 1 list -> many lists==原先为1, 附加很多
            List* oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            array()->count = newCount;
            if (oldList) array()->lists[addedCount] = oldList;
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
    }





__attribute__((cold, noinline))
static void
printReplacements(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count)
{
    uint32_t c;
    bool isMeta = cls->isMetaClass();

    // Newest categories are LAST in cats
    // Later categories override earlier ones.
    ///新的category在数组的后面, 如果多个category中有重复数据则编译晚的会覆盖前面的;
    for (c = 0; c < cats_count; c++) {
        category_t *cat = cats_list[c].cat;

        method_list_t *mlist = cat->methodsForMeta(isMeta);
        if (!mlist) continue;
...
}


总结: 通过runtime加载某个类的分类时, 把所有Category的方法, 属性, 协议数据放到一个大数组中(后面参加编译的Catetory数据会在数组的前面); 合并后的数组插入到原来的类的数据的前面;


有理解错误的地方,请不吝赐教;

通过指令将OC文件转换为C++文件
指令: xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc 文件.m -o 文件-arm64.cpp
如果需要链接其他框架, 使用-framework参数; 例: -framework UIKit


参考文章和下载链接
Apple 一些源码的下载地址
iOS 成员属性和成员变量的区别
iOS 属性(property)大揭秘

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342