链式编程与响应式编程

链式编程

在实际开发过程中,基本上链式开发思想很常见。第三方框架MasonryReactiveCocoa都大量用了这种思想。

链式编程其实就是将多个操作(多行代码)通过点号.链接在一起成为一句代码

block

block在链式开发中扮演这很重要的角色。通常我们调用一个block

Person * p = [[Person alloc] init];
[p result:^{
        
}]

然而我现在想将他改进一下用.语法将其调用出来,写法如下

-(void(^)(int))result{
    return ^(int num){
        NSLog(@"num:%d",num);
    };
}

此时调用的时候

Person * p = [[Person alloc] init];
p.result(3);

打印:

2017-09-23 12:15:08.650 链式编程[26528:1560398] num:3

接下来我们做一些细微的修改,就变成了链式编程

-(Person *(^)(int))result{
    static int sum = 0;
    return ^(int num){
        sum += num;
        NSLog(@"sum:%d",sum);
        return self;
    };
}

此时调用

 p.result(3).result(4).result(5);

打印:

2017-09-23 12:20:09.210 链式编程[26883:1581658] sum:3
2017-09-23 12:20:09.210 链式编程[26883:1581658] sum:7
2017-09-23 12:20:09.210 链式编程[26883:1581658] sum:12

细心的你应该早已发现Masorny里面的

make.left.top.equalTo(@10);

和RAC中的

 [[signal filter:^BOOL(NSNumber *  _Nullable value) {
        return value > 2;
    }]subscribeNext:^(id  _Nullable x) {
        
    }];

这些都是链式思想

响应式编程

响应式编程(反应式编程)是一种面向数据流和变化传播的编程范式。
相信看完解释后的同学应该第一时间就想到了KVO。

KVO

首先创建一个Person对象,添加name属性,然后给name添加KVO

[p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];

然后实现下面的函数

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context

p.name属性发生变化时候,程序就回到了这个代理方法,监听到了name属性变化。
不知道此时大家有没有疑惑,KVO到底能不能监听到成员变量呢。带着疑惑我做一个实验,将name属性改成成员变量,代码如下:

//@property (nonatomic,strong) NSString * name;


NSString * _name;

这个时候,我们发现当改变p->name的时候,KVO根本不回调。相信大家都知道,属性是成员变量+setter方法getter方法组成。这个时候得出以下结论:KVO监听的是属性的seter方法

修改isa

接下来,在添加KVO的属性时候加一个两个NSLog

NSLog(@"%@",object_getClass(p));
[p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:0];
NSLog(@"%@",object_getClass(p));

打印

2017-09-25 09:27:40.098 链式编程[79382:12699417] Person
2017-09-25 09:27:40.098 链式编程[79382:12699417] NSKVONotifying_Person

从打印结果,我惊奇发现p变成了NSKVONotifying_Person。原来苹果在添加KVO的时候,动态创建了Person一个子类,实现该属性的setter方法,修改原来Personisa指针指向NSKVONotifying_Person。当属性被修改,isa指向的中间类NSKVONotifying_Personsetter方法会被调用,接下来就是进行下面一系列的操作了。

手动实现KVO

既然明白了KVO的底层实现原理,那我们也尝试实现一个自己的KVO。分析下来,我们需要做的事情有:

  1. 在添加KVO的时候动态创建一个子类
  2. 在子类里面实现属性的setter属性
  3. isa指针指向子类
  4. 在子类setter调用父类方法修改属性
  5. 通知observe收到属性更新的动作

有经验的童鞋应该已经发现,修改isa指针苹果内部可以做,但是没有提供方法给开发者用。查阅资料发现可以用object_setClass进行代替。

接下来创建一个类别:NSObject+KVO,然后实现以下代码

- (void)lq_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
    
    //动态生成一个类
    //获取类名
    NSString * className = NSStringFromClass(self.class);
    NSString * newClassName = [NSString stringWithFormat:@"LQKVO_%@",className];
    const char * name = [newClassName UTF8String];
    
    //动态创建一个类
    Class myClass = objc_allocateClassPair([self class], name, 0);
    //注册类
    objc_registerClassPair(myClass);
    //修改isa
    object_setClass(self, myClass);
    
    //添加方法
    class_addMethod(myClass, @selector(setName:), (IMP)setName, "");
    
void setName(NSString * newName){
    NSLog(@"我收到消息了%@",newName);
}

在VC中实现

[p lq_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:0];

Command + R运行,控制台打印

2017-09-24 12:12:58.730 链式编程[69869:4654336] 我收到消息了<LQKVO_Person: 0x604000029500>

看到这个打印结果,我懵逼了,尼玛我传的不是name吗,为什么会打印我动态创建的这个类呢?难道是系统出问题了,接连运行几次结果还是一样。

去苹果的文档中心查找class_addMethod方法,从他的示例代码中我发现了下面的代码

void myMethodIMP(id self, SEL _cmd)
{
    // implementation ....
}

这时候我瞬间明白了,原来这个方法存在两个隐藏参数,于是将代码改成

void setName(id self, SEL _cmd, NSString * newName)
    NSLog(@"我收到消息了%@",newName);
}

Command + R运行,控制台打印

2017-09-24 12:24:02.532 链式编程[70499:4698280] 我收到消息了lq

完美,终于搞定了,继续完成下面的内容。

void setName(id self, SEL _cmd, NSString * newName){
    NSLog(@"我收到消息了%@",newName);
    //保存class
    id class = [self class];
    //改变类型[super setName:];
    object_setClass(self, class_getSuperclass(class));
    objc_msgSend();
}

当我拿到父类准备赋值的时候,准备调用父类的setName方法。
但是这里objc_msgSend却怎么都敲不出参数,Command点击进去看到以下内容。

#if !OBJC_OLD_DISPATCH_PROTOTYPES
OBJC_EXPORT void
objc_msgSend(void /* id self, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
#else

由于自己经验水平一般,真的没看懂那个宏具体是要干嘛,但是我肯定是苹果在某种特定的情况下将它禁止了。去网上查阅资料得知,苹果不推荐使用objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)该方法,可是我们一定要拿来玩玩,苹果也是不会禁止的。build Setting搜索objc_msgSend改成NO即可

image.png

经过自己一步一步的踩坑,终于完成以下代码

- (void)lq_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
    
    //动态生成一个类
    //获取类名
    NSString * className = NSStringFromClass(self.class);
    NSString * newClassName = [NSString stringWithFormat:@"LQKVO_%@",className];
    const char * name = [newClassName UTF8String];
    
    //动态创建一个类
    Class myClass = objc_allocateClassPair([self class], name, 0);
    //注册类
    objc_registerClassPair(myClass);
    //修改isa
    object_setClass(self, myClass);
    
    //添加方法
    class_addMethod(myClass, @selector(setName:), (IMP)setName, "v@:@");
    
    //将观察者保存到当前对象中
    objc_setAssociatedObject(self, @"key", observer, OBJC_ASSOCIATION_ASSIGN);    
}

void setName(id self, SEL _cmd, NSString * newName){
    NSLog(@"我收到消息了%@",newName);
    //保存class
    id class = [self class];
    //改变类型[super setName:];
    object_setClass(self, class_getSuperclass(class));
    objc_msgSend(self, @selector(setName:),newName);
    //回到子类
    object_setClass(self, class);
    //拿到当前属性
    id observer = objc_getAssociatedObject(self, @"key");
    objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:),@"name",self,@{@"name":newName},nil);
}

至此,自定义KVO已经完成了,赶快给自己点个赞吧!😁😁😁

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,635评论 0 9
  • 光影变化,夜晚才不会孤单, 记录生活中的每一种美, 变成一个美好的人, 做美好的事情。 拍摄地点:撒拉小馆,公园,...
    爱罗莎阅读 276评论 2 1
  • 1
    刘有才阅读 130评论 0 0
  • 你要如何原谅我的过错 像我希望的一样? 如何明白我的措辞 是我斟酌半生的思量 如何才能逃脱这牢笼 挣脱这枷锁 却一...
    Z___Y_阅读 216评论 0 0
  • 专门请了一天假,自己坐在万圣书园的醒客咖啡,来回顾2016和制定2017的计划。仪式感还是必要的。 希望2017年...
    一个本子阅读 97评论 0 0