iOS底层原理|iOS底层原理 - category的本质以及源码分析

开篇之前大家先思考这两个问题

Category的实现原理?
Category和Extension的区别是什么?
Class Extension在编译的时候,它的数据就已经包含在类信息中
Category是在运行时,才会将数据合并到类信息中
开始分析Category的源码
  • 1.创建个LSPerson类
@interface LSPerson : NSObject @end @implementation LSPerson @end

  • 2.创建个LSPerson的Test分类
@interface LSPerson (Test) @property (nonatomic,copy)NSString *name1; @property (nonatomic,assign)int age; @end @implementation LSPerson (Test) -(void)test1 { NSLog(@"LSPerson (Test1)"); } -(void)test2 { NSLog(@"LSPerson (Test2)"); } +(void)test3 { NSLog(@"LSPerson (Test3)"); } -(id)copy { return [[LSPerson alloc]init]; } @end

  • 从上面的文件中可以看到这个分类含有2个属性,2个方法,遵循了两个协议
  • 那么现在我们用clang生成cpp代码看一下文件里都有啥
  • clang命令如下
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc LSPerson+Test.m

struct _category_t { const char *name; //哪个类的分类 LSPerson struct _class_t *cls; //这个值没用到传的为0 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; //属性列表 };

  • 经过仔细查看,看到了cpp文件里有这么个结构体名字写的也很清楚,分类的结构体,存放分类的信息,那么在接着看在哪用到这个结构体了,又看到了下面代码
static struct _category_t _OBJC_$_CATEGORY_LSPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = { "LSPerson", 0, // &OBJC_CLASS_$_LSPerson, (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_LSPerson_$_Test, (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_LSPerson_$_Test, (const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_LSPerson_$_Test, (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_LSPerson_$_Test, };

  • 看上面这段代码是定义了一个_category_t类型的变量,变量名称就是_OBJC_$_CATEGORY_类名_$_分类名,这种格式变量就不会重复,然后进行赋值,有哪几个参数那个结构体看的也很清楚了,包含类名,对象方法列表,类方法列表,协议列表,属性列表
类名就是LSPerson也都明白,那么接着看_OBJC_$_CATEGORY_INSTANCE_METHODS_LSPerson_$_Test这个变量,可以看到是取这个变量地址然后转成这个类型(const struct _method_list_t *),然后搜索这个变量名字,发现如下代码
static struct /*_method_list_t*/ { unsigned int entsize; // sizeof(struct _objc_method) unsigned int method_count; struct _objc_method method_list[2]; } _OBJC_$_CATEGORY_INSTANCE_METHODS_LSPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = { sizeof(_objc_method), 2, {{(struct objc_selector *)"test1", "v16@0:8", (void *)_I_LSPerson_Test_test1}, {(struct objc_selector *)"test2", "v16@0:8", (void *)_I_LSPerson_Test_test2}} };

可以看出里面含有我们定义的两个对象方法,并且方法count=2
  • 我们从上面看到了_objc_method这个结构体我们在搜索它的结构,发现就是方法名字,方法类型,方法实现IMP
struct _objc_method { struct objc_selector * _cmd; const char *method_type; void*_imp; };

接着看类方法,搜索那个类方法变量,又看到如下代码
static struct /*_method_list_t*/ { unsigned int entsize; // sizeof(struct _objc_method) unsigned int method_count; struct _objc_method method_list[1]; } _OBJC_$_CATEGORY_CLASS_METHODS_LSPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = { sizeof(_objc_method), 1, {{(struct objc_selector *)"test3", "v16@0:8", (void *)_C_LSPerson_Test_test3}} };

【iOS底层原理|iOS底层原理 - category的本质以及源码分析】可以看出里面含有我们定义的一个类方法,并且方法count=1
然后在看协议列表,发现如下代码
static struct /*_protocol_list_t*/ { long protocol_count; // Note, this is 32/64 bit struct _protocol_t *super_protocols[2]; } _OBJC_CATEGORY_PROTOCOLS_$_LSPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = { 2, &_OBJC_PROTOCOL_NSCopying, &_OBJC_PROTOCOL_NSCoding };

可以看到包含着我们遵循的两个协议NSCopying,NSCoding,但是看起来是两个变量,再接着看这俩变量是啥
struct _protocol_t _OBJC_PROTOCOL_NSCopying __attribute__ ((used)) = { 0, "NSCopying", 0, (const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying, 0, 0, 0, 0, sizeof(_protocol_t), 0, (const char **)&_OBJC_PROTOCOL_METHOD_TYPES_NSCopying };

看到我们传了协议名称,还有方法列表变量,在看方法列表变量是啥
static struct /*_method_list_t*/ { unsigned int entsize; // sizeof(struct _objc_method) unsigned int method_count; struct _objc_method method_list[1]; } _OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying __attribute__ ((used, section ("__DATA,__objc_const"))) = { sizeof(_objc_method), 1, {{(struct objc_selector *)"copyWithZone:", "@24@0:8^{_NSZone=}16", 0}} };

由上面看到了这个方法列表里有我们实现的copy方法,底层调用的是copyWithZone,和alloc类似调用的是allocWithZone
那么接下来在看看协议的结构体如下,看起来也是一目了然
struct _protocol_t { void * isa; // NULL const char *protocol_name; const struct _protocol_list_t * protocol_list; // super protocols const struct method_list_t *instance_methods; const struct method_list_t *class_methods; const struct method_list_t *optionalInstanceMethods; const struct method_list_t *optionalClassMethods; const struct _prop_list_t * properties; const unsigned int size; // sizeof(struct _protocol_t) const unsigned int flags; // = 0 const char ** extendedMethodTypes; };

接下来看属性列表源码
static struct /*_prop_list_t*/ { unsigned int entsize; // sizeof(struct _prop_t) unsigned int count_of_properties; struct _prop_t prop_list[2]; } _OBJC_$_PROP_LIST_LSPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = { sizeof(_prop_t), 2, {{"name1","T@\"NSString\",C,N"}, {"age","Ti,N"}} };

确实看到了我们定义的两个name,age属性
由此我们得到结论就是
分类在编译的时候将分类的信息存在struct _category_t中,那么怎么在程序运行的时候是怎么加载到内存中的呢,接下来看runtime的源码

iOS底层原理|iOS底层原理 - category的本质以及源码分析
文章图片
屏幕快照 2018-11-21 上午11.33.26.png
  • 我们看到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); }

iOS底层原理|iOS底层原理 - category的本质以及源码分析
文章图片
1542772124422.jpg iOS底层原理|iOS底层原理 - category的本质以及源码分析
文章图片
E70CD585-A902-4604-AAA1-079FC44C8C1E.png iOS底层原理|iOS底层原理 - category的本质以及源码分析
文章图片
BEA8A89A-5082-4AE8-A001-C1E122EC2A78.png 由此我们得到以下结论,Category的加载处理过程
1.通过Runtime加载某个类的所有Category数据
2.把所有Category的方法、属性、协议数据,合并到一个大数组中
后面参与编译的Category数据,会在数组的前面
3.将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面
屏幕快照 2018-11-21 下午1.35.32.png 上图是Objc类对象,元类对象的底层结构,而我们分类添加的方法是添加到class_rw_t的方法列表里,从上面分析的代码可以看到访问的是 auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rw->methods.attachLists(mlists, mcount);
所以calss_ro_t的baseMethodList不会改变
接下来验证同时用分类,和类扩展添加属性,然后在获取属性列表看看顺序是啥样
  • 首先在LSPerson类扩展里添加两个属性,分类里的name,age属性不变
@interface LSPerson : NSObject @property (nonatomic,copy)NSString *name1; @property (nonatomic,copy)NSString *name2; @end @interface LSPerson() @property (nonatomic,copy)NSString *extensionPro1; @property (nonatomic,copy)NSString *extensionPro2; @end @implementation LSPerson@end@interface LSPerson (Test) @property (nonatomic,copy)NSString *categoryName1; @property (nonatomic,copy)NSString *categoryName2; @end @implementation LSPerson (Test) @end

  • 打印属性列表,使用此库比较方便 DLIntrospection
  • 可以看到打印一下结果证明
先是类本身的东西
然后编译的时候把类扩展的东西插在原来的前面
编译的时候同时把分类的信息存放在category_t结构体里
程序运行的时候利用runtime把分类的信息继续插在最前面
所以存放顺序应该是:
分类,类扩展,本类信息
( "@property (nonatomic, copy) NSString* categoryName1", "@property (nonatomic, copy) NSString* categoryName2", "@property (nonatomic, copy) NSString* extensionPro1", "@property (nonatomic, copy) NSString* extensionPro2", "@property (nonatomic, copy) NSString* name1", "@property (nonatomic, copy) NSString* name2" )

    推荐阅读