iOS中WKWebView和Native交互

前言

了解本文之前需要准备JS和WebView的一些基础知识,需要知道JS的基本语法和WebView调用JS的常用接口。

iOS实现JS和Native交互的WebView有UIWebView和WKWebView。通过KVC拿到UIWebView的JSContext,通过JSContext实现交互。
WKWebView有了新特性MessageHandler来实现JS调用原生方法。从实现思路是来讲,UIWebView和WKWebView是一样的。
所以,本文只介绍WKWebView上JS和Native的交互思路,UIWebView有需求的可以模仿实现。

JS和Native交互常用的场景

常用的分为下面几种场景:

  • H5获取Native用户信息(这种比较简单,只需要Native注入JS就行了,思路有三种下面介绍)

  • H5传递信息给Native,调用Native分享(这种属于JS调用Native)

  • Native告诉H5分享结果(这种属于Native调用JS)

下面一一介绍,实现如下:

H5获取Native用户信息

现有用户信息格式如下,需要注入到JS,供H5调用:

let userInfo = ["name": "wb", "sex": "male", "phone": "12333434"]

注入JS变量

Native注入JS变量实现如下

let userContent = WKUserContentController.init()

let userInfo = ["name": "wb", "sex": "male", "phone": "12333434"]
for key in userInfo.keys {
    let script = WKUserScript.init(source: "var \(key) = \"\(userInfo[key]!)\"", injectionTime: .atDocumentStart, forMainFrameOnly: true)
    userContent.addUserScript(script)
}

let config = WKWebViewConfiguration.init()
config.userContentController = userContent

let wkWebView: WKWebView = WKWebView.init(frame: UIScreen.main.bounds, configuration: config)
wkWebView.navigationDelegate = self
wkWebView.uiDelegate = self
view.addSubview(wkWebView)
view.insertSubview(wkWebView, at: 0)
wkWebView.load(URLRequest.init(url: URL.init(string: "http://192.168.2.1/js.html")!))

通过遍历userInfo的keys,把key作为变量,value作为String值,注入到JS上下文中。

在H5中实现调用如下

<!DOCTYPE html>
<html>

<head>
    <title>js Bridge demo</title>
    <script type="text/javascript">
    function btnClick() {
        try {
            alert(name)
            alert(sex)
            alert(phone)
        } catch (err) {
            alert(err)
        }
    }
    </script>
</head>

<body>
    <h1>js demo test</h1>
    <p style="text-align: center;">
        <button type="button" onclick="btnClick()" style="font-size: 100px;">test JS</button>
    </p>
</body>

</html>

注入JS对象

Native注入JS对象实现如下

let userContent = WKUserContentController.init()

let userInfo = ["name": "wb", "sex": "male", "phone": "12333434"]
let jsonData = try? JSONSerialization.data(withJSONObject: userInfo, options: .prettyPrinted)
let jsonText = String.init(data: jsonData!, encoding: String.Encoding.utf8)

let script = WKUserScript.init(source: "var userInfo = \(jsonText!)", injectionTime: .atDocumentStart, forMainFrameOnly: true)
userContent.addUserScript(script)
let config = WKWebViewConfiguration.init()
config.userContentController = userContent

let wkWebView: WKWebView = WKWebView.init(frame: UIScreen.main.bounds, configuration: config)
wkWebView.navigationDelegate = self
wkWebView.uiDelegate = self
view.addSubview(wkWebView)
view.insertSubview(wkWebView, at: 0)
wkWebView.load(URLRequest.init(url: URL.init(string: "http://192.168.2.1/js.html")!))

通过把userInfo字典转化成json,作为对象赋值给userInfo,注入JS上下文中。

在H5中实现调用如下

<!DOCTYPE html>
<html>

<head>
    <title>js Bridge demo</title>
    <script type="text/javascript">
    function btnClick() {
        try {
            alert(JSON.stringify(userInfo))
        } catch (err) {
            alert(err)
        }
    }
    </script>
</head>

<body>
    <h1>js demo test</h1>
    <p style="text-align: center;">
        <button type="button" onclick="btnClick()" style="font-size: 100px;">test JS</button>
    </p>
</body>

</html>
注入JS函数

Native注入JS函数实现如下

let userContent = WKUserContentController.init()

let userInfo = ["name": "wb", "sex": "male", "phone": "12333434"]
let jsonData = try? JSONSerialization.data(withJSONObject: userInfo, options: .prettyPrinted)
let jsonText = String.init(data: jsonData!, encoding: String.Encoding.utf8)

let script = WKUserScript.init(source: "var iOSApp = {\"getUserInfo\":function(){return \(jsonText!)}}", injectionTime: .atDocumentStart, forMainFrameOnly: true)
userContent.addUserScript(script)
let config = WKWebViewConfiguration.init()
config.userContentController = userContent

let wkWebView: WKWebView = WKWebView.init(frame: UIScreen.main.bounds, configuration: config)
wkWebView.navigationDelegate = self
wkWebView.uiDelegate = self
view.addSubview(wkWebView)
view.insertSubview(wkWebView, at: 0)
wkWebView.load(URLRequest.init(url: URL.init(string: "http://192.168.2.1/js.html")!))

通过封装getUserInfo匿名函数,执行函数return我们的对象,生成全局对象iOSApp,调用iOSApp.getUserInfo()
这样写的好处是,我们的H5在调用函数的时候,可以很容易知道哪些是原生注入,防止和本地造成冲突,便于理解。

在H5中实现调用如下

<!DOCTYPE html>
<html>

<head>
    <title>js Bridge demo</title>
    <script type="text/javascript">
    function btnClick() {
        try {
            alert(JSON.stringify(iOSApp.getUserInfo()))
        } catch (err) {
            alert(err)
        }
    }
    </script>
</head>

<body>
    <h1>js demo test</h1>
    <p style="text-align: center;">
        <button type="button" onclick="btnClick()" style="font-size: 100px;">test JS</button>
    </p>
</body>

</html>

以上讲了三种方式实现用户信息的传递,都是通过WKUserContentController注入JS实现的,实际上我也可以通过WebView的evaluateJavaScript方法实现注入。

evaluateJavaScript实现注入

同样的WebView的调用H5,提供了evaluateJavaScript接口,此接口既可以执行JS函数回调结果,也可以注入JS。

下面使用接口实现JS函数的注入

let userContent = WKUserContentController.init()
let config = WKWebViewConfiguration.init()
config.userContentController = userContent

let wkWebView: WKWebView = WKWebView.init(frame: UIScreen.main.bounds, configuration: config)
wkWebView.navigationDelegate = self
wkWebView.uiDelegate = self
view.addSubview(wkWebView)
view.insertSubview(wkWebView, at: 0)
wkWebView.load(URLRequest.init(url: URL.init(string: "http://192.168.2.1/js.html")!))

...

//代理方法加载完成
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    let userInfo = ["name": "wb", "sex": "male", "phone": "12333434"]
    let jsonData = try? JSONSerialization.data(withJSONObject: userInfo, options: .prettyPrinted)
    let jsonText = String.init(data: jsonData!, encoding: String.Encoding.utf8)

    webView.evaluateJavaScript("var iOSApp = {\"getUserInfo\":function(){return \(jsonText!)}}", completionHandler: nil)
}

在WebView加载完成之后,使用evaluateJavaScript实现了JS函数的注入,H5实现调用正常。

H5传递信息给Native,调用Native分享

很多时候H5需要传递信息给我们的Native,我们Native再执行相应的逻辑。

Native实现代码如下

let userContent = WKUserContentController.init()
userContent.add(self, name: "shareAction")
let config = WKWebViewConfiguration.init()
config.userContentController = userContent

let wkWebView: WKWebView = WKWebView.init(frame: UIScreen.main.bounds, configuration: config)
wkWebView.navigationDelegate = self
wkWebView.uiDelegate = self
view.addSubview(wkWebView)
view.insertSubview(wkWebView, at: 0)
wkWebView.load(URLRequest.init(url: URL.init(string: "http://192.168.2.1/js.html")!))

...

//代理方法,window.webkit.messageHandlers.xxx.postMessage(xxx)实现发送到这里
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    print(message.body)
    print(message.name)
    print(message.frameInfo.request)
    
    if message.name == "shareAction" {
        let list = message.body as! [String: String]
        print(list["title"]!)
        print(list["content"]!)
        print(list["url"]!)
    }
}

userContent.add(self, name: "shareAction")本地添加shareAction的接口声明,当JS调用shareAction回调代理方法,实现参数捕获(WKScriptMessage)。
这样我们本地就得到了分享的传参了,然后可以调用本地SDK实现分享的逻辑了。

H5实现代码如下

<!DOCTYPE html>
<html>

<head>
    <title>js Bridge demo</title>
    <meta charset="utf-8">
    <script type="text/javascript">
    function btnClick() {
        try {
            window.webkit.messageHandlers.shareAction.postMessage({"title":"分享", "content":"内容", "url":"链接"})
        } catch (err) {
            alert(err)
        }
    }
    </script>
</head>

<body>
    <h1>js demo test</h1>
    <p style="text-align: center;">
        <button type="button" onclick="btnClick()" style="font-size: 100px;">test JS</button>
    </p>
</body>

</html>

Native告诉H5分享结果

上面实现了JS传参数给Native,但是Native怎么告诉H5分享结果呢,下面是实现逻辑。

Native实现如下

let userContent = WKUserContentController.init()
userContent.add(self, name: "shareAction")
let config = WKWebViewConfiguration.init()
config.userContentController = userContent

let wkWebView: WKWebView = WKWebView.init(frame: UIScreen.main.bounds, configuration: config)
wkWebView.navigationDelegate = self
wkWebView.uiDelegate = self
view.addSubview(wkWebView)
view.insertSubview(wkWebView, at: 0)
wkWebView.load(URLRequest.init(url: URL.init(string: "http://192.168.2.1/js.html")!))

...

//代理方法,window.webkit.messageHandlers.xxx.postMessage(xxx)实现发送到这里
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    print(message.body)
    print(message.name)
    print(message.frameInfo.request)
    
    if message.name == "shareAction" {
        let list = message.body as! [Any]
        
        let dict = list[0] as! [String: String]
        print(dict["title"]!)
        print(dict["content"]!)
        print(dict["url"]!)
        
        let shareSucc = list[1] as! String//获取回调JS,通知H5分享成功了
        let script = "\(shareSucc)(true)"
        wkWebView?.evaluateJavaScript(script, completionHandler: nil)
    }
}

获取shareSucc的函数回调名称,在合适的时候我们可以通过这个JS函数回调,告诉H5我们的分享结果。

JS实现如下

<!DOCTYPE html>
<html>

<head>
    <title>js Bridge demo</title>
    <meta charset="utf-8">
    <script type="text/javascript">
    function shareSucc(isShare) {
        alert(isShare)
    }

    function btnClick() {
        try {
            window.webkit.messageHandlers.shareAction.postMessage([{ "title": "分享", "content": "内容", "url": "链接" }, "shareSucc"])
        } catch (err) {
            alert(err)
        }
    }
    </script>
</head>

<body>
    <h1>js demo test</h1>
    <p style="text-align: center;">
        <button type="button" onclick="btnClick()" style="font-size: 100px;">test JS</button>
    </p>
</body>

</html>

之前postMessage是发送的字典,由于我们的需求增多了,所以还是改成数组。
最后发送shareSucc的字符串,告诉Native我们有一个shareSucc的函数可以接收分享的结果。

JS和Native统一封装

上面讲了JS回调Native,Native回调JS,实现了我们常用的一些业务逻辑。
里面有很多重复的代码,实现起来也不友好,下面我们把这些重用的全部封装一下,改成好用的接口给上层,使Native和JS的开发人员都不用操心太多的实现细节。

H5界面的代码

<!DOCTYPE html>
<html>

<head>
    <title>js Bridge demo</title>
    <meta charset="utf-8">
    <script type="text/javascript">
    function shareSucc(isShare) {
        alert(isShare)
    }

    function reqUserInfoClick() {
        try {
            alert(iOSApp.getUserInfo())
        } catch (err) {
            alert(err)
        }
    }

    function reqShareClick() {
        try {
            iOSApp.shareAction("分享title", "分享content", "分享url", "shareSucc")
        } catch (err) {
            alert(err)
        }
    }
    </script>
</head>

<body>
    <h1>js demo test</h1>
    <p style="text-align: center;">
        <button type="button" onclick="reqUserInfoClick()" style="font-size: 100px;">获取用户信息</button>
        <button type="button" onclick="reqShareClick()" style="font-size: 100px;">执行分享</button>
    </p>
</body>

</html>

构造基础类JWebViewController

//
//  JWebViewController.swift
//  JSBridgeTest
//
//  Created by jackyshan on 2018/9/26.
//  Copyright © 2018年 GCI. All rights reserved.
//

import UIKit
import WebKit

class JWebViewController: UIViewController, WKUIDelegate, WKScriptMessageHandler {

    private var mAsyncScriptArray:[JKWkWebViewHandler] = []
    private var mSyncScriptArray:[JKWkWebViewHandler] = []
    
    private var wkWebView: WKWebView?

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }
    
    public func startUrl(_ url: URL) {
        let configuretion = WKWebViewConfiguration()
        configuretion.preferences = WKPreferences()
        configuretion.preferences.javaScriptEnabled = true
        configuretion.userContentController = WKUserContentController()
        if self.mAsyncScriptArray.count != 0 || self.mSyncScriptArray.count != 0 {
            // 在载入时就添加JS // 只添加到mainFrame中
            let script = WKUserScript(source: createScript(), injectionTime: .atDocumentStart, forMainFrameOnly: true)
            configuretion.userContentController.addUserScript(script)
        }

        //异步需要回调,所以需要添加handler
        for item in self.mAsyncScriptArray {
            configuretion.userContentController.add(self, name: item.name)
        }
        
        let wkWebView = WKWebView(frame: self.view.bounds, configuration: configuretion)
        wkWebView.uiDelegate = self
        self.view.insertSubview(wkWebView, at: 0)
        let request = URLRequest(url: url)
        wkWebView.load(request)
        self.wkWebView = wkWebView
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        
        //释放handler
        for item in self.mAsyncScriptArray {
            wkWebView?.configuration.userContentController.removeScriptMessageHandler(forName: item.name)
            wkWebView?.configuration.userContentController.removeAllUserScripts()
        }
    }
    
    // MARK: - 添加JS
    public func addAsyncJSFunc(functionName: String, parmers: [String], action: @escaping ([String:AnyObject]) -> Void) {
        var obj = self.mAsyncScriptArray.filter { (obj) -> Bool in
            return obj.name == functionName
        }.first
        
        if obj == nil {
            obj = JKWkWebViewHandler()
            obj!.name = functionName
            obj!.parmers = parmers
            obj!.action = action
            self.mAsyncScriptArray.append(obj!)
        }
    }

    public func addSyncJSFunc(functionName: String, parmers: [String]) {
        var obj = self.mSyncScriptArray.filter { (obj) -> Bool in
            return obj.name == functionName
            }.first
        
        if obj == nil {
            obj = JKWkWebViewHandler()
            obj!.name = functionName
            obj!.parmers = parmers
            self.mSyncScriptArray.append(obj!)
        }
    }
    
    // MARK: - 插入JS
    private func createScript() -> String {
        var result = "iOSApp = {"
        for item in self.mAsyncScriptArray {
            let pars = createParmes(dict: item.parmers)
            let str = "\"\(item.name!)\":function(\(pars)){window.webkit.messageHandlers.\(item.name!).postMessage([\(pars)]);},"
            result += str
        }
        for item in self.mSyncScriptArray {
            let pars = createParmes(dict: item.parmers)
            let str = "\"\(item.name!)\":function(){return JSON.stringify(\(pars));},"
            result += str
        }
        result = (result as NSString).substring(to: result.count - 1)
        result += "}"
        print("++++++++\(result)")
        return result
    }
    
    private func createParmes(dict: [String]) -> String {
        var result = ""
        for key in dict {
            result += key + ","
        }
        if result.count > 0 {
            result = (result as NSString).substring(to: result.count - 1)
        }
        return result
    }

    // MARK: - 执行JS
    public func actionJsFunc(functionName: String, pars: [AnyObject], completionHandler: ((Any?, Error?) -> Void)?) {
        var parString = ""
        for par in pars {
            parString += "\(par),"
        }
        
        if parString.count > 0 {
            parString = (parString as NSString).substring(to: parString.count - 1)
        }
        
        let function = "\(functionName)(\(parString));"
        wkWebView?.evaluateJavaScript(function, completionHandler: completionHandler)
    }

    // MARK: - WKUIDelegate
    public func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
        let alert = UIAlertController(title: "提示", message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "确定", style: .default, handler: { (_) -> Void in
            // We must call back js
            completionHandler()
        }))
        
        self.present(alert, animated: true, completion: nil)
    }

    // MARK: - WKScriptMessageHandler
    public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        
        let funcObjs = self.mAsyncScriptArray.filter { (obj) -> Bool in
            return obj.name == message.name
        }
        
        if let funcObj = funcObjs.first {
            let pars = message.body as! [AnyObject]
            var dict: [String: AnyObject] = [:]
            for i in 0..<funcObj.parmers.count {
                let key = funcObj.parmers[i]
                if pars.count > i {
                    dict[key] = pars[i]
                }
            }
            
            funcObj.action?(dict)
        }
    }
}

class JKWkWebViewHandler: NSObject {
    fileprivate var name:String!
    fileprivate var parmers:[String]!
    fileprivate var action:(([String:AnyObject]) -> Void)?
}

继承JWebViewController,实现业务

//
//  ViewController.swift
//  JSBridgeTest
//
//  Created by jackyshan on 2018/9/26.
//  Copyright © 2018年 GCI. All rights reserved.
//

import UIKit

class ViewController: JWebViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        let userInfo = ["name": "wb", "sex": "male", "phone": "12333434"]
        let jsonData = try? JSONSerialization.data(withJSONObject: userInfo, options: .prettyPrinted)
        let jsonText = String.init(data: jsonData!, encoding: String.Encoding.utf8)
        
        //添加getUserInfo脚本,返回用户信息
        addSyncJSFunc(functionName: "getUserInfo", parmers: [jsonText!])
        
        //添加shareAction脚本,获得分享参数
        addAsyncJSFunc(functionName: "shareAction", parmers: ["name", "sex", "phone", "shareBack"]) { [weak self] (dict) in
            print(dict["name"]!)
            print(dict["sex"]!)
            print(dict["phone"]!)
            
            //执行shareBack脚本,告诉H5分享结果
            self?.actionJsFunc(functionName: dict["shareBack"] as! String, pars: [true as AnyObject], completionHandler: nil)
        }
        
        //开始加载H5
        startUrl(URL.init(string: "http://192.168.2.1/js.html")!)
        
    }
    
}

讲解JWebViewController

构造JKWkWebViewHandler类,存储信息

class JKWkWebViewHandler: NSObject {
    fileprivate var name:String!
    fileprivate var parmers:[String]!
    fileprivate var action:(([String:AnyObject]) -> Void)?
}

添加JS,使用JKWkWebViewHandler存储

public func addAsyncJSFunc(functionName: String, parmers: [String], action: @escaping ([String:AnyObject]) -> Void) {
    var obj = self.mAsyncScriptArray.filter { (obj) -> Bool in
        return obj.name == functionName
    }.first
    
    if obj == nil {
        obj = JKWkWebViewHandler()
        obj!.name = functionName
        obj!.parmers = parmers
        obj!.action = action
        self.mAsyncScriptArray.append(obj!)
    }
}

public func addSyncJSFunc(functionName: String, parmers: [String]) {
    var obj = self.mSyncScriptArray.filter { (obj) -> Bool in
        return obj.name == functionName
        }.first
    
    if obj == nil {
        obj = JKWkWebViewHandler()
        obj!.name = functionName
        obj!.parmers = parmers
        self.mSyncScriptArray.append(obj!)
    }
}

创建JS脚本,使用iOSApp对象封装,异步回调传回Native的函数window.webkit.messageHandlers.xxx直接封装在JS函数中。
这样有一个好处,H5调用JS,直接iOSApp.xxx(xxx)就行了,不需要写window.webkit.messageHandlers.xxx这些代码。
这对于H5来说,跟平时写的JS脚本没有什么区别,方便了调用。
对于Native来说,帮H5做了JS的回调的封装,并通过handler回调得到自己想要的参数,通过这个封装,两端的工作都只需要关注
业务层就行了,继承JWebViewController,可以专心写业务逻辑。

private func createScript() -> String {
        var result = "iOSApp = {"
        for item in self.mAsyncScriptArray {
            let pars = createParmes(dict: item.parmers)
            let str = "\"\(item.name!)\":function(\(pars)){window.webkit.messageHandlers.\(item.name!).postMessage([\(pars)]);},"
            result += str
        }
        for item in self.mSyncScriptArray {
            let pars = createParmes(dict: item.parmers)
            let str = "\"\(item.name!)\":function(){return JSON.stringify(\(pars));},"
            result += str
        }
        result = (result as NSString).substring(to: result.count - 1)
        result += "}"
        print("++++++++\(result)")
        return result
}

构造JS,实现传参给H5页面

public func actionJsFunc(functionName: String, pars: [AnyObject], completionHandler: ((Any?, Error?) -> Void)?) {
    var parString = ""
    for par in pars {
        parString += "\(par),"
    }
    
    if parString.count > 0 {
        parString = (parString as NSString).substring(to: parString.count - 1)
    }
    
    let function = "\(functionName)(\(parString));"
    wkWebView?.evaluateJavaScript(function, completionHandler: completionHandler)
}

注入JS脚本到WKWebViewConfiguration中

let configuretion = WKWebViewConfiguration()
configuretion.preferences = WKPreferences()
configuretion.preferences.javaScriptEnabled = true
configuretion.userContentController = WKUserContentController()
if self.mAsyncScriptArray.count != 0 || self.mSyncScriptArray.count != 0 {
    // 在载入时就添加JS // 只添加到mainFrame中
    let script = WKUserScript(source: createScript(), injectionTime: .atDocumentStart, forMainFrameOnly: true)
    configuretion.userContentController.addUserScript(script)
}

//异步需要回调,所以需要添加handler
for item in self.mAsyncScriptArray {
    configuretion.userContentController.add(self, name: item.name)
}

let wkWebView = WKWebView(frame: self.view.bounds, configuration: configuretion)

合适的时候释放JS的handler,注意不释放的话,Controller不会调用deinit,发生内存泄露。

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    
    //释放handler
    for item in self.mAsyncScriptArray {
        wkWebView?.configuration.userContentController.removeScriptMessageHandler(forName: item.name)
        wkWebView?.configuration.userContentController.removeAllUserScripts()
    }
}

代码示例放到Github了,有需要的可以下载查看。

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,534评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,457评论 2 59
  • 秋天已快翩然而至.催老了这一季的秋容..夏天已逐渐远行.爱你已然随风而来..树上的叶子萧萧而下.带着我与你的点点滴...
    枫叶_禁区阅读 159评论 0 0
  • 今天想聊聊我为什么在简书写作?如果你也在写文章,想做个人品牌,想发展路径,想突破自己,希望今天的内容能给你一些参考...
    乐学艺术中心阅读 271评论 5 9
  • (11)飞驰的摩托 我父亲徐世光从摩托车在我们村出现的第一天,便展现出了强烈的好奇心。我在许多个下午路过十字路口时...
    7M奇案馆阅读 615评论 0 2