Masonry 源码解读(一)

2019-02-18  本文已影响4人  羽裳有涯

前言

iOS 开发中的布局方式,总体而言经过了三个时代。混沌初开之时,世间只有3.5英寸(iPhone 4、iPhone 4S),那个时候屏幕适配对于大多数 iOS 开发者来说并不是什么难题,用 frame 就能精确高效的定位。这之后,苹果发布了4英寸机型(iPhone 5、iPhone 5C、iPhone 5S),与此同时苹果也推出了 AutoresizingMask,用来协调子视图与父视图之间的关系。再之后,各种各样的 iPhone 和 iPad 纷纷面世,不仅仅是屏幕尺寸方面的差异,更有异形屏(iPhone X)。在此期间,苹果提出了 AutoLayout 技术,供开发者进行屏幕适配。

使用 AutoLayout 的方法也有两种——通过 Interface Builder 或者纯代码。前者一直是苹果官方文档里所鼓励的,�原因是苹果从最初到现在,对于 iOS 应用的想法都是小而美的,在他们的认知里,一个 APP 应该提供尽可能小的功能集,这也是为为何苹果迄今为止官方推荐的架构仍然是 MVC,官方推荐的开发方式仍是以 StoryBoard(Size Classes)。但是在一些项目较大的公司,StoryBoard 的某些特性(导致应用包过大,减缓启动速度,合并代码困难)又是不能为人所容忍的,便有了纯代码来实现 View 层的一群开发者(比如我)。

如果你曾经用代码来实现 AutoLayout,你会发现苹果提供的 API 的繁琐程度令人发指,这也是 Masonry这类框架被发明的原因。Masonry 是一个轻量级的布局框架,它使用更好的语法来封装 AutoLayout。Masonry 有自己的布局 DSL,它提供了一种链式的方式来描述你的 NSLayoutConstraints,从而得到更简洁和可读的布局代码。

接下来,我们就从 Masonry 中 README 提供的代码着手,看一看 Masonry 是如何帮助我们简化繁琐的 AutoLayout 代码的。

mas_makeConstraints:

举一个简单的例子,你想要一个视图填充它的父视图,但是在每一边间隔10个点。

UIView *superview = self.view;

UIView *view1 = [[UIView alloc] init];
view1.backgroundColor = [UIColor greenColor];
[superview addSubview:view1];

UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);

[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(superview.mas_top).with.offset(padding.top); //with is an optional semantic filler
    make.left.equalTo(superview.mas_left).with.offset(padding.left);
    make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom);
    make.right.equalTo(superview.mas_right).with.offset(-padding.right);
}];

我们想要实现约束的效果,是通过 mas_makeConstraints:这个方法来实现的,这个方法可以在任意 UIView 类及其子类上调用,说明其是一个分类方法,这也是这个方法加了 mas_ 前缀的原因。该方法声明在 UIView+MASAdditions.h 文件中,先来看一下这个方法的完整声明:

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block;

接下来是方法的实现:

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}

self.translatesAutoresizingMaskIntoConstraints = NO;

默认情况下,view上的autoresizing mask 会产生约束条件,以完全确定视图的位置。这允许AutoLayout 系统跟踪其布局被手动控制的viewframe(例如通过 setFrame:)。当你选择通过添加自己的约束来使用 AutoLayout 来定位视图时,必须 self.translatesAutoresizingMaskIntoConstraints = NO;。IB 会自动为你做这件事。

constraintMaker

在这之后,创造了一个 MASConstraintMaker 类型的对象 constraintMaker,MASConstraintMaker 的初始化方法为:

- (id)initWithView:(MAS_VIEW *)view;

可以看到,传入的 view 参数是由一个叫做 MAS_VIEW 的宏来作为参数声明的,MAS_VIEW 的定义是:

#if TARGET_OS_IPHONE || TARGET_OS_TV

    #import <UIKit/UIKit.h>
    #define MAS_VIEW UIView
    ...

#elif TARGET_OS_MAC

    #import <AppKit/AppKit.h>
    #define MAS_VIEW NSView
    ...

#endif

因为 Masonry 是一个跨平台的框架,于是通过预编译宏来让在不同的平台上,MAS_VIEW 代表的意义不同。接下来看初始化方法的实现:

- (id)initWithView:(MAS_VIEW *)view {
    self = [super init];
    if (!self) return nil;
    
    self.view = view;
    self.constraints = NSMutableArray.new;
    
    return self;
}

在初始化方法中,将传入的 view 通过一个弱指针(@property (nonatomic, weak) MAS_VIEW *view;)保留在了 constraintMaker 中,同时初始化了一个名为 �constraintsNSMutableArray,用来保存约束。

block(constraintMaker);

接着,constraintMaker 通过 block(constraintMaker);传递给了我们,而我们对它做了什么呢?

make.top.equalTo(superview.mas_top).with.offset(padding.top); 
make.left.equalTo(superview.mas_left).with.offset(padding.left);
make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom);
make.right.equalTo(superview.mas_right).with.offset(-padding.right);

我们将对视图约束的描述,以一种迥异于创建 NSLayoutConstraints对象的方式描述了我们对视图的约束,我们以其中的一句作为例子来看看 Masonry 是如何实现链式 DSL 的。

make.top.equalTo(superview.mas_top).with.offset(padding.top);

make.top

makeMASConstraintMaker 类型的对象,这个类型封装了一系列只读 MASConstraint 属性,top 就是其中之一,声明和实现如下:

@property (nonatomic, strong, readonly) MASConstraint *top;
- (MASConstraint *)top {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
}

addConstraintWithLayoutAttribute: 方法的实现为:

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

间接调用了 constraint:addConstraintWithLayoutAttribute: 方法:

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

让我们一句一句来看:

MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];

首先,会初始化一个 MASViewAttribute 类型的对象(viewAttribute),该类型的初始化方法是:

- (id)initWithView:(MAS_VIEW *)view layoutAttribute:(NSLayoutAttribute)layoutAttribute;

实现为:

- (id)initWithView:(MAS_VIEW *)view layoutAttribute:(NSLayoutAttribute)layoutAttribute {
    self = [self initWithView:view item:view layoutAttribute:layoutAttribute];
    return self;
}

- (id)initWithView:(MAS_VIEW *)view item:(id)item layoutAttribute:(NSLayoutAttribute)layoutAttribute {
    self = [super init];
    if (!self) return nil;
    
    _view = view;
    _item = item;
    _layoutAttribute = layoutAttribute;
    
    return self;
}

MASViewAttribute 是一个模型类,用于存储视图和视图对应的 NSLayoutAttribute

接下来会初始化一个 MASViewConstraint类型的对象(newConstraint):

MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];

MASViewConstraint的初始化方法是:

- (id)initWithFirstViewAttribute:(MASViewAttribute *)firstViewAttribute {
    self = [super init];
    if (!self) return nil;
    
    _firstViewAttribute = firstViewAttribute;
    self.layoutPriority = MASLayoutPriorityRequired;
    self.layoutMultiplier = 1;
    
    return self;
}

MASViewConstraint也是一个模型类,会通过刚才初始化的 viewAttribute作为初始化参数,并存储在_firstViewAttribute 的实例变量中。

接下来,由于constraint 参数为 nil, 所以直接走到这里:

if (!constraint) {
    newConstraint.delegate = self;
    [self.constraints addObject:newConstraint];
}

newConstraint 对象的代理设为 self (make),同时将其放置到 �constraints 数组中。

简而言之就是,�make 中的 top 方法会初始化一个 MASViewAttribute 类型的对象 viewAttribute,并通过该对象�初始化一个 MASViewConstraint 类型的对象 newConstraint,让 make 作为 newConstraint 对象的 delegate,并存储在 �makeconstraints 属性中。接下来,return newConstraint;

return newConstraint; 看似简简单单的�一句代码,却是 �Masonry 这套链式 DSL 能够生效的核心。

.equalTo

newConstraintMASViewConstraint 类型的对象,而 MASViewConstraint又是 MASConstraint 的子类。在 MASConstraint 中,声明了一系列的方法,例如:

/**
 *  Sets the constraint relation to NSLayoutRelationEqual
 *  returns a block which accepts one of the following:
 *    MASViewAttribute, UIView, NSValue, NSArray
 *  see readme for more details.
 */
- (MASConstraint * (^)(id attr))equalTo;

这个方法的返回值是一个接受 id 类型的 attr 参数�,返回 MASConstraint 类型的 block,这样写的意义是,既可以做到传递参数,返回 self,同时又确保了可以�实现链式调用的 DSL。

该方法的实现为:

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

在方法的实现中,调用了一个名为 equalToWithRelation 的内部方法,方法的实现为:

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

MASConstraint 其实是一个基类,其中 equalToWithRelation 方法本身的实现里只有一个名为MASMethodNotImplemented(); 的宏,这个宏的实现仅仅是抛出一个异常:

#define MASMethodNotImplemented() \
    @throw [NSException exceptionWithName:NSInternalInconsistencyException \
                                   reason:[NSString stringWithFormat:@"You must override %@ in a subclass.", NSStringFromSelector(_cmd)] \
                                 userInfo:nil]

而我们在 makeConstraints的时候,实际调用的是 MASViewConstraint 这个MASConstraint子类中的实现:

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

该方法接收两个参数,一个表示了对应的属性(mas_top),一个表示了相等关系(NSLayoutRelationEqual),进入方法后会先对我们传入的属性做一个类型判断,我们传入的是一个单个的属性,所以会落入 else 分支,同样是依赖断言做了一系列保护性的判断,并将相等关系和视图属性分别赋值给 layoutRelationsecondViewAttribute属性,并返回 self。

返回 self,看似简简单单的一个操作,却是 Masonry 能够实现链式 DSL 最重要的基石。(重要的事情说 n 遍)

superview.mas_top

再来看看我们传入的 mas_top,这是一个声明在 View+MASAdditions.h 当中的只读属性:

@property (nonatomic, strong, readonly) MASViewAttribute *mas_top;

简单生成并返回了一个 �MASViewAttribute 属性的对象�:

- (MASViewAttribute *)mas_top {
    return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeTop];
}

实现为:

- (id)initWithView:(MAS_VIEW *)view layoutAttribute:(NSLayoutAttribute)layoutAttribute {
    self = [self initWithView:view item:view layoutAttribute:layoutAttribute];
    return self;
}

- (id)initWithView:(MAS_VIEW *)view item:(id)item layoutAttribute:(NSLayoutAttribute)layoutAttribute {
    self = [super init];
    if (!self) return nil;
    
    _view = view;
    _item = item;
    _layoutAttribute = layoutAttribute;
    
    return self;
}

.with

我们继续往下看的时候,.with 同样是实现在 MASViewConstraint 中的一个方法:

- (MASConstraint *)with {
    return self;
}

同样是简简单单的返回了 self,而且是仅仅做了这一件事情。所以这个方法仅仅是一个语法……好吧都不能叫做语法糖,就叫语气助词吧,是为了让我们写出的 DSL 可读性更高而存在的。�当然了,你要是觉得多余,也是可以不写的。

.offset

接下来是 offset:

- (MASConstraint * (^)(CGFloat offset))offset;

就是简简单单的一个赋值操作罢了,写成这么复杂的原因就是实现可以传递参数的�链式调用。

- (MASConstraint * (^)(CGFloat))offset {
    return ^id(CGFloat offset){
        self.offset = offset;
        return self;
    };
}

�以上就是 make.top.equalTo(superview.mas_top).with.offset(padding.top); 在 Masonry �内部到底做了些什么,其余几句也是类似的,总而言之就是:

MASConstraintMaker 类型的对象属性(MASConstraint的子类) top(或其他任何你想要去布局的属性),进行了初始化,并通过返回MASViewConstraint类型的对象(newConstraint),不断地调用newConstraint 的对象方法,对 newConstraint��中的属性做了赋值,以确保其可以完整的表达一个 NSLayoutConstraints

install

在配置好我们想要的约束后,我们还需要�对视图施加约束:

return [constraintMaker install];

先来看一下 install 方法:

- (NSArray *)install;
- (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;
}

首先,会对 constraints 属性做一份 copy,之后遍历 constraints 中的所有 MASConstraint 及其子类型的属性,并调用其 install方法:

- (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];
    }
}

hasBeenInstalled

首先,在 install 之前,会做一次判断,看是否已经被 install 过:

if (self.hasBeenInstalled) {
    return;
}

判断依据则是:

- (BOOL)hasBeenInstalled {
    return (self.layoutConstraint != nil) && [self isActive];
}

layoutConstraint 是一个 MASLayoutConstraint 类型的 weak 属性,MASLayoutConstraintNSLayoutConstraint 的子类,�只是为了增加一个属性(mas_key)。

@property (nonatomic, weak) MASLayoutConstraint *layoutConstraint;

isActive 则是通过判断 layoutConstraint 是否响应isActive 以及 isActive 方法返回的结果,来综合决定。

- (BOOL)isActive {
    BOOL active = YES;
    if ([self supportsActiveProperty]) {
        active = [self.layoutConstraint isActive];
    }

    return active;
}
- (BOOL)supportsActiveProperty {
    return [self.layoutConstraint respondsToSelector:@selector(isActive)];
}

mas_installedConstraints

if ([self supportsActiveProperty] && self.layoutConstraint) {
    self.layoutConstraint.active = YES;
    [self.firstViewAttribute.view.mas_installedConstraints addObject:self];
    return;
}

如果 supportsActivePropertylayoutConstraint不为空,则将 layoutConstraint.active 设为 YES,并将其添加到 firstViewAttribute.viewmas_installedConstraints只读属性中去。

@property (nonatomic, readonly) NSMutableSet *mas_installedConstraints;

mas_installedConstraints 是一个可变集合,是通过分类给 MAS_View 类添加的关联对象,用来保存已经� active 的对象。

static char kInstalledConstraintsKey;

- (NSMutableSet *)mas_installedConstraints {
    NSMutableSet *constraints = objc_getAssociatedObject(self, &kInstalledConstraintsKey);
    if (!constraints) {
        constraints = [NSMutableSet set];
        objc_setAssociatedObject(self, &kInstalledConstraintsKey, constraints, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return constraints;
}

生成约束

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;

其实就是把我们再 block 中通过诸如 make.top.equalTo(superview.mas_top).with.offset(padding.top); 这样的语句配置的属性作为 MASLayoutConstraint 初始化方法的参数,生成一个约束。唯一需要注意的是,如果你设置的是一个 isSizeAttribute�,并且 secondViewAttribute 为 nil,会做一些额外的参数调整。

施加约束

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

�如果 secondViewAttribute.view 中的存在,就通过 mas_closestCommonSuperview 方法寻找最近的公共子视图:

/**
 *  Finds the closest common superview between this view and another view
 *
 *  @param  view    other view
 *
 *  @return returns nil if common superview could not be found
 */
- (instancetype)mas_closestCommonSuperview:(MAS_VIEW *)view;
- (instancetype)mas_closestCommonSuperview:(MAS_VIEW *)view {
    MAS_VIEW *closestCommonSuperview = nil;

    MAS_VIEW *secondViewSuperview = view;
    while (!closestCommonSuperview && secondViewSuperview) {
        MAS_VIEW *firstViewSuperview = self;
        while (!closestCommonSuperview && firstViewSuperview) {
            if (secondViewSuperview == firstViewSuperview) {
                closestCommonSuperview = secondViewSuperview;
            }
            firstViewSuperview = firstViewSuperview.superview;
        }
        secondViewSuperview = secondViewSuperview.superview;
    }
    return closestCommonSuperview;
}

递归求解。

如果设置的是一个尺寸约束(firstViewAttribute.isSizeAttribute),则施加在 firstViewAttribute.view 上。�否则施加在 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];
}

对视图施加约束,并将 layoutConstraint 存储在 self.layoutConstraint 属性中,同时把 self 存储到之前提到过的叫做 mas_installedConstraints 的关联对象中。至此,文章开始提到的例子业已完成。

iOS中实现链式调用DSL的基本原理及举例

上一篇下一篇

猜你喜欢

热点阅读