HyBrid APP简单入门之JS与Java互调以及JSBridge的实现

前言

HyBrid俗称混合开发。使用Android提供的组件——WebView,去加载放在本地或者服务器用h5编写的UI界面;js与java之间的互调,使得HyBrid APP的体验更加趋向原生APP。本章内容主要讲述在h5页面调用Java方法、在Android里调用h5里的js方法和jsbridge的简单实现。因此,拥有h5、css和js更容易上手。

三种APP开发方式比较

三种app比较.png

安全漏洞

在Android 4.2以下的WebView有个安全漏洞,外部网页通过得到Runtime对象,然后执行系统命令得到信息,原因出在addJavascriptInterface()方法。下面是漏洞的简单描述:
1、向WebView注册了一个叫“InterfaceName”的对象
2、js中可以访问到“InterfaceName”对象
3、js中通过“getClass”方法获取该对象的类型类
4、通过反射机制,得到该类的Runtime对象
5、调用静态方法执行系统命令
核心代码示例:

<script type="text/javascript">
            function execute(cmd) {
                return demo.getClass().forName('java.lang.Runtime').getMethod('getRuntime', null).invoke(null, null).exec(cmd);
            }
            
            execute(["ls", "/mnt/sdcard"]);
        </script>

解决方案:

  • Android 4.2以上:@JavascriptInterface
  • Android 4.2以下:自定义js和Android交互方式

因此,在讲述js与java互调时基于Android 4.2以上。这个了解了解就好。

项目结构

js与java互调项目结构.png

JS调用Java方法

main目录,选择new->Folder->Assets Folder,完成assets目录创建。然后新建一个文件夹,命名为jscalljava。接着新建一个空白的html文件命名为index

新建一个空Activity,命名为JSAndJavaActivity,且设置为启动Activity,代码如下:

public class JSAndJavaActivity extends AppCompatActivity {

    private WebView webView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_jsand_java);

        webView = findViewById(R.id.web_view);

        WebSettings settings = webView.getSettings();
        settings.setJavaScriptEnabled(true);

        // 第二个参数可以简单理解为android表示AndroidAndrJsInterface的对象
        // 在js,通过它调用AndroidAndrJsInterface类下的方法
        // 名字可以自定义
        webView.addJavascriptInterface(new AndroidAndrJsInterface(), "android");
        webView.setWebViewClient(new WebViewClient());
        webView.loadUrl("file:///android_asset/jscalljava/index.html");
    }

    class AndroidAndrJsInterface {

        // 该注解可以解决Android 4.2以上的安全漏洞,4.2以下没有这个注解
        @JavascriptInterface
        public void showToast() {
            Toast.makeText(JSAndJavaActivity.this, "我被js调用了", Toast.LENGTH_LONG).show();
        }

        @JavascriptInterface
        public void showToast(String info) {
            Toast.makeText(JSAndJavaActivity.this, "来自js的消息:" + info, Toast.LENGTH_LONG).show();
        }
    }
}

布局文件activity_jsand_java.xml的代码如下:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".JSAndJavaActivity">

    <WebView
        android:id="@+id/web_view"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginBottom="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    
</android.support.constraint.ConstraintLayout>

index.html添加如下代码:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
        <style type="text/css">
            div {
                margin: 0 auto;
                width: 200px;
            }
            button {
                width: 150px;
                font-weight: bolder;
                font-family: "微软雅黑";
            }
        </style>
    </head>
    <body>
        <div>
            <p>
                <button onclick="android.showToast()">调用Java无参方法</button>
            </p>
            <p>
                <button onclick="android.showToast('I am come from js.')">调用Java有参方法</button>
            </p>
        </div>
    </body>
</html>

Java调用JS方法

示例代码主要演示以下内容:

  • Android调用js的无参函数
  • Android调用js的有参函数
  • Android调用js的函数并获取返回值

在assets目录下新建文件夹,命名为javacalljs。然后新建一个空的html,命名为index

新建一个空Activity,命名为JavaAndJSActivity,且设置为启动Activity,代码如下:

public class JavaAndJSActivity extends AppCompatActivity implements View.OnClickListener {

    private Button btnNoParamter;
    private Button btnYesParamter;
    private Button btnNoParamterAndReturn;

    // 加载网页或者说H5页面
    private WebView webView;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_java_and_js);

        btnNoParamter = findViewById(R.id.btn_no_parameter);
        btnYesParamter = findViewById(R.id.btn_yes_parmater);
        btnNoParamterAndReturn = findViewById(R.id.btn_no_parmater_return);

        btnNoParamter.setOnClickListener(this);
        btnYesParamter.setOnClickListener(this);
        btnNoParamterAndReturn.setOnClickListener(this);

        webView = new WebView(this);

        WebSettings settings = webView.getSettings();
        settings.setJavaScriptEnabled(true); // 设置支持js脚本语言
        settings.setUseWideViewPort(true); // 支持双击-前提是页面要支持才显示
        settings.setBuiltInZoomControls(true); // 支持缩放按钮-前提是页面要支持才显示

        webView.setWebViewClient(new WebViewClient()); // 不跳转到默认浏览器
        webView.setWebChromeClient(new WebChromeClient()); // 支持js弹窗

        webView.addJavascriptInterface(new GetJsResult(), "Result");

        // 加载本地文件:file:///android_asset/文件具体路径
        // 网络资源,如:http://www.baidu.com
        // 此处asset后面是没有s的
        webView.loadUrl("file:///android_asset/javacalljs/index.html"); // 加载网络资源(需要网络权限),也可以时assets目录下的资源

        // 加载h5写的页面,会替换当前原生页面,在这里不需要
//        setContentView(webView);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            // 格式:WebView.loadUrl("javascript:js方法")
            case R.id.btn_no_parameter:
                // 调用js无参函数
                webView.loadUrl("javascript:noParamter()");
                break;
            case R.id.btn_yes_parmater:
                String info = "Hello,I am come from java.";
                // 调用js有参参数
                // 传递字符串要加个单引号,数字可以不加;传递数组可以传递json格式的字符串
                webView.loadUrl("javascript:yesParamter('" + info + "')");
                break;
            case R.id.btn_no_parmater_return:
                webView.loadUrl("javascript:returnResult()");
                break;
            default:
                break;
        }
    }

    class GetJsResult {
        @JavascriptInterface
        public void getResult(String res) {
            Toast.makeText(JavaAndJSActivity.this, "js返回的结果:" + res, Toast.LENGTH_SHORT).show();
        }
    }

}

布局文件activity_java_and_js.xml的代码如下:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">


    <Button
        android:id="@+id/btn_no_parameter"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:text="调用无参方法"
        app:layout_constraintBottom_toTopOf="@+id/btn_yes_parmater"
        app:layout_constraintEnd_toEndOf="@+id/btn_yes_parmater"
        app:layout_constraintStart_toStartOf="@+id/btn_yes_parmater" />

    <Button
        android:id="@+id/btn_yes_parmater"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginBottom="8dp"
        android:text="调用有参方法"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btn_no_parmater_return"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="调用无参且有返回值"
        app:layout_constraintEnd_toEndOf="@+id/btn_yes_parmater"
        app:layout_constraintStart_toStartOf="@+id/btn_yes_parmater"
        app:layout_constraintTop_toBottomOf="@+id/btn_yes_parmater" />
</android.support.constraint.ConstraintLayout>

index.html添加如下代码:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>java调用js</title>
        <script type="text/javascript">
            function noParamter() {
                alert("我是js无参函数");
            }
            
            function yesParamter(info) {
                alert("来自java的信息:" + info);
            }
            
            function returnResult() {
                var a = "我处理完了。"
                // 将结果返回给Android
                window.Result.getResult(a);
            }
        </script>
    </head>
    <body>
    </body>
</html>

小结:java和js互调的基本操作就讲完了,一些要注意地方已经在代码中注释了。

JSBridge的实现

JSBridge位置处于js和java之间,如下图所示:

jsbridge位置.png

在前面也曾提过,在Android 4.2以下的addJavascriptInterface()方法存在漏洞,其解决方案是JSBridge。简单来说就是自定义协议,暴漏对app没影响的信息,有影响的隐藏掉。当然,Android 4.2以上也可以使用这个方案。

java调用js依然采用WebView.loadUrl(),而js调用java就不能再采用addJavascriptInterface()了,需要换一个思路。WebChromeClient类,我们已经接触过了,它允许app显示js的弹窗。常见的弹窗有alert(警告框)、confirm(确认框)和prompt(提示框),前两个出现的频率相对后者更高,而prompt更加适合用来传递信息到Android,以下是它们在Android对应的源码实现:

/**
     * Tell the client to display a javascript alert dialog.  If the client
     * returns {@code true}, WebView will assume that the client will handle the
     * dialog.  If the client returns {@code false}, it will continue execution.
     * @param view The WebView that initiated the callback.
     * @param url The url of the page requesting the dialog.
     * @param message Message to be displayed in the window.
     * @param result A JsResult to confirm that the user hit enter.
     * @return boolean Whether the client will handle the alert dialog.
     */
    public boolean onJsAlert(WebView view, String url, String message,
            JsResult result) {
        return false;
    }

    /**
     * Tell the client to display a confirm dialog to the user. If the client
     * returns {@code true}, WebView will assume that the client will handle the
     * confirm dialog and call the appropriate JsResult method. If the
     * client returns false, a default value of {@code false} will be returned to
     * javascript. The default behavior is to return {@code false}.
     * @param view The WebView that initiated the callback.
     * @param url The url of the page requesting the dialog.
     * @param message Message to be displayed in the window.
     * @param result A JsResult used to send the user's response to
     *               javascript.
     * @return boolean Whether the client will handle the confirm dialog.
     */
    public boolean onJsConfirm(WebView view, String url, String message,
            JsResult result) {
        return false;
    }

    /**
     * Tell the client to display a prompt dialog to the user. If the client
     * returns {@code true}, WebView will assume that the client will handle the
     * prompt dialog and call the appropriate JsPromptResult method. If the
     * client returns false, a default value of {@code false} will be returned to to
     * javascript. The default behavior is to return {@code false}.
     * @param view The WebView that initiated the callback.
     * @param url The url of the page requesting the dialog.
     * @param message Message to be displayed in the window.
     * @param defaultValue The default value displayed in the prompt dialog.
     * @param result A JsPromptResult used to send the user's reponse to
     *               javascript.
     * @return boolean Whether the client will handle the prompt dialog.
     */
    public boolean onJsPrompt(WebView view, String url, String message,
            String defaultValue, JsPromptResult result) {
        return false;
    }

当app接受到要显示js的弹窗时,会根据弹窗的类型执行相应的方法,如prompt()对应onJsPrompt()。所以,我们可以重写onJsPrompt()方法,请求处理完后将其拦截,也就是返回true,那这个弹窗就不会显示了。换句话说,可以在这调用已经写好的Java方法。

接下就是要解决自定义协议了。我们可以模仿http的url格式,http://host:port/param=value,转换过来,JSBridge://className:callbackAddress/methodName?jsonObj。js向Android发送信息(url)必须按这个格式,而Java层只处理符合这个协议(格式)的请求,其它的一概不处理。下面对这个协议进行解释:

  • JSBridge:便于检验该url是否合格
  • className:要暴露出去的类的名字,但它不是js要调用的目标类,在本demo中是JSBridge
  • callbackAddress:js回调函数存在数组的位置,也就是下标
  • methodName:js要调用的方法,它的具体参数(比如个数)是无法得知的,在本demo中是showToast
  • jsonObj:真正传递给Android的信息,要求是json格式的字符串,至于具体是什么格式看需求了

最后,将协议转换成代码。

根据上述的项目结构图,在assets/jsbridge目录下新建空白的index.htmlJSBridge.js文件。新建CallBack类,负责将Java方法的执行结果通知js,其代码如下:

public class CallBack {

    private String mPort;

    private WebView mWebView;

    public CallBack(WebView webView, String mPort) {
        this.mPort = mPort;
        this.mWebView = webView;
    }

    /**
     * 通知js
     * @param jsonObject Java层处理完后返回给js层的信息
     */
    public void apply(JSONObject jsonObject) {
        if (mWebView != null) {
            mWebView.loadUrl("javascript:onAndroidFinished('" + mPort + "', " + String.valueOf(jsonObject) + ")");
        }
        Log.d("TAG", "CallBack:apply");
    }
}

新建Methods类,用于封装供js调用的方法且有以下约定:

  • 方法必须是publicstatic
  • 参数必须有3
  • 第一个参数必须是WebView,第二个参数必须是JSONObject,第三个参数必须是CallBack

只有满足以上三个条件的方法才能被js调用,才会暴露出去。其代码如下:

public class Methods {

    public static void showToast(WebView view, JSONObject param, CallBack callBack) {
        // 解析得到key=msg的值
        String message = param.optString("msg");

        Toast.makeText(view.getContext(), message, Toast.LENGTH_SHORT).show();

        if (callBack != null) {
            try {
                JSONObject result = new JSONObject();
                result.put("key", "value");
                result.put("key1", "value1");
                callBack.apply(result);
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
    }

}

新建JSBridge类,负责管理暴露给js的类和方法,以及根据js传入的url内容找到对应的java类,并执行指定的Java方法,代码如下:

public class JSBridge {

    // 存储需要暴露给js的方法
    private static Map<String, HashMap<String, Method>> exposedMethods = new HashMap<>();

    /**
     * 注册要暴露的类
     * @param exposeName JSBridge
     * @param classz 要暴露的类
     */
    public static void register(String exposeName, Class<?> classz) {
        // 将符合要求的classz类中的所有方法添加到exposedMethods中
        if (!exposedMethods.containsKey(exposeName)) {
            exposedMethods.put(exposeName, getAllMethod(classz));
        }
        Log.d("TAG", "JSBridge:register");
    }

    private static HashMap<String, Method> getAllMethod(Class injectedCls) {
        HashMap<String, Method> methodHashMap = new HashMap<>();

        // 获取该类的所有方法
        Method[] methods = injectedCls.getDeclaredMethods();

        for (Method method : methods) {
            // 剔除不符合要求的方法
            if (method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || method.getName() == null) {
                continue;
            }

            // 方法的参数
            Class[] paramters = method.getParameterTypes();
            // 进一步寻找符合要求的方法
            if (paramters != null && paramters.length == 3) {
                if (paramters[0] == WebView.class && paramters[1] == JSONObject.class && paramters[2] == CallBack.class) {
                    methodHashMap.put(method.getName(), method);
                }
            }
        }

        return methodHashMap;
    }

    /**
     * 调用相应的java方法去处理js的请求
     * @param webView WebView
     * @param urlString 根据协议,js层给java传递的信息
     * @return null
     */
    public static String callJava(WebView webView, String urlString) {
        String className = "";
        String methodName = "";
        String param = "";
        String port = "";

        // 验证该urlString是否符合协议的基本要求
        if (!urlString.equals("") && urlString != null && urlString.startsWith("JSBridge")) {
            Uri uri = Uri.parse(urlString);
            className = uri.getHost();   // 要调用的类
            param = uri.getQuery();      // js层给Java层传递的信息(json格式)
            port = uri.getPort() + "";   // js层回调函数的地址
            methodName = uri.getPath().replace("/", "");  // 要调用的方法

            if (exposedMethods.containsKey(className)) {
                // 找到该类的所有符合要求的方法
                HashMap<String, Method> methodHashMap = exposedMethods.get(className);

                if (methodHashMap != null && methodHashMap.size() != 0 && methodHashMap.containsKey(methodName)) {
                    // 根据方法名找到指定的方法
                    Method method = methodHashMap.get(methodName);
                    if (method != null) {
                        try {
                            // 在这里真正处理js的请求,CallBack用于告诉js层我的活干完了,该你了
                            method.invoke(null, webView, new JSONObject(param), new CallBack(webView, port));
                            Log.d("TAG", "JSBridge:callJava");
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }

        return null;
    }

}

新建JSBridgeChromeClient类且继承WebChromeClient,在此类处理js的请求,代码如下:

public class JSBridgeChromeClient extends WebChromeClient {

    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        // 在简单讲述是否调用result.confirm()的区别
        // 如果调用,js的回调函数才会被调用,参数的值是返回给js的
        // 如果没调用,js即使有回调函数也不会执行
        // 可以使用console.log()的方式来调式js,项目运行起来后可在run窗口查看
        result.confirm(JSBridge.callJava(view, message));
//        JSBridge.callJava(view, message);
        Log.d("TAG", "JSBridgeChromeClient");

        return true;
    }
}

新建JSBridgeActivity且设置为启动Activity,代码如下:

public class JSBridgeActivity extends AppCompatActivity {

    private WebView webView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_jsbridge);

        webView = findViewById(R.id.webView);
        webView.getSettings().setJavaScriptEnabled(true);
        webView.setWebViewClient(new WebViewClient());
        webView.setWebChromeClient(new JSBridgeChromeClient());
        webView.loadUrl("file:///android_asset/jsbridge/index.html");

        JSBridge.register("JSBridge", Methods.class);
        Log.d("TAG", "JSBridgeActivity");
    }
}

布局文件activity_jsbridge.xml的代码如下:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".jsbridge.JSBridgeActivity">

    <WebView
        android:id="@+id/webView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginBottom="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    
</android.support.constraint.ConstraintLayout>

JSBridge.js的代码如下:

var callbacks = new Array();

/**
 * js层调用Android层方法
 * @param {Object} obj Android层的类
 * @param {Object} method 该obj中的那个方法
 * @param {Object} params 使用json的数据格式给Android传递信息
 * @param {Object} callback js层的回调方法,当Android层处理好了js层要如何处理
 */
function jsCallAndroid(obj, method, params, callback) {
    // 保存callback回调函数
    var port = callbacks.length;
    callbacks[port] = callback;
    
    // 组合出符合规则的url,并传递给Java层
    var url = 'JSBridge://' + obj + ':' + port + '/' + method + '?' + JSON.stringify(params);
    
    window.prompt(url);
}

/**
 * 当js调用完Android层时执行
 * @param {Object} port 回调函数的地址,也就是在数组中的位置
 * @param {Object} jsonObj 从Android层传过来的参数
 */
function onAndroidFinished(port, jsonObj) {
    // 从callbacks取出对应的回调函数
    var callback = callbacks[port];
    
    callback(jsonObj);
    
    // 从callbacks中删除callback
    delete callbacks[port];
}

index.html的代码如下:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
        <script type="text/javascript" src="JSBridge.js" ></script>
    </head>
    <body>
        <button onclick="jsCallAndroid('JSBridge', 'showToast',
            {'msg':'hello I come from js.'},function(res) {alert(JSON.stringify(res))})">Js调用Android</button>
    </body>
</html>

对函数的调用过程进行概括:点击按钮,触发jsCallAndroid()方法,通过调用window.prompt(url)向Android发送请求(信息)。然后在JSBridgeChromeClient.onJsPrompt()方法对请求进行拦截处理,就是实现了js调用java,JSBridge.callJava(view, message)。执行到callJava()方法内部,会调用Methods.showToast()方法,紧接着会调用CallBack.apply(result)方法,最后是调用js的onAndroidFinished()方法。如果JsPromptResult.confirm()被调用了,js的回调函数会被同步调用。

总结

本章的内容就将完了。

HyBridDemo

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

推荐阅读更多精彩内容

  • 在Android开发中,能够实现Web调用Native代码的方法主要有以下方法:1.Schema:WebView拦...
    Drc15H阅读 1,702评论 0 5
  • 链接:https://www.jianshu.com/p/fd61e8f4049e 一、简介 这部分主要介绍下 W...
    柒黍阅读 1,779评论 0 4
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,350评论 0 17
  • 更多文章请关注:开发者技术前线 Android开发目前现状来说,开发者大部分时间花在UI的屏幕适配上,使用原生控件...
    Tamic阅读 17,593评论 19 80
  • WebView·开车指南 2016-08-31BugDev 北京市东城区首席Bug布道师开山之作,一整月交通事故血...
    53c021c38a1d阅读 825评论 0 1