引子
有道常见的面试题:为什么分类中无法定义实例变量?
答案很简单:每个类的内存布局在编译时期就已经确定了,运行时才加载的category无法添加实例变量,如果添加实例变量就会破坏类的内部布局...说是那么说,
但是问题来了。。。
1:为什么说category是在运行时加载的?
2:不能添加实例变量,那为什么能添加属性?(关键对象)
总不能人云亦云吧,那么我们怎么来验证它?记住一句话:在runtime源码面前一切秘密无所遁形。
先看catagory的在runtime里的结构体长什么样子
struct _category_t {
const char *name; // 1
struct _class_t *cls; // 2
const struct _method_list_t *instance_methods; // 3
const struct _method_list_t *class_methods; // 4
const struct _protocol_list_t *protocols; // 5
const struct _prop_list_t *properties; // 6
};
1:category小括号里写的名字
2:要扩展的类对象,编译期间这个值是不会有的,在app被runtime加载时才会根据name对应到类对象
3:这个category所有的-方法
4:这个category所有的+方法
5:这个category实现的protocol,比较不常用在category里面实现协议,但是确实支持的
6:这个category所有的property,这也是category里面可以定义属性的原因,不过这个property不会@synthesize实例变量,一般有需求添加实例变量属性时会采用objc_setAssociatedObject和objc_getAssociatedObject方法绑定方法绑定,不过这种方法生成的与一个普通的实例变量完全是两码事。
其实看到这里已经能回答第二个问题了。那么第六结论是如何得出的呢?就需要继续往下看。
OC的编译器是clang 并非gcc 我们先将一段category的代码用clang 重写一下
clang -rewrite-objc Gog.m
精简之后的关键代码
static struct _category_t _OBJC_$_CATEGORY_Gog_$_Extention __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Gog",
0, // &OBJC_CLASS_$_Gog,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Gog_$_Extention,
0,
0,
0
};
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_Gog_$_Extention,
};
我们可以看出
1: OBJC$CATEGORY_Gog$Extention category+类+扩展名的特地样式组合。
2:{}中的method_list_t存放了我们添加的方法。其他的列表若有也会存放对于的数据。
3:最后,编译器在DATA段下的objc_catlist section里保存了category_t的数组L_OBJC_LABELCATEGORY$(当然,如果有多个category,会生成对应长度的数组^^),用于运行期category的加载。
再看catagory如何添加进runtime
之前遇到过一个需求:AOP一个category的方法,那AOP需要在+load方法里写,load函数又在main之前。那么category必然也是在main之前起作用的。
事实上,在main函数之前,将runtime通过dyld动态加载进来的时候生效的。怎么验证,再来看runtime源码:
先从objc_init开始,其中大量出现的image并不是图片,而是一个二进制文件(可执行文件或 so 文件),里面是被编译过的符号、代码等,所以 ImageLoader 作用是将这些文件加载进内存,且每一个文件对应一个ImageLoader实例来负责加载。
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();
lock_init();
exception_init();
// Register for unmap first, in case some +load unmaps something
_dyld_register_func_for_remove_image(&unmap_image);
dyld_register_image_state_change_handler(dyld_image_state_bound,
1/*batch*/, &map_images);
dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images);
}
在load_images之后调用_read_images方法初始化map后的image,这里面干了很多的事情,像load所有的类、协议和category。
再仔细看category的初始化:
// Discover categories.
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
class_t *cls = remapClass(cat->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 (isRealized(cls)) {
remethodizeClass(cls);
classExists = YES;
}
}
if (cat->classMethods || cat->protocols
/* || cat->classProperties */)
{
addUnattachedCategoryForClass(cat, cls->isa, hi);
if (isRealized(cls->isa)) {
remethodizeClass(cls->isa);
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
getName(cls), cat->name);
}
}
}
}
__objc_catlist,就是上面category存放的数据段。
以上代码做的事:
1把category的实例方法、协议以及属性添加到类上。
2把category的类方法和协议添加到类的metaclass上。
具体怎么做,主要是两个方法addUnattachedCategoryForClass和remethodizeClass。
addUnattachedCategoryForClass实现映射,remethodizeClass去做具体操作。
再往下看category的各种列表是怎么最终添加到类上的。
点开attachCategoryMethods方法可以看到它将所有category的实例方法列表拼成了一个大的实例方法列表,再通过attachMethodLists去加到方法列表里
static void
attachCategoryMethods(class_t *cls, category_list *cats,
BOOL *inoutVtablesAffected)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
BOOL isMeta = isMetaClass(cls);
method_list_t **mlists = (method_list_t **)
_malloc_internal(cats->count * sizeof(*mlists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int i = cats->count;
BOOL fromBundle = NO;
while (i--) {
method_list_t *mlist = cat_method_list(cats->list[i].cat, isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= cats->list[i].fromBundle;
}
}
attachMethodLists(cls, mlists, mcount, NO, fromBundle, inoutVtablesAffected);
_free_internal(mlists);
}
结论:
1)category的方法没有“完全替换掉”原来类已经有的方法,而是将扩展的方法插到方法列表的前头,比如原方法列表<4,5,6,>,扩展的方法<1,2,3>,会变成<1,2,3,4,5,6>。
2)这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会结束。
最后为什么通过关联对象的方式能添加属性?
一句话解释就是:关联对象添加的属性并不是加到这个一个对象的内存中的,关联对象的内存存储有一个专门的地方统一管理,它的作用就是添加“伪属性“。如我们在项目中如此添加:
static NSString *imgNameKey = @"imgNameKey";
@implementation UIImageView (Attchment)
@dynamic imgName;
-(void)setImgName:(NSString *)imgName
{
objc_setAssociatedObject(self, &imgNameKey, imgName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)imgName
{
return objc_getAssociatedObject(self, &imgNameKey);
}
网友评论