深入理解RACSignal

本文基于Reactive 3.1.0

目录

1. RACSignal的定义
2. RACSignal与RACStream的关系
3. RACSignal是如何发送信号的?
4. RACSignal的扩展

一、 RACSignal的定义

ReactiveCocoa 将原有的Delegate,Block Callback,Target-Action,Timers,KVO都统一抽象成了RACSignal的消息处理机制,在ReactiveCocoa中, 核心类就是RACSignal, 所有的消息都是通过信号的方式进行传递,可以简单的理解这些信号就是一连串的状态,在状态发生改变的时候,对应的订阅者就会收到通知,然后执行相应的操作。 因此RAC的整个框架也都是围绕着RACSignal的概念进行组织的。

二、RACSignal与RACStream的关系

RACSignal 继承自RACStream, 同样继承自RACStream的还有另一个类:RACSequence(RACSequence 是 pull-driven的数据流, RACSignal 是push-driven的数据流, RACSequence 不在本文讨论范围之内)

RACStream 本身作为一个抽象类并不提供任何实现,RACStream本身也提供了一些抽象方法,不可以直接调用,直接调用的话会抛出异常。

在看上述方法的实现之前,先要讲一下RACSignal类簇

某些抽象方法指定了到了对应的子类里面,而这些子类也只是简单的对值进行存储,然后在订阅者订阅的时候,将值发送出去,不同的是,RACEmptySignal发送的是空值,RACErrorSignal发送的是error,RACReturnSignal发送的value, 以RACErrorSignal为例:

+ (RACSignal *)error:(NSError *)error {
    RACErrorSignal *signal = [[self alloc] init];
    signal->_error = error;
    return signal
}
 

- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber {
    return [RACScheduler.subscriptionScheduler schedule:^{
        [subscriber sendError:self.error];
    }];
}

复制代码

然后讲上述剩余抽象方法的实现:

bind (RACSignal的核心方法)

- (RACSignal *)bind:(RACSignalBindBlock (^)(void))block {
	NSCParameterAssert(block != NULL);
	return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
		RACSignalBindBlock bindingBlock = block();

		__block volatile int32_t signalCount = 1;   // indicates self

		RACCompoundDisposable *compoundDisposable = [RACCompoundDisposable compoundDisposable];

		void (^completeSignal)(RACDisposable *) = ^(RACDisposable *finishedDisposable) {
			if (OSAtomicDecrement32Barrier(&signalCount) == 0) {
				[subscriber sendCompleted];
				[compoundDisposable dispose];
			} else {
				[compoundDisposable removeDisposable:finishedDisposable];
			}
		};

		void (^addSignal)(RACSignal *) = ^(RACSignal *signal) {
			OSAtomicIncrement32Barrier(&signalCount);

			RACSerialDisposable *selfDisposable = [[RACSerialDisposable alloc] init];
			[compoundDisposable addDisposable:selfDisposable];

			RACDisposable *disposable = [signal subscribeNext:^(id x) {
				[subscriber sendNext:x];
			} error:^(NSError *error) {
				[compoundDisposable dispose];
				[subscriber sendError:error];
			} completed:^{
				@autoreleasepool {
					completeSignal(selfDisposable);
				}
			}];

			selfDisposable.disposable = disposable;
		};

		@autoreleasepool {
			RACSerialDisposable *selfDisposable = [[RACSerialDisposable alloc] init];
			[compoundDisposable addDisposable:selfDisposable];

			RACDisposable *bindingDisposable = [self subscribeNext:^(id x) {
				// Manually check disposal to handle synchronous errors.
				if (compoundDisposable.disposed) return;

				BOOL stop = NO;
				id signal = bindingBlock(x, &stop);

				@autoreleasepool {
					if (signal != nil) addSignal(signal);
					if (signal == nil || stop) {
						[selfDisposable dispose];
						completeSignal(selfDisposable);
					}
				}
			} error:^(NSError *error) {
				[compoundDisposable dispose];
				[subscriber sendError:error];
			} completed:^{
				@autoreleasepool {
					completeSignal(selfDisposable);
				}
			}];

			selfDisposable.disposable = bindingDisposable;
		}

		return compoundDisposable;
	}] setNameWithFormat:@"[%@] -bind:", self.name];
}
复制代码

bind的实现相对于其他抽象方法的实现较为复杂:

bind方法内部主要做了这么几件事情:

  1. 首先根据传入的block生成一个RACSignalBindBlock(返回值是RACSignal对象)类型的bindingBlock

  2. 声明2个block,一个是completeSignal,一个是addSignal,completeSignal用于新的signal send completed,addSignal用于sendNext、sendError、调用completeSignal。这里值得注意的是会在一开始声明一个int32_t类型的signalCount,在addSignalBlock中对signalCount执行了一下OSAtomicIncrement32Barrier原子操作,对signalCount进行+1,同时在completeSignalBlock里面进行减一操作,目的是防止bindSignal进行completed操作,而不是originSignal的sendCompleted操作而导致的completed操作。

  3. 订阅originSignal,一旦绑定的block转变成signal且不为空,且compoundDisposable没有被dispose,执行2中所说的addSignal,立即将值发送给订阅者subscriber,如果转变的signal为空或者要终止绑定,原始的信号就complete,当所有的信号都complete,发送completed信号给订阅者subscriber,如果中途信号出现了任何error,都会把这个错误发送给subscriber

在省略掉部分判断以及RACDisposable的处理之后,看bind的实现,其实是对原有的Signal进行了2次封装: 当原来的Signal发送消息之后,RACSignalBindBlock拿到原有Signal发送的信息,然后进行block处理,返回封装之后的signal, 而且不难发现,每一次的消息发送都会被包成一个signal,新的signal再进行消息的发送。 绑定之后的signal不会影响原有signal的订阅操作,类似于category操作。

example:

- (void)testBind
{
    RACSignal *signal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
       
        [subscriber sendNext:@1];
        [subscriber sendNext:@2];
        [subscriber sendNext:@3];
        [subscriber sendNext:@4];
        [subscriber sendCompleted];
        return [RACDisposable disposableWithBlock:^{
            NSLog(@"_______dispose");
        }];
    }];
    
    RACSignal *bindSignal = [signal bind:^RACSignalBindBlock _Nonnull{
        return ^RACSignal *(NSNumber *value, BOOL *stop) {
            value = @(value.integerValue * value.integerValue);
            if (value.integerValue > 5) {
                *stop = YES;
            }
            return [RACSignal return:value];
        };
    }];
    
    [signal subscribeNext:^(NSNumber * _Nullable x) {
        NSLog(@"signal subsbribeNext : %ld", x.integerValue);
    } completed:^{
        NSLog(@"signal subsbribeCompleted");
    }];
    
    [bindSignal subscribeNext:^(NSNumber *_Nullable x) {
        NSLog(@"bindSignal subsbribeNext : %ld", x.integerValue);
    } completed:^{
        NSLog(@"bindSignal subsbribeCompleted");
    }];
}
复制代码

concat

- (RACSignal *)concat:(RACSignal *)signal {
    ......
    RACDisposable *sourceDisposable = [self subscribeNext:^(id x) {
            ......
        } error:^(NSError *error) {
            ......
        } completed:^{
            RACDisposable *concattedDisposable = [signal subscribe:subscriber];
            [compoundDisposable addDisposable:concattedDisposable];
        }];
        ......
    }]];
}
复制代码

调用concat之后的signal的didSubscribe, 会先订阅前一个signal,并正常的执行前一个signal的didSubscribe,当前一个signal sendCompleted的时候,就开始订阅后一个signal,然后开始执行后一个signal的didSubscribe, 在concat之前,前后的signal会首先将各自的didSubscribe copy起来,然后在concat之后,新的signal的didSubscribe 再把对应的block copy。

值得注意的是:后一个signal是在前一个signal sendCompleted之后订阅的,那么如果前一个信号没有sendCompleted,后一个信号是不会被订阅的,因此concat是一个有序的signal组合,concat得到的新的signal能收到两个signal发送的消息值。

example:

- (void)testConcat
{
    RACSignal *firstSignal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
        [subscriber sendNext:@"1"];
        [subscriber sendNext:@"1"];
        [subscriber sendNext:@"1"];
        [subscriber sendCompleted];

        return nil;
    }];
    
    RACSignal *twoSignal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
        [subscriber sendNext:@"2"];
        [subscriber sendNext:@"2"];
        [subscriber sendCompleted];
        return nil;
    }];
    
    RACSignal *concatSignal = [firstSignal concat:twoSignal];
    [concatSignal subscribeNext:^(NSString * _Nullable x) {
        NSLog(@"-----concat: %@", x);
    }];
}
复制代码

zipWith

简化一下zipWith的实现代码:

- (RACSignal *)zipWith:(RACSignal *)signal {
    BOOL selfCompleted、otherCompleted = NO,  NSMutableArray selfValues、OtherValues;
    void (^sendCompleted) = ^{selfEmpty = (selfCompleted && selfValues.count == 0) otherEmpty = (otherCompleted && otherValues.count == 0); if(selfEmpty || otherEmpty) sendCompleted}
    void (^sendNext) = ^{
            if (selfValues.count == 0) return; if (otherValues.count == 0) return;
            RACTuple *tuple = RACTuplePack(selfValues[0], otherValues[0],
            [selfValues removeObjectAtIndex:0];[otherValues removeObjectAtIndex:0];
            sendNext(tuple))};
    ......
    [self subscribeNext:^(id x){
         ......
         [selfValues add: x]; sendNext()
         ......
    } completed:^{selfCompleted = Yes, sendCompleted()}];
    [other 如上];
}
复制代码

调用zipWith之后的signal的didSubscribe,两个signal会各自发送值,当第一个signal发送消息的时候,selfValues数组将值保存下来,并调用sendNext回调,sendNext里面会判断两个数组是否为空,有一个为空,则会return,在completed回调里面会将selfCompleted标志位置为Yes,并调用sendCompleted回调,在回调里面,同样也会判断标记位,但同时也会判断数组是否为空,因此这个判断条件的满足只有两个signal都发送消息一一配对发送出去才会走进,因为只有在sendNext回调里面,将两个数组的第一个元素取出来并打包成元祖RACTuple发送出去,并清空数组里面的第一个元素。

因为两个signal每次发送消息的时候,对应的数组都会先将值保存下来,只有在另一个signal也发送消息时,才会打包成RACTuple发送出去,也就是说,如果其中一个signal发送了消息,但是另一个signal一直没有发送消息,那么第一个signal发送的消息永远不会被zipWith之后的signal发送,这个值就没有意义啦,需要一一配对。

example:

- (void)testZipWith
{
    RACSignal *firstSignal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
        [subscriber sendNext:@"1"];
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [subscriber sendNext:@"2"];
        });
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [subscriber sendNext:@"3"];
        });
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [subscriber sendNext:@"4"];
            [subscriber sendCompleted];
        });
	dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [subscriber sendNext:@"5"];
            [subscriber sendCompleted];
        });
        return nil;
    }];
    
    RACSignal *secondSignal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
        [subscriber sendNext:@"A"];
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [subscriber sendNext:@"B"];
        });
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [subscriber sendNext:@"C"];
            [subscriber sendCompleted];
        });
	dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [subscriber sendNext:@"D"];
            [subscriber sendCompleted];
        });
        return nil;
    }];
    
    RACSignal *zipSignal = [firstSignal zipWith:secondSignal];
    [zipSignal subscribeNext:^(id  _Nullable x) {
        NSLog(@"zipWith subscribeNext : %@", x);
    } completed:^{
        NSLog(@"zipWith subscribeCompleted");
    }];
}
复制代码

那么到现在RACStream的几个抽象方法在RACSignal的实现就讲完啦。

三、RACSignal是如何发送信号的?

一个正常的信号工作处理过程是这样的:

整个流程是在信号创建之后调用subscirbeNext之后返回一个RACDisposable对象,然后在整个订阅过程中,生成了一个RACSubscriber对象,当向这个对象sendNext或者sendCompleted的时候,它就会向所有的订阅者发送一个消息,其实不难看出,整个的订阅和发送过程都和RACSubscriber有关,那么就来深入探究一下整个的订阅与发送过程。

RACSubscriber:

RACSubscriber的初始化是由nextBlock、errorBlock、completedBlock三个参数初始化的,那么当调用方去使用如下方法时:

-subscribeNext、 -subscribeNext:error: -subscribeNext:completed: 等等方法时,其实也只是上面的三个block的组合,不需要的block传NULL,看一下最长初始化的实现:


+ (instancetype)subscriberWithNext:(void (^)(id x))next error:(void (^)(NSError *error))error completed:(void (^)(void))completed {
    RACSubscriber *subscriber = [[self alloc] init];
    subscriber->_next = [next copy];
    subscriber->_error = [error copy];
    subscriber->_completed = [completed copy];
    return subscriber;
}

复制代码

这个初始化只是将三个block保存下来,在拿到RACSubscribe实例之后,会调用subscribe:方法,这个方法在RACSignal是一个抽象方法,需要RACSignal的类簇去实现:

- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber {
    NSCAssert(NO, @"This method must be overridden by subclasses");
    return nil;
}
复制代码

RACErrorSignal、RACEmptySignal、RACReturnSignal的实现都是类似的,比较简单,在RACSubscriptionScheduler中schedule闭包中执行block,在常规工作流程中正常调用的RACDynamicSignal类的subscribe: 方法:

- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber {
    RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable];
    subscriber = [[RACPassthroughSubscriber alloc] initWithSubscriber:subscriber signal:self disposable:disposable];
    if (self.didSubscribe != NULL) {
        RACDisposable *schedulingDisposable = [RACScheduler.subscriptionScheduler schedule:^{
            RACDisposable *innerDisposable = self.didSubscribe(subscriber);
            [disposable addDisposable:innerDisposable];
        }];
        [disposable addDisposable:schedulingDisposable];
    }
    return disposable;
}

复制代码

看这段代码:

在这个方法里面首先创建一个RACCompoundDisposable实例。

RACDisposable

RACCompoundDisposable虽然是RACDisposable的子类,但是可以加入多个RACDisposable实例,在执行RACCompoundDisposable的dispose方法,可以将加入的多个RACDisposable实例dispose,当RACCompoundDisposable被dispose的话,也会将容器内的所有RACDisposable实例dispose掉。然后又通过RACSubscribe、RACSignal、RACDisposable实例参数创建了一个RACPassthroughSubscriber实例,他的作用就是将所有的信号从一个RACSubscribe实例传递给内部的RACSubscribe,但其实看到内部发现,内部的RACSubscribe也是外部传入的。传入的RACCompoundDisposable作用是当这个实例被dispose的时候,内部的subscribe将收不到任何消息。

if (RACSIGNAL_NEXT_ENABLED()) {
    RACSIGNAL_NEXT(cleanedSignalDescription(self.signal), cleanedDTraceString(self.innerSubscriber.description), cleanedDTraceString([value description]));
}
复制代码

值得注意的是:RACPassthroughSubscriber初始化的时候传入了一个RACSignal,为了避免循环引用,使用了unsafe_unretained来持有这个signal,那么为什么不用weak呢,因为在这里用到RACSignal只有用来作为DTTrace 动态跟踪的一个探针,没必要使用weak,来做额外的性能消耗。

由此信号的订阅就讲完啦, 信号是如何发送的呢?

- (void)sendNext:(id)value {
    @synchronized (self) {
        void (^nextBlock)(id) = [self.next copy];
        if (nextBlock == nil) return;
        nextBlock(value);
    }
}
复制代码

看信号的发送你会发现十分敷衍 精简,将初始化的nextBlock、或者errorBlock copy一份,进行调用,copy的过程也是线程安全的,至此RACSignal的订阅与发送过程也讲完啦。

四、RACSignal的扩展

下面讲一下RACSignal的一些高级用法的源码实现:

flattenMap & flatten

- (RACStream *)flattenMap:(__kindof RACStream * (^)(id value))block {
    ......
    return [[self bind:^{
    .....
    id stream = block(value) ?: [class empty];
    NSCAssert([stream isKindOfClass:RACStream.class], @"Value returned from -flattenMap: is not a stream: %@", stream);
    return stream;
        };
    }] .....];
}
- (RACStream *)flatten {
    return [[self flattenMap:^(id value) {
        return value;
    }] ......];
}
复制代码

flattenMap的实现是放在其父类RACStream中的,但其实内部是调用了bind方法,对原信号进行block转换之后变成新的信号,flatten其实是对高阶信号(信号的信号)的一次降阶,如果不是高阶信号,那么就会命中Assert。

那么写一个Example:

- (void)testFlattenMap
{
    RACSignal *signal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
        
        [subscriber sendNext:@1];
        [subscriber sendNext:@2];
        [subscriber sendNext:@3];
        [subscriber sendCompleted];
        return nil;
    }];
    
    RACSignal *flattenSignal = [signal flattenMap:^__kindof RACSignal * _Nullable(NSNumber * _Nullable value) {
        return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
            [subscriber sendNext:@(value.integerValue * value.integerValue)];
            [subscriber sendCompleted];
            return nil;
        }];
    }];
    
    RACSignal *mapSignal = [signal map:^id _Nullable(NSNumber * _Nullable value) {
        return @(value.integerValue * 2);
    }];
    
    [flattenSignal subscribeNext:^(NSNumber * _Nullable x) {
        NSLog(@"flattenMap Result : %ld", x.integerValue);
    }];
    
    [mapSignal subscribeNext:^(NSNumber * _Nullable x) {
        NSLog(@"map Result : %ld", x.integerValue);
    }];
}
复制代码

看一下输出结果:

map & filter & mapReduce


- (RACStream *)map:(id (^)(id value))block {
    Class class = self.class;
    return [[self flattenMap:^(id value) {
        return [class return:block(value)];
    }] ......];
}
- (RACStream *)filter:(BOOL (^)(id value))block {
    Class class = self.class;
    return [[self flattenMap:^ id (id value) {
        if (block(value)) {
            return [class return:value];
        } else {
            return class.empty;
        }
    }] ......];
}

- (__kindof RACStream *)mapReplace:(id)object {
	return [[self map:^(id _) {
		return object;
	}] ......];
}
复制代码

map的实现是进行了一次flattenMap,对新的信号的值进行block(value),比较简单. filter的实现和map的实现类似,但是会把block的返回值作为判断条件,不满足则会返回空的signal。 mapReplace的操作是不管原先的signal发送什么消息,都会统一替换成object。(感觉这个需求会比较少)。

merge

+ (RACSignal *)merge:(id<NSFastEnumeration>)signals {
    ......
    for (RACSignal *signal in signals) {
        [copiedSignals addObject:signal];
    }
    return [[[RACSignal
        createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
            for (RACSignal *signal in copiedSignals) {
                [subscriber sendNext:signal];
            }
            [subscriber sendCompleted];
            return nil;
        }]flatten]; ......
}
复制代码

merge的操作接受参数是一个signal的数组,内部会创建一个copiedSignals的数组,然后依次发送这个数组中的信号,由于新的信号也是一个高阶信号,需要flatten操作,将flatten操作之后的值发送出去。

副作用相关:

doNext、doError、doCompleted、initially、finally

- (RACSignal *)doNext:(void (^)(id x))block {
    ......
    return [self subscribeNext:^(id x) {
        block(x);
        [subscriber sendNext:x];
    }......
}
- (RACSignal *)finally:(void (^)(void))block {
    return [[[self
        doError:^(NSError *error) {
            block();
        }]
        doCompleted:^{
            block();
        }]
        ......];
}
复制代码

信号的副作用操作最好放在这里面,比如说一些业务埋点,这样让读代码的人一目了然,看doNext、doError、doCompleted的源码,是在sendNext、sendError、sendCompleted之前执行block闭包,initially是在信号发送之前,执行defer操作,然后再return self之前执行一段block操作,在里面进行副作用操作,finally在doError和doCompleted插入一段block,然后进行相应的副作用操作。

多线程相关:

deliverOn

- (RACSignal *)deliverOn:(RACScheduler *)scheduler {
    return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
            [scheduler schedule:^{
                [subscriber sendNext:x];
            }];
    ......
}

复制代码

当信号去sendNext、sendError等的时候,会先执行RACScheduler 的schedule方法,看一下schedule方法的实现:

- (RACDisposable *)schedule:(void (^)(void))block {
    if (RACScheduler.currentScheduler == nil) return [self.backgroundScheduler schedule:block];
    block();
    return nil;
}

复制代码

首先判断currentScheduler是不是nil,则在backgroundScheduler执行block,否则在currentScheduler执行block,那么如何判断的currentScheduler是否为nil呢?


+ (RACScheduler *)currentScheduler {
    RACScheduler *scheduler = NSThread.currentThread.threadDictionary[RACSchedulerCurrentSchedulerKey];
    if (scheduler != nil) return scheduler;
    if ([self.class isOnMainThread]) return RACScheduler.mainThreadScheduler;
    return nil;
}

复制代码

是看线程字典threadDictionary有没有RACSchedulerCurrentSchedulerKey对应的value,如果有的话,返回scheduler,没有则看是否在主线程,如果在主线程,那么返回主线程scheduler,否则返回nil。

subscribeOn

- (RACSignal *)subscribeOn:(RACScheduler *)scheduler {
    return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
        RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable];
        RACDisposable *schedulingDisposable = [scheduler schedule:^{
            RACDisposable *subscriptionDisposable = [self subscribe:subscriber];
            [disposable addDisposable:subscriptionDisposable];
        }];
        [disposable addDisposable:schedulingDisposable];
        return disposable;
    }] setNameWithFormat:@"[%@] -subscribeOn: %@", self.name, scheduler];
}

复制代码

可以看到subscribeOn与deliverOn明显的不同是 subscribeOn 能够保证didSubscribe block()是在哪个scheduler执行,而deliverOn是保证sendNext、sendError、sendCompleted在哪个scheduler执行。

RACSignal的高级操作非常之多,在这里就不会再一一列举。

总结

在这里只是对RACSignal的简单了解,在RAC的世界里如沧海一粟,希望能继续在状态驱动的世界里畅游。 这有一个神奇的网站:各个操作动画展示 可以看到各个操作的动画的管道流。

转载于:https://juejin.im/post/5cdbd1d56fb9a0324b27e19b

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值