ViewChaos: iOS UI调试黑科技之实现原理

上一篇文章我给大家展示了ViewChaos强大的UI调试能力,相信有部分读者会对它的实现机制有兴趣,这一篇我给大家讲一下开发这个工具碰到的坑和一些功能实现的原理。如果你还没有看上一篇ViewChaos iOS UI 调试黑科技,请先看这篇文章。另外Github地址为
ViewChaos,�如果你感觉这个项目对你的iOS开发有帮助,请Star一下表示支持

怎么才能在不写一行代码的情况下启动ViewChaos

这个问题其实并不难,相信各位读者知道在Objective-C里,有一个方法叫load,利用它,在里面加上自己想要的代码,很容易便能在APP启动的时侯加入自己想要的东西。

+(void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken,^{
        //在这里面加入自己想要的功能,APP启动时会自动调用这个方法
    });
}

关于load方法的原理这里就不探究了,但问题是Swift已经没有这个方法了,所以只好用另一个办法,就是initialize方法,这个方法可以放在extension里面,和load方法不一样,当需要initialize的类每实例化一次,这个方法一次就会被调用一次。所以我们还要加入单次分派,来保证整个APP的生命周期只调用一次。

extension UIWindow {
    #if DEBUG  //这里用了宏
    public override  class func initialize(){  //initialize方法
    struct UIWindow_SwizzleToken {
         static var onceToken:dispatch_once_t = 0
        }
          //在这里面加入自己想要的功能,APP启动时会自动调用这个方法
    }
    #endif
}

这样ViewChaos就能随系统启动而不用写一行代码,但这里存在的问题是这样如何后来APP开发者也想写这种功能,如果他想用扩展UIWindow来实现自己的功能,会导致冲突。

更新,在最新的swift3.1里,苹果已经在代码里将 initialize方法警告未来会禁用,那么怎么办呢,对此我用了一篇文章在Swift3.1中 initialize被警告未来会禁用(disallow),那么来什么来代替它呢 给出了解决方案,里面还详细地给出了原理,各位读者可以参考这篇文章。

怎么才能在Debug模式下启用功能,而Release模式下自动关闭

这个很简单,上一段代码里我用了宏,这个宏说明只有在DEBUG模式下才会编译里面启动调试功能的的代码。所以Release自然就没有该功能了,但目前是Swift其实并不支持宏,而是通过Swift Compiler-Custom Flags的方式来实现的,在里面的Other Swift Flags里面加入-DDEBUG标记就行了,

将Other Swift Flags的Debug加上—DEBUG

怎么添加那个小圆球

启动会有小圆球

我们在UIWindowinitialize方法中使用了Method Swizzle 已经更新在ViewChaosStart类的awake方法里使用了Method Swizzle,这里就不解释什么是Method Swizzle了,我在这里替换了四个方法,其中makeKeyAndVisible方法是APP启动时UIWindow必定会调用的一个方法。我替换了这个方法,在里面加入了这个小球

//这个方法就不用我解释了吧。
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    window = UIWindow(frame: UIScreen.mainScreen().bounds)
    let mainViewController = ViewController()
    print(mainViewController.chaosName)
    let rootNavigationController = UINavigationController(rootViewController: mainViewController)
    window?.rootViewController = rootNavigationController
    window?.makeKeyAndVisible()//这个方法被我替换,加入了小球
    return true
}

//替换系统的makeKeyAndVisible方法
 Chaos.hookMethod(UIWindow.self, originalSelector: #selector(UIWindow.makeKeyAndVisible), swizzleSelector: #selector(UIWindow.vcMakeKeyAndVisible))
 
//自定义的makeKeyAndVisible方法
 public  func vcMakeKeyAndVisible(){
    self.vcMakeKeyAndVisible()//看起来是死循环,其实不是,因为已经交换过了
    if self.frame.size.height > 20  
        let viewChaos = ViewChaos()
        self.addSubview(viewChaos)  /加入小球
        UIApplication.sharedApplication().applicationSupportsShakeToEdit = true //启用摇一摇功能
    }
}

这里要解释一下if self.frame.size.height > 20这行代码,这里是判断该UIWindow对象是不是状态栏,因是iOS最上面的信号条也是个UIWindow对象,所以过滤掉。

如果启动摇一摇功能

见上面代码,添加UIApplication.sharedApplication().applicationSupportsShakeToEdit = true就能启动摇一摇了,当然,关闭也可以用这个属性。然后再在
public override func motionBegan(motion: UIEventSubtype, withEvent event: UIEvent?)方法里处理事件就OK了,当然苹果还提供了

public override func motionEnded(motion: UIEventSubtype, withEvent event: UIEvent?)  //摇一摇结束
public override func motionCancelled(motion: UIEventSubtype, withEvent event: UIEvent?)   //摇一摇取消,我不知道这个事件是会怎么触发的

这两个方法。

如何放大View并获取该点的颜色


这个功能比较有意思,首先在放大镜模式下App里面的点击和触摸事件都要让它失效,不然会起冲突。我定义了一个叫ZoomViewBrace的View。它的作用是起承担override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?)事件的,这样就可以屏蔽掉原页面里的点击和触摸事件,就可以对该View做放大操作了。

放大的View名叫ZoomView,它是一个UIWindow对象,它有个viewToZoom的属性,当我们用手触摸时,截图的View传给该属性,然后再将坐标点也传进去,再调用setNeedsDisplay方法,
ZoomView就会自动调用下面的方法,将放大自己1.5倍后再绘制出来。

 override func drawLayer(layer: CALayer, inContext ctx: CGContext) { 
        CGContextTranslateCTM(ctx, self.frame.size.width / 2, self.frame.size.height / 2)
        CGContextScaleCTM(ctx, 1.5, 1.5)    //放大1.5倍
        CGContextTranslateCTM(ctx, -1 * self.pointToZoom!.x, -1 * self.pointToZoom!.y)
        self.viewToZoom?.layer.renderInContext(ctx)
    }

这样就有放大效果了

然后就是该点颜色显示功能,实现它的步骤是这样的,首先获取viewToZoom的那个View,生成一张截图,再转化成UnsafeMutablePointer<CUnsignedChar>对象,这里面包含了该截图的颜色信息。接下来就是根据坐标点提取RBG值了。这样就能获取该点颜色了。
这里的代码稍微有点长,就不写出来了,建议有兴趣的读者看源码。

如何显示所有View的边框和透明值

边框模式

透明模式

这个其实非常简单,就用一个递归加上循环不停在获取UIWindow下里面所有的View的位置,再生成一个和其位置一样的View,显示这个View的边框,再插入这些VIewUIWindow就OK啦,透明度也一样。这里设置了该Viewtag值,是为了在移除时更方便地判断该View是不是插入的边框View

    private func showBorderView(view:UIView){
      for v in view.subviews{
           let fm = v.convertRect(v.bounds, toView: self) //坐标位置转换。
           let vBorder = UIView(frame: fm)
           vBorder.layer.borderWidth = 0.5
           vBorder.tag = -5000
           vBorder.layer.borderColor = UIColor.redColor().CGColor
           self.insertSubview(vBorder, atIndex: 500)  //插入到最上面
           showBorderView(v)
       }
    }

如何实现点击显示该View的标记线

边框模式
边框模式

这个地方工作量也不小,下面一步一步教你实现这个功能

首先你还是需要一个屏蔽的View,这里我定义一个HolderMarkView来实现,它屏蔽该页面本身的一些View的点击事件和其他一些触摸事件。然后就是在UIWindow里面插入这个View

 let v = HolderMarkView(frame: self.bounds)
 v.tag = -2000                                //设置标签为-2000,是为了删除方便
 self.insertSubview(v, at: 1000)              //插入到UIWindow里,并且设置到最上面

HolderMarkView里需要重写override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)方法,touches可以获取到你用手指在屏幕上触摸坐标点,这个坐标点就是全局的屏幕坐标点,然后再获取该坐标点下面的View。基体实现原理请参考下一小节,
下一步就是标记获取到View,这里面稍微有些复杂,

  • 1: 获取该View的父View,再在父View注入测试边框,因为是显示该View到父View的标记
guard let supView = view.superview else {
           return
}
registerBorderTestView(view: supView)            // 在父View的边框注入测试View 

static func registerBorderTestView(view:UIView){
     let minWh = 1.0 / UIScreen.main.scale //获取最小宽度
     let leftBorderView  = BorderAttachView(frame: CGRect(x: 0, y: 0, width: minWh, height: view.bounds.size.height)) //左边框
     let rightBorderView = BorderAttachView(frame: CGRect(x: view.bounds.size.width - minWh, y: 0, width: minWh, height: view.bounds.size.height))//右边框
     let topBorderView = BorderAttachView(frame: CGRect(x: 0, y: 0, width: view.bounds.size.width, height: minWh))//上边框
     let bottomBorderView = BorderAttachView(frame: CGRect(x: 0, y: view.bounds.size.height - minWh, width: view.bounds.size.height, height: minWh))//下边框
     view.addSubview(leftBorderView) 
     view.addSubview(rightBorderView)
     view.addSubview(topBorderView)
     view.addSubview(bottomBorderView)   //然后添加上下左右四条测试边框View
 }

registerBorderTestView方法就是给这个View加上四个宽度不超过1像素的边框,然后就能用来计算其各个子View之间的距离啦,这个测试View继承于AbstractView,这是为了和普通View区分出来。

 var arrViewFrameObjs = [FrameObject]()  //声明一个保存所有需要测量和标记的数组
 for sub in supView.subviews{    //从这个父View下的所有的子View做一个循环
       if !(sub is AbstractView ){       // 如果不是AbstractView需要详细判断
            if sub.alpha < 0.01 {        //过滤透明的View
                continue 
            }
           if sub.frame.size.width < 2 {      //过滤太小的View
               continue  
           }
           if sub != view {   //如果不是自身,就过滤掉,因为标记模式只显示自己的标记
               continue
           }
       }
      let frameObject = FrameObject(frame: sub.frame, attachedView: sub) 
      arrViewFrameObjs.append(frameObject) //实例化FrameObject再添加到数组
}
//这样就添加了父View注入的测试View和该View到arrViewFrameObjs数组里
  • 3: 获取了所有需要测量的View后,就是获取距离线了,用一个双循环搞定
for sourceFrameObj in arrViewFrameObjs{
    for var targetFrameObj in arrViewFrameObjs{
         if sourceFrameObj.attachedView is AbstractView && targetFrameObj.attachedView is AbstractView {
              continue //过滤测试的AbstractView
         }
          // 寻找两个View有没有水平连线
         var hLine = horizontalLine(frameObj1: sourceFrameObj, frameObj2: targetFrameObj)
         if hLine != nil {
            hLine?.belongToFrame = sourceFrameObj.frame.size.width < 1 ? targetFrameObj.frame : sourceFrameObj.frame
            arrLines.append(hLine!)    //添加水平线
            targetFrameObj.leftInjectedObjs.append(hLine!)
         }
         // 寻找两个View有没有垂直连线
         var vLine = verticalLine(frameObj1: sourceFrameObj, frameObj2: targetFrameObj)
         if vLine != nil{
               vLine?.belongToFrame = sourceFrameObj.frame.size.height < 1 ? targetFrameObj.frame : sourceFrameObj.frame
               arrLines.append(vLine!)      //添加垂直线
               targetFrameObj.topInjectedObjs.append(vLine!)
         }
    }
}

上面代码里面的关键方法horizontalLine(frameObj1: sourceFrameObj, frameObj2: targetFrameObj)就是测量获取两个View之前有没有连线,下面以horizontalLine为例子

static func horizontalLine(frameObj1:FrameObject,frameObj2:FrameObject)->Line?{
    if abs(frameObj1.frame.origin.x  - frameObj2.frame.origin.x) < 3 {
       return nil
    }
    // 如果frameObj1在frameObj2右边,就返回nil
    if frameObj1.frame.origin.x + frameObj1.frame.size.width > frameObj2.frame.origin.x {
         return nil
     }
    let obj1RightX = frameObj1.frame.origin.x + frameObj1.frame.size.width //第一个View的右上点
    let obj1Height = frameObj1.frame.size.height   //第一个View的高度
    let obj2LeftX = frameObj2.frame.origin.x        //第二个View的左上点
    let obj2Height = frameObj2.frame.size.height   //第二个View的高度
    var handle:CGFloat = 0
    let pointY = approperiatePoint(interval1: Interval(start:frameObj1.frame.origin.y,length:obj1Height), interval2: Interval(start:frameObj2.frame.origin.y,length:obj2Height), handle: &handle)
//获取这两个View左边连接线最合适的连接坐标点,是一个Y坐标点
    let line = Line(point1: ShortPoint(x:obj1RightX,y:pointY,handle:handle), point2: ShortPoint(x:obj2LeftX,y:pointY,handle:handle))
//根据两个点生成Line对象,也就是线了
    return line
}

static func approperiatePoint(interval1,interval2,handle:inout) 方法是从两个View的左边找到一个合适的点来做这条连线,这里代码比较长就不放出来了

  • 4 找出重复的线并删除,为什么会有这一步呢让我们看看下面的情况
穿透黄色View的线

从上面的图可以看出,Moonlight这个UILable从左边射出了两条线,并且这两条线是重合的。一条连接左边的黄色View,长度是16px,另一条连接最左边的边框,长度是105px。通常这两条线如果没有重合,那么可以不处理它,但如果重合在一起,在View比较复杂的情况下界面会比较乱,下面Category这个UILable也是一样的。这还只是垂直线,再加上水平线,就更乱了,所以需要移除这些重复的线。

下面是移除重复的线的效果

移除重复的线的效果

可见移除这些重复的线后界面清爽不少呢,只显示了距离自己最近的View的距离。
那么怎么删除重复的线呢,见下面的代码

        let minValue:CGFloat = 5 //设置一个最小的距离值,如果两条线的距离小于这个数,那么就要移除一条
        for  obj in arrViewFrameObjs{
            obj.leftInjectedObjs =  obj.leftInjectedObjs.sorted{$0.point1.point.y > $1.point1.point.y}   // 排序:Y值:从大到小
            var i = 0
            var baseLine:Line?   //基准线
            var compareLine:Line?  //比较线
            if obj.leftInjectedObjs.count > 0{
                baseLine = obj.leftInjectedObjs[i]   //基准线为第一条
            }
            while i < obj.leftInjectedObjs.count{
                if i + 1 < obj.leftInjectedObjs.count{
                    compareLine = obj.leftInjectedObjs[i+1]   //比较线为基准线的下一条
                    if abs(baseLine!.point1.point.y - compareLine!.point1.point.y) < minValue{  //比较两条线的y轴的距离。如果小于最小值,那么需要移除其中的一条
                        if baseLine!.lineWidth > compareLine!.lineWidth{  //比较两条线长度
                            arrLines.removeWith(condition: { (l) -> Bool in  
                                l == baseLine!    //如果基准线比比较线长,移除基准线
                            })
                            baseLine = compareLine  //同时将比较线赋值给基准线
                        }
                        else{
                            arrLines.removeWith(condition: { (l) -> Bool in
                                l == compareLine!  //如果比较线比基准线长,移除比较线
                            })
                        }
                    }
                    else{
                        baseLine = compareLine  //如果距离比较OK,将比较线赋值给基准线继续循环
                    }
                }
                i = i + 1
            }
}

上面的代码加上注释后不难理解,移除这些线后就做最后一步,打标记了。

  • 5 将保存在集合里的线统一放在一个View里,再绘制出来
        var taggintView:TaggingView?
        for s in supView.subviews{
            if s is TaggingView{ //先判断标记的View存不存在TaggingView,如果存在,就直接赋值给它。
                taggintView  = s as? TaggingView 
                break
            }
        }
        if taggintView == nil {
             taggintView = TaggingView(frame: supView.bounds, lines: arrLines) //如果不存在就新建TaggingView
            taggintView?.attachedView = supView
        }
        else{
            taggintView?.addLines(arrLines)   //如果存在就加上这些线并绘制上
        }
        
        supView.addSubview(taggintView!) //�添加到需要绘制的View上
        view.isMarked = true

最后一步就是使用TaggingView把所有的线绘制出来了

 override func draw(_ rect: CGRect) {
        super.draw(rect)
        guard let context = UIGraphicsGetCurrentContext() else{
            return
        }
        guard let mLimes = lines else {
            return
        }
        
        for line in mLimes {
            context.setLineWidth(2.0 / UIScreen.main.scale)    //设置宽度
            context.setAllowsAntialiasing(true)      //设置抗锯齿
            context.setStrokeColor(red: 1, green: 0, blue: 70.0/255.0, alpha: 1)//设置线条颜色
            context.beginPath()
            context.move(to: line.point1.point)
            context.addLine(to: line.point2.point)
            context.strokePath()    //绘制这条线
            let str = String.init(format: "%.0f px", line.lineWidth)
            let position = CGRect(x: line.centerPoint.x - 15 < 0 ? 3 :  line.centerPoint.x - 15 , y: line.centerPoint.y - 6 < 3 ? 0 : line.centerPoint.y - 6 , width: 30, height: 16)
             (str as NSString).draw(in: position, withAttributes: [NSFontAttributeName:UIFont.systemFont(ofSize: 7),NSForegroundColorAttributeName:UIColor.red,NSBackgroundColorAttributeName:UIColor(red: 1, green: 1, blue: 0, alpha: 0.5)]) //再绘制出这条线的长度
        }
    }

绘制的代码比较简单,就这样,整个标记过程全部完成了。我相信大部分读者看了上面的代码可能还是不知所解,最好结合Demo调试和代码一起看才能深入理解这些功能是怎么实现的。下面继续。

如果获取绿色小球下的View

这个ViewChaos最为核心的功能;首先,我定义了一个 arrViewHit的数组,它是一个[UIView]对象,它的作用是用来保存位于该小球下的所有的View,当小球上touchesBegain事件触发或者touchesMove事件触发时,不停地调用topView方法。而topView方法就是获取该点(你手指触摸的那个点)下面最上层的View

    override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
        if !isTouch
        {
            return
        }
        
        let touch = touches.first
        let point = touch?.locationInView(self.window)    //获取触摸点
        self.frame = CGRect(x: point!.x - CGFloat(left), y: point!.y - CGFloat(top), width: self.frame.size.width, height: self.frame.size.height)//这是为了精准定位.,要处理当前点到top和left的位移
        if  let view = topView(self.window!, point: point!) //如果下面有View
        {
            let fm = self.window?.convertRect(view.bounds, fromView: view)   //获取转换后的坐标
            viewTouch = view
            viewBound.frame = fm!
            lblInfo.text = "\(view.dynamicType) l:\(view.frame.origin.x.format(".1f"))t:\(view.frame.origin.y.format(".1f"))w:\(view.frame.size.width.format(".1f"))h:\(view.frame.size.height.format(".1f"))"
            windowInfo.alpha = 1
            windowInfo.hidden = false  //获取该View的Frame信息并显示在最上面。
        }
    }

    func topView(view:UIView,point:CGPoint)->UIView?{ //从arrViewHit里面取出最外面有View
        arrViewHit .removeAll()   //清空arrViewHit里面所有View
        hitTest(view, point: point)  //抓取触摸点下所有View并保存到arrViewHit的立法 
        let viewTop = arrViewHit.last //取出最上面的那个
        arrViewHit.removeAll()
        return viewTop
    }

topView就是取arrViewHit里面的最后一个View,最后一个View就是位于小球下的整个View层级最上面的ViewhitTest这个方法会将所有位置小球下的View放进arrViewHit里面。
下面看看hitTest这个方法

 func hitTest(view:UIView, point:CGPoint){
        var pt = point
        if view is UIScrollView{
            pt.x += (view as! UIScrollView).contentOffset.x    //设置偏移量
            pt.y += (view as! UIScrollView).contentOffset.y
        }
        if view.pointInside(point, withEvent: nil) && !view.hidden && view.alpha > 0.01 && view != viewBound && !view.isDescendantOfView(self){//这里的判断很重要.
            arrViewHit.append(view) //如果该点在这个View中,那么把这个View添加到arrViewHit
            for subView in view.subviews{
                let subPoint = CGPoint(x: point.x - subView.frame.origin.x , y: point.y - subView.frame.origin.y)
                hitTest(subView, point: subPoint) //遍历该View下所有子View,然后递归调用hitTest方法获取所有符合条件的View
            }
        }//四个条件,当前触摸的点一定要在要抓的View里面,View不能是隐藏的或者透明的,View不是我们用于定位的边界View,同时也不是我们用于定位的View.也就是说isDescendantOfView
    }

首先如果该View是UIScrollView的话,需要把contentOffset加上去。然后这里有四个条件需要判断:当前触摸的点一定要在要抓的View里面,View不能是隐藏的或者透明的,View不是我们用于定位的边界View,同时也不是我们用于定位的View.也就是说isDescendantOfView。然后如果这些条件都满足,那么添加这个ViewarrViewHit里面。然后再对这个View的所有subviews递归调用这个方法,注意坐标需要转换一下。所有方法递归完成之后,arrViewHit里面会保存所有满足条件的View,也就是所有位于小球下面的View,然后取最后一个出来就行了。

实现显示View所有信息的表格

表格显示View的基本信息
表格显示View的基本信息

获取到了想要的View,那么获取到View的一些基本信息并将这些信息显示到表格里就比较简单了,主要是业务和逻辑比较多,需要写很多代码处理。操控View也是一样,写好这些逻辑代码就OK了,并没有多少难点,有兴趣有读者可以去看源代码。比较长,但是也比较简单。

这篇文章主要给读者详解了ViewChaos的一些实现原理和难点,主要面向有兴趣看源码和和想了解实现机制的读者。其实深入研究这个库对自己的iOS能力提升还是比较大的。有什么问题可以即时联系。如果大家觉得这个库对你的项目的帮助的话。或者也可以学到一些新技术的话,可以给个Star, 谢谢。再次放出地址ViewChaos

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

推荐阅读更多精彩内容