iOS底层原理 - Category

首先我们定义一个分类,后面我们对该分类进行一系列的分析。

@implementation Person (man)
-(void)pWork{
    NSLog(@"work");
}
-(void)pStudy{
    NSLog(@"pStudy");
}
+(void)pEat{
    NSLog(@"eat");
}
@end

1. 底层结构

利用lldb命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+man.m 转换为 c++文件。

  • Person (man) 实际转换成_category_t 结构体类型
// _category_t 结构
struct _category_t {
    // class 名称
    const char *name;
    struct _class_t *cls;
    // 实例方法列表
    const struct _method_list_t *instance_methods;
    // 类方法列表
    const struct _method_list_t *class_methods;
    // 协议列表
    const struct _protocol_list_t *protocols;
    // 属性列表
    const struct _prop_list_t *properties;
};

// ------------  下面为具体详细信息 ---------------
// Person (man) 实例 
static struct _category_t _OBJC_$_CATEGORY_Person_$_man __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "Person",
    0, // &OBJC_CLASS_$_Person,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_man,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_man,
    0,
    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_man,
};

// 实例方法列表
static struct /*_method_list_t*/ {
  // sizeof 
    unsigned int entsize;  // sizeof(struct _objc_method)
  // 方法的个数
    unsigned int method_count;
  // 方法信息列表
    struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_man __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    2,
    {{(struct objc_selector *)"pWork", "v16@0:8", (void *)_I_Person_man_pWork},
    {(struct objc_selector *)"pStudy", "v16@0:8", (void *)_I_Person_man_pStudy}}
};

// 类方法列表
static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_man __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"pEat", "v16@0:8", (void *)_C_Person_man_pEat}}
};

// 属性列表
static struct /*_prop_list_t*/ {
    unsigned int entsize;  // sizeof(struct _prop_t)
    unsigned int count_of_properties;
    struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_Person_$_man __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_prop_t),
    1,
    {{"name","T@\"NSString\",C,N"}}
};

  • 代码分析:

_OBJC_$_CATEGORY_Person_$_man :Person(man)分类的底层结构, 如果Person有多个分类,则会有多个 _category_t 结构体与之对应
_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_man : 实例方法列表结构体,存放实例方法相关信息
_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_man:类方法列表结构体,存放类方法相关信息

2. Category的加载过程

  1. 在编译之后,分类的底层结构为struct category_t 类型,里面存放着对象方法、类方法、属性、协议信息
  2. 程序运行时,当初始化类时,通过Runtime加载该类的所有Category数据,并把所有Category的方法、属性、协议数据,合并到数组中,
  3. 最后将数组中信息合并到类信息中(类对象、元类对象)
2.1 流程概括

合并具体流程可以通过源码(objc4-779.1)进行查看。

在类初始化时,会将分类中方法、属性、协议等合并到原类中。通过 _objc_init 入口,进行查看。

objc-os.mm 中: 文件名
_objc_init: 类初始化入扣
map_images : 模块、镜像加载
map_images_nolock

objc-runtime-new.h
_read_images:读取镜像,也就是加载可执行文件到内存中
remethodizeClass: 重构方法列表
attachCategories: 遍历所有分类,将分类的方法、属性、协议合并到对应的三个数组中
attachLists: 将三个数组中对应的方法、属性、协议追加到原类中
realloc、memmove、 memcpy

源码分析:
  void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        /* 传过来的二维数组
         [[method_t,method_t,...], [method_t], [method_t,method_t],...]
         ------------------------  ----------  -------------------
            分类A中的方法列表         分类B中方法列表  分类C中方法列表      ...
         
         addedCount = 3
         */
        if (hasArray()) {
            // many lists -> many lists
            // 列表中原有元素总数
            uint32_t oldCount = array()->count;
            // 列表中拼接后的元素总数
            uint32_t newCount = oldCount + addedCount;
            // 根据总数重新分配内存
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            // 更新元素总数
            array()->count = newCount;
            // 内存移动
            // [[],[],[],[原有第一个元素],[原有第二个元素]]
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            // 内存拷贝
        /*
         [[分类A方法列表],[分类B方法列表],[分类A方法列表],[原有第一个元素],[原有第二个元素]]
         示例:
         [  [method_t,method_t,...],
            [method_t],
            [method_t,method_t],
            [原有第一个元素],
            [原有第二个元素]
         ]
 */
            memcpy(array()->lists, addedLists,
                   addedCount * sizeof(array()->lists[0]));
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
        } 
        else {
            // 1 list -> many lists
            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]));
        }
    }


通过上面源码分析得知:

  1. 分类会利用内存移动、内存复制,将分类中方法追加到原类方法前面,所以当分类方法与原类方法一致时,会优先调用分类中方法。
  2. 多个分类有同样方法时,按编译前后顺序进行加载,越后编译,分类方法则越早被调用。

运行时决议:是指Category中定义的方法,在运行时才被加入到类的方法列表当中的。

Category可以添加属性,但是并不会自动生成成员变量及set/get方法。因为category_t结构体中并不存在成员变量。通过之前对对象的分析我们知道成员变量是存放在实例对象中的,并且编译的那一刻就已经决定好了。而分类是在运行时才去加载的。那么我们就无法再程序运行时将分类的成员变量中添加到实例对象的结构体中。因此分类中不可以添加成员变量

3. 如何调用原类方法

通过上面的介绍我们知道,如果分类中的方法与原类一致,在进行方法调用时,在方法列表中查询时优先找到的是分类中方法,进行调用。而非覆盖掉原类方法。

那么我们如何调用原类中方法呢?
因为我们知道,分类中方法是会优先被找到的。所以我们可以利用runtime获取方法列表,然后对方法列表进行遍历,找到最后一个跟我们调用的方法一致的Method。

@interface ZSView : UIView
-(void)redView;
@end

#import "ZSView.h"
@implementation ZSView
-(void)redView{
    NSLog(@"1111");
}
@end


@interface ZSView (leo)
-(void)redView;
@end
#import "ZSView+leo.h"
@implementation ZSView (leo)
-(void)redView{
    NSLog(@"22222");
}
@end

- (void)callOriginalMethod {
    ZSView *v = [[ZSView alloc] init];

    unsigned int count;
    Method *methods = class_copyMethodList([v class], &count);
    SEL sel = nil;
    Method meth = nil;
    SEL oriSel = @selector(redView);
      for (int i = 0; i < count; i++) {
          Method method = methods[i];
          SEL selector = method_getName(method);
          if (selector == oriSel) {
              sel = selector;
              meth = method;
          }
          NSString *name = NSStringFromSelector(selector);
          NSLog(@"实例方法:%@",name);
      }
      free(methods);
    
      //打印输出:22222
    [v performSelector:sel]; //仍会按照消息转发进行方法查找。
    // 打印输出:11111
    IMP imp = method_getImplementation(meth);
   //强制转换后调用w
    ((void (*)(id, SEL))imp)(v,oriSel);//IMP 为方法实现,直接进行调用
}

5. 给分类添加成员变量

Category可以添加属性,但是并不会自动生成成员变量及set/get方法。因为category_t结构体中并不存在成员变量。成员变量是存放在实例对象中的,并且编译的那一刻就已经决定好了对象分配的内存大小。而分类是在运行时才去加载的。
根本原因是程序在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,因此 Category 中不能添加属性!只能使用 AssociatedObject 增加关联的关系!

// 设置关联对象
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy)
// 获取关联对象的值
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)


6. Category的应用场景

  • 以把类的实现分开在几个不同的文件里面(单一职责原则)
  • 扩展系统或第三方库
  • 由于iOS开发中代理模式的广泛应用,一个类要遵守几个protocol的情况比较常见。我们也可以尝试使用Category分离不同protocol的实现。UITableView

你可能感兴趣的:(iOS底层原理 - Category)