WebView交互架构项目实战(三):多进程WebView使用实践

*本文介绍自己在使用WebView的过程中遇到的一些问题的解决方法和对WebView的一些优化实践*

*浏览器缓存知识介绍:*

浏览器缓存之 Expires , max-age, Etag , Last-Modified (其中Expires,max-age是客户端在这个时间之前不去向服务器端发送请求验证资源是否有更新,
Etag, Last-Modified是服务器决定是否需要返回资源,未更新的资源不需要返回)

Expires
  http/1.0中定义的header,是最基础的浏览器缓存处理,表示资源在一定时间内从浏览器的缓存中获取资源,不需要请求服务器验证获取资源,从而达到快速获取资源,缓解服务器压力的目的。
  在response的header中的格式为:Expires: Thu, 01 Dec 1994 16:00:00 GMT (必须是GMT格式)
应用:
  1、可以在html页面中添加<meta http-equiv="Expires" content="Thu, 01 Dec 1994 16:00:00"/> 来给页面设置缓存时间。
  2、对于图片、css等文件则需要在IIS或者apache等运行容器中进行规则配置来让容器在请求资源的时候添加在responese的header中。

max-age
Cache-Control中设置资源在本地缓存时间的一个值,单位为:秒(s),其他值还有private、no-cache、must-revalidate等
Last-modified

望文知义,根据这个词条的直译应该是上次修改(时间),通过修改服务器端的文件后再请求,发现response的header中的Last-modified改变了
更新原理:
  1、在浏览器首次请求某个资源时,服务器端返回的状态码是200 (ok),内容是你请求的资源,同时有一个Last-Modified的属性标记(Reponse Header),标识此文件在服务期端最后被修改的时间,格式:Last-Modified:Tue, 24 Feb 2009 08:01:04 GMT
  2、浏览器第二次请求该资源时,根据HTTP协议的规定,浏览器会向服务器传送If-Modified-Since报头(Request Header),询问该文件是否在指定时间之后有被修改过,格式为:If-Modified-Since:Tue, 24 Feb 2009 08:01:04 GMT
  3、如果服务器端的资源没有变化,则服务器返回304状态码(Not Modified),内容为空,这样就节省了传输数据量。当服务器端代码发生改变,则服务器返回200状态码(ok),内容为请求的资源,和第一次请求资源时类似。从而保证在资源没有修改时不向客户端重复发出资源,也保证当服务器有变化时,客户端能够及时得到最新的资源。

注:如果If-Modified-Since的时间比服务器当前时间(当前的请求时间request_time)还晚,会认为是个非法请求
ETag

http/1.1 中增加的header,HTTP协议规格说明定义ETag为“被请求变量的实体值” 。另一种说法是,ETag是一个可以与Web资源关联的记号(token)。典型的Web资源可以一个Web页,但也可能是JSON或XML文档。服务器单独负责判断记号是什么及其含义,并在HTTP响应头中将其传送到客户端。
ETag的格式
不同类型的Web服务器生成ETag的策略以及生成的格式是不同的:
1、apache1.3和2.x的Etag格式是:inode-size-timestamp。
2、IIS5.0和6.0的Etag格式为Filetimestamp:Changenumber。
更新原理:
1、当浏览器首次请求资源的时候,服务器会返回200的状态码(ok),内容为请求的资源,同时response header会有一个ETag标记,该标记是服务器端根据容器(IIS或者Apache等等)中配置的ETag生成策略生成的一串唯一标识资源的字符串,ETag格式为 ETag:"856247206"
2、当浏览器第2次请求该资源时,浏览器会在传递给服务器的request中添加If-None-Match报头,询问服务器改文件在上次获取后是否修改了,报头格式:If-None-Match:"856246825"
3、服务器在获取到浏览器的请求后,会根据请求的资源查找对应的ETag,将当前服务器端指定资源对应的Etag与request中的If-None-Match进行对比,如果相同,说明资源没有修改,服务器返回304状态码(Not Modified),内容为空;如果对比发现不相同,则返回200状态码,同时将新的Etag添加到返回浏览器的response中。

几者之间的关系

Expires 与max-age
Expires存在HTTP 1.0 版本, 标识本地缓存的截止时间,允许客户端在这个时间之前不去向服务器端发送请求验证资源是否有更新
max-age是HTTP 1.1版本新增的, 标识资源可以在本地缓存多少秒,存储的是更新间隔。
Expires 的一个缺点就是,返回的到期时间是服务器端的时间,这样存在一个问题,如果浏览器所在机器的时间与服务器的时间相差很大,那么误差就很大,所以在HTTP 1.1版开始,使用Cache-Control: max-age替代。
注: 如果max-age和Expires同时存在,则被Cache-Control的max-age覆盖。
Expires =max-age + “每次下载时的当前的request时间”
所以一旦重新下载的页面后,expires就重新计算一次,但last-modified不会变化
Last-Modified和Expires
  使用Last-Modified标识在资源未修改时返回的response内容为空,可以节省一点带宽,但是还是逃不掉发一个HTTP请求出去,需要浏览器连接一次服务器端。
而Expires标识却使得浏览器干脆连HTTP请求都不用发,但是当用户强制刷新的时候,就算URI设置了Expires,浏览器也会发一个HTTP请求给服务器端验证资源的更新,所以,Last-Modified还是要用的,而且要和Expires一起用。

Etag和Expires
和 Last-Modified和Expires的情况类似,需要Expires控制请求的频率,Etag在强制刷新时作为验证资源是否更新

Last-Modified和Etag
分布式系统里多台机器间文件的last-modified必须保持一致,以免负载均衡到不同机器导致比对失败
分布式系统尽量关闭掉Etag(每台机器生成的etag都会不一样)
Last-Modified和ETags请求的http报头一起使用,服务器首先产生Last-Modified/Etag标记,服务器可在稍后使用它来判断页面是否已经被修改,来决定文件是否继续缓存
过程如下:
1.客户端请求一个页面(A)。
2.服务器返回页面A,并在给A加上一个Last-Modified/ETag。
3.客户端展现该页面,并将页面连同Last-Modified/ETag一起缓存。
4.客户再次请求页面A,并将上次请求时服务器返回的Last-Modified/ETag一起传递给服务器。
5.服务器检查该Last-Modified或ETag,并判断出该页面自上次客户端请求之后还未被修改,直接返回响应304和一个空的响应体。
注:
1、Last-Modified和Etag头都是由WebServer发出的HttpReponse Header,WebServer应该同时支持这两种头。
2、WebServer发送完Last-Modified/Etag头给客户端后,客户端会缓存这些头;
3、客户端再次发起相同页面的请求时,将分别发送与Last-Modified/Etag对应的HttpRequestHeader:If-Modified-Since和If-None-Match。我们可以看到这两个Header的值和WebServer发出的Last-Modified,Etag值完全一样;

4、通过上述值到服务器端检查,判断文件是否继续缓存;
从资源更新原理来看Last-Modified和Etag基本是类似的,那为什么http协议中要搞2个标识呢?
Last-Modified存在的问题:
1、在集群服务器上各个服务器上的文件时间可能不同。
2、如果用旧文件覆盖新文件,因为时间更前,浏览器不会请求这个更旧的文件。
3、时间精度为s级,对文件修改精度有严格要求的场景不能满足

为什么使用Etag请求头?
Etag 主要为了解决 Last-Modified 无法解决的一些问题。
1、一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;
2、某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒)
3、某些服务器不能精确的得到文件的最后修改时间;
为此,HTTP/1.1引入了 Etag(Entity Tags).Etag仅仅是一个和文件相关的标记,可以是一个版本标记,比如说v1.0.0或者说"2e681a-6-5d044840"这么一串看起来很神秘的编码。但是HTTP/1.1标准并没有规定Etag的内容是什么或者说要怎么实现,唯一规定的是Etag需要放在""内。尤其是在做断点下载/续传时,表现得比较明显

页面加载速度优化
影响页面加载速度的因素有非常多,我们在对 WebView 加载一个网页的过程进行调试发现,每次加载的过程中都会有较多的网络请求,除了 web 页面自身的 URL 请求,还会有 web 页面外部引用的JS、CSS、字体、图片等等都是个独立的 http 请求。这些请求都是串行的,这些请求加上浏览器的解析、渲染时间就会导致 WebView 整体加载时间变长,消耗的流量也对应的真多。接下来我们就来说说几种优化方案来是怎么解决这个问题的。
选择合适的 WebView 缓存

WebView 缓存看似就是开启几个开关的问题,但是要弄懂这几种缓存机制还是很有深度。下图是腾讯某工程师总结六种 H5 常用的缓存机制的优势及适用场景。

浏览器缓存机制:

主要前端负责,Android 端不需要进行特别的配置。
Dom Storage(Web Storage)存储机制:

配合前端使用,使用时需要打开 DomStorage 开关。

WebView myWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
webSettings.setDomStorageEnabled(true);

Web SQL Database 存储机制:

虽然已经不推荐使用了,但是为了兼容性,还是提供下 Android 端使用的方法

WebView myWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
webSettings.setDatabaseEnabled(true);
final String dbPath = getApplicationContext().getDir("db",Context.MODE_PRIVATE).getPath();
webSettings.setDatabasePath(dbPath)

Application Cache 存储机制

Application Cache(简称 AppCache)似乎是为支持 Web App 离线使用而开发的缓存机制。它的缓存机制类似于浏览器的缓存(Cache-Control 和 Last-Modified)机制,都是以文件为单位进行缓存,且文件有一定更新机制。但 AppCache 是对浏览器缓存机制的补充,不是替代。
不过根据官方文档,AppCache 已经不推荐使用了,标准也不会再支持。现在主流的浏览器都是还支持 AppCache的,以后就不太确定了。同样给出 Android 端启用 AppCache 的代码。

WebView myWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
webSettings.setAppCacheEnabled(true);
final String cachePath = getApplicationContext().getDir("cache",Context.MODE_PRIVATE).getPath();
webSettings.setAppCachePath(cachePath);
webSettings.setAppCacheMaxSize(510241024);

Indexed Database 存储机制

IndexedDB 也是一种数据库的存储机制,但不同于已经不再支持的 Web SQL Database。IndexedDB 不是传统的关系数据库,可归为 NoSQL 数据库。IndexedDB 又类似于 Dom Storage 的 key-value 的存储方式,但功能更强大,且存储空间更大。
Android 在4.4开始加入对 IndexedDB 的支持,只需打开允许 JS 执行的开关就好了。

WebView myWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
webSettings.setJavaScriptEnabled(true);

File System API

File System API 是 H5 新加入的存储机制。它为 Web App 提供了一个虚拟的文件系统,就像 Native App 访问本地文件系统一样。由于安全性的考虑,这个虚拟文件系统有一定的限制。Web App 在虚拟的文件系统中,可以进行文件(夹)的创建、读、写、删除、遍历等操作。很可惜到目前,Android 系统的 WebView 还不支持 File System API。
简单的介绍完了上面六种 H5 常用的缓存模式,想必大家能对 Android WebView 所支持的缓存模式有个粗略的了解。如果想和前端更好的配合使用 Android WebView 所支持的缓存,建议看下这篇文章《H5 缓存机制浅析 移动端 Web 加载性能优化》

*常用资源预加载:*

上面介绍的缓存技术,能优化二次启动 WebView 的加载速度,那首次加载 H5 页面的速度该怎么优化呢?上面分析了一次加载过程会有许多外部依赖的 JS、CSS、图片等资源需要下载,那我们能不能提前将这些资源下载好,等H5 加载时直接替换呢?
好在从 API 11(Android 3.0)开始,WebView 引入了 shouldInterceptRequest 函数,这个函数有两种重载。
public WebResourceResponse shouldInterceptRequest(WebView webView, String url) 从 API 11 引入,API 21 废弃
public WebResourceResponse shouldInterceptRequest (WebView view, WebResourceRequest request) 从 API 21 开始引入
考虑到目前大多数 App 还要支持 API 14,所以还是使用 shouldInterceptRequest (WebView view, String url) 为例。

WebView mWebView = (WebView) findViewById(R.id.webview);
mWebView.setWebViewClient(new WebViewClient() {
@Override
public WebResourceResponse shouldInterceptRequest(WebView webView, final String url) {
WebResourceResponse response = null;
// 检查该资源是否已经提前下载完成。我采用的策略是在应用启动时,用户在 wifi 的网络环境下 // 提前下载 H5 页面需要的资源。
boolean resDown = JSHelper.isURLDownValid(url);
if (resDown) {
jsStr = JsjjJSHelper.getResInputStream(url);
if (url.endsWith(".png")) {
response = getWebResourceResponse(url, "image/png", ".png");
} else if (url.endsWith(".gif")) {
response = getWebResourceResponse(url, "image/gif", ".gif");
} else if (url.endsWith(".jpg")) {
response = getWebResourceResponse(url, "image/jepg", ".jpg");
} else if (url.endsWith(".jepg")) {
response = getWebResourceResponse(url, "image/jepg", ".jepg");
} else if (url.endsWith(".js") && jsStr != null) {
response = getWebResourceResponse("text/javascript", "UTF-8", ".js");
} else if (url.endsWith(".css") && jsStr != null) {
response = getWebResourceResponse("text/css", "UTF-8", ".css");
} else if (url.endsWith(".html") && jsStr != null) {
response = getWebResourceResponse("text/html", "UTF-8", ".html");
}
}
// 若 response 返回为 null , WebView 会自行请求网络加载资源。
return response;
}
});

private WebResourceResponse getWebResourceResponse(String url, String mime, String style) {
WebResourceResponse response = null;
try {
response = new WebResourceResponse(mime, "UTF-8", new FileInputStream(new File(getJSPath() + TPMD5.md5String(url) + style)));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return response;
}

public String getJsjjJSPath() {
String splashTargetPath = JarEnv.sApplicationContext.getFilesDir().getPath() + "/JS";
if (!TPFileSysUtil.isDirFileExist(splashTargetPath)) {
TPFileSysUtil.createDir(splashTargetPath);
}
return splashTargetPath + "/";
}

**
*1:常用 JS 本地化及延迟加载***

**
**资源等文件(不需要更新)本地存储,在需要的时候直接从本地获取。哪些资源需要我们去存储在本地呢,当然是一些不会被更新的资源,例如图片文件,js文件,css文件,比预加载更粗暴的优化方法是直接将常用的 JS 脚本本地化,直接打包放入 apk 中。比如 H5 页面获取用户信息,设置标题等通用方法,就可以直接写入一个 JS 文件,放入 asserts 文件夹,在 WebView 调用了onPageFinished() 方法后进行加载。需要注意的是,在该 JS 文件中需要写入一个 JS 文件载入完毕的事件,这样前端才能接受都爱 JS 文件已经种植完毕,可以调用 JS 中的方法了。 附上一段本地化的 JS 代码。

javascript: ;
(function() {
try{
window.JSBridge = {
'invoke': function(name) {
var args = [].slice.call(arguments, 1),
callback = args.pop(),
params, obj = this[name];
if (typeof callback !== 'function') {
params = callback;
callback = function() {}
} else {
params = args[0]
} if (typeof obj !== 'object' || typeof obj.func !== 'function') {
callback({
'err_msg': 'system:function_not_exist'
});
return
}
obj.callback = callback;
obj.params = params;
obj.func(params)
},
'on': function(event, callback) {
var obj = this['on' + event];
if (typeof obj !== 'object') {
callback({
'err_msg': 'system:function_not_exist'
});
retrun
}
if (typeof callback !== 'undefined') obj.callback = callback
},
'login': {
'func': function(params) {
prompt("login", JSON.stringify(params))
},
'params': {},
'callback': function(res) {}
},
'settitle': {
'func': function(params) {
prompt("settitle",JSON.stringify(params))
},
'params': {},
'callback': function(res) {}
},
}catch(e){
alert('demo.js error:'+e);
}
var readyEvent = document.createEvent('Events');
readyEvent.initEvent('JSBridgeReady', true, true);
document.dispatchEvent(readyEvent)
})();

关于 JS 延迟加载
Android 的 OnPageFinished 事件会在 Javascript 脚本执行完成之后才会触发。如果在页面中使 用JQuery,会在处理完 DOM 对象,执行完 $(document).ready(function() {}); 事件自会后才会渲染并显示页面。而同样的页面在 iPhone 上却是载入相当的快,因为 iPhone 是显示完页面才会触发脚本的执行。所以我们这边的解决方案延迟 JS 脚本的载入,这个方面的问题是需要Web前端工程师帮忙优化的。

**
*2:使用第三方 WebView 内核***

WebView 的兼容性一直也是困扰我们 Android 开发者的一个大问题,不说 Android 4.4 版本 Google 使用了Chromium 替代 Webkit 作为 WebView 内核,就看看国内众多的第三方 ROM 都有可能会对原生的 WebView 做出修改,这时候如果出现兼容问题,是非常难定位到问题和解决的。
在一次使用微信浏览订阅公众号文章的过程中,发现微信的 H5 页面有一行 『QQ 浏览器 X5 内核提供技术支持』。顺着这个线索我就找到了腾讯浏览服务。发现腾讯已经把这个功能开放了,而且集成的 SDK 很小只有212 KB。这是很惊人的,通过介绍才发现这个 SDK 是可以共享微信和手机 QQ 的 X5 内核。这就很方便了,作为国内市场最不可或缺的两个 App,我们能只需要集成一个很小的 SDK 就可以共享使用 X5 内核了,不得不说腾讯还是很有想法的。
简单摘录些功能亮点,想必能让大家高潮一番。详细内容大家可以直接到腾讯浏览服务看看,我相信不会让你们失望的。
网页浏览能力
Web页面crash率降低75%
页面打开速度提升35%
流量节省60%
阅读模式
去除网页中广告等杂质
优化文章的阅读体验
文件打开能力
包括会话页的互传文件及邮件中附件
支持doc、ppt、xls、pdf等办公格式
支持jpg、gif、png、bmp等图片格式
支持zip、rar等压缩文件
支持mp3、mp4、RMVB等音视频格式
视频菜单能力
支持屏幕调节等常规视频菜单功能
灵活切换全屏&小窗功能

*3:加快HTML网页装载完成的速度*

默认情况html代码下载到WebView后,webkit开始解析网页各个节点,发现有外部样式文件或者外部脚本文件时,会异步发起网络请求下载文件,但如果在这之前也有解析到image节点,那势必也会发起网络请求下载相应的图片。在网络情况较差的情况下,过多的网络请求就会造成带宽紧张,影响到css或js文件加载完成的时间,造成页面空白loading过久。解决的方法就是告诉WebView先不要自动加载图片,等页面finish后再发起图片加载。

故在WebView初始化时设置如下代码:
public void int () {
if(Build.VERSION.SDK_INT >= 19) {
webView.getSettings().setLoadsImagesAutomatically(true);
} else {
webView.getSettings().setLoadsImagesAutomatically(false);
}
}

同时在WebView的WebViewClient实例中的onPageFinished()方法添加如下代码:
@Override
public void onPageFinished(WebView view, String url) {
if(!webView.getSettings().getLoadsImagesAutomatically()) {
webView.getSettings().setLoadsImagesAutomatically(true);
}
}

从上面的代码,可以看出我们对系统API在19以上的版本作了兼容。因为4.4以上系统在onPageFinished时再恢复图片加载时,如果存在多张图片引用的是相同的src时,会只有一个image标签得到加载,因而对于这样的系统我们就先直接加载。

**
*4:自定义出错界面***

当WebView加载页面出错时(一般为404 NOT FOUND),安卓WebView会默认显示一个卖萌的出错界面。但我们怎么能不让用户发现原来我使用的是网页应用呢,我们期望的是用户在网页上得到是如原生般应用的体验,那就先要从干掉这个默认出错页面开始。当WebView加载出错时,我们会在WebViewClient实例中的onReceivedError()方法接收到错误,我们就在这里做些手脚:
@Override
public void onReceivedError (WebView view, int errorCode, String description, String failingUrl) {
super.onReceivedError(view, errorCode, description, failingUrl);
loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
mErrorFrame.setVisibility(View.VISIBLE);
}

从上面可以看出,我们先使用loadDataWithBaseURL清除掉默认错误页内容,再让我们自定义的View得到显示(mErrorFrame为蒙在WebView之上的一个LinearLayout布局,默认为View.GONE)。

远程网页需访问本地资源

当我们在WebView中加载出从web服务器上拿取的内容时,是无法访问本地资源的,如assets目录下的图片资源,因为这样的行为属于跨域行为(Cross-Domain),而WebView是禁止的。解决这个问题的方案是把html内容先下载到本地,然后使用loadDataWithBaseURL加载html。这样就可以在html中使用 file:///android_asset/xxx.png 的链接来引用包里面assets下的资源了。示例如下:
private void loadWithAccessLocal(final String htmlUrl) {
new Thread(new Runnable() {
public void run() {
try {
final String htmlStr = NetService.fetchHtml(htmlUrl);
if (htmlStr != null) {
TaskExecutor.runTaskOnUiThread(new Runnable() {
@Override
public void run() {
loadDataWithBaseURL(htmlUrl, htmlStr, "text/html", "UTF-8", "");
}
});
return;
}
} catch (Exception e) {
Log.e("Exception:" + e.getMessage());
}

  TaskExecutor.runTaskOnUiThread(new Runnable() {  
    @Override  
    public void run() {  
      onPageLoadedError(-1, "fetch html failed");  
    }  
  });  
}  

}).start();
}

上面有几点需要注意:
•从网络上下载html的过程应放在工作线程中
•html下载成功后渲染出html的步骤应放在UI主线程,不然WebView会报错
•html下载失败则可以使用我们前面讲述的方法来显示自定义错误界面

5:WebView 导致的内存泄露

Android 中的 WebView 存在很大的兼容性问题,不仅仅是 Android 系统版本的不同对 WebView 产生很大的差异,另外不同的厂商出货的 ROM 里面 WebView 也存在着很大的差异。更严重的是标准的 WebView 存在内存泄露的问题,看这里WebView causes memory leak - leaks the parent Activity。所以通常根治这个问题的办法是为 WebView 开启另外一个进程,通过 AIDL 与主进程进行通信,WebView 所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。
这段话来自胡凯翻译的 Google Android 内存优化之 OOM 。这里提到的让 WebView 独立运行在一个进程里,用完 WebView 后直接销毁这个进程,即使内存泄露了,也不会影响到主进程。微信,手 Q 等 App 也采用了这个方案。但是这就涉及到了跨进程通讯,处理起来就比较麻烦。
另外个解决方案,就是使用自己封装的 WebView,比如上面提到的 X5 内核,且使用 WebView 的时候,不在 XML 里面声明,而是在代码中直接 new 出来,传入 application context 来防止 activity 引用被滥用。

WebView webView = new WebView(getContext().getApplicationContext();
webFrameLayout.addView(webView, 0);

在使用了这个方式后,基本上 90% 的 WebView 内存泄漏的问题便得以解决。

6:客户端UI优化

怎么让用户看不到WebView加载前的白色页面呢?首次加载后页面的跳转可以用上面的步骤进行优化,可以提供给用户一个很好的体验,那加载的第一页呢?我们需要WebView预加载页面,这个该怎么做到的呢?下面提供两种方法:
ViewPager,将欢迎页面与WebView页面一起放进ViewPager中,设置预加载页面个数,使WebView所在页面可以预加载,在加载完毕的时候切换到WebView所在页面。
FrameLayout,将欢迎页面与WebView页面的布局合在一起,显示在一个页面内,起始隐藏WebView布局,待WebView加载完毕,隐藏欢迎布局,显示WebView布局。
使用FrameLayout简单一些,两种方法都是需要对WebChromeClient的onProgressChanged进行监听,加载完毕进行页面切换,如下:
webView.setWebChromeClient(new WebChromeClient() {
@Override
public void onProgressChanged(WebView view, int newProgress) {
super.onProgressChanged(view, newProgress);
if (newProgress >= 100) {
// 切换页面
}
}
});

*7:WebView独立进程*

有效增大App的运存,减少由webview引起的内存泄露对主进程内存的占用。
避免WebView的Crash影响App主进程的运行。
拥有对WebView独立进程操控权。

WebView进程与其他进程通讯的方式

把webview独立进程之后会发现,埋点功能和接收主进程数据都不正常了,这里就涉及到进程间通讯的问题了;
进程通讯无非就是那几种,aidl,messager,content provider,广播;
在这里就不再复述了,我是采用广播的方式来做的。

*8:WebView硬件加速导致页面渲染闪烁*

4.0以上的系统我们开启硬件加速后,WebView渲染页面更加快速,拖动也更加顺滑。但有个副作用就是,当WebView视图被整体遮住一块,
然后突然恢复时(比如使用SlideMenu将WebView从侧边滑出来时),这个过渡期会出现白块同时界面闪烁。
解决这个问题的方法是在过渡期前将WebView的硬件加速临时关闭,过渡期后再开启,代码如下:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { webview.setLayerType(View.LAYER_TYPE_SOFTWARE, null);}

9:webview的配置

WebSettings用于管理WebView状态配置,当WebView第一次被创建时,WebView包含着一个默认的配置,这些默认的配置将通过get方法返回,通过WebView中的getSettings方法获得一个WebSettings对象,如果一个WebView被销毁,在WebSettings中所有回调方法将抛出IllegalStateException异常。

1、setSupportZoom(boolean support)
设置WebView是否支持使用屏幕控件或手势进行缩放,默认是true,支持缩放。
getSettings.setSupportZoom(false);
2、setMediaPlaybackRequiresUserGesture(boolean require)
设置WebView是否通过手势触发播放媒体,默认是true,需要手势触发。
getSettings.setMediaPlaybackRequiresUserGesture(false);
3、setBuiltInZoomControls(boolean enabled)
设置WebView是否使用其内置的变焦机制,该机制集合屏幕缩放控件使用,默认是false,不使用内置变焦机制。
getSettings.setBuiltInZoomControls(true);
4、setDisplayZoomControls(boolean enabled)
设置WebView使用内置缩放机制时,是否展现在屏幕缩放控件上,默认true,展现在控件上。
getSettings.setDisplayZoomControls(false);
5、setAllowFileAccess(boolean allow)
设置在WebView内部是否允许访问文件,默认允许访问。
getSettings.setAllowFileAccess(false);
6、setAllowContentAccess(boolean allow)
设置WebView是否使用其内置的变焦机制,该机制结合屏幕缩放控件使用,默认是false,不使用内置变焦机制。
getSettings.setAllowContentAccess(false);
7、setLoadWithOverviewMode(boolean overview)
设置WebView是否使用预览模式加载界面。
getSettings.setLoadWithOverviewMode(false);
8、setSaveFormData(boolean save)
设置WebView是否保存表单数据,默认true,保存数据。
getSettings.setSaveFormData(false);
9、setTextZoom(int textZoom)
设置WebView中加载页面字体变焦百分比,默认100,整型数。
getSettings.setTextZoom(100);
10、setAcceptThirdPartyCookies(boolean accept)
设置WebView访问第三方Cookies策略,参考CookieManager提供的方法:setShouldAcceptThirdPartyCookies。
getSettings.setAcceptThirdPartyCookies(false);
11、setUseWideViewPort(boolean use)
设置WebView是否使用viewport,当该属性被设置为false时,加载页面的宽度总是适应WebView控件宽度;当被设置为true,当前页面包含viewport属性标签,在标签中指定宽度值生效,如果页面不包含viewport标签,无法提供一个宽度值,这个时候该方法将被使用。
getSettings.setUseWideViewPort(false);
12、setSupportMultipleWindows(boolean support)
设置WebView是否支持多屏窗口,参考WebChromeClient#onCreateWindow,默认false,不支持。
getSettings.setSupportMultipleWindows(true);
13、setLayoutAlgorithm(LayoutAlgorithm l)
设置WebView底层的布局算法,参考LayoutAlgorithm#NARROW_COLUMNS,将会重新生成WebView布局
getSettings.setLayoutAlgorithm(LayoutAlgorithm l);
14、setStandardFontFamily(String font)
设置WebView标准字体库字体,默认字体“sans-serif”。
getSettings.setStandardFontFamily("sans-serif");
15、setFixedFontFamily(String font)
设置WebView固定的字体库字体,默认“monospace”。
getSettings.setFixedFontFamily("monospace");
16、setSansSerifFontFamily(String font)
设置WebView Sans SeriFontFamily字体库字体,默认“sans-serif”。
getSettings.setSansSerifFontFamily("sans-serif");
17、setSerifFontFamily(String font)
设置WebView seri FontFamily字体库字体,默认“sans-serif”。
getSettings.setSansSerifFontFamily("sans-serif");
18、setCursiveFontFamily(String font)
设置WebView字体库字体,默认“cursive”
getSettings.setCursiveFontFamily("cursive");
19、setFantasyFontFamily(String font)
设置WebView字体库字体,默认“fantasy”。
getSettings.setFantasyFontFamily("fantasy");
20、setMinimumFontSize(int size)
设置WebView字体最小值,默认值8,取值1到72
getSettings.setMinimumFontSize(8);
21、setMinimumLogicalFontSize(int size)
设置WebView逻辑上最小字体值,默认值8,取值1到72
getSettings.setMinimumLogicalFontSize(8);
22、setDefaultFontSize(int size)
设置WebView默认值字体值,默认值16,取值1到72
getSettings.setDefaultFontSize(16);
23、setDefaultFixedFontSize(int size)
设置WebView默认固定的字体值,默认值16,取值1到72
getSettings.setDefaultFixedFontSize(16);
24、setLoadsImagesAutomatically(boolean flag)
设置WebView是否加载图片资源,默认true,自动加载图片
getSettings.setLoadsImagesAutomatically(false);
25、setBlockNetworkImage(boolean flag)
设置WebView是否以http、https方式访问从网络加载图片资源,默认false
getSettings.setBlockNetworkImage(true);
26、setBlockNetworkLoads(boolean flag)
设置WebView是否从网络加载资源,Application需要设置访问网络权限,否则报异常
getSettings.setBlockNetworkLoads(true);
27、setJavaScriptEnabled(boolean flag)
设置WebView是否允许执行JavaScript脚本,默认false,不允许
getSettings.setJavaScriptEnabled(true);
28、setAllowUniversalAccessFromFileURLs(boolean flag)
设置WebView运行中的脚本可以是否访问任何原始起点内容,默认true
getSettings.setAllowUniversalAccessFromFileURLs(false);
29、setAllowFileAccessFromFileURLs(boolean flag)
设置WebView运行中的一个文件方案被允许访问其他文件方案中的内容,默认值true
getSettings.setAllowFileAccessFromFileURLs(false);
30、setGeolocationDatabasePath(String databasePath)
设置WebView保存地理位置信息数据路径,指定的路径Application具备写入权限
getSettings.setGeolocationDatabasePath(String path);
31、setAppCacheEnabled(boolean flag)
设置Application缓存API是否开启,默认false,设置有效的缓存路径参考setAppCachePath(String path)方法
getSettings.setAppCacheEnabled(true);
32、setAppCachePath(String appCachePath)
设置当前Application缓存文件路径,Application Cache API能够开启需要指定Application具备写入权限的路径
getSettings.setAppCachePath(String appCachePath);
33、setDatabaseEnabled(boolean flag)
设置是否开启数据库存储API权限,默认false,未开启,可以参考setDatabasePath(String path)
getSettings.setDatabaseEnabled(false);
34、setDomStorageEnabled(boolean flag)
设置是否开启DOM存储API权限,默认false,未开启,设置为true,WebView能够使用DOM storage API
getSettings.setDomStorageEnabled(true);
35、setGeolocationEnabled(boolean flag)
设置是否开启定位功能,默认true,开启定位
getSettings.setGeolocationEnabled(false);
36、setJavaScriptCanOpenWindowsAutomatically(boolean flag)
设置脚本是否允许自动打开弹窗,默认false,不允许
getSettings.setJavaScriptCanOpenWindowsAutomatically(true);
37、setDefaultTextEncodingName(String encoding)
设置WebView加载页面文本内容的编码,默认“UTF-8”。
getSettings.setDefaultTextEncodingName("UTF-8");
38、setUserAgentString(String ua)
设置WebView代理字符串,如果String为null或为空,将使用系统默认值
getSettings.setUserAgentString(String ua);
39、setNeedInitialFocus(boolean flag)
设置WebView是否需要设置一个节点获取焦点当被回调的时候,默认true
getSettings.setNeedInitialFocus(false);
40、setCacheMode(int mode)
重写缓存被使用到的方法,该方法基于Navigation Type,加载普通的页面,将会检查缓存同时重新验证是否需要加载,如果不需要重新加载,将直接从缓存读取数据,允许客户端通过指定LOAD_DEFAULT、LOAD_CACHE_ELSE_NETWORK、LOAD_NO_CACHE、LOAD_CACHE_ONLY其中之一重写该行为方法,默认值LOAD_DEFAULT
getSettings.setCacheMode(WebSettings.LOAD_DEFAULT);

LOAD_CACHE_ONLY: 不使用网络,只读取本地缓存数据
LOAD_DEFAULT: 根据cache-control决定是否从网络上取数据。
LOAD_CACHE_NORMAL: API level 17中已经废弃, 从API level 11开始作用同LOAD_DEFAULT模式
LOAD_NO_CACHE: 不使用缓存,只从网络获取数据.
LOAD_CACHE_ELSE_NETWORK,只要本地有,无论是否过期,或者no-cache,都使用缓存中的数据。
如:www.taobao.com的cache-control为no-cache,在模式LOAD_DEFAULT下,无论如何都会从网络上取数据,如果没有网络,就会出现错误页面;在LOAD_CACHE_ELSE_NETWORK模式下,无论是否有网络,只要本地有缓存,都使用缓存。本地没有缓存时才从网络上获取。
www.360.com.cn的cache-control为max-age=60,在两种模式下都使用本地缓存数据。
根据以上两种模式,建议缓存策略为,判断是否有网络,有的话,使用LOAD_DEFAULT,无网络时,使用LOAD_CACHE_ELSE_NETWORK。

41、setMixedContentMode(int mode)
设置当一个安全站点企图加载来自一个不安全站点资源时WebView的行为,android.os.Build.VERSION_CODES.KITKAT默认为MIXED_CONTENT_ALWAYS_ALLOW,android.os.Build.VERSION_CODES#LOLLIPOP默认为MIXED_CONTENT_NEVER_ALLOW,取值其中之一:MIXED_CONTENT_NEVER_ALLOW、MIXED_CONTENT_ALWAYS_ALLOW、MIXED_CONTENT_COMPATIBILITY_MODE.
getSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);

下面贴上我自己的配置代码:
WebSettings settings = webview.getSettings();
settings.setJavaScriptEnabled(true);//启用js
settings.setJavaScriptCanOpenWindowsAutomatically(true);//js和android交互
String cacheDirPath = PathCommonDefines.WEBVIEW_CACHE;
settings.setAppCachePath(cacheDirPath); //设置缓存的指定路径
settings.setAllowFileAccess(true); // 允许访问文件
settings.setAppCacheEnabled(true); //设置H5的缓存打开,默认关闭
settings.setUseWideViewPort(true);//设置webview自适应屏幕大小
settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NARROW_COLUMNS);//设置,可能的话使所有列的宽度不超过屏幕宽度
settings.setLoadWithOverviewMode(true);//设置webview自适应屏幕大小
settings.setDomStorageEnabled(true);//设置可以使用localStorage
settings.setSupportZoom(false);//关闭zoom按钮
settings.setBuiltInZoomControls(false);//关闭zoom
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
webview.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
webview.setWebViewClient(new WebViewClient() {
@Override public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url); return false; }
@Override public void onLoadResource(WebView view, String url) { }
@Override public void onPageFinished(WebView view, String url) { } });

*10:html5跳原生界面*

**
**网页跳原生界面的方法有很多种,比如js调java方法,或者是通过uri scheme啦,也可以通过自己解析url来做。
在这儿,考虑到兼容性,拦截的是url,并且在清单文件中自定义了scheme~

webview.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { parserURL(url); //解析url,如果存在有跳转原生界面的url规则,则跳转原生。 return super.shouldOverri deUrlLoading(view, url); } @Override public void onPageFinished(WebView view, String url) { super.onPageFinished (view, url); } @Override public void onLoadResource(WebView view, String url) { super.onLoadResource(view, url); } });

清单文件中,声明一下 就可以在自带浏览器通过uri scheme跳到本app页面了,这个activity作为各个页面的分发页面,通过 这个界面解析数据决定接下来要跳转哪个页面:

<activity
android:name=".ui.webview.CommWebviewActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:process=":webview"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateHidden">
<intent-filter>
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />

 <action android:name="android.intent.action.VIEW" /> 


<data 
   android:host="xxxx.com" 
   android:scheme="kingp2p" /> 

</intent-filter>
</activity>

Android中处理网页时我们必然用到WebView,这里我们有这样一个需求,我们想让WebView在处理网络请求的时候将某些请 求拦截替换成某些特殊的资源。具体一点儿说,在WebView加载 http://m.sogou.com 时,会加载一个logo图片,我们的需 求就是将这个logo图片换成另一张图片。

shouldOverrideUrlLoading(拦截url加载,除资源请求的url) shouldInterceptRequest(拦截所有url请求)

shouldInterceptRequest:在每一次请求资源时,都会通过这个函数来回调,比如超链接、JS文件、CSS文件、图片等,
也就是说浏览器中每一次请求资源时,都会回调回来,无论任何资源!

但是必须注意的是shouldInterceptRequest函数是在非UI线程中执行的,在其中不能直接做UI操作,如果需要做UI操作,则需要利用Handler来实现
好在Android中的WebView比较强大,从API 11(Android 3.0)开始, shouldInterceptRequest被引入就是为了解决这一类的问题。
shouldInterceptRequest这个回调可以通知主程序WebView处理的资源(css,js,image等)请求,并允许主程序进行处理后返回数据。如果主程序返回的数据为null,WebView会自行请求网络加载资源,否则使用主程序提供的数据。注意这个回调发生在非UI线程中,所以进行UI系统相关的操作是不可以的。
shouldInterceptRequest有两种重载。
public WebResourceResponse shouldInterceptRequest (WebView view, String url) 从API 11开始引入,API 21弃用
public WebResourceResponse shouldInterceptRequest (WebView view, WebResourceRequest request) 从API 21开始引入

本次例子暂时使用第一种,即shouldInterceptRequest (WebView view, String url)。
示例代码

WebView webView = new WebView(this);

webView.setWebViewClient(new WebViewClient() {
@Override

public WebResourceResponse shouldInterceptRequest(WebView view, String url) {

Log.i(LOGTAG, "shouldInterceptRequest url=" + url + ";threadInfo" + Thread.currentThread());
WebResourceResponse response = null;

if (url.contains("logo")) {

try {

InputStream localCopy = getAssets().open("droidyue.png");
response = new WebResourceResponse("image/png", "UTF-8", localCopy);}
catch (IOException e) {

e.printStackTrace();}}return response;}});

}
setContentView(webView);
webView.loadUrl("http://m.sogou.com");

}

其中WebResourceResponse需要设定三个属性,MIME类型,数据编码,数据(InputStream流形式)。

shouldOverrideUrlLoading(拦截url加载,除资源请求的url) shouldInterceptRequest(拦截所有url请求)

shouldOverrideUrlLoading:
这个函数会在加载超链接时回调过来
对网页中超链接按钮的响应。当按下某个连接时WebViewClient会调用这个方法,并传递参数:按下的url。
通过重写shouldOverrideUrlLoading,可以实现对网页中超链接的拦截;
webView.setWebViewClient(new WebViewClient(){
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url); //在当前的webview中跳转到新的url
return true;

} });

shouldoverrideurlloading返回true表明点击网页里面的链接还是在当前的webview里跳转,不跳到浏览器那边。
这个方法在手动调用WebView.loadUrl(url);时是不会被调用的,再进一步点击才会走这个方法。
返回值是boolean类型,表示是否屏蔽WebView继续加载URL的默认行为,因为这个函数是WebView加载URL前回调的,所以如果我们return true,则WebView接下来就不会再加载这个URL了,所有处理都需要在WebView中操作,包含加载。如果我们return false,则系统就认为上层没有做处理,接下来还是会继续加载这个URL的。在利用shouldOverrideUrlLoading来拦截URL时,如果return true,则会屏蔽系统默认的显示URL结果的行为,不需要处理的URL也需要调用loadUrl()来加载进WebVIew,不然就会出现白屏;如果return false,则系统默认的加载URL行为是不会被屏蔽的,所以一般建议大家return false,我们只关心我们关心的拦截内容,对于不拦截的内容,让系统自己来处理即可。

WebChromeClient与WebViewClient的区别
WebView的内部实现并不是完全使用Chrome的内核,而是部分使用Chome内核,其它都是与Chrome不相同的;
WebViewClient:在影响View的事件到来时,会通过WebViewClient中的方法回调通知用户
WebChromeClient:当影响浏览器的事件到来时,就会通过WebChromeClient中的方法回调通知用法。
通过上面的对比,我们发现WebViewClient和WebChromeClient都是针对不同事件的回调,而google将这些回调进行分类集合,就产生了WebViewClient、WebChromeClient这两个大类,其中管理着针对不同类型的回调而已。
在程序中,我们只需要加上mWebView.setWebChromeClient(new WebChromeClient());就可以实现confrim()、alert()、prompt()的弹出效果了
如:mWebView.setWebViewClient(new WebViewClient()); mWebView.setWebChromeClient(new WebChromeClient());
如果需要使网页中的confrim()、alert()、prompt()函数生效,需要设置WebChromeClient!
除了alert,prompt,confirm以外,其它时候都不需要强制设置WebChromeClient
在使用onJsAlert来拦截alert对话框时,如果不需要再弹出alert对话框,一定要return true;在return false以后,会依然调用系统的默认处理来弹出对话框的

*11:LoadData()与loadDataWithBaseURL()*

**
**通过loadUrl()来加载本地页面和在线地址的方式需要很长的加载时间,而LoadData()与loadDataWithBaseURL(),它们不是用来加载整个页面文件的,而是用来加载一段代码片的
比如可以先通过http将网页HTML数据从服务器下载回来,在通过这两个方法就可以很快把页面加载出来,没有很长的访问网络cDN解析等时间

在使用loadData时,在数据里面不能出现英文字符:’#’, ‘%’, ‘\’ , ‘?’ 这四个字符,如果有的话可以用 %23, %25,%27, %3f,这些字符来替换
Android给我们提供了一个专门用来转码的函数:URLEncoder.encode(String s, String charsetName) ,它能将冲突的字符进行转义,然后再传给webview,这样webview在加载时就不会有冲突了,如:mWebView.loadData(URLEncoder.encode(summary, "utf-8"), "text/html", "utf-8");
loadData()应该是不能加载图片的,为了防止字符冲突,在传递loadData的数据时,必须使用URLEncoder.encode()函数来转义,页面的编码格式必须与代码中传参的编码格式一致,不然会导致乱码

public void loadDataWithBaseURL(String baseUrl, String data,String mimeType, String encoding, String historyUrl)
参数意义如下:
String baseUrl:基准URL,不需要可以传null,它的意思是,如果data中的url是相对地址,则就会加上基准url来拼接出完整的地址,比如baseUrl是http://img6.ph.126.net,data中有个Img标签,它的内容是:<img src='hBiG96B8egigBULxUWcOpA==/109212290980771276.jpg'>,很明显src的地址不是本地地址也不是在线地址,那它就是一个相对地址,所以加上baseUrl以后才是它的完整地址:http://img6.ph.126.net/hBiG96B8egigBULxUWcOpA==/109212290980771276.jpg
String mimeType:MIME类型
String encoding:编码方式
String historyUrl:当前的历史记录所要存储的值。如果不需要可以传Null,loadDataWithBaseURL它本身并不会向历史记录中存储数据,要想实现历史记录,需要我们自己来实现;有关历史记录的实现方式是比较复杂的,历史记录是以Key/value的方式存储在一个historyList里的,当前进后退时,会用Key来取出对应的value值来加载进webview中。而Key就是这里的baseUrl,Value就是这里的historyUrl;history所指向的必须是一个页面,并且页面存在于SD卡中或程序中(assets);

如果网页地址是绝对地址,本地文件地址也是绝对地址,而另外一个是相对地址的情况下,绝对的http地址,通过file指定的本地地址,对于这两类的绝对地址,baseUrl是不起作用的,而对于第三个相对地址,是会启用baseUrl的来拼接完整地址的。

这两种方法,我建议使用后者,虽然loadData的历史记录不需要我们自己来实现,但在使用时,加载上后者比前者快一到两倍。
另外loadData不能加载图片,而loadDataWithBaseURL是可以加载图片的

*总结:*

**
**1:图片延迟加载:图片的网络请求会造成带宽紧张,影响到css或js文件加载完成的时间,造成页面空白loading过久

2: 常用资源预加载,shouldInterceptRequest直接替换

3:常用资源的缓存shouldInterceptRequest拦截保存

4:合理使用http缓存策略:Expires , max-age, Etag , Last-Modified (其中Expires,max-age是客户端在这个时间 之前 不去向服务器端发送请求验证资源是否有更新, Etag, Last-Modified是服务器决定是否需要返回资源,未更新的资源不需要返回)

5:getSettings.setCacheMode建议缓存策略为,判断是否有网络,有的话,使用LOAD_DEFAULT,无网络时,使用LOAD_ CACHE_ELSE_NETWORK。

6:使用JSBridge,支持异步回调

7:将HTML以接口的形式返回,webView用loadDataWithBaseURL加载HTML,并且可以解决跨域行为的问题

8:使用第三方 WebView 内核 如腾讯的X5 内核

9:WebView 导致的内存泄露:让 WebView 独立运行在一个进程里减少由webview引起的内存泄露对主进程内存的占用, 且不在 XML 里面声明,而是在代码中直接 new 出来,传入 application context 来防止 activity 引用被滥用

10:WebView.HitTestResult 这个函数可获取到我们点击的目标类型来得到当前点击的资源是什么,可实现长按下载图片

11:支持URLscheme和魔窗来完成应用内和应用间和微信中页面的跳转和恢复

12:还有就是具体的业务逻辑的优化

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

推荐阅读更多精彩内容