源码阅读 - [Masonry]

源码结构分析

Masonry是iOS一个用于自动布局的第三方框架,可以很方便的给UIView添加布局约束,从而进行自动的布局;

底层的基础API

Masonrys是在NSLayoutContraint上的基础上进行封装的,接下来先看下NSLayoutContraint的用法

NSLayoutContraint使用示例

假设有superView和subView两个view,将subView添加到superView上,给这两个view添加约束,使这两个view在左上角重合,subView的宽高都为50;

	//创建superView
	UIView *superView = [[UIView alloc]initWithFrame:CGRectMake(30, 200, SCREEN_WIDTH - 60, 300)];
    superView.backgroundColor = [UIColor lightGrayColor];
    
    //创建subView
    UIView *subView = [[UIView alloc]init];
    subView.backgroundColor = [UIColor yellowColor];
    subView.frame = CGRectMake(0, 0, 100, 100);
    
    /**
     添加约束之前,需要将视图添加到父视图上;
     */
    [superView addSubview:subView];
    
    /**
     当使用autolayout布局时,需要将视图的translatesAutoresizingMaskIntoConstraints属性设置为NO,不设置的话约束白加
     */
    subView.translatesAutoresizingMaskIntoConstraints = NO;
    
    /**
        七个参数
        1、要约束的视图view1
        2、约束类型attr1,宽,高,中心,上,下,左右
        3、约束方式,等于,小于等于。。。
        4、约束参照视图view2
        5、参照视图的参照属性attr2,上下左右。。。
        6、乘数multiplier,倍数
        7、约束值constant。
     
     约束结果:view1.attr1 等于(约束方式) view2.attr2*multiplier +constant
     */
    
    NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:subView attribute:(NSLayoutAttributeTop) relatedBy:(NSLayoutRelationEqual) toItem:superView attribute:(NSLayoutAttributeBottom) multiplier:1 constant:30];
    NSLayoutConstraint *constraint1 = [NSLayoutConstraint constraintWithItem:subView attribute:(NSLayoutAttributeLeft) relatedBy:(NSLayoutRelationEqual) toItem:superView attribute:(NSLayoutAttributeLeft) multiplier:1 constant:30];
    NSLayoutConstraint *constraint2 = [NSLayoutConstraint constraintWithItem:subView attribute:(NSLayoutAttributeWidth) relatedBy:(NSLayoutRelationEqual) toItem:nil attribute:NSLayoutAttributeWidth multiplier:1 constant:50];
    NSLayoutConstraint *constraint3 = [NSLayoutConstraint constraintWithItem:subView attribute:(NSLayoutAttributeHeight) relatedBy:(NSLayoutRelationEqual) toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:50];
    
    /**
     约束添加到哪个视图
     1、自身约束。不需要依赖其他视图,如宽高数值,view2 = null,attr2 = NSLayoutAttributeNotAnAttribute。约束添加到view1上
     2、相对父视图约束。约束添加到父视图上。
     3、相对兄弟视图约束。约束添加到兄弟视图共同的父视图上 (别说面试造航母了, 这不就是是两个链表的第一个公共节点面试题应用场景)
        View+MASAdditions 类中的mas_closestCommonSuperview方法便是找父视图
     */
    
    [superView addConstraint:constraint];
    [superView addConstraint:constraint1];
    [subView addConstraint:constraint2];
    [subView addConstraint:constraint3];
    

NSLayoutConstraint对象的创建

NSLayoutConstraint的创建对应的方法为:

+ (instancetype)constraintWithItem:(id)view1 attribute:(NSLayoutAttribute)attr1 relatedBy:(NSLayoutRelation)relation toItem:(nullable id)view2 attribute:(NSLayoutAttribute)attr2 multiplier:(CGFloat)multiplier constant:(CGFloat)c API_AVAILABLE(macos(10.7), ios(6.0), tvos(9.0));

从这个方法中可以看出,创建约束需要七个参数:

  • 指定要约束的视图view1

  • 约束属性attr1,宽、高、左、右、上、下…

  • 约束关系relation,等于、小于等于…

  • 约束参照视图view2.如果设置具体宽高数据,可以传空

  • 参照视图的参照属性attr2, view2为空时传NSLayoutAttributeNotAnAttribute,传其他的也一样

  • 乘数multiplier,倍数

  • 约束常量值constant。

最终得到的约束结果:view1.attr1 等于(约束方式) view2.attr2multiplier +constant。*

Masonry添加约束流程解析

使用实例

假设有一个progessSlider,他的父视图为processControl上,兄弟视图有一个playeButton,都已经添加到父视图上,现在要求的centerY和left与processControl一致,right相对于button的left向左偏移10,高度为5;
使用masonry给slider添加布局约束的方式如下所示:

 [progessSlider mas_makeConstraints:^(MASConstraintMaker *make) {
        make.centerY.left.equalTo(processControl);
        make.right.equalTo(playeButton.mas_left).offset(-10);
        make.height.mas_equalTo(5);
 }];

打断点进行调试,看整个添加约束的流程:

  1. 先调用UIView的分类MASAdditions中的mas_makeConstraints方法,在该方法中调用constraintMaker的初始化方法,传给该方法要设置约束的view(progessSlider),生产maker对象,传给block回调出去,然后再调用maker的install方法;
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}

  1. 在constraintMaker的初始化方法的初始化方法initWithView中,会记录下要设置约束的当前view,同时创建一个数组用来存储即将使用maker添加的约束:
- (id)initWithView:(MAS_VIEW *)view {
    self = [super init];
    if (!self) return nil;
    
    self.view = view;
    self.constraints = NSMutableArray.new;
    
    return self;
}
  1. 在传给mas_makeConstraints方法的block参数中,使用回调出来的maker进行一一添加约束,添加约束属性是left和centerY,他们保持和processControl相等;
make.centerY.left.equalTo(processControl);

这里拆开来看,
第一步,先调用maker的centerY方法,而在maker中,centerY方法的调用链是centerY:–>addConstraintWithLayoutAttribute:–>constraint:nil addConstraintWithLayoutAttribute:
可以看出,最终在addConstraintWithLayoutAttribute方法中创建了一个MASViewContraint类型的约束对象newConstraint,将这个对象的代理设置为maker,添加到maker存储约束的数组中,最后返回这个对象;

- (MASConstraint *)centerY {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeCenterY];
}

- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];
}

- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
    MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
    if ([constraint isKindOfClass:MASViewConstraint.class]) {
        //replace with composite constraint
        NSArray *children = @[constraint, newConstraint];
        MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
        compositeConstraint.delegate = self;
        [self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
        return compositeConstraint;
    }
    if (!constraint) {
        newConstraint.delegate = self;
        [self.constraints addObject:newConstraint];
    }
    return newConstraint;
}

第二步,调用返回的newConstraint对象的left方法,在MASViewConstraint中,left方法的调用链是
left:–>addConstraintWithLayoutAttribute:–>delegate的constraint:self addConstraintWithLayoutAttribute:layoutAttribute

- (MASConstraint *)left {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}

- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    NSAssert(!self.hasLayoutRelation, @"Attributes should be chained before defining the constraint relation");

    return [self.delegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
}

因为newContraint(针对maker.view的centerY)的代理是maker,因此会调用maker的constraint:self addConstraintWithLayoutAttribute:layoutAttribute方法,在这个方法中会根据left布局属性和maker.view创建一个新的MASViewConstraint对象,然后将maker.view原来centerY约束对象和现在的left约束对象创建一个组合约束对象compositeConstraint,将compositeConstraint替换掉maker的约束数组中的left约束对象,然后将compositeConstraint的代理设置为maker,返回compositeConstraint。

第三步,调用返回compositeConstraint的equalTo方法,equalTo方法的调用链为equalTo:–>MASCompositeContraint的equalToWithRelation:–>子约束MASViewConstraint的equalToWithRelation:,
在compositeConstraint的equalToWithRelation方法中,会为newConstraint对象中每个子约束对象设置layoutRelation和secondViewAttribute。

调用equalTo返回的是(MASConstraint * (^)(id))类型block,再调用这个block,传给attribute对象,返回的是compositeConstraint;

- (MASConstraint * (^)(id))equalTo {
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    };
}


- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
    return ^id(id attr, NSLayoutRelation relation) {
        for (MASConstraint *constraint in self.childConstraints.copy) {
            constraint.equalToWithRelation(attr, relation);
        }
        return self;
    };
}

- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
    return ^id(id attribute, NSLayoutRelation relation) {
        if ([attribute isKindOfClass:NSArray.class]) {
            NSAssert(!self.hasLayoutRelation, @"Redefinition of constraint relation");
            NSMutableArray *children = NSMutableArray.new;
            for (id attr in attribute) {
                MASViewConstraint *viewConstraint = [self copy];
                viewConstraint.layoutRelation = relation;
                viewConstraint.secondViewAttribute = attr;
                [children addObject:viewConstraint];
            }
            MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
            compositeConstraint.delegate = self.delegate;
            [self.delegate constraint:self shouldBeReplacedWithConstraint:compositeConstraint];
            return compositeConstraint;
        } else {
            NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
            self.layoutRelation = relation;
            self.secondViewAttribute = attribute;
            return self;
        }
    };
}

其他两个条设置约束的过程跟上述相似,不再赘述。

 make.left.equalTo(playeButton.mas_right).offset(-10);
 make.height.mas_equalTo(5);

maker添加约束完毕,就调用maker的install方法,install方法的调用链为maker的install–>maker的约束数组中存储的每个约束的install

------------------MASConstraintMaker.m
- (NSArray *)install {
    if (self.removeExisting) {
        NSArray *installedConstraints = [MASViewConstraint installedConstraintsForView:self.view];
        for (MASConstraint *constraint in installedConstraints) {
            [constraint uninstall]
        }
    }
    NSArray *constraints = self.constraints.copy;
    for (MASConstraint *constraint in constraints) {
        constraint.updateExisting = self.updateExisting;
        [constraint install];
    }
    [self.constraints removeAllObjects];
    return constraints;
}
------------------MASViewConstraint.m
- (void)install {
    if (self.hasBeenInstalled) {
        return;
    }
    
    if ([self supportsActiveProperty] && self.layoutConstraint) {
        self.layoutConstraint.active = YES;
        [self.firstViewAttribute.view.mas_installedConstraints addObject:self];
        return;
    }
    
    MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item;
    NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
    MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item;
    NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;

    // alignment attributes must have a secondViewAttribute
    // therefore we assume that is refering to superview
    // eg make.left.equalTo(@10)
    if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
        secondLayoutItem = self.firstViewAttribute.view.superview;
        secondLayoutAttribute = firstLayoutAttribute;
    }
    
    MASLayoutConstraint *layoutConstraint
        = [MASLayoutConstraint constraintWithItem:firstLayoutItem
                                        attribute:firstLayoutAttribute
                                        relatedBy:self.layoutRelation
                                           toItem:secondLayoutItem
                                        attribute:secondLayoutAttribute
                                       multiplier:self.layoutMultiplier
                                         constant:self.layoutConstant];
    
    layoutConstraint.priority = self.layoutPriority;
    layoutConstraint.mas_key = self.mas_key;
    
    if (self.secondViewAttribute.view) {
        MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view];
        NSAssert(closestCommonSuperview,
                 @"couldn't find a common superview for %@ and %@",
                 self.firstViewAttribute.view, self.secondViewAttribute.view);
        self.installedView = closestCommonSuperview;
    } else if (self.firstViewAttribute.isSizeAttribute) {
        self.installedView = self.firstViewAttribute.view;
    } else {
        self.installedView = self.firstViewAttribute.view.superview;
    }


    MASLayoutConstraint *existingConstraint = nil;
    if (self.updateExisting) {
        existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint];
    }
    if (existingConstraint) {
        // just update the constant
        existingConstraint.constant = layoutConstraint.constant;
        self.layoutConstraint = existingConstraint;
    } else {
        [self.installedView addConstraint:layoutConstraint];
        self.layoutConstraint = layoutConstraint;
        [firstLayoutItem.mas_installedConstraints addObject:self];
    }
}

整体设计思想

约束的抽象类MASContraint

首先,Masonry将约束封装为一个MASContraint类,这个类是一个抽象类,里面定义了一系列的抽象方法,这些抽象方法大概可以分为以下几类:
(1)设置布局常量的setter方法:

- (void)setInsets:(MASEdgeInsets __unused)insets { MASMethodNotImplemented(); }

- (void)setSizeOffset:(CGSize __unused)sizeOffset { MASMethodNotImplemented(); }

- (void)setCenterOffset:(CGPoint __unused)centerOffset { MASMethodNotImplemented(); }

- (void)setOffset:(CGFloat __unused)offset { MASMethodNotImplemented(); }

(2)设置布局倍数的setter方法:

- (MASConstraint * (^)(CGFloat multiplier))multipliedBy { MASMethodNotImplemented(); }

- (MASConstraint * (^)(CGFloat divider))dividedBy { MASMethodNotImplemented(); }

(3)设置布局优先级的setter方法:

- (MASConstraint * (^)(MASLayoutPriority priority))priority { MASMethodNotImplemented(); }

(4)设置布局关系的setter方法:

- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation { MASMethodNotImplemented(); }

在MASConstraint的拓展类中,有一个updateExisting的bool值属性和实现了MASConstraintDelegate协议的delegate

@interface MASConstraint ()

/**
 *  Whether or not to check for an existing constraint instead of adding constraint
 */
@property (nonatomic, assign) BOOL updateExisting;

/**
 *	Usually MASConstraintMaker but could be a parent MASConstraint
 */
@property (nonatomic, weak) id<MASConstraintDelegate> delegate;

/**
 *  Based on a provided value type, is equal to calling:
 *  NSNumber - setOffset:
 *  NSValue with CGPoint - setPointOffset:
 *  NSValue with CGSize - setSizeOffset:
 *  NSValue with MASEdgeInsets - setInsets:
 */
- (void)setLayoutConstantWithValue:(NSValue *)value;

@end

而在在MASConstraint的分类abstract中,定义了一个很重要的抽象方法addConstraintWithLayoutAttribute:

- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute __unused)layoutAttribute {
    MASMethodNotImplemented();
}

另外,在MASConstraint类中,还定义了一些属性链方法,这些方法最终都会调到abstract分类中的抽象方法addConstraintWithLayoutAttribute;

- (MASConstraint *)left {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}

具体行为的子类MASCompositeConstraint和MASViewConstraint

接着Masonry设计了MASContraint的两个子类,MASCompositeConstraint和 MASViewConstraint,用来表示具体的约束, MASViewConstraint表示一个view属性和另一个views属性的单个约束,MASCompositeConstraint表示一个view属性与另一个view属性的组合约束(也就是单个约束的组合)。

在两个子类中,实现了MASConstraint定义的所有抽象方法。

前面我们知道,在调用MASContraint的属性链方法left、right、top等来添加约束时,会调用抽象方法addConstraintWithLayoutAttribute:,
由于抽象方法在子类中被覆盖,所以
如果是MASCompositeConstraint类型的子类调用属性链方法,则会调到子类MASCompositeConstraint的addConstraintWithLayoutAttribute方法;
如果是MASViewConstraint类型的子类调用属性链方法,则会调到子类MASViewConstraint的addConstraintWithLayoutAttribute方法;

同理,在调用MASContraint的其他抽象方法,最终会调到子类的对应抽象方法。

约束的代理MASConstraintMaker

在MASCompositeConstraint和 MASViewConstraint的addConstraintWithLayoutAttribute:中,对象本身不做任何的操作,而是抛给它的代理MASConstraintMaker对象执行代理方法constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute添加约束;

MASConstraintMaker作为MASCompositeConstraint和MASViewConstraint的代理,主要负责为某个view记录他的所有属性约束,存储在constraint数组中,等view的所有属性约束添加完成后,才将这些约束一一添加到到应该添加的view上

 [progessSlider mas_makeConstraints:^(MASConstraintMaker *make) {
 		//约束1,processSlider的centerY、left属性与processControl的centerY、left属性约束
        make.centerY.left.equalTo(processControl);
        //约束2,processSlider的rightt属性与playeButton的centerY、left属性约束
        make.right.equalTo(playeButton.mas_left).offset(-10);
        //约束3,processSlider的height属性约束;
        make.height.mas_equalTo(5);
 }];

类与类之间的关系

在这里插入图片描述

一些细节

自动装箱语法

因为MASConstraint的equalTo方法只能传对象类型,传其他类型会出现问题,因此这里使用了自动装箱,只要定义了MAS_SHORTHAND_GLOBALS 这个宏,传递给equalTo方法的非对象类型参数会自动封装成NSValue或NSNumber类型。

/**
 *  Convenience auto-boxing macros for MASConstraint methods.
 *
 *  Defining MAS_SHORTHAND_GLOBALS will turn on auto-boxing for default syntax.
 *  A potential drawback of this is that the unprefixed macros will appear in global scope.
 */
#define mas_equalTo(...)                 equalTo(MASBoxValue((__VA_ARGS__)))
#define mas_greaterThanOrEqualTo(...)    greaterThanOrEqualTo(MASBoxValue((__VA_ARGS__)))
#define mas_lessThanOrEqualTo(...)       lessThanOrEqualTo(MASBoxValue((__VA_ARGS__)))

#define mas_offset(...)                  valueOffset(MASBoxValue((__VA_ARGS__)))


#ifdef MAS_SHORTHAND_GLOBALS

#define equalTo(...)                     mas_equalTo(__VA_ARGS__)
#define greaterThanOrEqualTo(...)        mas_greaterThanOrEqualTo(__VA_ARGS__)
#define lessThanOrEqualTo(...)           mas_lessThanOrEqualTo(__VA_ARGS__)

#define offset(...)                      mas_offset(__VA_ARGS__)

#endif
/**
 *  Given a scalar or struct value, wraps it in NSValue
 *  Based on EXPObjectify: https://github.com/specta/expecta
 */
static inline id _MASBoxValue(const char *type, ...) {
    va_list v;
    va_start(v, type);
    id obj = nil;
    if (strcmp(type, @encode(id)) == 0) {
        id actual = va_arg(v, id);
        obj = actual;
    } else if (strcmp(type, @encode(CGPoint)) == 0) {
        CGPoint actual = (CGPoint)va_arg(v, CGPoint);
        obj = [NSValue value:&actual withObjCType:type];
    } else if (strcmp(type, @encode(CGSize)) == 0) {
        CGSize actual = (CGSize)va_arg(v, CGSize);
        obj = [NSValue value:&actual withObjCType:type];
    } else if (strcmp(type, @encode(MASEdgeInsets)) == 0) {
        MASEdgeInsets actual = (MASEdgeInsets)va_arg(v, MASEdgeInsets);
        obj = [NSValue value:&actual withObjCType:type];
    } else if (strcmp(type, @encode(double)) == 0) {
        double actual = (double)va_arg(v, double);
        obj = [NSNumber numberWithDouble:actual];
    } else if (strcmp(type, @encode(float)) == 0) {
        float actual = (float)va_arg(v, double);
        obj = [NSNumber numberWithFloat:actual];
    } else if (strcmp(type, @encode(int)) == 0) {
        int actual = (int)va_arg(v, int);
        obj = [NSNumber numberWithInt:actual];
    } else if (strcmp(type, @encode(long)) == 0) {
        long actual = (long)va_arg(v, long);
        obj = [NSNumber numberWithLong:actual];
    } else if (strcmp(type, @encode(long long)) == 0) {
        long long actual = (long long)va_arg(v, long long);
        obj = [NSNumber numberWithLongLong:actual];
    } else if (strcmp(type, @encode(short)) == 0) {
        short actual = (short)va_arg(v, int);
        obj = [NSNumber numberWithShort:actual];
    } else if (strcmp(type, @encode(char)) == 0) {
        char actual = (char)va_arg(v, int);
        obj = [NSNumber numberWithChar:actual];
    } else if (strcmp(type, @encode(bool)) == 0) {
        bool actual = (bool)va_arg(v, int);
        obj = [NSNumber numberWithBool:actual];
    } else if (strcmp(type, @encode(unsigned char)) == 0) {
        unsigned char actual = (unsigned char)va_arg(v, unsigned int);
        obj = [NSNumber numberWithUnsignedChar:actual];
    } else if (strcmp(type, @encode(unsigned int)) == 0) {
        unsigned int actual = (unsigned int)va_arg(v, unsigned int);
        obj = [NSNumber numberWithUnsignedInt:actual];
    } else if (strcmp(type, @encode(unsigned long)) == 0) {
        unsigned long actual = (unsigned long)va_arg(v, unsigned long);
        obj = [NSNumber numberWithUnsignedLong:actual];
    } else if (strcmp(type, @encode(unsigned long long)) == 0) {
        unsigned long long actual = (unsigned long long)va_arg(v, unsigned long long);
        obj = [NSNumber numberWithUnsignedLongLong:actual];
    } else if (strcmp(type, @encode(unsigned short)) == 0) {
        unsigned short actual = (unsigned short)va_arg(v, unsigned int);
        obj = [NSNumber numberWithUnsignedShort:actual];
    }
    va_end(v);
    return obj;
}

#define MASBoxValue(value) _MASBoxValue(@encode(__typeof__((value))), (value))

反思借鉴之处

适用于给对象添加一系列属性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Albert_YuHan

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值