最近有在面试iOS职位,以前的一些知识点很多都忘记了,因此在复习的过程中也总结出文章发出来一起共享下。
首先我们先从问题出发
什么是分类?
分类是Object-C语言的一种特性,允许你在不修改原有类的前提下,为已有类动态添加实例方法、类方法、属性和协议。
分类的作用?
- 扩展已有类的功能,为系统类和三方类添加新的方法。
- 分体体积庞大的类,便于代码维护。
- 可以在分类中**声明和实现私有方法,**只在本模块使用,避免暴漏到头文件。
分类不能做什么?
分类不能添加实例变量,需要通过关联对象技术实现添加实例变量的效果。
源码分析
源码是基于runtime 680版本
通过上面的三个问题,我们对分类有了大体的认识,接下来我们需要通过源码分析,在使用分类的时候需要注意的问题。
//运行时环境初始化入口
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();
lock_init();
exception_init();
// Register for unmap first, in case some +load unmaps something
//在dyld中注册回调函数
_dyld_register_func_for_remove_image(&unmap_image);
dyld_register_image_state_change_handler(dyld_image_state_bound,
1/*batch*/, &map_2_images);
dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images);
}
oc runtime的初始化入口,app启动后由dyld调用这个入口。在dyld_register_image_state_change_handler函数调用里面注册了map_2_images方法回调。这个是镜像加载方法的回调,接下来在去查看map_2_images的函数实现。
const char *
map_2_images(enum dyld_image_states state, uint32_t infoCount,
const struct dyld_image_info infoList[])
{
rwlock_writer_t lock(runtimeLock);
return map_images_nolock(state, infoCount, infoList);
}
map_2_images函数中返回了map_images_nolock函数的实现。
const char *
map_images_nolock(enum dyld_image_states state, uint32_t infoCount,
const struct dyld_image_info infoList[])
{
//.......
//其它不相关代码
_read_images(hList, hCount);
firstTime = NO;
return NULL;
}
在map_images_nolock方法中,很多不相关的代码我们直接忽略,只关注_read_images方法。
void _read_images(header_info **hList, uint32_t hCount)
{
//.....
//其它代码
// Process this category.
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
bool classExists = NO;
//判断是否有分类方法、或者协议、或者属性,
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
//把分类未拼接的方法,放到一个全局的hash表中。
addUnattachedCategoryForClass(cat, cls, hi);
//类完成初始化
if (cls->isRealized()) {
//重新整合方法列表。
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}
if (cat->classMethods || cat->protocols
/* || cat->classProperties */)
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}
}
//.....
//其它代码
}
当cls有方法列表且完成初始化后,会调用remethodizeClass处理分类方法。
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;
runtimeLock.assertWriting();
isMeta = cls->isMetaClass();
// Re-methodizing: check for more categories
//获取未拼接category_list
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
if (PrintConnecting) {
_objc_inform("CLASS: attaching categories to class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
//传入未拼接category_list
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}
从unattachedCategoriesForClass方法获取未拼接category_list 然后调用关键方法attachCategories
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
// fixme rearrange to remove these intermediate allocations
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int propcount = 0;
int protocount = 0;
//i是分类的个数
int i = cats->count;
bool fromBundle = NO;
//倒序遍历
while (i--) {
//获取一个category_t
auto& entry = cats->list[i];
//从获取一个category_t获取方法列表
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
//mlists是二维数组,结构大概是这样[[method_t,method_t],[method_t,method_t,method_t]]
// 分类1 分类2
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
property_list_t *proplist = entry.cat->propertiesForMeta(isMeta);
if (proplist) {
proplists[propcount++] = proplist;
}
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
//这里是真正往宿主类拼接分类方法的入口
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
rw->properties.attachLists(proplists, propcount);
free(proplists);
rw->protocols.attachLists(protolists, protocount);
free(protolists);
}
倒序遍历数组中的每一个category_t,从category_t获取method_list_t,最后把method_list_t添加到mlists里面,执行rw->methods.attachLists(mlists, mcount);进行最后的拼接。
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
//有要拼接的数据
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;
/*
array()->lists + addedCount 数组首元素新的指针地址
array()->lists 原数组首个元素指针地址
oldCount * sizeof(array()->lists[0]) 移动的内存大小
总结来说就是把以前的方法列表向后移动,把需要增加的方法列表腾出地方。
*/
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
//addedLists拷贝到array()->lists的前面。
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]));
}
}
最终的方法列表的拼接是在这里面完成。首先是通过memmove() 把以前的方法列表向后移动,然后在经过memcpy() 把需要新增加的方法列表插入到以前方法列表的前面,完成分类的拼接工作。
runtime 在方法调用时,会使用selector查询方法列表,首次查到后立即返回。
经过上面的代码分析,我们得出了以下几个结论
- 分类同名方法会覆盖宿主类同名方法。
- 多个分类存在同名方法时候,最后编译的分类的同名方法会生效。