iOS Category 实现解析

一、Category 是什么

categoryObject-C 2.0 以后添加的语言特性。c++

1-一、category的做用

  • 一、为已有的类添加方
  • 二、能够减小单个文件的体积
  • 三、把不一样的功能添加到不一样的Category
  • 四、能够按需加载功能
  • 五、声明私有有方法
  • 六、公开framework的私有方法

1-二、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-三、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-四、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 方法指针

二、Category 的加载

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

2-一、_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的回调,有如下三个事件:bash

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

2-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 方法。app

  • 进入map_images_nolock内部,这个方法实现至关长,咱们只须要关注其内部调用的_read_images方法。函数

  • _read_images的做用就是读取Segment sction来初始化。布局

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

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

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

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

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

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

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

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

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

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

//在 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更加安全。

    • 咱们能够看到,分类添加方法实际上是发生了内存移动。因此类原有的方法即便在分类中从新实现,也只是被覆盖而不会消失,还能够经过访问方法列表调用。

    • 方法调用实际上是遍历方法列表找到合适的方法而后调用,在调用过程会先使用二分查找。若是先找到后面有合适的方法,并不会马上返回该方法IMP指针,而是向前查找是否有同名方法,若是有就返回前面方法的IMP指针不然返回后面的。因此会确保前面的方法先被调用。

2-1-二、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-一、load方法调用规则

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

2-1-2-二、loadImages

  • loadImages里会调用 prepare_load_methods((const headerType *)mh);
  • loadImages里会调用 call_load_methods();
2-1-2-2-一、 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-一、 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方法的指针而后发送一个消息

三、总结

  • 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:全局容器
相关文章
相关标签/搜索