~ read.

从自己实现isa-swizzling到说一些Runtime的内容

请叫我背景大人

前段时间写了一篇isa-swizzling?什么鬼?的文章,就想说稍微实现一下isa-swizzling。然而其实实现起来非常简单,但是其中的一些Runtime内容以及一些其他的知识还是值得一讲的。

好吧,我承认我本来是想写自己实现KVO,然后我发现太长了,想说先从isa-swizzling开始。然而这并不妨碍我可能还会更一篇来说一些手动实现KVO,但是谁知道呢。如果还完全不了解isa-swizzling干了什么的同学(路人甲:谁是什么~,路人乙:你好污,巫妖王),isa-swizzling?什么鬼?不多说,自己看。

关门放狗,不对,是代码

static void DefaultSetterForKVO(id self, SEL _cmd, void *value)   {  
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *key = [self obtainGetterFromSetter:setterName];
    [self willChangeValueForKey:key];
    Class subCls = object_getClass(self);
    Class supCls = class_getSuperclass(subCls);
    struct objc_super superInfo = {
        self,
        supCls
    };
    ((void (*) (void * , SEL, ...))objc_msgSendSuper)(&superInfo, _cmd, value);
    [self didChangeValueForKey:key];
}

- (void)py_addObserver:(NSObject *)observer
            forKeyPath:(NSString *)keyPath
               options:(NSKeyValueObservingOptions)options
               context:(void *)context {
    // isa-swizzling implement
    NSString *newName = [@"PYKVONotifying_" stringByAppendingString:NSStringFromClass(object_getClass(self))]; 
    NSString *setterName = [[@"set" stringByAppendingString:[[[keyPath uppercaseString] substringToIndex:1] stringByAppendingString:[keyPath substringFromIndex:1]]] stringByAppendingString:@":"];
    Class subCls = objc_allocateClassPair(object_getClass(self), [newName UTF8String], 0);
    class_addMethod(subCls, NSSelectorFromString(setterName), (IMP)DefaultSetterForKVO, "v@:@");
    objc_registerClassPair(subCls);
    object_setClass(self, subCls);
}

其实代码挺简单的,也就这20+行代码,但是里面有的一些东西还是可以看看的。

解析一:addObserver:forKeyPath:options:context:

上面是重写的addObserver:forKeyPath:options:context:一小部分,但是这里面的知识点也可圈可点。我们先从addObserver开始看吧。

前两句没什么好说的,其实就是根据原有类名以及keyPath来获得新类名以及需要重写的方法setter方法(不对啊,keyPath可能带'.'之类的啊),保安有人捣乱。好吧,其实这里就是简单的忽略keyPath,假设不存在那些特殊情况。

接下来剩下的三行代码分别是动态生成类以及以及添加新的方法以及注册类。我们说说最后一个调用object_setClass的作用,为什么要这么做呢?我们都说了是isa-swizzling了当然第一个作用就是通过这个方法来“偷梁换柱”,将isa换成我们新产生的这个类,那么为什么有必要将isa指向新的类呢?其实主要目的是:我们都知道objc_class中保存着我们实例方法的objc_method_list,而一个类的isa才是指向这个类的Class,也就是objc_class。也就是说Objc在查找一个类的实现方法是通过isa去查找具体Class的objc_method_list。 最终就会变成下面这样:

isa-swizzling-UML.png

解析二:DefaultSetterForKVO

估计有些小伙伴要抓狂了,一方面是为什么static void DefaultSetterForKVO(id self, SEL _cmd, void *value)这样的函数声明,另一方面objc_superobjc_msgSendSuper这句是什么鬼。不急,在说这些之前,我们先做一个clang -rewrite-objc的操作,解析的内容是下面这些内容的.m文件:

// Person.h文件
@interface Person : NSObject

@property (nonatomic, copy) NSString *id;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger age;

@end

// Person.m文件
@implementation Person

- (void)test {
    [super description];
}

@end

然后我们从中截取一部分对我们目前来说有用的信息,至于里面的一些其他内容小伙伴可以从Github上找到对应的Person.cpp文件查看。

struct __rw_objc_super {  
    struct objc_object *object; 
    struct objc_object *superClass; 
    __rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {} 
};

// @implementation Person
static void _I_Person_test(Person * self, SEL _cmd) {  
    ((NSString *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Person"))}, sel_registerName("description"));
}

static NSString * _I_Person_id(Person * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Person$_id)); }  
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_Person_setId_(Person * self, SEL _cmd, NSString *id) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _id), (id)id, 0, 1); }

static NSString * _I_Person_name(Person * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Person$_name)); }  
static void _I_Person_setName_(Person * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _name), (id)name, 0, 1); }

static NSUInteger _I_Person_age(Person * self, SEL _cmd) { return (*(NSUInteger *)((char *)self + OBJC_IVAR_$_Person$_age)); }  
static void _I_Person_setAge_(Person * self, SEL _cmd, NSUInteger age) { (*(NSUInteger *)((char *)self + OBJC_IVAR_$_Person$_age)) = age; }  
// @end

static struct /*_method_list_t*/ {  
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[7];
} _OBJC_$_INSTANCE_METHODS_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    7,
    {{(struct objc_selector *)"test", "v16@0:8", (void *)_I_Person_test},
    {(struct objc_selector *)"id", "@16@0:8", (void *)_I_Person_id},
    {(struct objc_selector *)"setId:", "v24@0:8@16", (void *)_I_Person_setId_},
    {(struct objc_selector *)"name", "@16@0:8", (void *)_I_Person_name},
    {(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_Person_setName_},
    {(struct objc_selector *)"age", "Q16@0:8", (void *)_I_Person_age},
    {(struct objc_selector *)"setAge:", "v24@0:8Q16", (void *)_I_Person_setAge_}}
};

我们来尝试看看,首先可以看到上面有@implementation Person@end被注释了,在机上里面的内容我们可以看出来这一部分是Person.m文件转换成C之后的代码。而根据下面的一个struct,我们可以看得出来,其实clang为我们分装成具体objc_method_list方面之后好查找。

同时我们也可以看到原本我们的方法都被_I_Person_方法名(Person *self, SEL _cmd, [其他参数...])的形式。而这个形式正是我们上面的DefaultSetterForKVO的形式,也就是说我们可以通过这样写直接转换成- (void)DefaultSetterForKVO:(void *)value的形式, 只是我们直接将其转换成C的形式来实现。

然后我们来看看test方法的实现,看起来好像很复杂,我们一点点分解,先看这一段(__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Person"))},其实这一段的作用就是创建一个最开头看到的__rw_objc_super的struct,而这个__rw_objc_super就和我们上面objc_super是等价的。然后就是调用objc_msgSendSuper这个C方法,这句话很纸老虎,其实它很多内容都是在进行类型转换。那么通过这个[super description]可以看得出来,这个其实就是调用了objc_msgSendSuper,然后是需要创建一个objc_super的struct作为第一个参数,而第二个参数是selector。那么根据我们objc_msgSendSuper的了解,如果之后函数还有其他参数就应该添加在之后。

从这里也能看出@我就叫Sunny怎么了博客中神经病院objc runtime入院考试中的第一题的解释中为什么reveiver还是self的答案。

那么在DefaultSetterForKVO中原本我们的实现应该是:

[self willChangeValueForKey:key];
[super setter:value];
[self didChangeValueForKey:key];

但是我们的setter是根据keyPath进行变化的,所以这里通过C来实现,并且将其转换成了objc_msgSendSuper的调用,最终实现上面这三行代码。

小结

到这里如果我们通过手动实现isa-swizzling基本实现了,也知道了里面细节的内容。其实整体的逻辑非常简单,就是讲原本的类的isa替换成新生成类,并且新生成类是原本类的子类。在新生成类中添加插入willChangeValueForKeydidChangeValueForKey的调用。

就这样结束了?当然不是,通过一些实验发现其实isa-swizzling应该还有做一些其他的工作,例如通过这样替换isa后原本的classsuperClassisKindOfClass等一些跟类有关的相关方法应该也会重新实现。否则通过调用class方法即将返回新生成子类。但是这个这里就不具体讲了。

关于Demo的代码小伙伴可以在我的Github上找到。

广告

接下来是广告时间,本人开了一个订阅号,有兴趣的小伙伴关注一下:iOS周记。

可以查找关键字:Weekly_iOS进行关注,也可以扫一扫下方的二维码 订阅号

comments powered by Disqus