iOS Runtime学习笔记(对象、类、分类、消息机制、KVO、KVC)

对象模型

新建一个类:

#import <Foundation/Foundation.h>

@interface CustomClass : NSObject

@end

@implementation CustomClass

@end

int main(int argc, char * argv[]) {
    CustomClass *customObject = [[CustomClass alloc] init];
}

在终端运行以下命令:

xcrun -sdk iphonesimulator12.0 clang -rewrite-objc CustomObject.m

.m文件被编译成C++文件,截取部分代码如下:

...
typedef struct objc_object CustomClass;
...
int main(int argc, char * argv[]) {
    CustomClass *customObject = ((CustomClass *(*)(id, SEL))(void *)objc_msgSend)((id)((CustomClass *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("CustomClass"), sel_registerName("alloc")), sel_registerName("init"));
}
...

可以看到我们定义的CustomClass被定义成objc_object结构体,在main函数里面的CustomClass *customObject其实就是struct objc_object *customObject,所以customObject的本质就是一个指向objc_object结构体的指针。

objc_object的源码:

struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

可以看到objc_object有一个Class类型的isa变量,这个isa是指向该实例所属的类的。而Class其实本质就是objc_class

typedef struct objc_class *Class;

objc_class的源码:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() { 
        return bits.data();
    }
    ...
};

类继承自对象, 所有类其实也是对象,它的isa指向的类就是这个类的元类(meta-class)。元类只有一个对象,就是类对象。类方法就在类对象里面。

给一个对象发送消息,会到这个对象所属的类里面找对应的方法;给一个类发送消息,会到这个类的元类(也就是类对象所属的类)里面去找对应的方法。

而元类也是类,它也有isa指针,这个isa指针统一都直接指向NSObject的元类。而NSObject的元类的isa则指向自己。另外NSObject的元类的父类则指向NSObject。而其他子类的元类指向其父类的元类。

调用实例的方法,会到类里面找。调用类的方法,也就是元类的实例方法,因为类就是元类的实例,回到元类里面找,在元类里面找不到就会去元类的父类,最后来到NSObject元类,NSObject元类的父类就是NSObject。

objc_method的定义:

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
} 

SEL的定义:

typedef objc_selector * SEL;

可以理解为区分方法的 ID。

在Object-C中可以通过以下方法获得SEL:

SEL @selector(<selector)

SEL sel_registerName(const char * _Nonnull str)

IMP的定义:

typedef id (*IMP)(id, SEL, ...); 

指向最终实现程序的内存地址的指针。

消息机制

在Object-C里的函数调用其实都是发送消息,[receiver massage]会被编译成

objc_msgSend(receiver, selector)

如果有参数,则为:

objc_msgSend(receiver, selector, arg1, arg2, ...)

objc_class 里面还有一个变量:struct objc_cache *cache,它主要是为了调用的性能进行优化的。每当实例对象接收到一个消息时,它会先到cache中去找能够响应的方法,找不到再去方法列表里面找。找到之后就会保存到cache里面。已备下次再被调用。

如果对象接收到无法解读的消息,那将会调用其所属类的以下类方法:

+ (BOOL)resolveInstanceMethod:(SEL)sel

sel就是那个未知的选择子,返回的BOOL类型来表示这个类能否新增一个实例方法来处理这个选择子。所以本类有机会新增一个处理此选择子的方法。一般是在resolveInstanceMethod:方法里面调用class_addMethod方法来动态添加方法,但前提是实现代码已经提前写好了。

如果上述方法返回的是NO,那么当前接受者还有第二次机会,runtime还会调用该对象的以下方法,看能否将该消息转发给其他接受者处理:

- (id)forwardingTargetForSelector:(SEL)aSelector

如果此时还是返回nil,那么会来到完整的消息转发。runtime会调用以下方法:

 - (void)forwardInvocation:(NSInvocation *)anInvocation

anInvocation包含了那条尚未处理的消息的全部细节,包括选择子、目标以及参数。这个步骤可以修改消息的内容,比如追加参数或者改变选择子等。如果本类不处理此消息,会调用超类的同名方法。最后传到NSObject时会调用“doesNotRecognizeSelector:”抛出异常。

Category

我们先给前面自定义的类实现一个分类:

@interface CustomClass : NSObject

@end

@implementation CustomClass

- (void)originMethod {
    NSLog(@"originMethod");
}

@end

@interface CustomClass(MyAddition)<NSCopying>

@property (nonatomic, copy) NSString *customProperty;

@end

@implementation CustomClass(MyAddition)

+ (void)addClassMethod {
    NSLog(@"addClassMethod");
}

- (void)addInstanceMethod {
    NSLog(@"addInstanceMethod");
}

- (id)copyWithZone:(NSZone *)zone {
    return nil;
}

@end

运行以下命令,得到一个C++文件

clang -rewrite-objc CustomClass.m

这个文件很长,先看其中的一部分:

static struct _category_t _OBJC_$_CATEGORY_CustomClass_$_MyAddition __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "CustomClass",
    0, // &OBJC_CLASS_$_CustomClass,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_CustomClass_$_MyAddition,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_CustomClass_$_MyAddition,
    (const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_CustomClass_$_MyAddition,
    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_CustomClass_$_MyAddition,
};

可以看到这个就是分类的定义,其本质就是category_t,可以在runtime的源码中找到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;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta) {
        if (isMeta) return nil; // classProperties;
        else return instanceProperties;
    }
};

其中定义了分类的名字、类、实例方法、类方法、实现的协议、属性。

对应起来OBJC_$_CATEGORY_INSTANCE_METHODS_CustomClass_$_MyAddition则保存了所添加的实例方法:

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_CustomClass_$_MyAddition __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    2,
    {{(struct objc_selector *)"addInstanceMethod", "v16@0:8", (void *)_I_CustomClass_MyAddition_addInstanceMethod},
    {(struct objc_selector *)"copyWithZone:", "@24@0:8^{_NSZone=}16", (void *)_I_CustomClass_MyAddition_copyWithZone_}}
};

method_list_t 里面包含了方法的大小、数目、方法数组。

OBJC_$_CATEGORY_CLASS_METHODS_CustomClass_$_MyAddition保存了类方法;

OBJC_CATEGORY_PROTOCOLS_$_CustomClass_$_MyAddition是协议列表:

static struct /*_protocol_list_t*/ {
    long protocol_count;  // Note, this is 32/64 bit
    struct _protocol_t *super_protocols[1];
} _OBJC_CATEGORY_PROTOCOLS_$_CustomClass_$_MyAddition __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    1,
    &_OBJC_PROTOCOL_NSCopying
};

OBJC_$_PROP_LIST_CustomClass_$_MyAddition属性列表:

static struct /*_prop_list_t*/ {
    unsigned int entsize;  // sizeof(struct _prop_t)
    unsigned int count_of_properties;
    struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_CustomClass_$_MyAddition __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_prop_t),
    1,
    {{"customProperty","T@\"NSString\",C,N"}}
};

在DATA段我们还可以看到一个保存分类列表的数组:

static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
    &_OBJC_$_CATEGORY_CustomClass_$_MyAddition,
};

Category被附加到类上面是在objc-os.mm的_objc_init方法里发生的,_objc_init里面的调用的map_images最终会调用objc-runtime-new.mm里面的_read_images方法,而在_read_images方法的结尾,有以下的代码片段:

    // Discover categories. 
    for (EACH_HEADER) {
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        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  
                /* ||  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);
                }
            }
        }
    }

1、通过_getObjc2CategoryList拿到所有的分类;
2、对每个分类,拿到所属的类;
3、Register the category with its target class;addUnattachedCategoryForClass(cat, cls, hi);
4、Rebuild the class's method lists (etc) if the class is realized;remethodizeClass(cls);

来看addUnattachedCategoryForClass的源码:

static void addUnattachedCategoryForClass(category_t *cat, Class cls, 
                                          header_info *catHeader)
{
    runtimeLock.assertWriting();

    // DO NOT use cat->cls! cls may be cat->cls->isa instead
    NXMapTable *cats = unattachedCategories();
    category_list *list;

    list = (category_list *)NXMapGet(cats, cls);
    if (!list) {
        list = (category_list *)
            calloc(sizeof(*list) + sizeof(list->list[0]), 1);
    } else {
        list = (category_list *)
            realloc(list, sizeof(*list) + sizeof(list->list[0]) * (list->count + 1));
    }
    list->list[list->count++] = (locstamped_category_t){cat, catHeader};
    NXMapInsert(cats, cls, list);
}

注释里说是把分类注册到类中,其实就是把类和category做一个关联映射。remethodizeClass则真正在把方法添加到类中:

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)" : "");
        }
        
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}

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);
        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);
}

一个关键的代码:auto rw = cls->data();

根据objc_class的定义我们可以知道rw就是class_rw_t,而class_rw_t里面还包含着class_ro_t。事实上,类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t 中,而class_ro_t则存储了当前类在编译期就已经确定的属性、方法以及遵循的协议。所以分类是把方法添加到class_rw_t当中的

分类本质上就是一个保存了一系列方法的结构体,在运行时分类的方法会被附加到类的方法前面。

KVO原理

KVO即Key-Value-Observer键值观察,是通过isa-swizzling技术实现的:

  • 1、当类A的实例第一次被观察的时候,系统会在运行期动态地创建类A的派生类NSKVONotifying_A,其实就是A的子类
  • 2、类NSKVONotifying_A会重写被观察的属性的setter方法,在修改值之前会调用willChangeValueForKey:方法,修改值之后会调用didChangeValueForKey:方法,这两个方法最终都会被调用到observeValueForKeyPath:ofObject:change:context:方法中;
  • 3、类NSKVONotifying_A重写class方法,返回类A,类NSKVONotifying_A还会重写dealloc方法释放资源;
  • 4、被观察对象的isa指针从指向原来的 A 类,被 KVO 机制修改为指向系统新创建的子类 NSKVONotifying_A类,来实现当前类属性值改变的监听。

KVO的使用其实比较容易崩溃,addObserver和removeObserver需要是成对的,如果重复remove则会导致NSRangeException类型的Crash,如果忘记remove则会在观察者释放后再次接收到KVO回调时Crash。苹果官方推荐的方式是,在init的时候进行addObserver,在dealloc时removeObserver,这样可以保证add和remove是成对出现的,是一种比较理想的使用方式。

KVOController是Facebook的一个KVO开源第三方框架,它具有原生KVO所有的功能,但规避了原生KVO的很多问题,而且支持block回调。

KVC

- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;

先查找setKey或_setKey方法存不存在,如果存在则直接调用设值,否则询问是否可以直接访问变量+ (BOOL)accessInstanceVariablesDirectly,如果可以则直接设置,否则报错

- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;

先查找getKey、key、isKey、_isKey方法存不存在,如果存在则直接调用,否则询问是否可以直接访问变量+ (BOOL)accessInstanceVariablesDirectly,如果可以则直接读取,否则报错。

被监听的实例一定有set方法,所以KVC会触发KVO。

strong、weak、assign、copy

有一个值为B,它的内存地址为A,现在把A赋值给C会发生什么呢?如下例子:

NSMutableString *C = [[NSMutableString alloc] initWithString:@"B"];

这里的A就是创建字符串@"B"时的值地址。引用计数的概念就是针对A的,但A赋值给其他变量或者指针设置为nil,如A=nil,都会导致引用计数有所增减。当内容区域引用计数为0的时候就会将数据B抹除。

使用copy、strong、retain、weak、assign区别就在:

  • 是否对A有引用计数增加;
  • 是否开辟新的内存。
@property (nonatomic, strong) NSMutableString *strongC;
@property (nonatomic, weak) NSMutableString *weakC;
@property (nonatomic, assign) NSMutableString *assignC;
@property (nonatomic, copy) NSMutableString *copyC;
...

NSMutableString *C = [[NSMutableString alloc] initWithString:@"B"];
self.strongC = C;
self.weakC = C;
self.copyC = C;
self.assignC = C;

strongC会增加A的引用计数;weakC不会增加A的引用计数;copyC也不会增加A的引用计数,它会开辟一块新的内存。

assign和weak类似,都不会增加A的引用计数,但是区别在于但weak指向的对象被释放后weak指针会被置为nil,而assign不会,就会导致野指针的问题。但assign修饰基本数据类型是安全的,而weak只能修饰对象类型。

retain和strong一样,会增加A的引用计数,但在MRC时会有差别。

copy NSString NSArray不会开辟新的内存,因为它们是不可变变量。

对不可变对象进行copy,是浅拷贝;其余的对不可变对象进行mutableCopy,对可变对象进行copy,对可变对象进行mutableCopy都是深拷贝。

copy之后得到的都是不可变对象,mutableCopy之后得到的都是可变对象。

@property (nonatomic, assign) float a;是保存在它所在的对象所存在的堆上面的。

iOS中copy,strong,retain,weak和assign的区别

参考

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,709评论 0 9
  • 前言 Runtime是什么 Runtime的实现原理消息传递机制Runtime基础数据结构NSObject & i...
    Supremodeamor阅读 534评论 0 1
  • 参考链接: http://www.cnblogs.com/ioshe/p/5489086.html 简介 Runt...
    乐乐的简书阅读 2,135评论 0 9
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,192评论 0 7
  • Objective-C语言是一门动态语言,他将很多静态语言在编译和链接时期做的事情放到了运行时来处理。这种动态语言...
    tigger丨阅读 1,398评论 0 8