SnapKit 源碼解讀

SnapKit 是一個(gè)使用 Swift 編寫(xiě)而來(lái)的 AutoLayout 框架, 通過(guò)使用 Snapkit, 我們可以通過(guò)簡(jiǎn)短的代碼完成布局
例如, 我們要一個(gè) label 居中展示

snplabel.snp.makeConstraints { (make) in
    make.center.equalTo(self.view.snp.center)
}

如果不用 SnapKit, 我們需要做

rawlabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint(item: rawlabel, attribute: .centerX, relatedBy: .equal, toItem: self.view, attribute: .centerX, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: rawlabel, attribute: .centerY, relatedBy: .equal, toItem: self.view, attribute: .centerY, multiplier: 1, constant: 20).isActive = true

看起來(lái)很神奇的 SnapKit 是如何實(shí)現(xiàn)的?

分析源碼

我們從最開(kāi)始的 snplabel.snp 開(kāi)始
你也許猜到了, 這個(gè)是通過(guò)給 view 添加一個(gè)擴(kuò)展實(shí)現(xiàn)的
這個(gè)在ConstraintView+Extensions.swift 文件里面, 這個(gè)文件里面有很多廢棄的方法, 為了方便查看, 我們先直接去掉這些廢棄的方法, 去掉之后, 就是這樣的

public extension ConstraintView {
     public var snp: ConstraintViewDSL {
        return ConstraintViewDSL(view: self)
    }
}

擴(kuò)展

你也許注意到, 并不是直接擴(kuò)展的 UIView, 我們來(lái)看看 ConstraintView 的定義

#if os(iOS) || os(tvOS)
    public typealias ConstraintView = UIView
#else
    public typealias ConstraintView = NSView
#endif

可以看到, SnapKit 為了實(shí)現(xiàn)多平臺(tái)將 ConstraintView 分別定義為 UIView 和 NSView 的別名. 我們這里也為了簡(jiǎn)單起見(jiàn), 不考慮多平臺(tái)適配, 我們將 ConstraintView 都替換為 UIView

public extension UIView {
     public var snp: ConstraintViewDSL {
        return ConstraintViewDSL(view: self)
    }
}

可以看到, snp 最后是生成了一個(gè) ConstraintViewDSL 對(duì)象

ConstraintViewDSL

ConstraintViewDSL 類(lèi)的構(gòu)造函數(shù)很簡(jiǎn)單, 就是將 view 保存起來(lái)

internal init(view: UIView) {
    self.view = view
}

而makeConstraints 函數(shù)也是定義如下, 這里看到, 這里只是將傳進(jìn)來(lái)的閉包傳遞給ConstraintMaker 這個(gè)類(lèi)去處理了

public func makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
    ConstraintMaker.makeConstraints(item: self.view, closure: closure)
}

ConstraintMaker

ConstraintMaker.makeConstraints 的實(shí)現(xiàn)如下所示

internal static func makeConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) {
    let maker = ConstraintMaker(item: item)
    closure(maker)
    var constraints: [Constraint] = []
    for description in maker.descriptions {
        guard let constraint = description.constraint else {
            continue
        }
        constraints.append(constraint)
    }
    for constraint in constraints {
        constraint.activateIfNeeded(updatingExisting: false)
    }
}

從這里可以看到一個(gè)大致流程, 首先是構(gòu)造一個(gè) maker, 然后調(diào)用閉包, 閉包內(nèi)部會(huì)添加一些約束, 接下來(lái)就是獲取這些約束, 最后將約束激活.
這個(gè)類(lèi)的構(gòu)造函數(shù)依舊很簡(jiǎn)單

internal init(item: LayoutConstraintItem) {
    self.item = item
    self.item.prepare()
}
LayoutConstraintItem

這里出現(xiàn)了一個(gè)新的類(lèi)型 LayoutConstraintItem, 表示一個(gè)可布局的對(duì)象, 通過(guò)查看定義, 可以看到是一個(gè)協(xié)議, UIView 和 ConstraintLayoutGuide 都實(shí)現(xiàn)了這個(gè)協(xié)議, 內(nèi)部實(shí)現(xiàn)了一些方法, 其中就有這個(gè) prepare

internal func prepare() {
        if let view = self as? UIView {
            view.translatesAutoresizingMaskIntoConstraints = false
        }
    }

這一步其實(shí)就是禁用 View 的 AutoresizeMask 轉(zhuǎn)換.

回到最開(kāi)始的閉包, 里面我們寫(xiě)的make.center.equalTo(self.view.snp.center)
通過(guò)上下文我們可以猜到, 我們可以通過(guò)這個(gè)函數(shù)生成一些約束對(duì)象.
首先我們都知道, 每一個(gè)約束, 首先需要添加到一個(gè)對(duì)象上面, 還需要約束的屬性, 關(guān)系(大于, 等于,小于), 如果不是常量類(lèi)型, 還需要另一個(gè)依賴(lài)的對(duì)象, 以及依賴(lài)的屬性, 系數(shù)以及一個(gè)偏移常量.
這里的 make.center 就是說(shuō)添加到當(dāng)前, 并設(shè)置約束屬性為 center, equalTo, 則是表示關(guān)系為等于, self.view.snp.center, 則表示依賴(lài)的對(duì)象是 self.view, 依賴(lài)的屬性也是 center, 系數(shù)及偏移值這里均沒(méi)有指定, 表示使用默認(rèn)值
那 make.center 這個(gè)是如何實(shí)現(xiàn)的? 通過(guò)查找定義, 可以發(fā)現(xiàn)實(shí)現(xiàn)如下

public var center: ConstraintMakerExtendable {
    return self.makeExtendableWithAttributes(.center)
}

這個(gè)只是一個(gè)簡(jiǎn)便方法, 具體的實(shí)現(xiàn)繼續(xù)去查看定義

internal func makeExtendableWithAttributes(_ attributes: ConstraintAttributes) -> ConstraintMakerExtendable {
    let description = ConstraintDescription(item: self.item, attributes: attributes)
    self.descriptions.append(description)
    return ConstraintMakerExtendable(description)
}

可以看到流程為首先根據(jù)約束屬性及需要添加約束的對(duì)象生成一個(gè)描述, 然后將其添加內(nèi)部的一個(gè)數(shù)組, 也就是之前 makeConstraints 中第一個(gè) for 循環(huán)鎖遍歷的數(shù)組, 最后返回一個(gè) ConstraintMakerExtendable 對(duì)象

ConstraintAttributes

首先我們來(lái)看看這個(gè)屬性center
ConstraintAttributes 本身是一個(gè) OptionSet, 里面定義了許多屬性, 例如 left, right, center

internal struct ConstraintAttributes : OptionSet {
    
    internal private(set) var rawValue: UInt
    internal init(rawValue: UInt) {
        self.rawValue = rawValue
    }
    internal static var left: ConstraintAttributes { return self.init(1) }
    internal static var top: ConstraintAttributes {  return self.init(2) }
    internal static var right: ConstraintAttributes { return self.init(4) }
    ...這里有省略
    internal static var center: ConstraintAttributes { return self.init(768) }

使用 OptionSet 的意義在于, 可以通過(guò)組合操作, 同時(shí)添加多個(gè)屬性, 例如, center 這個(gè)屬性就是由 centerX 和 centerY 復(fù)合而來(lái).

ConstraintDescription

這個(gè)類(lèi)是一個(gè)描述類(lèi), 用于描述一條具體的約束, 里面包含了約束的屬性, 關(guān)系等

public class ConstraintDescription {
    internal let item: LayoutConstraintItem
    internal var attributes: ConstraintAttributes
    internal var relation: ConstraintRelation? = nil
    internal var sourceLocation: (String, UInt)? = nil
    internal var label: String? = nil
    internal var related: ConstraintItem? = nil
    internal var multiplier: ConstraintMultiplierTarget = 1.0
    internal var constant: ConstraintConstantTarget = 0.0
    internal var priority: ConstraintPriorityTarget = 1000.0
    internal lazy var constraint: Constraint? = ...
    internal init(item: LayoutConstraintItem, attributes: ConstraintAttributes) {
        self.item = item
        self.attributes = attributes
    }

回到ConstraintMaker.makeConstraints 中的第一個(gè) for 循環(huán), 里面就是去獲取 description.constraint 已達(dá)到最終構(gòu)造約束的目的

ConstraintMakerExtendable

makeExtendableWithAttributes 最后返回的時(shí)候, 返回的是一個(gè)ConstraintMakerExtendable 對(duì)象
這個(gè)類(lèi)的主要目的是為了實(shí)現(xiàn)鏈?zhǔn)降亩鄬傩? 例如, make.center.equalTo(self.view.snp.center) 這一句可以寫(xiě)為, make.centerX.centerY.equalTo(self.view.snp.center)

public class ConstraintMakerExtendable: ConstraintMakerRelatable {
    public var left: ConstraintMakerExtendable {
        self.description.attributes += .left
        return self
    }
    ...
}
ConstraintMakerRelatable

另外, ConstraintMakerExtendable 繼承自 ConstraintMakerRelatable, 這個(gè)類(lèi)主要是負(fù)責(zé)構(gòu)造一個(gè)關(guān)系, 例如 equalTo

public func equalTo(_ other: ConstraintRelatableTarget, _ file: String = #file, _ line: UInt = #line) -> ConstraintMakerEditable {
    return self.relatedTo(other, relation: .equal, file: file, line: line)
}
internal func relatedTo(_ other: ConstraintRelatableTarget, relation: ConstraintRelation, file: String, line: UInt) -> ConstraintMakerEditable {
    let related: ConstraintItem
    let constant: ConstraintConstantTarget
    
    if let other = other as? ConstraintItem {
        guard other.attributes == ConstraintAttributes.none ||
              other.attributes.layoutAttributes.count <= 1 ||
              other.attributes.layoutAttributes == self.description.attributes.layoutAttributes ||
              other.attributes == .edges && self.description.attributes == .margins ||
              other.attributes == .margins && self.description.attributes == .edges else {
            fatalError("Cannot constraint to multiple non identical attributes. (\(file), \(line))");
        }
        
        related = other
        constant = 0.0
    } else if let other = other as? UIView {
        related = ConstraintItem(target: other, attributes: ConstraintAttributes.none)
        constant = 0.0
    } else if let other = other as? ConstraintConstantTarget {
        related = ConstraintItem(target: nil, attributes: ConstraintAttributes.none)
        constant = other
    } else if #available(iOS 9.0, OSX 10.11, *), let other = other as? ConstraintLayoutGuide {
        related = ConstraintItem(target: other, attributes: ConstraintAttributes.none)
        constant = 0.0
    } else {
        fatalError("Invalid constraint. (\(file), \(line))")
    }
    
    let editable = ConstraintMakerEditable(self.description)
    editable.description.sourceLocation = (file, line)
    editable.description.relation = relation
    editable.description.related = related
    editable.description.constant = constant
    return editable
}

equalTo 只是對(duì)內(nèi)部函數(shù)relatedTo 的一個(gè)簡(jiǎn)單調(diào)用

ConstraintRelatableTarget

這是一個(gè)協(xié)議, 表示一個(gè)可以被依賴(lài)的目標(biāo), 我們?cè)谑謱?xiě) NSLayoutConstraint 的時(shí)候, 依賴(lài)對(duì)象可以為 view, 可以為ConstraintLayoutGuide, 也可以為空, 為空的時(shí)候, 表示使用絕對(duì)值
ConstraintRelatableTarget 是一個(gè)協(xié)議, 分別有 Int, Double, CGPoint等字面值, 也有UIView, ConstraintLayoutGuide , 同時(shí), 也有ConstraintItem, 讓我們可以指定依賴(lài)的具體值, 我們之前的代碼 make.center.equalTo(self.view.snp.center) 中的self.view.snp.center 就是 ConstraintItem 對(duì)象

ConstraintItem

view.snp 返回的是一個(gè) ConstraintViewDSL, ConstraintViewDSL 是繼承自 ConstraintAttributesDSL, 而ConstraintAttributesDSL 則是繼承自 ConstraintBasicAttributesDSLConstraintAttributesDSLConstraintBasicAttributesDSL 中定義了大量的布局屬性, 如 top, bottom 等

public var center: ConstraintItem {
    return ConstraintItem(target: self.target, attributes: ConstraintAttributes.center)
}
...其他均類(lèi)似

可以看到這里面構(gòu)造了一個(gè) ConstraintItem 對(duì)象

public final class ConstraintItem {
    
    internal weak var target: AnyObject?
    internal let attributes: ConstraintAttributes
    
    internal init(target: AnyObject?, attributes: ConstraintAttributes) {
        self.target = target
        self.attributes = attributes
    }
    
    internal var layoutConstraintItem: LayoutConstraintItem? {
        return self.target as? LayoutConstraintItem
    }
}

這個(gè)類(lèi)也很簡(jiǎn)單, 主要就是保存一下布局的目標(biāo)對(duì)象與目標(biāo)屬性

回到 relateTo 這個(gè)方法中, 這個(gè)方法有4 個(gè)主要分支
第一個(gè)分支就是對(duì)象為 ConstraintItem 的分支
首先使用了 guard 判斷了是否為一個(gè)合法的對(duì)象, 之后就進(jìn)入后續(xù)處理, 而對(duì)于 UIView 和 ConstraintLayoutGuide 則直接將屬性設(shè)置為 none, 而字面值類(lèi)型, 則直接將值保存起來(lái)
獲取了 related 與 constant 之后, 后續(xù)會(huì)使用 description 生成一個(gè) ConstraintMakerEditable, 并在之后, 修改 description , 添加新增的屬性.

ConstraintMakerEditable

ConstraintMakerEditable 這個(gè)類(lèi)主要是設(shè)置Autolayout 中的兩個(gè)常量multiplier 和 constant 與優(yōu)先級(jí)
使用方法如make.center.equalTo(self.view.snp.center).offset(20)

再次回到makeConstraints

通過(guò)上面的若干步驟, 完成了對(duì) ConstraintDescription 的設(shè)置, 現(xiàn)在可以用他來(lái)生成 Constraint 了, 生成的部分在ConstraintDescription 的 constraint 屬性里面,

internal lazy var constraint: Constraint? = {
    guard let relation = self.relation,
          let related = self.related,
          let sourceLocation = self.sourceLocation else {
        return nil
    }
    let from = ConstraintItem(target: self.item, attributes: self.attributes)
    
    return Constraint(
        from: from,
        to: related,
        relation: relation,
        sourceLocation: sourceLocation,
        label: self.label,
        multiplier: self.multiplier,
        constant: self.constant,
        priority: self.priority
    )
}()

Constraint 創(chuàng)建過(guò)程很像NSLayoutConstraint

Constraint

這個(gè)類(lèi)主要就是生成和操縱 NSLayoutConstraint.
構(gòu)造函數(shù)有點(diǎn)長(zhǎng), 下面是去掉一些簡(jiǎn)單的賦值和多平臺(tái)適配后的代碼

internal init(...) {
    self.layoutConstraints = []
    // get attributes
    let layoutFromAttributes = self.from.attributes.layoutAttributes
    let layoutToAttributes = self.to.attributes.layoutAttributes
    
    // get layout from
    let layoutFrom = self.from.layoutConstraintItem!
    
    // get relation
    let layoutRelation = self.relation.layoutRelation
    
    for layoutFromAttribute in layoutFromAttributes {
        // get layout to attribute
        let layoutToAttribute: NSLayoutAttribute
        if layoutToAttributes.count > 0 {
            if self.from.attributes == .edges && self.to.attributes == .margins {
                switch layoutFromAttribute {
                case .left:
                    layoutToAttribute = .leftMargin
                case .right:
                    layoutToAttribute = .rightMargin
                case .top:
                    layoutToAttribute = .topMargin
                case .bottom:
                    layoutToAttribute = .bottomMargin
                default:
                    fatalError()
                }
            } else if self.from.attributes == .margins && self.to.attributes == .edges {
                switch layoutFromAttribute {
                case .leftMargin:
                    layoutToAttribute = .left
                case .rightMargin:
                    layoutToAttribute = .right
                case .topMargin:
                    layoutToAttribute = .top
                case .bottomMargin:
                    layoutToAttribute = .bottom
                default:
                    fatalError()
                }
            } else if self.from.attributes == self.to.attributes {
                layoutToAttribute = layoutFromAttribute
            } else {
                layoutToAttribute = layoutToAttributes[0]
            }
        } else {
            if self.to.target == nil && (layoutFromAttribute == .centerX || layoutFromAttribute == .centerY) {
                layoutToAttribute = layoutFromAttribute == .centerX ? .left : .top
            } else {
                layoutToAttribute = layoutFromAttribute
            }
        }
        // get layout constant
        let layoutConstant: CGFloat = self.constant.constraintConstantTargetValueFor(layoutAttribute: layoutToAttribute)
        
        // get layout to
        var layoutTo: AnyObject? = self.to.target
        
        // use superview if possible
        if layoutTo == nil && layoutToAttribute != .width && layoutToAttribute != .height {
            layoutTo = layoutFrom.superview
        }
        
        // create layout constraint
        let layoutConstraint = LayoutConstraint(
            item: layoutFrom,
            attribute: layoutFromAttribute,
            relatedBy: layoutRelation,
            toItem: layoutTo,
            attribute: layoutToAttribute,
            multiplier: self.multiplier.constraintMultiplierTargetValue,
            constant: layoutConstant
        )
        
        // set label
        layoutConstraint.label = self.label
        
        // set priority
        layoutConstraint.priority = self.priority.constraintPriorityTargetValue
        
        // set constraint
        layoutConstraint.constraint = self
        
        // append
        self.layoutConstraints.append(layoutConstraint)
    }
}

函數(shù)中第一行的self.layoutConstraints = [] 使用來(lái)存放所有最后生成的NSLayoutConstraint
后面的兩行是獲取兩個(gè)對(duì)象的約束屬性. 而 layoutFrom 則是約束屬性的起始對(duì)象, 在我們最初那段代碼中, 就表示了snplabel 這個(gè)視圖.
后面則是獲取約束的關(guān)系, 如等于, 大于
主要的代碼都在那個(gè)循環(huán)中, 主要邏輯是遍歷添加在起始對(duì)象上的約束屬性, 然后獲取預(yù)支對(duì)應(yīng)的目標(biāo)對(duì)象及目標(biāo)對(duì)象的約束屬性, 最后生成 LayoutConstraint
其中第一個(gè) if else 分支中在確定目標(biāo)屬性該使用何種值, 通過(guò)分析可以看出, 我們之前那段代碼, 其實(shí)可以將make.center.equalTo(self.view.snp.center) 中直接寫(xiě)為make.center.equalTo(self.view)(這個(gè)實(shí)現(xiàn)原理在第一個(gè)else 語(yǔ)句中的 else 語(yǔ)句中實(shí)現(xiàn))
后面則是根據(jù)不同的目標(biāo)屬性, 獲取適當(dāng)?shù)钠浦? 以及獲取目標(biāo)對(duì)象.
后面 LayoutConstraint(xxx) 中的 LayoutConstraint 其實(shí)只是一個(gè) NSLayoutConstraint 的子類(lèi), 只是在其中添加了一個(gè)標(biāo)簽與創(chuàng)建者(Constraint) 的引用

activateIfNeeded

makeConstraints最后一步則是激活, 在 iOS 8 以前, 所有的依賴(lài)屬性, 都必須使用 view.addConstraint(xxx) 方法將依賴(lài)激活, iOS 8 后, 則直接將依賴(lài)激活即可生效.
activateIfNeeded 則是將依賴(lài)激活使其生效

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,908評(píng)論 6 541
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,324評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 178,018評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,675評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,417評(píng)論 6 412
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,783評(píng)論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,779評(píng)論 3 446
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,960評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,522評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,267評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,471評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,009評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,698評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 35,099評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 36,386評(píng)論 1 294
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,204評(píng)論 3 398
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,436評(píng)論 2 378

推薦閱讀更多精彩內(nèi)容

  • (一)Masonry介紹 Masonry是一個(gè)輕量級(jí)的布局框架 擁有自己的描述語(yǔ)法 采用更優(yōu)雅的鏈?zhǔn)秸Z(yǔ)法封裝自動(dòng)布...
    木易林1閱讀 2,366評(píng)論 0 3
  • Masonry是一個(gè)輕量級(jí)的布局框架,擁有自己的描述語(yǔ)法,采用更優(yōu)雅的鏈?zhǔn)秸Z(yǔ)法封裝自動(dòng)布局,簡(jiǎn)潔明了并具有高可讀性...
    3dcc6cf93bb5閱讀 1,785評(píng)論 0 1
  • (轉(zhuǎn)載于http://www.hangge.com/blog/cache/detail_1097.html) Sw...
    6309b0eed1c7閱讀 2,808評(píng)論 7 2
  • iOS_autoLayout_Masonry 概述 Masonry是一個(gè)輕量級(jí)的布局框架與更好的包裝AutoLay...
    指尖的跳動(dòng)閱讀 1,182評(píng)論 1 4
  • 如果你 難得鼓起的勇氣 于瞬間倒塌 你會(huì)怎樣傾心重筑? 如果你 在平凡不過(guò)的旅途中偶遇 從未謀面的路人 給彼此個(gè)微...
    喝月亮閱讀 201評(píng)論 1 4