1、Category 是什么
category是 Object-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函数启动前,系统内核会启动dyld把App依赖的各种库加载到内存,其中包括libobjc (OC和runtime)。_objc_init是Objcet-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映射到内存时
当image被dyld加载到内存后会调用回调_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位置后面,memmove和memcpy都能很好的实现。 -
当
src位置在dst位置前面,也就内存拷贝重叠的时候memcpy可能导致原数据改变而失败,使用memmove更加安全。 -
memmove实现的时候会判断src和dst位置的前后,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:读取imageaddUnattachedCategoryForClass:添加category到classremethodizeClass:重新分配类的内存attachCategories:添加操作prepareMethodLists:去重、绑定类名、排序attachLists:改变列表大小 并添加元素memmove:内存移动拷贝memcpy:内存拷贝
load_images:加载prepare_load_methods:准备加载schedule_class_load:递归调用loadadd_class_to_loadable_list:获取全局class容器并调用loadloadable_classes:全局容器
_getObjc2NonlazyCategoryList:获取categoryadd_category_to_loadable_list:获取全局category容器并调用loadloadable_categories:全局容器