Swift | 实现UI组件的点击事件全埋点

BSGestureRecognizerForwarder

final class BSGestureRecognizerForwarder: NSObject {
    weak var target: UIResponder?
    var action: Selector?

    init(target: UIResponder?, action: Selector?) {
        self.target = target
        self.action = action
    }

    @objc func swizzledAction(for sender: UIGestureRecognizer) {
        guard let target = self.target, let action = self.action else {
            return
        }

        if let gesture = sender as? UITapGestureRecognizer {
            Logger.shared.track(UIControlEvent(date: .init(), object: self, action: action, params: [
                "Action": action,
                "Target": target,
                "Event": "UITapGestureRecognizer",
                "Gesture": gesture
            ]))
        }

        if target.responds(to: action), target.canPerformAction(action, withSender: nil) {
            target.perform(action, with: nil)
        }
    }
}

struct UIResponderHooker {
    static var touchesBeganIMP: IMP!
}

extension UIResponder {
    private static let dispatchOnce: Void = {
        if let method = class_getInstanceMethod(UIResponder.self, #selector(touchesBegan(_:with:))) {
            UIResponderHooker.touchesBeganIMP = method_getImplementation(method)
        }
        switchSelector(#selector(UIResponder.touchesBegan(_:with:)), #selector(UIResponder.self_touchesBegan(_:with:)))
    }()

    @objc class func startupTracking() {
        dispatchOnce
    }

    @objc func self_touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        typealias ClosureType = @convention(c) (UIResponder, Selector, Set<UITouch>, UIEvent?) -> Void
        let touchesBegan: ClosureType = unsafeBitCast(UIResponderHooker.touchesBeganIMP, to: ClosureType.self)
        touchesBegan(self, #selector(touchesBegan(_:with:)), touches, event)

        guard !touches.isEmpty, let event = event else {
            return
        }

        let className = NSStringFromClass(type(of: self))
        let containsFilteredName: Bool = [
            "AppDelegate", "UIApplication", "UIWindow", "UITransitionView",
            "UIStackView", "UITableViewCellContentView", "UIDropShadowView"
        ].first(where: {
            className.contains($0)
        }) != nil

        if className.hasPrefix("MobileDesign") ||
            (!className.hasPrefix("_") && !containsFilteredName) {
            Logger.shared.track(UIControlEvent(date: .init(), object: self, action: #selector(touchesBegan(_:with:)), params: [
                "Touches": touches,
                "Event": event
            ]))
        }
    }
}

extension UIResponder {
    private enum ASKeys {
        static var bsGestureActionsKey = "GestureActionsKey"
    }

    var bsGestureActions: [BSGestureRecognizerForwarder]? {
        get {
            objc_getAssociatedObject(self, &ASKeys.bsGestureActionsKey) as? [BSGestureRecognizerForwarder]
        }
        set {
            objc_setAssociatedObject(self, &ASKeys.bsGestureActionsKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
        }
    }
}

extension UITapGestureRecognizer {
    private static let dispatchOnce: Void = {
        switchSelector(#selector(Self.init(target:action:)), #selector(Self.self_init(target:action:)))
        switchSelector(#selector(Self.addTarget(_:action:)), #selector(Self.self_addTarget(_:action:)))
        switchSelector(#selector(Self.removeTarget(_:action:)), #selector(Self.self_removeTarget(_:action:)))
    }()

    @objc class func startupTracking() {
        guard self == UITapGestureRecognizer.self else {
            return
        }
        dispatchOnce
    }

    @objc func self_init(target: Any?, action: Selector?) -> UIGestureRecognizer {
        if let responder = target as? UIResponder {
            if responder.bsGestureActions == nil {
                responder.bsGestureActions = []
            }

            let forwarder = BSGestureRecognizerForwarder(target: responder, action: action)
            responder.bsGestureActions?.append(forwarder)
            return self_init(target: forwarder, action: #selector(forwarder.swizzledAction(for:)))
        }
        return self_init(target: target, action: action)
    }

    @objc func self_addTarget(_ target: Any, action: Selector) {
        guard let responder = target as? UIResponder else {
            return self_addTarget(target, action: action)
        }
        if responder.bsGestureActions == nil {
            responder.bsGestureActions = []
        }

        let forwarder = BSGestureRecognizerForwarder(target: responder, action: action)
        responder.bsGestureActions?.append(forwarder)
        self_addTarget(forwarder, action: #selector(forwarder.swizzledAction(for:)))
    }

    @objc func self_removeTarget(_ target: Any?, action: Selector?) {
        if let responder = target as? UIResponder,
            let action = action,
            let actions = responder.bsGestureActions {
            for index in 0 ..< actions.count {
                let ac = actions[index]
                guard let bsTarget = ac.target, let bsAction = ac.action else {
                    continue
                }
                guard let responderClass = object_getClass(responder) else {
                    continue
                }
                let responderClassName = NSStringFromClass(responderClass)

                guard let targetClass = object_getClass(bsTarget) else {
                    continue
                }
                let targetClassName = NSStringFromClass(targetClass)

                if responderClassName == targetClassName, action.description == bsAction.description {
                    responder.bsGestureActions?.remove(at: index)
                    break
                }
            }
        }
        self_removeTarget(target, action: action)
    }
}

extension UIControl {
    private static let dispatchOnce: Void = {
        switchSelector(#selector(UIControl.sendAction(_:to:for:)), #selector(UIControl.self_sendAction(_:to:for:)))
    }()

    @objc override class func startupTracking() {
        dispatchOnce
    }

    @objc private func self_sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) {
        Logger.shared.track(UIControlEvent(date: .init(), object: self, action: action, params: [
            "Action": action,
            "Target": target ?? "empty-target",
            "Event": event ?? "empty-event"
        ]))

        self_sendAction(action, to: target, for: event)
    }
}

extension UIViewController {
    @objc override class func startupTracking() {
        let selfViewWillAppearSelector = #selector(self_viewWillAppear(_:))
        let orgViewWillAppearSelector = #selector(viewWillAppear(_:))
        switchSelector(orgViewWillAppearSelector, selfViewWillAppearSelector)

        let selfViewDidAppearSelector = #selector(self_viewDidAppear(_:))
        let orgViewDidAppearSelector = #selector(viewDidAppear(_:))
        switchSelector(orgViewDidAppearSelector, selfViewDidAppearSelector)

        let selfViewWillLayoutSubviewsSelector = #selector(self_viewWillLayoutSubviews)
        let orgViewWillLayoutSubviewsSelector = #selector(viewWillLayoutSubviews)
        switchSelector(orgViewWillLayoutSubviewsSelector, selfViewWillLayoutSubviewsSelector)

        let selfViewDidLayoutSubviewsSelector = #selector(self_viewDidLayoutSubviews)
        let orgViewDidLayoutSubviewsSelector = #selector(viewDidLayoutSubviews)
        switchSelector(orgViewDidLayoutSubviewsSelector, selfViewDidLayoutSubviewsSelector)
    }

    @objc private func self_viewWillAppear(_ animated: Bool) {
        self_viewWillAppear(animated)

        Logger.shared.track(UIViewControllerEvent(lifeCycle: .viewWillAppear, date: .init(), object: self, action: NSSelectorFromString(#function), params: [
            "animated": animated
        ]))
    }

    @objc private func self_viewDidAppear(_ animated: Bool) {
        self_viewDidAppear(animated)
        Logger.shared.track(UIViewControllerEvent(lifeCycle: .viewDidAppear, date: .init(), object: self, action: NSSelectorFromString(#function), params: [
            "animated": animated
        ]))
    }

    @objc private func self_viewWillLayoutSubviews() {
        self_viewWillLayoutSubviews()
        Logger.shared.track(UIViewControllerEvent(lifeCycle: .viewWillLayoutSubviews, date: .init(), object: self, action: NSSelectorFromString(#function), params: ""))
    }

    @objc private func self_viewDidLayoutSubviews() {
        self_viewDidLayoutSubviews()
        Logger.shared.track(UIViewControllerEvent(lifeCycle: .viewDidLayoutSubviews, date: .init(), object: self, action: NSSelectorFromString(#function), params: ""))
    }
}

struct UIViewControllerEvent: TrackableEvent {
    enum LifeCycle {
        case viewDidLoad
        case viewWillAppear, viewDidAppear
        case viewWillLayoutSubviews, viewDidLayoutSubviews
    }

    var lifeCycle: LifeCycle
    var date: Date
    var object: CustomStringConvertible
    var category: Logger.TrackingCategory {
        switch lifeCycle {
        case .viewWillAppear: return .display(subevent: .willAppear)
        case .viewDidAppear: return .display(subevent: .didAppear)
        case .viewWillLayoutSubviews: return .layout(subevent: .willLayoutSubviews)
        case .viewDidLayoutSubviews: return .layout(subevent: .didLayoutSubviews)
        default:
            return .unspecified
        }
    }

    var action: Selector
    var params: CustomStringConvertible
}

struct UIControlEvent: TrackableEvent {
    var date: Date
    var object: CustomStringConvertible
    var category: Logger.TrackingCategory {
        guard let control = object as? UIControl else {
            return .unspecified
        }
        switch control.allControlEvents {
        case .touchUpInside:
            return .touching(subevent: .upInside)
        default:
            return .touching(subevent: .any)
        }
    }

    var action: Selector
    var params: CustomStringConvertible
}

class AppDelegate: AppControllingDelegate {
    private lazy var debugShortcutHandler = Bundle.entityBundleHasMultipleEnvironments ? DebugEnvironmentHandlingModel() : nil

    lazy var deepLinkProvider = DeepLinkProvider()
    lazy var appControllerRouter = AppControllerRouter(
        featuresConfig: featuresConfig,
        debugShortcutHandler: debugShortcutHandler,
        deepLinkProvider: deepLinkProvider
    )

    override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        UIResponder.startupTracking()
        UIControl.startupTracking()
        UIViewController.startupTracking()
        UITapGestureRecognizer.startupTracking()
        URLSession.startTracking()
        setup()
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }

    override func makeAppController() -> AppControllerTemplate {
        appControllerRouter.routingAppController
    }
}

extension AppDelegate {
    private func setup() {
        let config = AppSetupConfiguration(
            entityBundle: Bundle.firstEntityBundle,
            featuresConfig: featuresConfig,
            appInitializationStrategyFactory: appInitializationStrategyFactory,
            deepLinkProvider: deepLinkProvider,
            appLaunch: featuresConfig.preAppBundleInitConfig.appLaunch,
            firebaseWrapper: firebaseWrapper,
            debugShortcutHandler: debugShortcutHandler,
            widgetRegistrationProvidings: widgetRegistrationProvidings,
            keychainItemMigratorProvider: featuresConfig.preAppBundleInitConfig.keychainItemMigratorProvider
        )

        let steps = AppSetupStepsFactory.getDefaultSteps(config: config)
        steps.executeAll()
        appControllerRouter.route(to: .rasp)
    }
}

extension AppDelegate: QuickActionShortCutsHandler {
    public func resetQuickActionShortCut() {
        if let quickActionShortcutStep = appControllerRouter.quickActionShortcutStep {
            quickActionShortcutStep.execute()
        }
    }
}

extension URLSession {
    static let sizzle: Void = {
        let orgMethod = #selector(URLSession.shared.dataTask(with:completionHandler:) as ((URLRequest, @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask))
        switchSelector(orgMethod, #selector(swizzle_dataTask(with:completionHandler:)))
    }()

    @objc class func startTracking() {
        guard self == URLSession.self else {
            return
        }
        sizzle
    }

    @objc private func swizzle_dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
        let requestDate = Date()
        Logger.shared.track(NetworkEvent(object: self, date: requestDate, category: .network(subevent: .didSendRequest), action: NSSelectorFromString(#function), params: TrackableRequest(request: request)))

        return swizzle_dataTask(with: request) { data, response, error in
            if let response = response as? HTTPURLResponse {
                let duration = fabs(requestDate.timeIntervalSinceNow)

                let cate: Logger.TrackingCategory.Networking = error != nil
                    ? .didReceiveResponseWithError
                    : .didReceiveResponse

                Logger.shared.track(NetworkEvent(object: self, date: .init(), category: .network(subevent: cate), action: NSSelectorFromString(#function), params: TrackableResponse(duration: duration, data: data, response: response, error: error)))
            }

            completionHandler(data, response, error)
        }
    }
}

extension NSObject {
    static func switchSelector(_ originalSelector: Selector, _ swizzledSelector: Selector) {
        guard
            let originalMethod = class_getInstanceMethod(self, originalSelector),
            let swizzledMethod = class_getInstanceMethod(self, swizzledSelector) else {
            return
        }
        let didAddMethod: Bool = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
        if didAddMethod {
            class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod)
        }
    }
}

struct TrackableRequest: Requestable, CustomStringConvertible {
    let request: URLRequest

    init(request: URLRequest) {
        self.request = request
    }

    public var urlString: String {
        request.url?.absoluteString ?? "empty-url"
    }

    public var headers: [String: String] {
        request.allHTTPHeaderFields ?? [:]
    }

    public var methodString: String {
        request.httpMethod ?? "empty-method"
    }

    var description: String {
        "[Method: \(methodString), URL: \(urlString), Headers: \(headers)]"
    }
}

struct TrackableResponse: Responsable, CustomStringConvertible {
    var duration: Double
    var data: Data?
    var response: HTTPURLResponse
    var error: Error?

    public var urlString: String {
        response.url?.absoluteString ?? "empty-url"
    }

    public var errorString: String? {
        error?.localizedDescription
    }

    public var statusCode: Int {
        response.statusCode
    }

    private let NF: NumberFormatter = {
        let nf = NumberFormatter()
        nf.maximumFractionDigits = 2
        return nf
    }()

    var description: String {
        let duration = NF.string(from: .init(value: duration)) ?? "NaN"
        let base = "[Duration: \(duration)s, Status: \(statusCode), URL: \(urlString)"
        if let error = errorString {
            return base + ", Error: \(error)]"
        }
        if let data = data {
            return base + ", Data: \(data.description)]"
        }
        return base
    }
}

public struct NetworkEvent: TrackableEvent {
    public var object: CustomStringConvertible
    public var date: Date
    public var category: Logger.TrackingCategory
    public var action: Selector
    public var params: CustomStringConvertible
}

let df = DateFormatter()
public struct Logger {
    public static var shared = Logger()
    private static let oslog = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "no_bundle_id", category: "UITracking")

    var savePath: String {
        df.locale = Calendar.current.locale
        df.dateFormat = "yyyy-MM-dd"
        let name = "BS_Track_Log_" + df.string(from: .init()) + ".log"
        return "\(NSHomeDirectory())/Documents/\(name)"
    }

    var saveURL: URL {
        .init(fileURLWithPath: savePath)
    }

    lazy var logsString: String = {
        if FileManager.default.fileExists(atPath: savePath),
            let data = try? Data(contentsOf: saveURL),
            let logs = String(data: data, encoding: .utf8) {
            return logs
        }
        return ""
    }()

    public mutating func track(_ event: TrackableEvent) {
        if !logsString.isEmpty {
            logsString += "\n" + event.description
        } else {
            logsString = event.description
        }

        // write to system logs
        var logType = OSLogType.info
        if event.category == .network(subevent: .didReceiveResponseWithError) {
            logType = .error
        }
        os_log(logType, log: Logger.oslog, "%{public}@", event.description)

        #if DEBUG
            print("BS_Track_Log path: \(savePath)")
            print(logsString)
        #endif

        // write to sandbox logs
        guard let data = logsString.data(using: .utf8) else {
            return
        }
        do {
            try data.write(to: saveURL, options: .atomic)
        } catch {
            print(error)
        }
    }
}

public extension Logger {
    enum TrackingCategory {
        case unspecified
        case tracking(subevent: Tracking)
        case touching(subevent: Touching)
        case display(subevent: Displaying)
        case layout(subevent: Layouting)
        case network(subevent: Networking)
    }
}

extension Logger.TrackingCategory: Equatable {
    public static func == (lhs: Self, rhs: Self) -> Bool {
        switch (lhs, rhs) {
        case (.unspecified, .unspecified),
             (.tracking(.start), .tracking(.start)),
             (.tracking(.end), .tracking(.end)),
             (.touching(.any), .touching(.any)),
             (.display(.willAppear), .display(.willAppear)),
             (.display(.didAppear), .display(.didAppear)),
             (.layout(.willLayoutSubviews), .layout(.willLayoutSubviews)),
             (.layout(.didLayoutSubviews), .layout(.didLayoutSubviews)),
             (.network(.didSendRequest), .network(.didSendRequest)),
             (.network(.didReceiveResponse), .network(.didReceiveResponse)),
             (.network(.didReceiveResponseWithError), .network(.didReceiveResponseWithError)):
            return true
        default: return false
        }
    }
}

public extension Logger.TrackingCategory {
    enum Tracking {
        case start, end
    }

    enum Touching {
        case any, upInside
    }

    enum Displaying {
        case willAppear, didAppear
    }

    enum Layouting {
        case willLayoutSubviews
        case didLayoutSubviews
    }

    enum Networking {
        case didSendRequest
        case didReceiveResponse
        case didReceiveResponseWithError
    }
}

extension Logger.TrackingCategory: CustomStringConvertible {
    public var description: String {
        switch self {
        case .unspecified: return "Unspecified"
        case .tracking(let subevent):
            switch subevent {
            case .start: return "Tracking->Start"
            case .end: return "Tracking->End"
            }
        case .touching(let subevent):
            switch subevent {
            case .any: return "Touching->Any"
            case .upInside: return "Touching->UpInside"
            }
        case .layout(let subevent):
            switch subevent {
            case .willLayoutSubviews:
                return "Layout->WillLayoutSubviews"
            case .didLayoutSubviews:
                return "Layout->DidLayoutSubviews"
            }
        case .display(let subevent):
            switch subevent {
            case .willAppear:
                return "Display->WillAppear"
            case .didAppear:
                return "Display->DidAppear"
            }
        case .network(let subevent):
            switch subevent {
            case .didSendRequest:
                return "Network->DidSendRequest"
            case .didReceiveResponse:
                return "Network->DidReceiveResponse"
            case .didReceiveResponseWithError:
                return "Network->DidReceiveResponseWithError"
            }
        }
    }
}

public protocol Requestable {
    var headers: [String: String] { get }
    var urlString: String { get }
    var methodString: String { get }
}

public protocol Responsable {
    var urlString: String { get }
    var errorString: String? { get }
    var statusCode: Int { get }
}

public protocol TrackableEvent {
    var category: Logger.TrackingCategory { get }
    var object: CustomStringConvertible { get set }
    var date: Date { get set }
    var action: Selector { get set }
    var params: CustomStringConvertible { get set }
}

extension TrackableEvent {
    public var comment: String {
        switch category {
        case .unspecified: return "An undefined event was fired"
        case .tracking(let subevent):
            switch subevent {
            case .start: return "A test case tracking is started"
            case .end: return "A test case tracking is ended"
            }
        case .touching(let subevent):
            switch subevent {
            case .any: return "An any touch event was fired"
            case .upInside: return "An user touch up inside event was fired"
            }
        case .layout(let subevent):
            switch subevent {
            case .willLayoutSubviews:
                return "A page will layout subviews"
            case .didLayoutSubviews:
                return "A page finished layouting subviews"
            }
        case .display(let subevent):
            switch subevent {
            case .willAppear:
                return "A page will be displayed on screen"
            case .didAppear:
                return "A page is displayed on screen"
            }
        case .network(let subevent):
            switch subevent {
            case .didSendRequest:
                return "An HTTP request was sent"
            case .didReceiveResponse:
                return "An HTTP response is received"
            case .didReceiveResponseWithError:
                return "An HTTP response is received with error"
            }
        }
    }
}

extension TrackableEvent {
    public var description: String {
        df.locale = Calendar.current.locale
        df.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
        let dateString = df.string(from: date)
        return "[\(dateString)] [\(comment)] [\(category)] [\(object)->\(action)->\(params)]"
    }
}

#!/bin/bash

components=(
ActiveInactiveLabel
)
num=0

echo "" > GDLRefs.txt

echo "Pulling git updates..."
git pull

for component in ${components[*]}
do
    num=`expr $num + 1`
    echo "Searching for $component: $num/${#components[*]}"
    echo "$component: " >> GDLRefs.txt

    refs=`find iOS -name *.swift -exec grep $component -rlnws {} \;`
    refs=`echo $refs | tr -d '[ ]'`
    refs=${refs//iOS/ iOS}
    for ref in ${refs[*]}
    do
        echo $ref
        echo $ref >> GDLRefs.txt
    done
    echo " " >> GDLRefs.txt
done

echo "Done of searchings."

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

推荐阅读更多精彩内容