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

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

Category的实现原理?
Category和Extension的区别是什么?

Class Extension在编译的时候,它的数据就已经包含在类信息中
Category是在运行时,才会将数据合并到类信息中

开始分析Category的源码

  • 1.创建个LSPerson类
@interface LSPerson : NSObject
@end
@implementation LSPerson
@end
  • 2.创建个LSPerson的Test分类
@interface LSPerson (Test)<NSCopying,NSCoding>
@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}}
};

可以看出里面含有我们定义的一个类方法,并且方法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的源码


屏幕快照 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);
}
1542772124422.jpg
E70CD585-A902-4604-AAA1-079FC44C8C1E.png
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)<NSCopying,NSCoding>
@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"
)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容