iOS Category 实现解析

771 阅读6分钟

1、Category 是什么

categoryObject-C 2.0 之后添加的语言特性。

1-1、category的作用

  • 从架构上说,可以分类代码增加代码的可读性,外部可以按需加载功能
  • 为已有的类扩展属性、方法、协议
  • 复写私有方法
  • 公开私有方法
  • 模拟多继承

1-2、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;
    struct property_list_t *_classProperties;
    // Fields below this point are not always present on disk.
    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }
    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
变量注解
const char *name类名
classref_t cls原来的类的指针(一开始为空,编译时期最后根据name绑定)
struct method_list_t *instanceMethods分类声明的实例方法列表
struct method_list_t *classMethods分类声明的类方法列表
struct protocol_list_t *protocols分类遵守协议列表
struct property_list_t *instanceProperties分类声明的实例属性列表
struct property_list_t *_classProperties分类声明的类属性列表

1-3、method_list_t

method_list_t 是一个范型容器,里面存放的是 method_t 结构体

struct method_list_t : entsize_list_tt<method_t, method_list_t, 0x3> {
    bool isFixedUp() const;
    void setFixedUp();

    uint32_t indexOfMethod(const method_t *meth) const {
        uint32_t i = 
            (uint32_t)(((uintptr_t)meth - (uintptr_t)this) / entsize());
        assert(i < count);
        return i;
    }
};
template <typename Element, typename List, uint32_t FlagMask>
struct entsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;
    Element first;
};
变量注解
typename Element元素类型
typename List用于指定容器类型
uint32_t FlagMask标记位

1-4、method_t

    struct method_t {
    SEL name;
    const char *types;
    MethodListIMP imp;

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};
变量注解
SEL name方法名
const char *types方法签名
MethodListIMP方法指针

2、Category 的加载

  • image这里指的不是图片,是Mach-O格式的二进制文件,dyld就是苹果加载image的动态加载器
  • main函数启动前,系统内核会启动dyldApp依赖的各种库加载到内存,其中包括libobjc (OC和runtime)
  • _objc_initObjcet-C runtime的入口函数,这里面主要功能就是读取Mach-O文件OC对应的Segment sction,并根据其中的代码信息完成OC的内存布局,以及初始化runtime相关数据结构

2-1、_objc_init 分析

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();
    //关于线程key的绑定--比如每线程数据的析构函数
    tls_init();
    //运行系统的C++静态构造函数,在dyld调用我们的静态构造函数之前,libc会调用_objc_init(),所以我们必须自己做
    static_init();
    //无源码,就是说objc的异常完全才有c++那一套
    lock_init();
    //初始化异常处理系统,比如注册异常的回调函数,来监控异常
    exception_init();
    
    //仅供objc运行时使用,注册处理程序,以便在映射、取消映射和初始化objc镜像文件时调用
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

主要关注最后一句代码

_dyld_objc_notify_register(&map_images, load_images, unmap_image);
void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped);

这句代码注册了dyld关于加载images的回调,有以下三个事件:

  • image映射到内存时
  • image被init时
  • image被移除时

2-1-1、image映射到内存时

imagedyld加载到内存后会调用回调_dyld_objc_notify_mapped

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);
}
  • 我们可以看到map_images实际上调用了map_images_nolock 方法。

  • 进入map_images_nolock内部,这个方法实现相当长,我们只需要关注其内部调用的_read_images方法。

  • _read_images的作用就是读取Segment sction来初始化。

    • _read_images中,会读取类的各种信息。其中: category_t **catlist = _getObjc2CategoryList(hi, &count); 就是读取分类的信息了。

    • 在读取到分类信息以后,会调用 addUnattachedCategoryForClass(cat, cls, hi); 这把当前分类加载到类当中。

    • addUnattachedCategoryForClass方法 的最后会调用NXMapInsert(cats, cls, list);这是在做底层数据结构的映射,我们可以简单理解为建立了cats(分类结构体)cls(类)的关联。

    • 在建立了关联以后,就需要把分类中的方法和属性一一添加到类当中。remethodizeClass(cls->ISA());这句对当前类进行重排列。在 remethodizeClass内部会调用attachCategories(cls, cats, true /*flush caches*/);

    • attachCategories会重新声明方法列表,协议列表,属性列表的内存空间并把方法、协议、属性添加到当前类。

2-1-1-1、分类属性的添加

    //在 attachCategories 方法中
    rw->properties.attachLists(proplists, propcount);
    free(proplists);

2-1-1-2、分类属性的添加

    //在 attachCategories 方法中
    rw->protocols.attachLists(protolists, protocount);
    free(protolists);

2-1-1-3、分类方法的添加

    //在 attachCategories 方法中
     auto rw = cls->data();
    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
  • 会先调用prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
  • prepareMethodLists方法里调用了fixupMethodList(mlist, methodsFromBundle, true/*sort*/);
  • fixupMethodList方法是准备好需要添加的方法列表,主要做了以下工作:
    • 比较方法列表,去重
    • mlist里的方法都填充当前类的类名
    • mlist里的方法根据内存地址排序
  • 准备好方法列表以后就会调用attachLists方法把方法添加到原来的类的方法列表里:
    • 改变原来方法列表的大小,主要通过memmove方法、memcpy方法 实现,这两个方法都用于内存拷贝。

      • memmove(array()->lists + addedCount, array()->lists,
                oldCount * sizeof(array()->lists[0]));
        memcpy(array()->lists, addedLists,  
               addedCount * sizeof(array()->lists[0]));
        
      • void   *memcpy(void *__dst, const void *__src, size_t __n);
        void   *memmove(void *__dst, const void *__src, size_t __len);
        
      • memcpy方法是把当前src指向的位置拷贝len的长度放到 dst的位置

      • memmove方法在当前src指向的位置加上n的长度与 dst的位置不重叠的时候和memcpy方法一致,当发生内存重叠的时候memmove能够保证源串在被覆盖之前将重叠区域的字节拷贝到目标区域中。

      • src位置在dst位置后面,memmovememcpy都能很好的实现。

      • src位置在dst位置前面,也就内存拷贝重叠的时候memcpy可能导致原数据改变而失败,使用memmove更加安全。

      • memmove实现的时候会判断srcdst位置的前后,src在前则从尾开始移动拷贝数据,dst在前则从头开始移动拷贝。

    • 我们可以看到,分类添加方法其实是发生了内存移动。所以类原有的方法即使在分类中重新实现,也只是被覆盖而不会消失,还可以通过访问方法列表调用。

    • 方法调用其实是遍历方法列表找到合适的方法然后调用,在调用过程会先使用二分查找。如果先找到后面有合适的方法,并不会立刻返回该方法IMP指针,而是向前查找是否有同名方法,如果有就返回前面方法的IMP指针否则返回后面的。所以会确保前面的方法先被调用。

2-1-2、image被init时

_dyld_objc_notify_register(&map_images, load_images, unmap_image);
void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped);

二进制文件初始化完成后会调用loadImages

2-1-2-1、load方法调用规则

  • 类的load方法一定在其父类的load方法调用后调用
  • 分类的load方法一定在当前类load方法调用后调用
  • 分类的load方法调用顺序和编译顺序有关

2-1-2-2、loadImages

  • loadImages里会调用 prepare_load_methods((const headerType *)mh);
  • loadImages里会调用 call_load_methods();
2-1-2-2-1、 prepare_load_methods((const headerType *)mh);
  • 依据当前调用规则,把当前类和分类进行重排列
  • Mach-O文件加载类的列表,遍历调整当前类的顺序
  •   void prepare_load_methods(const headerType *mhdr)
      {
          size_t count, i;
    
          runtimeLock.assertLocked();
    
          //从 Macho 文件加载类的列表
          classref_t *classlist = 
           _getObjc2NonlazyClassList(mhdr, &count);
          for (i = 0; i < count; i++) {
              //数组:[<cls,method>,<cls,method>,<cls,method>] 有顺序
              schedule_class_load(remapClass(classlist[i]));
          }
    
          //针对分类的操作!
          category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
          for (i = 0; i < count; i++) {
              category_t *cat = categorylist[i];
              Class cls = remapClass(cat->cls);
              if (!cls) continue;  // category for ignored weak-linked class
           realizeClass(cls);
           assert(cls->ISA()->isRealized());
              add_category_to_loadable_list(cat);
          }
      }
    
    • schedule_class_load方法会基于当前类的指针进行递归调用。从当前类开始找父类直到NSObject为止,然后开始一级一级向下调用load方法。

    • 这个递归调用就是为了保证当前类的load方法一定在其父类的load方法调用后调用。

    •   //递归调用
        static void schedule_class_load(Class cls)
        {
            if (!cls) return;
            assert(cls->isRealized());  // _read_images should realize
            if (cls->data()->flags & RW_LOADED) return;
            // Ensure superclass-first ordering
            schedule_class_load(cls->superclass);
            add_class_to_loadable_list(cls);
            cls->setInfo(RW_LOADED); 
        }
      
      • add_class_to_loadable_list方法就是分配内存空间并把当前的类添加到一个全局的容器loadable_classes之中。添加顺序也是NSObject -> ... -> superclass -> class。添加完成后我们就可以得到一个当前类类的全局容器,里面存放了当前class以及method
          //分配空间
          if (loadable_classes_used == loadable_classes_allocated) {
          loadable_classes_allocated = loadable_classes_allocated*2 + 16;
          loadable_classes = (struct loadable_class *)
          realloc(loadable_classes, loadable_classes_allocated * sizeof(struct loadable_class));
          }
          //添加到全局容器
          loadable_classes[loadable_classes_used].cls = cls;
          loadable_classes[loadable_classes_used].method = method;
      
    • 处理完当前类以后通过add_category_to_loadable_list方法对分类做相同的处理,得到loadable_categories这个装有所有分类的容器。Mach-O文件中哪个分 类在前面,哪个分类就会被先调用

3-1-2-2-1、 call_load_methods();
do {
    // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            //先调用类的 load 方法
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);
  • 优先调用当前类的load方法 (loadable_classes容器)
  • 后调用分类的load方法 (loadable_categories容器)
  • (*load_method)(cls, SEL_load); 拿到load方法的指针然后发送一个消息

3、总结

  • objc_init
    • _dyld_objc_notify_register:注册回调
      • map_images:映射
        • map_images_nolock
          • _read_images:读取image
            • addUnattachedCategoryForClass:添加categoryclass
            • remethodizeClass:重新分配类的内存
              • attachCategories:添加操作
                • prepareMethodLists:去重、绑定类名、排序
                • attachLists:改变列表大小 并添加元素
                  • memmove:内存移动拷贝
                  • memcpy:内存拷贝
      • load_images:加载
        • prepare_load_methods:准备加载
          • schedule_class_load:递归调用load
            • add_class_to_loadable_list:获取全局class容器并调用load
              • loadable_classes:全局容器
          • _getObjc2NonlazyCategoryList:获取category
          • add_category_to_loadable_list:获取全局category容器并调用load
            • loadable_categories:全局容器