Kingfisher 3.x 学习(一)

序言

Kingfisher是喵神的一个异步下载和缓存图片的Swift库,类似于OC 的SDWebImage
中文简介github地址 最近才开始学习swift, 所以对于之前的swift 版本不是很了解,直接以最新版本来学习了 由于个人水平有限 如有错误还望包涵

一、Kingfisher的架构

阅读他人优秀代码是一个提高自身代码水平很好的方法。花了几天的时间,看了Kingfisher的源代码,里面包含的很多知识点,让我受益匪浅。3.x版本相比与之前的版本,一个重要的改变就是protocol的灵活运用,更加面向协议编程。当然还有其他很多知识,比如多线程,枚举,闭包,Extension 等等应用。 Kingfisher中CoreExtensionHelpers 三个目录结构 共20个文件

Core
image.swift 文件内部对 UIImage 以及 NSData 进行了拓展, 包含判定图片类型、图片解码以及Gif数据处理等操作
Indicator.swift 图片加载时loading指示
ImageCache.swift 主要负责将加载过的图片缓存至本地。
ImageDownloader.swift 负责下载网络图片。
ImagePrefetcher.swift 可用于提前指定一些图片下载
ImageProcessor.swift 可用于将下载的数据合成图片对象
CacheSerializer.swift 可用于图像对象序列化成图像数据存储到磁盘缓存和从磁盘缓存将图片数据反序列化成图像对象。
RequestModifier.swift 下载图像请求修改器。
ImageTransition.swift 过渡动画效果 使用UIViewAnimationOptions动画效果
KingfisherManager.swift Kingfisher 管理控制类,拥有图片下载及缓存功能
KingfisherOptionsInfo.swift 枚举KingfisherOptionsInfoItem 配置 Kingfisher 行为的参数,包括 是否自定义缓存对象 是否自定义下载器 是否过渡动画 是否设置下载低优先级 是否强制刷新 是否仅获取缓存图片 是否仅缓存至内存、是否允许图像后台解码等设置。
Filter.swift 图像过滤器
Resource.swift 记录了图片的下载地址和缓存Key。
Kingfisher.swift 添加KingfisherCompatible通用协议 kf新属性

Extension
ImageView+Kingfisher.swift UIButton+Kingfisher.swift NSButton+KingfisherUIImageView UIButton NSButton 进行了拓展 主要用于提供 Kingfisher 的外部接口。

Helpers
String+MD5.swift 负责图片缓存时对文件名进行MD5加密操作。
Box.swift 一个简单泛型类
ThreadHelper.swift中的 dispatch_async_safely_main_queue 函数接受一个闭包 利用 NSThread.isMainThread 判断并将其放置在主线程中执行

二、Kingfisher.swift

主要文件ImageView+Kingfisher,KingfisherManager,ImageCache,ImageDownloader,废话不多说直接代码学习

运行demo 下面有这么一段代码:

    let url = URL(string:"https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/kingfisher-\(indexPath.row + 1).jpg")!
    (cell as! CollectionViewCell).cellImageView.kf.setImage(with: url,
                                           placeholder: nil,
                                           options: [.transition(.fade(1))],
                                           progressBlock: { receivedSize, totalSize in
                                            print("\(indexPath.row + 1): \(receivedSize)/\(totalSize)")
            },
                                           completionHandler: { image, error, cacheType, imageURL in
                                            print("\(indexPath.row + 1): Finished")
        })

首先调用的UIImageViewkf属性 之前是调用UIImageViewExtension中的kf_setImage ,现已弃用 那kf 属性是如何实现的?
下面是Kingfisher.swift源码

  • 自定义了不同平台下的一些类型别名 swift中的typealias 相当于OC中的typedef
#if os(macOS)
    import AppKit
    public typealias Image = NSImage
    public typealias Color = NSColor
    public typealias ImageView = NSImageView
    typealias Button = NSButton
#else
    import UIKit
    public typealias Image = UIImage
    public typealias Color = UIColor
    #if !os(watchOS)
    public typealias ImageView = UIImageView
    typealias Button = UIButton
    #endif
#endif
  • 申明了泛型类Kingfisher 实现了一个简单构造器,其中上面的cellImageView就是base属性
public final class Kingfisher<Base> {
    public let base: Base
    public init(_ base: Base) {
        self.base = base
    }
}

  • 申明KingfisherCompatible协议 有一个可读属性kf 其类型是关联类型
/**
 A type that has Kingfisher extensions.
 */

public protocol KingfisherCompatible {
    associatedtype CompatibleType
    var kf: CompatibleType { get }
}
  • KingfisherCompatible协议的实现 属性kf关联Kingfisher类型 返回一个Kingfisher实例 base 参数就是传入的self
public extension KingfisherCompatible {
    public var kf: Kingfisher<Self> {
        get { return Kingfisher(self) }
    }
}
  • Image ImageView Button 遵守 KingfisherCompatible 协议 所以上边的self参数就是遵守了协议的类型 因此base属性即cellImageView
extension Image: KingfisherCompatible { }
#if !os(watchOS)
extension ImageView: KingfisherCompatible {}
extension Button: KingfisherCompatible { }
#endif

三、ImageView+Kingfisher

现在来说说setImage这个方法的实现 这个方法是在Kingfisher的Extension 中实现 并且要求Base属于UIImageView类型 即where Base: ImageView 由于kf 属性关联了Kingfisher
所以可以调用(cell as! CollectionViewCell).cellImageView.kf.setImage
Extensions目录下的三个文件都是类似实现的 这里就以ImageView+Kingfisher.swift为例
下面方法是外部使用Kingfisher最频繁也是最重要的方法
第一个参数Resource是一个URL遵守的Protocol,一般传入图片的URL,不可为空
第二个参数placeholder是一个默认的占位图,可为空
第三个参数KingfisherOptionsInfo 是个枚举数组,配置Kingfisher下载图片的一些操作行为
第四个参数DownloadProgressBlock是个下载进度闭包,可以用于更新下载UI
第五个参数completionHandler是个下载完成闭包,闭包参数包含图片,错误,缓存类型,URL 信息

extension Kingfisher where Base: ImageView {
    /**
     Set an image with a resource, a placeholder image, options, progress handler and completion handler.
     
     - parameter resource:          Resource object contains information such as `cacheKey` and `downloadURL`.
     - parameter placeholder:       A placeholder image when retrieving the image at URL.
     - parameter options:           A dictionary could control some behaviors. See `KingfisherOptionsInfo` for more.
     - parameter progressBlock:     Called when the image downloading progress gets updated.
     - parameter completionHandler: Called when the image retrieved and set.
     
     - returns: A task represents the retrieving process.
     
     - note: Both the `progressBlock` and `completionHandler` will be invoked in main thread.
     The `CallbackDispatchQueue` specified in `optionsInfo` will not be used in callbacks of this method.
     */
    @discardableResult 忽略返回值警告
    public func setImage(with resource: Resource?,
                         placeholder: Image? = nil,
                         options: KingfisherOptionsInfo? = nil,
                         progressBlock: DownloadProgressBlock? = nil,
                         completionHandler: CompletionHandler? = nil) -> RetrieveImageTask
    {
        当传入的resource为空时 使用guard语句提前退出 Resource是一个协议 URL遵守此协议 Resource有两个属性 cacheKey和downloadURL
        guard let resource = resource else {
            base.image = placeholder
            completionHandler?(nil, nil, .none, nil)
            return .empty
        }
        图片加载过程中是否显示placeholder
        var options = options ?? KingfisherEmptyOptionsInfo
        if !options.keepCurrentImageWhileLoading {
            base.image = placeholder
        }
        如果indicator存在,开启转圈动画 indicator 通过属性关联存取
        let maybeIndicator = indicator
        maybeIndicator?.startAnimatingView()
        关联属性绑定下载的URL
        setWebURL(resource.downloadURL)

        默认开启加载所有GIF图片数据,显示GIF 动态图片
        if base.shouldPreloadAllGIF() {
            options.append(.preloadAllGIFData)
        }
        调用KingfisherManager的方法来获取图片
        let task = KingfisherManager.shared.retrieveImage(
            with: resource,
            options: options,
            progressBlock: { receivedSize, totalSize in
                下载进度回调
                if let progressBlock = progressBlock {
                    progressBlock(receivedSize, totalSize)
                }
            },
            completionHandler: {[weak base] image, error, cacheType, imageURL in
                确保线程安全
                DispatchQueue.main.safeAsync {
                    确保返回的图片与URL对应一致
                    guard let strongBase = base, imageURL == self.webURL else {
                        return
                    }
                    self.setImageTask(nil)
                    没有图片返回停止动画返回错误
                    guard let image = image else {
                        maybeIndicator?.stopAnimatingView()
                        completionHandler?(nil, error, cacheType, imageURL)
                        return
                    }
                    是否需要过渡动画 transitionItem 为 options中第一个.transition
                    需要过渡动画需要满足以下情况
                    1.transitionItem存在且不为.transition(.none)
                    2.options.forceTransition存在 或者 cacheType == .none 
                    guard let transitionItem = options.firstMatchIgnoringAssociatedValue(.transition(.none)),
                        case .transition(let transition) = transitionItem, ( options.forceTransition || cacheType == .none) else
                    {
                        maybeIndicator?.stopAnimatingView()
                        strongBase.image = image
                        completionHandler?(image, error, cacheType, imageURL)
                        return
                    }
                    过渡动画
                    #if !os(macOS)
                        UIView.transition(with: strongBase, duration: 0.0, options: [],
                                          animations: { maybeIndicator?.stopAnimatingView() },
                                          completion: { _ in
                                            UIView.transition(with: strongBase, duration: transition.duration,
                                                              options: [transition.animationOptions, .allowUserInteraction],
                                                              animations: {
                                                                // Set image property in the animation.
                                                                设置图片,如果是自定义动画 在定义动画回调中设置图片,代码在ImageTransition.swift
                                                                transition.animations?(strongBase, image)
                                                              },
                                                              completion: { finished in
                                                                动画结束回调
                                                                transition.completion?(finished)
                                                                completionHandler?(image, error, cacheType, imageURL)
                                                              })
                                          })
                    #endif
                }
            })
        setImageTask(task)
   return task
    }
    /**
     Cancel the image download task bounded to the image view if it is running.
     Nothing will happen if the downloading has already finished.
     */
    取消下载
    public func cancelDownloadTask() {
        imageTask?.downloadTask?.cancel()
    }
}

ImageView+Kingfisher 中的WebUR indicatorType indicator imageTask 属性均使用属性关联技术实现数据的存取

四 、KingfisherManager

该类是Kingfisher唯一的一个管理调度类。这个类有下载和缓存两大功能模块 主要包含了两个属性 两个方法
public var cache: ImageCache 图片缓存属性
public var downloader: ImageDownloader 图片下载属性
func downloadAndCacheImage 下载并且缓存图片方法
func tryToRetrieveImageFromCache 获取缓存图片

ImageView+Kingfisher中最后图片的获取就是由KingfisherManager的单例实现的retrieveImage

  • 外部调用获取图片方法
func retrieveImage(with resource: Resource,
        options: KingfisherOptionsInfo?,
        progressBlock: DownloadProgressBlock?,
        completionHandler: CompletionHandler?) -> RetrieveImageTask{
        let task = RetrieveImageTask()
        if let options = options, options.forceRefresh {
             强制刷新 从网络获取图片
            _ = downloadAndCacheImage(
                with: resource.downloadURL,
                forKey: resource.cacheKey,
                retrieveImageTask: task,
                progressBlock: progressBlock,
                completionHandler: completionHandler,
                options: options)
        } else {
            从缓存获取图片
            tryToRetrieveImageFromCache(
                forKey: resource.cacheKey,
                with: resource.downloadURL,
                retrieveImageTask: task,
                progressBlock: progressBlock,
                completionHandler: completionHandler,
                options: options)
        }
        return task
    }
  • 下载并且缓存图片的方法
   func downloadAndCacheImage(with url: URL,
                             forKey key: String,
                      retrieveImageTask: RetrieveImageTask,
                          progressBlock: DownloadProgressBlock?,
                      completionHandler: CompletionHandler?,
                                options: KingfisherOptionsInfo?) -> RetrieveImageDownloadTask?
    {
        获取下载器 并开启下载 
        let options = options ?? KingfisherEmptyOptionsInfo
        let downloader = options.downloader
        return downloader.downloadImage(with: url, retrieveImageTask: retrieveImageTask, options: options,
            progressBlock: { receivedSize, totalSize in
                progressBlock?(receivedSize, totalSize)
            },
            completionHandler: { image, error, imageURL, originalData in

                let targetCache = options.targetCache
                if let error = error, error.code == KingfisherError.notModified.rawValue {
                    // Not modified. Try to find the image from cache.
                    // (The image should be in cache. It should be guaranteed by the framework users.)
                   如果有错误并且没有修改过URL 返回缓存图片
                    targetCache.retrieveImage(forKey: key, options: options, completionHandler: { (cacheImage, cacheType) -> () in
                        completionHandler?(cacheImage, nil, cacheType, url)
                    })
                    return
                }
                缓存图片 
                if let image = image, let originalData = originalData {
                    targetCache.store(image,
                                      original: originalData,
                                      forKey: key,
                                      processorIdentifier:options.processor.identifier,
                                      cacheSerializer: options.cacheSerializer,
                                      toDisk: !options.cacheMemoryOnly,
                                      completionHandler: nil)
                }

                completionHandler?(image, error, .none, url)

            })
    }
  • 优先从缓存获取图片,如缓存中没有,在从网络获取图片
    func tryToRetrieveImageFromCache(forKey key: String,
                                       with url: URL,
                              retrieveImageTask: RetrieveImageTask,
                                  progressBlock: DownloadProgressBlock?,
                              completionHandler: CompletionHandler?,
                                        options: KingfisherOptionsInfo?)
    {
        打破下面diskTask内部闭包保持的循环引用,完成之后取消磁盘任务引用,避免循环引用,释放内存
        let diskTaskCompletionHandler: CompletionHandler = { (image, error, cacheType, imageURL) -> () in
            // Break retain cycle created inside diskTask closure below
            retrieveImageTask.diskRetrieveTask = nil
            completionHandler?(image, error, cacheType, imageURL)
        }
        
        let targetCache = options?.targetCache ?? cache
        let diskTask = targetCache.retrieveImage(forKey: key, options: options,
            completionHandler: { image, cacheType in
                if image != nil {
                     成功返回图片
                    diskTaskCompletionHandler(image, nil, cacheType, url)
                } else if let options = options, options.onlyFromCache {
                    返回失败 并且设置只从缓存获取图片 返回没有缓存错误
                    let error = NSError(domain: KingfisherErrorDomain, code: KingfisherError.notCached.rawValue, userInfo: nil)
                    diskTaskCompletionHandler(nil, error, .none, url)
                } else {
                    返回失败 再从网络下载图片
                    self.downloadAndCacheImage(
                        with: url,
                        forKey: key,
                        retrieveImageTask: retrieveImageTask,
                        progressBlock: progressBlock,
                        completionHandler: diskTaskCompletionHandler,
                        options: options)
                }
            }
        )
        retrieveImageTask.diskRetrieveTask = diskTask
    }

五、 KingfisherOptionsInfo

上面代码多次用到options这个参数,它的参数类型是KingfisherOptionsInfo是一个类型别名
public typealias KingfisherOptionsInfo = [KingfisherOptionsInfoItem]
KingfisherOptionsInfoItem 是一个枚举 配置 Kingfisher所有功能行为 下面是详细中文注释

public enum KingfisherOptionsInfoItem {

    这个成员的关联值是一个ImageCache对象。 Kingfisher使用指定的缓存对象处理 相关业务,包括试图检索缓存图像和存储下载的图片。
    case targetCache(ImageCache)

    这个成员的关联值应该是一个ImageDownloader对象。Kingfisher将使用这个下载器下载的图片。
    case downloader(ImageDownloader)

    如果从网络下载的图片 Kingfisher将使用“ImageTransition这个枚举动画。从内存或磁盘缓存时默认过渡不会发生。如果需要,设置ForceTransition
    case transition(ImageTransition)

    有关“浮动”值将被设置为图像下载任务的优先级。值在0.0 ~ 1.0之间。如果没有设置这个选项,默认值(“NSURLSessionTaskPriorityDefault”)将被使用。
    case downloadPriority(Float)

    如果设置,将忽略缓存,开启一个下载任务的资源
    case forceRefresh
    
    如果设置 即使缓存的图片也将开启过渡动画
    case forceTransition

    如果设置,Kingfisher只会在内存中缓存值而不是磁盘
    case cacheMemoryOnly

    如果设置 Kingfisher只会从缓存中加载图片
    case onlyFromCache
    
    在使用之前在后台线程解码图像
    case backgroundDecode
    
    当从缓存检索图像时 这个成员的关联值将被用作目标队列的调度时回调。如果没 有设置, Kingfisher将使用主要quese回调
    case callbackDispatchQueue(DispatchQueue?)
    
    将检索到的图片数据转换成一个图时 这个成员变量将被用作图片缩放因子。图像分辨率,而不是屏幕尺寸。你可能处理时需要指定正确的缩放因子@2x或@3x Retina图像。
    case scaleFactor(CGFloat)
    
    是否所有的GIF应该加载数据。默认false,只显示GIF中第一张图片。如果true,所有的GIF数据将被加载到内存中进行解码。这个选项主要是用于内部的兼容性。你不应该把直接设置它。“AnimatedImageView”不会预加载所有数据,而一个正常的图像视图(“UIImageView”或“NSImageView”)将加载所有数据。选择使用相应的图像视图类型而不是设置这个选项。
    case preloadAllGIFData
  
    发送请求之前用于改变请求。这是最后的机会你可以修改请求。您可以修改请求一些定制的目的,如添加身份验证令牌头,进行基本的HTTP身份验证或类似的url映射。原始请求默认情况下将没有任何修改
    case requestModifier(ImageDownloadRequestModifier)
    
    下载完成时,处理器会将下载的数据转换为一个图像。如果缓存连接到下载器(当你正在使用KingfisherManager或图像扩展方法),转换后的图像也将被缓存
    case processor(ImageProcessor)
    
    提供一个CacheSerializer 可用于图像对象序列化成图像数据存储到磁盘缓存和从磁盘缓存将图片数据反序列化成图像对象
    case cacheSerializer(CacheSerializer)
    
    保持现有的图像同时设置另一个图像图像视图。通过设置这个选项,imageview的placeholder参数将被忽略和当前图像保持同时加载新图片
    case keepCurrentImageWhileLoading
}

下面是自定义<== 运算符 比较两个KingfisherOptionsInfoItem 是否相等 相等返回true 否则返回false

precedencegroup ItemComparisonPrecedence {
    associativity: none
    higherThan: LogicalConjunctionPrecedence
}

infix operator <== : ItemComparisonPrecedence

// This operator returns true if two `KingfisherOptionsInfoItem` enum is the same, without considering the associated values.
func <== (lhs: KingfisherOptionsInfoItem, rhs: KingfisherOptionsInfoItem) -> Bool {
    switch (lhs, rhs) {
    case (.targetCache(_), .targetCache(_)): return true
    case (.downloader(_), .downloader(_)): return true
    case (.transition(_), .transition(_)): return true
    case (.downloadPriority(_), .downloadPriority(_)): return true
    case (.forceRefresh, .forceRefresh): return true
    case (.forceTransition, .forceTransition): return true
    case (.cacheMemoryOnly, .cacheMemoryOnly): return true
    case (.onlyFromCache, .onlyFromCache): return true
    case (.backgroundDecode, .backgroundDecode): return true
    case (.callbackDispatchQueue(_), .callbackDispatchQueue(_)): return true
    case (.scaleFactor(_), .scaleFactor(_)): return true
    case (.preloadAllGIFData, .preloadAllGIFData): return true
    case (.requestModifier(_), .requestModifier(_)): return true
    case (.processor(_), .processor(_)): return true
    case (.cacheSerializer(_), .cacheSerializer(_)): return true
    case (.keepCurrentImageWhileLoading, .keepCurrentImageWhileLoading): return true
    default: return false
    }
}

下面是对CollectionType的一个扩展 返回匹配的第一个相同枚举值 上面过渡动画就有用到

public extension Collection where Iterator.Element == KingfisherOptionsInfoItem {
    func firstMatchIgnoringAssociatedValue(_ target: Iterator.Element) -> Iterator.Element? {
        return index { $0 <== target }.flatMap { self[$0] }
    }
    
    func removeAllMatchesIgnoringAssociatedValue(_ target: Iterator.Element) -> [Iterator.Element] {
        return self.filter { !($0 <== target) }
    }
}

KingfisherOptionsInfo中有很多的类似的属性get方法 如下是关于图片编码的,默认返回DefaultCacheSerializer.default。如果要自定义图片编码,可以添加自定义CacheSerializerOptions数组

   public var cacheSerializer: CacheSerializer {
        if let item = firstMatchIgnoringAssociatedValue(.cacheSerializer(DefaultCacheSerializer.default)),
            case .cacheSerializer(let cacheSerializer) = item
        {
            return cacheSerializer
        }
        return DefaultCacheSerializer.default
    }

结束

至此 ,我们对Kingfisher对整体架构已经有比较清晰的认识了 如下图所示

Kingfisher.png

由于源代码比较多,一些注释都写在代码部分,可能看起来有点怪 用简书也有段时间,但这还是第一次自己写文章 接下来我会继续学习下载模块和缓存模块的过程等等 如有错误,希望大家不吝指正

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

推荐阅读更多精彩内容