iOS底层原理总结 -- 利用Runtime源码 分析Category的底层实现

2,445 阅读9分钟

窥探iOS底层实现--OC对象的本质(一)

窥探iOS底层实现--OC对象的本质(二)

窥探iOS底层实现--OC对象的分类:instance、class、meta-calss对象的isa和superclass

窥探iOS底层实现-- KVO/KVC的本质

iOS底层原理总结 -- 利用Runtime源码 分析Category的底层实现 ...

前言:

本文总结了一下Category中的内部去实现部分,代码部分较多,添加了注释,阅读起来可能比较枯燥。但是请大家务必坚持读完。会有更多的收货。

本文中涉及到Class类对象的内部结构的知识,请参考文章 窥探iOS底层实现--OC对象的分类:instance、class、meta-calss对象的isa和superclass

思考:

  1. Category的实现原理?
  2. 为什么Category的中的方法会优先调用?
  3. 延伸问题 - 如果多个分类中都实现了同一个方法,那么在调用该方法的时候会优先调用哪一个方法?
  4. 扩展和分类的区别?

Category 基本实现

首先 看一下分类代码代码的实现 可选择性跳过

///> main.h
int main(int argc, const char *argv[]){
	@autoreleasepool{
	  Person *person = [[Person alloc] init]
  	[person run];
	  [person test];
  	[person eat];
	}
	return 0
}

///> person
@interface Person: NSObject
@end
@implementation Person
- (void)run{
  Nslog(@"run")
}
@end

///> person+test
@interface Person(test)
- (void)test;
@end
@implementation Person(test)
- (void)test{
  Nslog(@"test")
}
@end
  
///> person+Eat
@interface Person(eat)
- (void)eat;
@end
@implementation Person(eat)
- (void)eat{
  Nslog(@"eat")
}
@end

分类的底层结构体 编译完毕之后

编译完毕的时候 一开始程序运行的时候 所有分类的方法 一开始都存放在 结构体中(每一个分类都有一个新的结构体对象),

编译完毕之后 category存放在 结构体category_t中 并没有合并到 原始类中 每一个分类都会生成catrgory_t的结构体, 在运行时的时候才会将分类中的方法、协议、属性等 合并到原始的类中去。

下面是源码观看的过程在每一步都给出了注释, 有点枯燥,但是看完之后会很受益。

分类代码 C\C++源码分析

利用:

xcrun -sdk iphoneos clang -arch arm64 -  OC源文件 -o 输出的CPP文件

命令可以查看转化为C\C++代码。会有生成一个xxx.cpp的文件就是我们想要的文件

可以将其拖入到xcode中, 方便搜索

接下来直接搜索 category_t 得出如下结构体 我已经将注释放在后面了

struct _category_t{
  const char *name;   ///> 分类的名字
  struct _class_t *cls;   ///> class
  const struct _method_list_t *instance_methods;   ///> 实例方法列表
	const struct _method_list_t *class_methods;   ///>  类方法列表
	const struct _protocol_list_t *protocols;    ///>  协议
	const struct _prop_list_t *properties;   ///> 属性
}

每创建一个类都会 根会根据如下方法创建一个category_t的结构体

static struct _category_t OBJC_$CATEGORY_PERSON_$_Test __attribute__ ((userd, section("__DATA,__objc_const")))={
  ///>  属于那个类的分类
  "Person",  
  ///> class
  0,
  ///>  对象方法列表
  (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$Test,
  ///>  类方法列表
  (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$Test,
  ///>  协议列表 
  0, // (const _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_PERSON_$_Test,	
  ///>  属性列表
  0, // (const _prop_list_t *)&_OBJC_$_PROP_LIST_PERSON_$_Test,
}

接下来查看一下 Runtime的源码是怎么将分类合并的,

Runtime源码分析

​ 首先下载Runtimed的源码。 ------ 这里用xcode打开

  1. 搜索 "catrgory_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);
    };
    

    可以看到 Runtime中的结构和上面的category_t的结构类似。

  2. Runtime的程序入口文件为objc-os.mm 文件,

  3. 我这里直接到 有关Category的代码部分 在objc-runtime-new.mm文件中 搜搜Discover categories. 的注释代码

      // Discover categories. 
       for (EACH_HEADER) {
            /**
             catlist 是一个二维数组,
             每一个分类都会创建一个category_t的结构体
             这里的二维数组放了两个分类结构体的内容 如代码中的 eat和test
             catlist = [[],[]]
             */
            category_t **catlist = 
                _getObjc2CategoryList(hi, &count);
            bool hasClassProperties = hi->info()->hasCategoryClassProperties();
    
            ///> 将每一个数组中的内容遍历
            for (i = 0; i < count; i++) {
                ///> 获取 单独的category_t结构体
                category_t *cat = catlist[i];
                ///> 重新映射class 取出结构体的class
                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);
                    }
                }
            }
        }
    

    以上代码中找到了 核心的方法: remethodizeClass 使用了两次 , 重新组织类的方法和元类的方法

  4. command+单机,进入

    static void remethodizeClass(Class cls)
    {
        category_list *cats;
        bool isMeta;
        runtimeLock.assertWriting();
        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)" : "");
            }
            
         		///> 附加分类的代码调用 , 传入了 类对象、分类。
    	      ///> cls: [Person class]
    				///> cats: [category_t(test), category_t(eat)]
            attachCategories(cls, cats, true /*flush caches*/);        
            free(cats);
        }
    }
    
  5. command 进入 attachCategories(cls, cats, true /flush caches/); 方法

    ///> cls: [Person class]
    ///> cats: [category_t(test), category_t(eat)]
    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
        ///> malloc 分配内存
        ///> 方法数组  二维数组  eg:[[method_t,method_t], [method_t,method_t]]
        method_list_t **mlists = (method_list_t **)
            malloc(cats->count * sizeof(*mlists));
          
        ///> 属性数组 eg:[[property_t,property_t], [property_t,property_t]]
        property_list_t **proplists = (property_list_t **)
            malloc(cats->count * sizeof(*proplists));
      
        ///> 协议数组 eg:[[protocol_t,protocol_t], [protocol_t,protocol_t]]
        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: 类对象结构体中 有一个erw的结构,
         这一步骤就是将数据合并到类对象的 rw结构中去  请参照文章:
         将所有的分类的对象方法  附加到类对象中去!
         也就是 在运行d时的时候讲 分类的数据合并到了原始的类对象中!!
         */
        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);
    }
    
  6. command 进入 rw->methods.attachLists(mlists, mcount); 方法中

        /**
         addedLists: [[method_t, method_t],[method_t, method_t]]
         addedCount: 2      s二维数组的数量
         */
        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;
                ///> realloc 重新分配内存   newCont
                ///> 为了合并分类中的数组 扩充原来数组的大小
                ///> 需要重新分配内存
                setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
                array()->count = newCount;
                
                /*
                 内存移动
                    array()->lists 原来的方法列表
                    addedCount 分类的数组的count
                 
                    将原来的方法列表挪动到新的位置,
                    (array()->lists + addedCount  addedCount是挪动的位数
                    相当有将原来的方法放到了最后!
                 */
                memmove(array()->lists + addedCount, array()->lists, 
                        oldCount * sizeof(array()->lists[0]));
                
                /*
                 内存挪动 拷贝
                 array()->lists 原来的方法列表
                 addedLists 传进来的分类list
                 
                 上面的方法已经将 类的方法列表做到了扩充  并且类原始带的方法列表向后挪动的 addedCount的位数
                 为的就是 将传入的分类的方法列表 拷贝到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]));
            }
        }
    

总结分类的一些问题

由源码分析我们可以得知,

  1. Category的实现原理?

    原理:底层结构是结构体 categoty_t 创建好分类之后分两个阶段:

    1. 编译阶段:

      将每一个分类都生成所对应的 category_t结构体, 结构体中存放 分类的所属类name、class、对象方法列表、类方法列表、协议列表、属性列表。

    2. Runtime运行时阶段:

      将生成的分类数据合并到原始的类中去,某个类的分类数据会在合并到一个大的数组当中(后参与编译的分类会在数组的前面),分类的方法列表,属性列表,协议列表等都放在二维数组当中,然后重新组织类中的方法,将每一个分类对应的列表的合并到原始类的列表中。(合并前会根据二维数组的数量扩充原始类的列表,然后将分类的列表放入前面)

  2. 为什么Category的中的方法会优先调用?

    如上所述, 在扩充数组的时候 会将原始类中拥有的方法列表移动到后面, 将分类的方法列表数据放在前面,所以分类的数据会优先调用

  3. 延伸问题 - 如果多个分类中都实现了同一个方法,那么在调用该方法的时候会优先调用哪一个方法?

    在多个分类中拥有相同的方法的时候, 会根据编译的先后顺序 来添加分类方法列表, 后编译的分类方法在最前面,所以要看 Build Phases --> compile Sources中的顺序。 后参加编译的在前面。

  4. 扩展和分类的区别

    扩展@interface 是匿名分类, 不是分类。 就是属性添加 在编译的时候就加入到了类中

    category在runtime中才合并的。


参考:

  1. Runtime源码地址:Source Browser:OBJective-c源码找到objc4,下载版本号最大是最新的源码
  2. MJ老师底层相关视频

再次感谢!!


如有错误之处还请各位大神指出!!