iOS Runtime之——Category及其底层原理探索
1.概述
苹果在Objective-C 2.0中推出了新的语言特性Category,为已有类提供新的行为、扩展现有功能。如今Category已经应用到了iOS应用的各个角落。那么本文就对其进行深入剖析,揭开其真实面目。
源码基于objc4-750,下载地址:objc4-750。
2.Category特点
- 可以减少单个文件的体积
- 可以把不同的功能组织到不同的Category中
- 可以按需加载
- 可以声明私有方法
- 把framework的私有方法公开
2.Category真面目
众所周知,所有的OC类和对象,在runtime层都是用struct表示的,category也不例外。在runtime层obj-runtime-new.h中,category使用结构体category_t表示:
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);
};
- name:类名
- cls:类
- instanceMethod:给类添加的实例方法列表
- classMethods:给类添加的类方法列表
- protocols:实现的所有的协议列表
- instanceProperties:给类添加的所有属性
从中我们可以发现,category可以添加实例方法、类方法、实现协议、添加属性,但是不能添加实例变量。因为category是在运行期执行的,而在运行期对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对变异性语言来说是不可能的。
Category与Extension的区别?
- Category:运行时期确定、不能添加变量(添加变量会影响对象的内存布局)。
- Extension:类的一部分、编译时期确定、一般用来隐藏类的私有属性、对象的内存布局(分配内存空间)已确。
3.Category加载逻辑
我们知道OC程序的启动运行是依赖于runtime的,而OC的runtime是通过OS X与iOS通过dyld(苹果的动态加载器,用来加载image,即Mach-O格式的二进制文件)动态加载的。
而dyld会将app所依赖的各种库文件加载到内存空间中,包括libobjc库、runtime等,这些工作是在main函数中进行的。
OC的runtime入口方法为_objc_init。_objc_init主要用于读取Mach-O文件对应的Segment section,并根据其中的数据代码信息,完成OC的内存布局以及runtime相关的数据结构的初始化工作。
下面我们就对源码进行分析。
4.Category底层原理探索
objc源码:https://opensource.apple.com/tarballs/objc4/
编译时期:每次编译后的东西都存放到machO可执行文件中
category初始化:
extern category_t **_getObjc2CategoryList(const header_info *hi, size_t *count);
而category的初始化循环调用了_getObjc2CategoryList方法,_getObjc2CategoryList方法定义如下:
#define GETSECT(name, type, sectname) \
type *name(const headerType *mhdr, size_t *outCount) { \
return getDataSection<type>(mhdr, sectname, nil, outCount); \
} \
type *name(const header_info *hi, size_t *outCount) { \
return getDataSection<type>(hi->mhdr(), sectname, nil, outCount); \
}
存放了__objc_catlist section 段记录的所有的 Category。
objc源码:
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();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
- map_images:是一个回调函数,dyld将image(Mach-O格式的二进制文件)加载进内存时,会触发该回调函数。在该函数中将我们的分类加载到类当中的。
- load_images:dyld初始化image模块时,load_images。。。
- unmap_image:当将dyld移除内存时,调用该回调函数。
我们重点研究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);
}
map_images_nolock:
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);
}
}
_read_images函数:初始化map_images后的image,包括load所有的类、协议及Category,+load方法
就是在这里边调用的,load方法后面我们会详细讲解。
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses) {
...
//关键代码
// Discover categories.
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) {
// Category's target class is missing (probably weak-linked).
// Disavow any knowledge of this category.
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}
// 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);
}
}
}
}
}
开头定义的catlist就是编译器为我们准备的category_t数组,遍历取出其中的category_t,这段代码很容易理解,主要就是完成以下操作:
- 将category的实例方法、协议及属性添加到类上:addUnattachedCategoryForClass(cat, cls, hi);。
- 将类方法、协议添加到类的metaclass中:addUnattachedCategoryForClass(cat, cls->ISA(), hi);。
addUnattachedCategoryForClass:(cat, cls, hi);是将类和category做一个关联映射。至于category的各种列表是怎么添加到类上的?请看remethodizeClass。
remethodizeClass(cls):处理类的添加操作。
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;
runtimeLock.assertLocked();
isMeta = cls->isMetaClass();
// Re-methodizing: check for more categories
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
if (PrintConnecting) {
_objc_inform("CLASS: attaching categories to class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}
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,]
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);
}
将类的category_list中的所有方法分别取出来组成一个method_list_t,给 prepareMethodLists函数。
static void
prepareMethodLists(Class cls, method_list_t **addedLists, int addedCount,
bool baseMethods, bool methodsFromBundle)
{
runtimeLock.assertLocked();
if (addedCount == 0) return;
// Don't scan redundantly
bool scanForCustomRR = !cls->hasCustomRR();
bool scanForCustomAWZ = !cls->hasCustomAWZ();
// There exist RR/AWZ special cases for some class's base methods.
// But this code should never need to scan base methods for RR/AWZ:
// default RR/AWZ cannot be set before setInitialized().
// Therefore we need not handle any special cases here.
if (baseMethods) {
assert(!scanForCustomRR && !scanForCustomAWZ);
}
// Add method lists to array.
// Reallocate un-fixed method lists.
// The new methods are PREPENDED to the method list array.
for (int i = 0; i < addedCount; i++) {
method_list_t *mlist = addedLists[i];
assert(mlist);
// Fixup selectors if necessary
if (!mlist->isFixedUp()) {
fixupMethodList(mlist, methodsFromBundle, true/*sort*/);
}
// Scan for method implementations tracked by the class's flags
if (scanForCustomRR && methodListImplementsRR(mlist)) {
cls->setHasCustomRR();
scanForCustomRR = false;
}
if (scanForCustomAWZ && methodListImplementsAWZ(mlist)) {
cls->setHasCustomAWZ();
scanForCustomAWZ = false;
}
}
}
接着将prepareMethodLists处理后的method_list_t交给attachLists进行最后的处理。
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;
//拷贝操作
//void *memmove(void *__dst, const void *__src, size_t __len);表示将src指针指向位置开始len长度到dst。
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
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]));
}
}
void tryFree() {
if (hasArray()) {
for (uint32_t i = 0; i < array()->count; i++) {
try_free(array()->lists[i]);
}
try_free(array());
}
else if (list) {
try_free(list);
}
}
template<typename Result>
Result duplicate() {
Result result;
if (hasArray()) {
array_t *a = array();
result.setArray((array_t *)memdup(a, a->byteSize()));
for (uint32_t i = 0; i < a->count; i++) {
result.array()->lists[i] = a->lists[i]->duplicate();
}
} else if (list) {
result.list = list->duplicate();
} else {
result.list = nil;
}
return result;
}
}
- memmove表示表示将src指针指向位置开始len长度的数据移动到dst位置,
- memcpy表示表示将src指针指向位置开始len长度的数据拷贝到dst位置。
最终通过attachLists函数最终将所有分类方法添加到类的方法列表中。
Category加载完成之后需要注意以下几点:
1).Category并不会替换原类的同名方法,而是会在类方法列表中存放两个同名方法。
2).Category的方法会被存放到新方法列表的最前面,而原类的方法存放到后边。另外运行时在查找类方法时是顺序查找的,这也是为什么Category方法会优先调用的原因。