搜一下面试相关,发现都会问到分类,也就是Category的知识点,整理一下,以备使用。😄
基本信息
分类,英文名Category。Objective-C 2.0之后添加的语言特性。
-
作用
动态的为已有类扩充对象方法、类方法、协议和属性。
-
优点
减少单个文件的体积
将不同的功能组织到不同的Category
按需加载需要的Category
如何添加?
创建文件时,File Type选择Category。创建完成后文件名称为AA+B.h。
Category的加载
在App启动一章中指出,Category是在main之前,runtime层进行加载的。下面开始深扒代码。
深扒之前,先列出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;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
现在打开runtime源码,先找到runtime初始化入口_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(); //【环境变量初始化】
tls_init(); //【线程】
static_init(); //【C++静态构造函数】
lock_init(); //【锁】
exception_init(); //【异常】
//【重点在这里】map_images,用于加载资源模块
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
进入到map_images。
void
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);
}
继续。
void
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
const struct mach_header * const mhdrs[])
{
.....
//【前面的内容不展示】,直接来到重点地方
if (hCount > 0) {
//【读取镜像模块】
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
firstTime = NO;
}
现在进入到加载部分,只展示主要代码。_read_images的作用是把对应section里的数据取出,加工,然后添加到缓存中。
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
//【直接来到分类加载的位置】
for (EACH_HEADER) {
//【这里是个二维数组】
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
//【分类指定的类没有加载,出现问题】
if (!cls) {
.....省略代码
}
// 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)
{
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
|| (hasClassProperties && 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);
}
}
}
}
}
在remethodizeClass方法中,具体调用的是attachCategories,我们直接看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;
int i = cats->count;//!!【倒序,获取最新的分类】
bool fromBundle = NO;
while (i--) {
auto& entry = cats->list[i];
//【取出分类中的方法列表、属性列表、协议列表,放入到对应的二维数组中】
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
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);
}
如何合并,重点在attachLists方法中,查看。addedLists中的数据,来自category
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;
//!!!【重点】【向后移动原有类的内容】
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
//!!!【重点】【复制分类的内容】
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
.....
}
Q&A
问题一:两个分类中都实现了A,最后调用的是谁的?
如果有两个分类均实现了同样的方法A,对A的调用,取决于分类的编译顺序,最后编译的那个分类的同名方法最终生效。【代码中倒序取分类】
问题二:分类和原有类都有的A,调用的是谁的?
分类,因为分类的方法在原有类的方法之前,原有类的方法并没有被覆盖,只是顺序问题。
问题三:关于分类中添加属性
分类中可以添加属性,但是,属性并不能自动生成成员变量,也没有set和get方法。因为分类是在编译后运行时加载,此时对象的内存布局已经确定,无法将成员变量添加到实例对象的结构体中。这就引申出下面一个问题。
问题四:能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?
不能。编译后,类已经注册在runtime中,类结构体中的实例变量的内存大小已经确定。
可以。运行时创建的类,添加实例变量,是在objc_allocateClassPair之后,objc_registerClassPair之前。
问题五:分类能添加成员变量吗?
不能。只能通过关联对象(objc_setAssociatedObject)来模拟实现成员变量。
在objc2之前的结构体objc_class中,成员变量和方法列表定义如下:
struct objc_ivar_list *ivars; //【成员变量列表指针】
struct objc_method_list **methodLists; //【二维数组】
ivars是objc_ivar_list(成员变量列表)指针;methodLists是指向objc_method_list指针的指针。在Runtime中,objc_class结构体大小是固定的,不可能往这个结构体中添加数据,只能修改。所以ivars指向的是一个固定区域,只能修改成员变量值,不能增加成员变量个数。methodList是一个二维数组,所以可以修改*methodLists的值来增加成员方法,虽没办法扩展methodLists指向的内存区域,却可以改变这个内存区域的值(存储的是指针)。
在objc2之后,查看class_rw_t中的class_ro_t,class_ro_t中存放的是编译时就确定的内容,可以看到
const ivar_list_t * ivars;
问题六:如果声明了两个同名的分类会怎样?
会报错,所以第三方的分类,一般都带有命名前缀
问题七:分类和扩展的区别
扩展只能以声明的形式存在,多数情况下寄生在宿主类的.m中。一般用于声明私有方法、私有属性、私有成员变量。
举个常用的例子:
.m文件中
@interface ViewController ()
@end
@implementation ViewController
@end
这就是一个扩展,编译期就已经决定。
补充
在_read_images方法中,有一个重要的方法realizeClassWithoutSwift,用于初始化class的结构体。
详细见文章iOS 底层探索 - 类的加载