提到android的webView,我想大家对它都有点恨之入骨,因为它和ios的UIWebView的性能实在差的太远了,尤其在4.4以下,加载个页面慢的要死,出现白屏时间过长、没有网络的时候加载直接给你加载出内核自带的页面等等,如果对它不管的话体验实在是太差了,作为一个优秀的程序员,这些事情是无法忍受的,那么怎么才能让webview的加载速度变快呢,这得想一下到底是什么造成了h5界面加载慢,h5里的什么因素会影响加载速度?
当App首次打开时,默认是并不初始化浏览器内核的,只有当创建WebView实例的时候,才会创建WebView的基础框架。所以与浏览器不同,App中打开WebView的第一步并不是建立连接,而是启动浏览器内核,所以和电脑浏览器对比的话,第一个慢就体现在浏览器内核初始化上,也就是说我们第一次启动程序,调用loadUrl()方法的时候,会先初始化浏览器内核,而后才会从服务器获取界面到本地,并进行渲染(渲染其实就是解析(把html,css,js全部加载到本地解析出dom树,然后最后画到屏幕上)),大体的步骤如:DOM下载→DOM解析→CSS请求+下载→CSS解析→渲染→绘制→合成
,如果js脚本放置位置不当或过于繁琐的话,也会堵塞并影响渲染的时间。
最终合成之后才会回调WebViewClient的onPageFinished方法,onPageFinished回调方法在我们自定义WebView的时候经常用到,例如在渲染网页的之后那一片的空白期是不是很讨厌,这时候可以在调用loadUrl()之后,让界面先呈现一个加载动画,在页面渲染完之后再隐藏加载动画,这样总比一片空白的界面看起来要舒服的多。
在DOM下载这个环节,如果数据量太多,肯定也会影响速度,比如放置的图片过多,去渲染图片的这段时间是不是也会增加webview的渲染速度,ok,那么有没有一种方式,让图片最后下载呢,答案是有的,WebView的依赖类WebSettings类有这么一个方法setBlockNetworkImage(true)方法,设置了这个方法之后,图片的渲染将会被堵塞,直到调用onPageFinished方法时,设置setBlockNetworkImage(false)开始显示图片。
上面我们已经做了两部优化,第一步在渲染的时候开启加载动画,让用户看起来舒服一点。第二步渲染时关掉图片渲染。经过这两部,h5的加载会快那么一丢丢,但远远达不到我想要的结果。在弱网络场景下白屏时间还是非常长,用户体验非常糟糕,用户能忍受的时间极限是什么,处在用户的角度想的话,4、5秒(无法忍受啊),既然无法忍受,那么我们必须设置超时时间了,如果超时了,给用户呈现一个网络差的界面,当然这个界面必须可以点击重新加载的,在loadUrl()的时候或onPageStarted的时候开启计时器,如果规定时间内,当前的网页渲染进度还没有超过30%,认为它超时显示网络不好的界面。
那么上面实现了第三部优化(实现网页加载超时器),虽然做了这三步优化,但是加载还是慢啊,如果没有网络的情况下,是不是应该优先加载缓存,让用户看到界面,或者就算有网的情况下,如果有缓存的话,应该先加载缓存,就算网络非常慢,用户也可以感觉到,界面一下就出来的感觉。那么第四步优化(开启webview自带的缓存)
String appCachePath = context.getCacheDir().getAbsolutePath();
settings.setAppCacheMaxSize(1024 * 1024 * 8);
settings.setAppCacheEnabled(true);
settings.setDomStorageEnabled(true);
settings.setDatabaseEnabled(true);
settings.setDatabasePath(appCachePath);
settings.setAppCachePath(appCachePath);
public void setDataFrom() {
if (NetWork_Util.isNetworkConnected(context)) {
getSettings().setCacheMode(WebSettings.LOAD_DEFAULT);
} else {
getSettings().setCacheMode(WebSettings.LOAD_CACHE_ONLY);
}
}
onReceivedError的方法,在此方法里判断是不是网络没有连接,如果网络没有连接则接着提示网络不好的界面,当然也有可能服务器挂了,这时再新增一个服务器崩掉的界面,直接给404就ok。
这样就完成了第四步优化(开启webview自带缓存,没有网的情况下加载缓存),可是效果只是好了那么一丢丢,既然js有时会堵塞界面的渲染,那么可否把js延迟加载,答案是肯定的,js可以延迟加载,如果你们公司有很好的前端工程师的话,这个活可以放心的交给他们,如果没有的话,这个活我们自己也可以搞定,这就完成了第五步(js的延迟加载)。
这样优化之后就完了吗,还远远不够啊亲,既然第一次创建webview的加载时,需要先初始化浏览器的内核,并且必须初始化内核之后才去渲染网页,这是一个串行操作,在初始化的过程中网络就在那闲置着,是不是很浪费,能不能一边初始化一边并行的去拉取网络的数据,等初始化完了之后,直接将网络获取的网页数据交给内核渲染,避免了DOM的依赖内核下载,答案是可以的,那么第六步优化就是让初始化和DOM下载并行化。
并行之后的速度是大大提升的,如果网络很差呢,你并行还是没什么暖用,ok,既然都并行了,那么就把并行的数据缓存起来,即将DOM数据全部缓存到本地,只要本地有缓存,不管你的网络是好是坏,先给你显示缓存的数据再说,这样第七步优化(创建我们自己的DOM缓存(不用webview自带的,不好操作))就出来了。
第七步优化之后,体验会大大提升,因为就算网络很差,咱们可以直接显示缓存吗,就像把网页放在项目中一样,但是这么做之后会有一个缺陷,如果网页的数据变了呢?试想一下,有的页面用为什么选择h5,一个优点是实时更新,那么你会说了,可以采用插件化或动态配置或热修复实现实时更新吗,这也是可以的,但是任何方案都有弊端的,既然用h5实现实时更新的话,你每次加载的都是缓存,肯定都是旧的界面,这和最初的理念是相对的,那么第八步优化来了(为缓存附上过期时间(当然这个时间可以和服务器通过请求头进行合作)),通俗的讲就是只要有缓存,就先将缓存显示给用户看,然后判断本地缓存是否过期,如果没有过期,就停止操作,如果过期,从服务器重新获取DOM数据,获取成功后重新加载最新的数据,并把以前老的缓存覆盖掉。
第八步优化后还会存在一个问题,什么问题,就是你从缓存的界面重新加载最新的界面这中间会闪那么一下(又是白屏),怎么办,或者说前端只是改了少量的数据,曹,没必要将整个界面全部加载吧,能不能采用局部刷新,答案是可以的,那么第九步优化(采用增量更新(js实现局部刷新)),如果你是开发前端的那么你肯定会动态操作Dom树吧,何为增量更新,就是我们将h5界面拆分成不怎么不变的模板文件,和经常变化的数据文件,每次缓存的时候将h5拆分位模板文件和数据文件,如果当前数据过期的话,重新加载,然后判断是模板变了,还是数据变了,如果模板变了的话,重新加载网页,如果只是数据变了的话,那么利用js方法调用实现局部刷新。
俗话说没有完美的方案,只有不断完善的方案,优化到第九步的时候,h5的本地体验不能说完美吧,但可以说很棒了。
其中最后的三个优化方案就是从腾讯VasSonic源码中所得,VasSonic的优点就是:1、实现内核初始化和DOM下载的并行。2、实现动态缓存。3、实现增量更新。
零近过年,作为程序员的我也一直在思考,自己和这些知名大公司里的大神的差距到底在哪里,为什么他们能写出来的这么好的框架,自己最初却不知道怎么搞?思考了许久,我认为和他们造成差距的最主要的原因就是1、一颗追求程序卓越的心(研究源码可以坐的座位上一天不带动的精神)。2、不断探索新技术的激情(没有激情哪有动力研究源码)。
接下来进行VasSonic源码的代入,如果你要用VasSonic来加快你的h5渲染的话,接入它很简单,如下
# 终端接入指引-Android版本
----
## 1.Sdk引入配置
在模块的build.gradle文件里面加入
```
compile 'com.tencent.sonic:sdk:3.0.0-alpha'
```
## 2.代码接入
### (1).创建一个类继承SonicRuntime
SonicRuntime类主要提供sonic运行时环境,包括Context、用户UA、ID(用户唯一标识,存放数据时唯一标识对应用户)等等信息。以下代码展示了SonicRuntime的几个方法。
```java
public class HostSonicRuntime extends SonicRuntime {
public HostSonicRuntime(Context context) {
super(context);
}
/**
* 获取用户UA信息
* @return
*/
@Override
public String getUserAgent() {
return "";
}
/**
* 获取用户ID信息
* @return
*/
@Override
public String getCurrentUserAccount() {
return "";
}
/**
* 创建sonic文件存放的路径
* @return
*/
@Override
public File getSonicCacheDir() {
String path = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "sonic/";
File file = new File(path.trim());
if(!file.exists()){
file.mkdir();
}
return file;
}
}
```
### (2).创建一个类继承SonicSessionClient
SonicSessionClient主要负责跟webView的通信,比如调用webView的loadUrl、loadDataWithBaseUrl等方法。
```java
public class SonicSessionClientImpl extends SonicSessionClient {
private WebView webView;
public void bindWebView(WebView webView) {
this.webView = webView;
}
/**
* 调用webView的loadUrl
*/
@Override
public void loadUrl(String url, Bundle extraData) {
webView.loadUrl(url);
}
/**
* 调用webView的loadDataWithBaseUrl方法
*/
@Override
public void loadDataWithBaseUrl(String baseUrl, String data, String mimeType, String encoding,
String historyUrl) {
webView.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl);
}
}
```
### (3).新建包含webView的Activity(或者Fragment等),在activity中完成sonic的接入。这里通过简单的demo展示如何接入
```java
public class SonicTestActivity extends Activity {
public final static String PARAM_URL = "param_url";
public final static String PARAM_MODE = "param_mode";
private SonicSession sonicSession;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// init sonic engine if necessary, or maybe u can do this when application created
if (!SonicEngine.isGetInstanceAllowed()) {
SonicEngine.createInstance(new SonicRuntimeImpl(getApplication()), new SonicConfig.Builder().build());
}
SonicSessionClientImpl sonicSessionClient = null;
// if it's sonic mode , startup sonic session at first time
SonicSessionConfig.Builder sessionConfigBuilder = new SonicSessionConfig.Builder();
// create sonic session and run sonic flow
sonicSession = SonicEngine.getInstance().createSession(url, sessionConfigBuilder.build());
if (null != sonicSession) {
sonicSession.bindClient(sonicSessionClient = new SonicSessionClientImpl());
} else {
// this only happen when a same sonic session is already running,
// u can comment following code to feedback for default mode to
throw new UnknownError("create session fail!");
}
// start init flow ... in the real world, the init flow may cost a long time as startup
// runtime、init configs....
setContentView(R.layout.activity_browser);
// init webview
WebView webView = (WebView) findViewById(R.id.webview);
webView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
if (sonicSession != null) {
sonicSession.getSessionClient().pageFinish(url);
}
}
@TargetApi(21)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
return shouldInterceptRequest(view, request.getUrl().toString());
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
if (sonicSession != null) {
return (WebResourceResponse) sonicSession.getSessionClient().requestResource(url);
}
return null;
}
});
WebSettings webSettings = webView.getSettings();
// add java script interface
// note:if api level if lower than 17(android 4.2), addJavascriptInterface has security
// issue, please use x5 or see https://developer.android.com/reference/android/webkit/
// WebView.html#addJavascriptInterface(java.lang.Object, java.lang.String)
webSettings.setJavaScriptEnabled(true);
webView.removeJavascriptInterface("searchBoxJavaBridge_");
intent.putExtra(SonicJavaScriptInterface.PARAM_LOAD_URL_TIME, System.currentTimeMillis());
webView.addJavascriptInterface(new SonicJavaScriptInterface(sonicSessionClient, intent), "sonic");
// init webview settings
webSettings.setAllowContentAccess(true);
webSettings.setDatabaseEnabled(true);
webSettings.setDomStorageEnabled(true);
webSettings.setAppCacheEnabled(true);
webSettings.setSavePassword(false);
webSettings.setSaveFormData(false);
webSettings.setUseWideViewPort(true);
webSettings.setLoadWithOverviewMode(true);
// webview is ready now, just tell session client to bind
if (sonicSessionClient != null) {
sonicSessionClient.bindWebView(webView);
sonicSessionClient.clientReady();
} else { // default mode
webView.loadUrl(url);
}
}
@Override
public void onBackPressed() {
super.onBackPressed();
}
@Override
protected void onDestroy() {
if (null != sonicSession) {
sonicSession.destroy();
sonicSession = null;
}
super.onDestroy();
}
}
```
SonicTestActivity是一个含有webView的demo代码,里面展示了sonic的整体流程。主要分为6个步骤:
**Step1**:在activity onCreate的时候创建SonicRuntime并且初始化SonicEngine。为sonic初始化运行时需要的环境
```java
if (!SonicEngine.isGetInstanceAllowed()) {
SonicEngine.createInstance(new SonicRuntimeImpl(getApplication()), new SonicConfig.Builder().build());
}
```
**Setp2**:通过SonicEngine.getInstance().createSession来为要加载的url创建一个SonicSession对象,同时为session绑定client。session创建之后sonic就会异步加载数据了。
```java
SonicSessionConfig.Builder sessionConfigBuilder = new SonicSessionConfig.Builder();
// create sonic session and run sonic flow
sonicSession = SonicEngine.getInstance().createSession(url, sessionConfigBuilder.build());
if (null != sonicSession) {
sonicSession.bindClient(sonicSessionClient = new SonicSessionClientImpl());
}
```
**Step3**:设置javascript,这个主要是设置页面跟终端的js交互方式。
按照sonic的规范,webView打开页面之后页面会通过js来获取sonic提供的一些数据(比如页面需要刷新的数据)。Demo里使用的是标准的js交互代码,第三方可以替换为自己的js交互实现方式(比如提供jsbridge伪协议等)。
```java
webSettings.setJavaScriptEnabled(true);
webView.removeJavascriptInterface("searchBoxJavaBridge_");
webView.addJavascriptInterface(new SonicJavaScriptInterface(sonicSessionClient, intent), "sonic");
```
**Step4**:为clinet绑定webview,在webView准备发起loadUrl的时候通过SonicSession的onClientReady方法通知sonicSession: webView ready可以开始loadUrl了。这时sonic内部就会根据本地的数据情况执行webView相应的逻辑(执行loadUrl或者loadData等)。
```java
if (sonicSessionClient != null) {
sonicSessionClient.bindWebView(webView);
sonicSessionClient.clientReady();
}
```
**Step5**:在webView资源拦截的回调中调用session.onClientRequestResource(url)。通过这个方法向sonic获取url对应的WebResourceResponse数据。这样内核就可以根据这个返回的response的内容进行渲染了。(如果sonic在webView ready的时候执行的是loadData的话,是不会走到资源拦截这里的)
```java
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
if (sonicSession != null) {
return (WebResourceResponse) sonicSession.getSessionClient().requestResource(url);
}
return null;
}
```
if (!SonicEngine.isGetInstanceAllowed()) {
SonicEngine.createInstance(new SonicRuntimeImpl(getApplication()), new SonicConfig.Builder().build());
}
其中SonicRuntimeImpl需要你自定义,这个类的主要作用就是
1、设置用户浏览器信息:
public String getUserAgent() {
return "Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Mobile Safari/537.36";
}
2、获取用户id信息:
public String getCurrentUserAccount() {
return "VasSonic-client";
}
3、设置缓存文件地址:
public File getSonicCacheDir() {
String path = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "zp/";
File file = new File(path.trim());
if(!file.exists()){
file.mkdir();
}
return file;
//return super.getSonicCacheDir();
}
4、设置Cookie:
public boolean setCookie(String url, List cookies) {
if (!TextUtils.isEmpty(url) && cookies != null && cookies.size() > 0) {
CookieManager cookieManager = CookieManager.getInstance();
for (String cookie : cookies) {
cookieManager.setCookie(url, cookie);
}
return true;
}
return false;
}
5、创建资源流(并行现在DOM,将DOM流设置给webView,从而让webView加载并行下载的资源,而不通过内核加载,加快响应时间):
public Object createWebResourceResponse(String mimeType, String encoding, InputStream data, Map headers) {
WebResourceResponse resourceResponse = new WebResourceResponse(mimeType, encoding, data);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
String cookie = SharedPreferencesUtils.getString(
BaseApplication.getInstance(), "Cookie", null);
headers.put("Set-Cookie", cookie);
resourceResponse.setResponseHeaders(headers);
}
return resourceResponse;
}
而SonicConfig类一看名字就是为了保存一些属性的类:
/**
* 默认最多保存5session
*/
int MAX_PRELOAD_SESSION_COUNT = 5;
/**
* 默认不可用时间,累加
*/
long SONIC_UNAVAILABLE_TIME = 6 * 60 * 60 * 1000;
/**
* 默认最大在sd卡保存html文件的总大小为30M
*/
long SONIC_CACHE_MAX_SIZE = 30 * 1024 * 1024;
/**
* 默认资源最大保存60M超过就删除命中率最低的一个文件
*/
long SONIC_RESOURCE_CACHE_MAX_SIZE = 60 * 1024 * 1024;
/**
* 默认检查缓存的时间间隔为一天.
*/
long SONIC_CACHE_CHECK_TIME_INTERVAL = 24 * 60 * 60 * 1000L;
/**
* 在同一个时间最多可以开3个任务去下载资源
*/
public int SONIC_MAX_NUM_OF_DOWNLOADING_TASK = 3;
/**
* 在过期前的age的最大时间
*/
int SONIC_CACHE_MAX_AGE = 5 * 60 * 1000;
/**
*是否用SHA1检查文件的完整性
*/
public boolean VERIFY_CACHE_FILE_WITH_SHA1 = true;
/**
* 是否在创建缓存前自动初始化数据库
*/
boolean AUTO_INIT_DB_WHEN_CREATE = true;
/**
*当session创建的时候是否弄一个cookie持久化
*/
boolean GET_COOKIE_WHEN_SESSION_CREATE = true;
创建完SonicEngine引擎之后,就该创建SonicSession会话了:
sonicSession = SonicEngine.getInstance().createSession(url, sessionConfigBuilder.build());
新开的浏览器窗口会生成新的Session,但子窗口除外。子窗口会共用父窗口的Session。例如,在链接上右击,在弹出的快捷菜单中选择"在新窗口中打开"时,子窗口便可以访问父窗口的Session
腾讯这里赋予名字的含义,就是为每一个url,VasSonic将会赋予一个会话,用享元模式将它储存起来,每个会话都包括该url记录了相关的网页和资源地址,比如已经是第二次加载的网页的话,并且本地的缓存没有过期,将根据该会话找到网页缓存和对应的资源缓存并加载进来,接下来看一下SonicSession的配置对象SonicSessionConfig属性的构成,如下:
/**
* 连接超时时间
*/
int CONNECT_TIMEOUT_MILLIS = 5000;
/**
* 读超时时间
*/
int READ_TIMEOUT_MILLIS = 15000;
/**
* Buffer缓存大小,读数据时的设置
*/
int READ_BUF_SIZE = 1024 * 10;
/**
* 默认到期时间为3分钟
*/
long PRELOAD_SESSION_EXPIRED_TIME = 3 * 60 * 1000;
/**
* 是否支持拆分数据,将易变部分拆分成data,将不变部分拆分成模板(就是将h5分为容易变得部分和不容易变的部分)
*/
boolean ACCEPT_DIFF_DATA = true;
/**
* 数据是否和用户绑定,一个用户对应一种缓存
*/
boolean IS_ACCOUNT_RELATED = true;
/**
* 是否需要在网络不好的时候重新加载网页,网不好,用缓存.
*/
boolean RELOAD_IN_BAD_NETWORK = false;
/**
* 创建的时候是否自动加载缓存和从网络中获取
*/
boolean AUTO_START_WHEN_CREATE = true;
/**
* 是否检查响应头信息Cache-Control
*/
boolean SUPPORT_CACHE_CONTROL = false;
/**
* 是否使用本地服务器
*/
boolean SUPPORT_LOCAL_SERVER = false;
/**
* 当网络不可用时的吐丝
*/
String USE_SONIC_CACHE_IN_BAD_NETWORK_TOAST = "亲,您的网络实在是太差了!";
/**
* 默认session的模式为快速
*/
int sessionMode = SonicConstants.SESSION_MODE_QUICK;
/**
* 加载缓存时候的本地拦截器,可以定义一份自己的缓存
*/
SonicCacheInterceptor cacheInterceptor = null;
/**
* 自定义网络调用规则,采用哪种方式获得网页的数据,比如这个框架默认采用URLConnection的方式
*/
SonicSessionConnectionInterceptor connectionInterceptor = null;
/**
*客户端需要发送给服务器的头信息
*/
Map customRequestHeaders = null;
/**
* 给WebResourceResponse设值头信息,5.0以上不设置可能会抛异常
*/
Map customResponseHeaders = null;
下面是设置自己的缓存和获取数据的策略
if (MainActivity.MODE_SONIC_WITH_OFFLINE_CACHE == mode) {
sessionConfigBuilder.setCacheInterceptor(new SonicCacheInterceptor(null) {
@Override
public String getCacheData(SonicSession session) {
return null; // offline pkg does not need cache
}
});
sessionConfigBuilder.setConnectionInterceptor(new SonicSessionConnectionInterceptor() {
@Override
public SonicSessionConnection getConnection(SonicSession session, Intent intent) {
return new OfflinePkgSessionConnection(BrowserActivity.this, session, intent);
}
});
}
webView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
if (sonicSession != null) {
sonicSession.getSessionClient().pageFinish(url);
}
}
@TargetApi(21)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
return shouldInterceptRequest(view, request.getUrl().toString());
}
/**
* 拦截所有的url请求,若返回非空,则不再进行网络资源请求,而是使用返回的资源数据
*/
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
if (sonicSession != null) {
return (WebResourceResponse) sonicSession.getSessionClient().requestResource(url);
}
return null;
}
});
onPageFinished最后的调用方法是SonicSession的一个实现方法
public boolean onClientPageFinished(String url) {
if (isMatchCurrentUrl(url)) {
SonicUtils.log(TAG, Log.INFO, "session(" + sId
+ ") onClientPageFinished:url=" + url + ".");
wasOnPageFinishInvoked.set(true);
return true;
}
return false;
}
webView.addJavascriptInterface(new SonicJavaScriptInterface(sonicSessionClient, intent), "sonic");
这个类做的主要工作就是将经常改变的数据重新还给h5界面,前端开发人员用动态改变Dom树的方式,实现局部刷新,代码如下:
@JavascriptInterface
public void getDiffData() {
// the callback function of demo page is hardcode as 'getDiffDataCallback'
getDiffData2("getDiffDataCallback");
}
@JavascriptInterface
public void getDiffData2(final String jsCallbackFunc) {
if (null != sessionClient) {
sessionClient.getDiffData(new SonicDiffDataCallback() {
//将改变的数据交给js处理更新
@Override
public void callback(final String resultData) {
Runnable callbackRunnable = new Runnable() {
@Override
public void run() {
String jsCode = "javascript:" + jsCallbackFunc + "('"+ toJsString(resultData) + "')";
sessionClient.getWebView().loadUrl(jsCode);
}
};
if (Looper.getMainLooper() == Looper.myLooper()) {
callbackRunnable.run();
} else {
new Handler(Looper.getMainLooper()).post(callbackRunnable);
}
}
});
}
}
别忘了打开webview自带的缓存策略,自带缓存和自己创建的缓存结合使用,效果会更好。
// init webview settings
webSettings.setAllowContentAccess(true);
webSettings.setDatabaseEnabled(true);
webSettings.setDomStorageEnabled(true);
webSettings.setAppCacheEnabled(true);
webSettings.setSavePassword(false);
webSettings.setSaveFormData(false);
webSettings.setUseWideViewPort(true);
if (null != sonicSession) {
sonicSession.bindClient(sonicSessionClient = new SonicSessionClientImpl());
}
SonicSessionClientImpl是干啥的?看一下
public class SonicSessionClientImpl extends SonicSessionClient {
private WebView webView;
public void bindWebView(WebView webView) {
this.webView = webView;
}
public WebView getWebView() {
return webView;
}
@Override
public void loadUrl(String url, Bundle extraData) {
Log.i("huoying", "常规操作");
webView.loadUrl(url);
}
@Override
public void loadDataWithBaseUrl(String baseUrl, String data, String mimeType, String encoding, String historyUrl) {
Log.i("huoying", "非常规操作");
webView.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl);
}
@Override
public void loadDataWithBaseUrlAndHeader(String baseUrl, String data, String mimeType, String encoding, String historyUrl, HashMap headers) {
loadDataWithBaseUrl(baseUrl, data, mimeType, encoding, historyUrl);
}
public void destroy() {
if (null != webView) {
webView.destroy();
webView = null;
}
}
}
这里可以理解为webview的中介者,通过这个类来间接实现webview加载数据,不直接和webview打交道。最后不要忘了设置下面这几句代码,如下:
if (sonicSessionClient != null) {
sonicSessionClient.bindWebView(webView);
sonicSessionClient.clientReady();
} else { // default mode
webView.loadUrl(url);
}
这句话的意思就是将webView绑定到SonicSessionClientImpl,并设置状态为已经准备好,可以加载网页了,到此VasSonic的使用就介绍完了,接下来进入源码看一看源码是怎么实现的,好废话不多说,先看一下创建会话的时候,都做了哪些事情?创建会话首先进入的是下面这个方法:
public synchronized SonicSession createSession( String url, SonicSessionConfig sessionConfig) {
if (isSonicAvailable()) {
//创建sessionId
String sessionId = makeSessionId(url, sessionConfig.IS_ACCOUNT_RELATED);
//sessionID不为空的话
if (!TextUtils.isEmpty(sessionId)) {
SonicSession sonicSession = lookupSession(sessionConfig, sessionId, true);
if (null != sonicSession) {
sonicSession.setIsPreload(url);
} else if (isSessionAvailable(sessionId)) { // 缓存中未存在
sonicSession = internalCreateSession(sessionId, url, sessionConfig);
}
return sonicSession;
}
} else {
runtime.log(TAG, Log.ERROR, "createSession fail for sonic service is unavailable!");
}
return null;
}
isSonicAvailable()首先判断是否正在改变数据库,如果正在修改数据库,那么创建session将不进行,直接返回null。然后调用makeSessionId创建session的唯一标识,sessionID,先来看一看makeSessionId的实现:
public String makeSessionId(String url, boolean isAccountRelated) {
if (isSonicUrl(url)) {
StringBuilder sessionIdBuilder = new StringBuilder();
try {
Uri uri = Uri.parse(url);
sessionIdBuilder.append(uri.getAuthority()).append(uri.getPath());
if (uri.isHierarchical()) {
String sonicRemainParams = uri.getQueryParameter(SonicConstants.SONIC_REMAIN_PARAMETER_NAMES);
TreeSet remainParamTreeSet = new TreeSet();
if (!TextUtils.isEmpty(sonicRemainParams)) {
Collections.addAll(remainParamTreeSet, sonicRemainParams.split(SonicConstants.SONIC_REMAIN_PARAMETER_SPLIT_CHAR));
}
TreeSet parameterNamesTreeSet = new TreeSet(getQueryParameterNames(uri));
if (!remainParamTreeSet.isEmpty()) {
parameterNamesTreeSet.remove(SonicConstants.SONIC_REMAIN_PARAMETER_NAMES);
}
for (String parameterName : parameterNamesTreeSet) {
if (!TextUtils.isEmpty(parameterName) && (parameterName.startsWith(SonicConstants.SONIC_PARAMETER_NAME_PREFIX) || remainParamTreeSet.contains(parameterName))) {
sessionIdBuilder.append(parameterName).append(uri.getQueryParameter(parameterName));
}
}
}
} catch (Throwable e) {
log(TAG, Log.ERROR, "makeSessionId error:" + e.getMessage() + ", url=" + url);
sessionIdBuilder.setLength(0);
sessionIdBuilder.append(url);
}
String sessionId;
//sessionID=当前的userAccount加上url的特殊位组成的md5值
if (isAccountRelated) {
sessionId = getCurrentUserAccount() + "_" + SonicUtils.getMD5(sessionIdBuilder.toString());
} else {
sessionId = SonicUtils.getMD5(sessionIdBuilder.toString());
}
return sessionId;
}
return null;
}
private SonicSession lookupSession(SonicSessionConfig config, String sessionId, boolean pick) {
if (!TextUtils.isEmpty(sessionId) && config != null) {
SonicSession sonicSession = preloadSessionPool.get(sessionId);
if (sonicSession != null) {
//判断session缓存是否过期,以及sessionConfig是否发生变化
if (!config.equals(sonicSession.config) ||
sonicSession.config.PRELOAD_SESSION_EXPIRED_TIME > 0 && System.currentTimeMillis() - sonicSession.createdTime > sonicSession.config.PRELOAD_SESSION_EXPIRED_TIME) {
if (runtime.shouldLog(Log.ERROR)) {
runtime.log(TAG, Log.ERROR, "lookupSession error:sessionId(" + sessionId + ") is expired.");
}
//到期了的话,从数据池里面清除
preloadSessionPool.remove(sessionId);
//并销毁
sonicSession.destroy();
return null;
}
if (pick) {
preloadSessionPool.remove(sessionId);
}
}
return sonicSession;
}
return null;
}
private SonicSession internalCreateSession(String sessionId, String url, SonicSessionConfig sessionConfig) {
if (!runningSessionHashMap.containsKey(sessionId)) {
SonicSession sonicSession;
//默认是quikly
if (sessionConfig.sessionMode == SonicConstants.SESSION_MODE_QUICK) {
sonicSession = new QuickSonicSession(sessionId, url, sessionConfig);
} else {
sonicSession = new StandardSonicSession(sessionId, url, sessionConfig);
}
//设置session的状态改变的时候的调用
sonicSession.addSessionStateChangedCallback(sessionCallback);
//开始拉取网络,?
if (sessionConfig.AUTO_START_WHEN_CREATE) {
sonicSession.start();
}
return sonicSession;
}
if (runtime.shouldLog(Log.ERROR)) {
runtime.log(TAG, Log.ERROR, "internalCreateSession error:sessionId(" + sessionId + ") is running now.");
}
return null;
}
SonicSession(String id, String url, SonicSessionConfig config) {
this.id = id;
this.config = config;
this.sId = (sNextSessionLogId++);
this.srcUrl = statistics.srcUrl = url.trim();
this.createdTime = System.currentTimeMillis();
fileHandler = new Handler(SonicEngine.getInstance().getRuntime()
.getFileThreadLooper(), new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case FILE_THREAD_SAVE_CACHE_ON_SERVER_CLOSE: {
final SonicServer sonicServer = (SonicServer) msg.obj;
saveSonicCacheOnServerClose(sonicServer);
return true;
}
/**
* 下载完了,开始保存数据
*/
case FILE_THREAD_SAVE_CACHE_ON_SESSION_FINISHED: {
final String htmlString = (String) msg.obj;
doSaveSonicCache(server, htmlString);
return true;
}
}
return false;
}
});
SonicConfig sonicConfig = SonicEngine.getInstance().getConfig();
if (sonicConfig.GET_COOKIE_WHEN_SESSION_CREATE) {
SonicRuntime runtime = SonicEngine.getInstance().getRuntime();
String cookie = runtime.getCookie(srcUrl);
if (!TextUtils.isEmpty(cookie)) {
intent.putExtra(SonicSessionConnection.HTTP_HEAD_FIELD_COOKIE,
cookie);
}
}
if (SonicUtils.shouldLog(Log.INFO)) {
SonicUtils.log(TAG, Log.INFO, "session(" + sId + ") create:id="
+ id + ", url = " + url + ".");
}
}
下面看看session具体做的事情如下:
public void start() {
// 如果当前状态是正在运行的话
if (!sessionState.compareAndSet(STATE_NONE, STATE_RUNNING)) {
SonicUtils.log(TAG, Log.DEBUG, "session(" + sId
+ ") start error:sessionState=" + sessionState.get() + ".");
return;
}
SonicUtils.log(TAG, Log.INFO, "session(" + sId
+ ") now post sonic flow task.");
for (WeakReference ref : sessionCallbackList) {
SonicSessionCallback callback = ref.get();
if (callback != null) {
callback.onSonicSessionStart();
}
}
statistics.sonicStartTime = System.currentTimeMillis();
isWaitingForSessionThread.set(true);
SonicEngine.getInstance().getRuntime()
.postTaskToSessionThread(new Runnable() {
@Override
public void run() {
runSonicFlow(true);
}
});
// 通知session的状态改变了
notifyStateChange(STATE_NONE, STATE_RUNNING, null);
}
这个方法首先将当前状态标记为会话运行的状态,防止这个方法被同一个会话执行两次,然后回调session的生命周期回调方法,然后通过线程池执行子任务,最后回调通知状态现在的会话状态是正在运行。
public void postTaskToSessionThread(Runnable task) {
SonicSessionThreadPool.postTask(task);
}
private boolean execute(Runnable task) {
try {
executorServiceImpl.execute(task);
return true;
} catch (Throwable e) {
SonicUtils.log(TAG, Log.ERROR, "execute task error:" + e.getMessage());
return false;
}
}
executorServiceImpl就是下面java1.5以后带的线程池ExecutorService
private final ExecutorService executorServiceImpl;
private void runSonicFlow(boolean firstRequest) {
if (STATE_RUNNING != sessionState.get()) {
SonicUtils.log(TAG, Log.ERROR, "session(" + sId
+ ") runSonicFlow error:sessionState=" + sessionState.get()
+ ".");
return;
}
statistics.sonicFlowStartTime = System.currentTimeMillis();
String cacheHtml = null;
SonicDataHelper.SessionData sessionData;
sessionData = getSessionData(firstRequest);
if (firstRequest) {
// 第一次加载缓存。肯定缓存回来为null
cacheHtml = SonicCacheInterceptor.getSonicCacheData(this);
statistics.cacheVerifyTime = System.currentTimeMillis();
SonicUtils
.log(TAG,
Log.INFO,
"session("
+ sId
+ ") runSonicFlow verify cache cost "
+ (statistics.cacheVerifyTime - statistics.sonicFlowStartTime)
+ " ms");
handleFlow_LoadLocalCache(cacheHtml); // local cache if exist before
// connection
}
boolean hasHtmlCache = !TextUtils.isEmpty(cacheHtml) || !firstRequest;
final SonicRuntime runtime = SonicEngine.getInstance().getRuntime();
/**
* isNetworkValid默认为true
*/
if (!runtime.isNetworkValid()) {
// Whether the network is available
if (hasHtmlCache
&& !TextUtils
.isEmpty(config.USE_SONIC_CACHE_IN_BAD_NETWORK_TOAST)) {
runtime.postTaskToMainThread(new Runnable() {
@Override
public void run() {
if (clientIsReady.get()
&& !isDestroyedOrWaitingForDestroy()) {
runtime.showToast(
config.USE_SONIC_CACHE_IN_BAD_NETWORK_TOAST,
Toast.LENGTH_LONG);
}
}
}, 1500);
}
SonicUtils.log(TAG, Log.ERROR, "session(" + sId
+ ") runSonicFlow error:network is not valid!");
} else {
handleFlow_Connection(hasHtmlCache, sessionData);
statistics.connectionFlowFinishTime = System.currentTimeMillis();
}
// Update session state
/**
* 改变session的状态
*/
switchState(STATE_RUNNING, STATE_READY, true);
isWaitingForSessionThread.set(false);
// Current session can be destroyed if it is waiting for destroy.
if (postForceDestroyIfNeed()) {
SonicUtils.log(TAG, Log.INFO, "session(" + sId
+ ") runSonicFlow:send force destroy message.");
}
}
static SessionData getSessionData(String sessionId) {
SQLiteDatabase db = SonicDBHelper.getInstance().getWritableDatabase();
SessionData sessionData = getSessionData(db, sessionId);
if (null == sessionData) {
sessionData = new SessionData();
}
return sessionData;
}
"CREATE TABLE IF NOT EXISTS " + Sonic_SESSION_TABLE_NAME + " ( " +
"id integer PRIMARY KEY autoincrement" +
" , " + SESSION_DATA_COLUMN_SESSION_ID + " text not null" +
" , " + SESSION_DATA_COLUMN_ETAG + " text not null" +
" , " + SESSION_DATA_COLUMN_TEMPLATE_EAG + " text" +
" , " + SESSION_DATA_COLUMN_HTML_SHA1 + " text not null" +
" , " + SESSION_DATA_COLUMN_UNAVAILABLE_TIME + " integer default 0" +
" , " + SESSION_DATA_COLUMN_HTML_SIZE + " integer default 0" +
" , " + SESSION_DATA_COLUMN_TEMPLATE_UPDATE_TIME + " integer default 0" +
" , " + SESSION_DATA_COLUMN_CACHE_EXPIRED_TIME + " integer default 0" +
" , " + SESSION_DATA_COLUMN_CACHE_HIT_COUNT + " integer default 0" +
" ); ";
sessionID:会话的唯一标识
//页面内容唯一标识
eTag
//模板唯一标识
templateTag
htmlSha1
//h5的大小
htmlSize
模板更新时间
templateUpdateTime
//多长时间内不采用缓存
UnavailableTime
//缓存过期时间,可以由服务端控制
cacheExpiredTime
/ /缓存命中率,每取一次缓存命中率就会+1
cacheHitCount
第二张表记录资源文件的表,何为资源文件?比如一个H5界面中嵌套的js脚本、css脚本还有图片的地址等等,这些放在外面的话就构成了h5的资源文件,光下载了h5文件是远远不够的,还需要加载h5的资源文件,然后经过渲染,h5界面才能完全显示出来。
"CREATE TABLE IF NOT EXISTS " + Sonic_RESOURCE_TABLE_NAME + " ( " +
"id integer PRIMARY KEY autoincrement" +
" , " + RESOURCE_DATA_COLUMN_RESOURCE_ID + " text not null" +
" , " + RESOURCE_DATA_COLUMN_RESOURCE_SHA1 + " text not null" +
" , " + RESOURCE_DATA_COLUMN_RESOURCE_SIZE + " integer default 0" +
" , " + RESOURCE_DATA_COLUMN_LAST_UPDATE_TIME + " integer default 0" +
" , " + RESOURCE_DATA_COLUMN_CACHE_EXPIRED_TIME + " integer default 0" +
" ); ";
static String getSonicCacheData(SonicSession session) {
//如果前面你设置了缓存拦截的话,缓存文件内容的获取将通过你自己的缓存策略获取,否则走框架的缓存获取
SonicCacheInterceptor interceptor = session.config.cacheInterceptor;
if (null == interceptor) {
return SonicCacheInterceptorDefaultImpl.getCacheData(session);
}
String htmlString = null;
while (null != interceptor) {
htmlString = interceptor.getCacheData(session);
if (null != htmlString) {
break;
}
interceptor = interceptor.next();
}
return htmlString;
}
还记得在用这个框架的时候,你可以选择设不设置缓存拦截器,如果不设置的话,那么就会从框架默认的缓存策略中获得缓存,否则获取你自己保存的缓存,看一下这个框架自带的缓存的获取,如下:
public static String getCacheData(SonicSession session) {
if (session == null) {
SonicUtils.log(TAG, Log.INFO, "getCache is null");
return null;
}
/**
* 首先通过数据库获取SessionData
*/
SonicDataHelper.SessionData sessionData = SonicDataHelper.getSessionData(session.id);
boolean verifyError;
String htmlString = "";
//判断数据是否合法,数据表中当前session的存储一定要用eTag和htmlSha1的两个值
// verify local data
if (TextUtils.isEmpty(sessionData.eTag) || TextUtils.isEmpty(sessionData.htmlSha1)) {
verifyError = true;
SonicUtils.log(TAG, Log.INFO, "session(" + session.sId + ") runSonicFlow : session data is empty.");
} else {
//数据合法的时候走下面这些内容,首先更新命中率,命中率在sd卡数据量超过设置的大小的时候进行删除的时候,先删除命中率低的数据,也就是最近最少使用的数据
SonicDataHelper.updateSonicCacheHitCount(session.id);
//得到缓存文件的路径文件
File htmlCacheFile = new File(SonicFileUtils.getSonicHtmlPath(session.id));
//读取文件中的字节流并转化为字符串
htmlString = SonicFileUtils.readFile(htmlCacheFile);
verifyError = TextUtils.isEmpty(htmlString);
if (verifyError) {
SonicUtils.log(TAG, Log.ERROR, "session(" + session.sId + ") runSonicFlow error:cache data is null.");
} else {
if (SonicEngine.getInstance().getConfig().VERIFY_CACHE_FILE_WITH_SHA1) {
//验证数据库中的Sha1编码和刚得到的字符串的Sha1是否匹配
if (!SonicFileUtils.verifyData(htmlString, sessionData.htmlSha1)) {
verifyError = true;
htmlString = "";
SonicEngine.getInstance().getRuntime().notifyError(session.sessionClient, session.srcUrl, SonicConstants.ERROR_CODE_DATA_VERIFY_FAIL);
SonicUtils.log(TAG, Log.ERROR, "session(" + session.sId + ") runSonicFlow error:verify html cache with sha1 fail.");
} else {
SonicUtils.log(TAG, Log.INFO, "session(" + session.sId + ") runSonicFlow verify html cache with sha1 success.");
}
} else {
/**
* 判断size大小是否一样,保证数据是一致的,(比如特殊情况,一方保存失败)
*/
if (sessionData.htmlSize != htmlCacheFile.length()) {
verifyError = true;
htmlString = "";
SonicEngine.getInstance().getRuntime().notifyError(session.sessionClient, session.srcUrl, SonicConstants.ERROR_CODE_DATA_VERIFY_FAIL);
SonicUtils.log(TAG, Log.ERROR, "session(" + session.sId + ") runSonicFlow error:verify html cache with size fail.");
}
}
}
}
// if the local data is faulty, delete it
/**
* 如果验证的环节出现错误,清除当前的缓存
*/
if (verifyError) {
long startTime = System.currentTimeMillis();
SonicUtils.removeSessionCache(session.id);
sessionData.reset();
SonicUtils.log(TAG, Log.INFO, "session(" + session.sId + ") runSonicFlow:verify error so remove session cache, cost " + +(System.currentTimeMillis() - startTime) + "ms.");
}
return htmlString;
}
}
1、判断数据是否合法,数据表中当前session的存储一定要用eTag和htmlSha1的两个值
2、数据合法的时候走下面这些内容,首先更新命中率,命中率在sd卡数据量超过设置的大小的时候进行删除的时候,先删除命中率低的数据,也就是最近最少使用的数据,得到缓存文件的路径文件,读取文件中的字节流并转化为字符串
3、验证数据库中的Sha1编码和刚得到的字符串的Sha1是否匹配
4、判断size大小是否一样,保证数据是一致的,(比如特殊情况,一方保存失败)
5、如果验证的环节出现错误,清除当前的缓存
接着走主流程,获得完缓存之后,接着通过handleFlow_LoadLocalCache(cacheHtml);这个方法要对缓存进行预加载,如下:
protected void handleFlow_LoadLocalCache(String cacheHtml) {
// 如果html不为null的话,那么加载html,发送命令CLIENT_CORE_MSG_PRE_LOAD
Message msg = mainHandler.obtainMessage(CLIENT_CORE_MSG_PRE_LOAD);
//如果缓存内容不为null,那么发送消息预加载cacheHtml
if (!TextUtils.isEmpty(cacheHtml)) {
msg.arg1 = PRE_LOAD_WITH_CACHE;
msg.obj = cacheHtml;
} else {
//没有缓存的时候发送信息,开始加载view
SonicUtils.log(TAG, Log.INFO, "session(" + sId
+ ") runSonicFlow has no cache, do first load flow.");
msg.arg1 = PRE_LOAD_NO_CACHE;
}
//发送消息
mainHandler.sendMessage(msg);
/**
* 回调session的生命周期回调
*/
for (WeakReference ref : sessionCallbackList) {
SonicSessionCallback callback = ref.get();
if (callback != null) {
// 回调开始加载缓存
callback.onSessionLoadLocalCache(cacheHtml);
}
}
}
if (wasLoadDataInvoked.compareAndSet(false, true)) {
SonicUtils
.log(TAG,
Log.INFO,
"session("
+ sId
+ ") handleClientCoreMessage_PreLoad:PRE_LOAD_WITH_CACHE load data.");
String html = (String) msg.obj;
sessionClient.loadDataWithBaseUrlAndHeader(srcUrl, html,
"text/html", SonicUtils.DEFAULT_CHARSET, srcUrl,
getCacheHeaders());
Log.i("huoying", "重新加载缓存");
} else {
SonicUtils
.log(TAG,
Log.ERROR,
"session("
+ sId
+ ") handleClientCoreMessage_PreLoad:wasLoadDataInvoked = true.");
}
if (wasLoadUrlInvoked.compareAndSet(false, true)) {
SonicUtils
.log(TAG,
Log.INFO,
"session("
+ sId
+ ") handleClientCoreMessage_PreLoad:PRE_LOAD_NO_CACHE load url.");
sessionClient.loadUrl(srcUrl, null);
} else {
SonicUtils
.log(TAG,
Log.ERROR,
"session("
+ sId
+ ") handleClientCoreMessage_PreLoad:wasLoadUrlInvoked = true.");
}
}
直接通过loadUrl去加载网页,这时候如果浏览器内核还没有初始化的话,这个时候就可以先去服务器拉取数据了,在回到主线流程,这个框架通过handleFlow_Connection(hasHtmlCache, sessionData)方法开始连接服务器,并获取数据,也就说,如果没有缓存的话,在第一次执行loadUrl的时候,开始初始化浏览器内核,而session会话会在子线程中开始连接服务器获取数据,实现并行,从而加快浏览器的渲染时间。接下来来看一下这个方法,如下:
protected void handleFlow_Connection(boolean hasCache,
SonicDataHelper.SessionData sessionData) {
// create connection for current session
statistics.connectionFlowStartTime = System.currentTimeMillis();
/**
* 1、首先通过expiredTime属性判断缓存有没有过期,如果缓存没有过期,则回调命中的方法,然后直接缓存,因为你采用缓存加载数据了,就没有必要重新从服务器上获取了
*/
if (config.SUPPORT_CACHE_CONTROL
&& statistics.connectionFlowStartTime < sessionData.expiredTime) {
if (SonicUtils.shouldLog(Log.DEBUG)) {
SonicUtils
.log(TAG,
Log.DEBUG,
"session("
+ sId
+ ") won't send any request in "
+ (sessionData.expiredTime - statistics.connectionFlowStartTime)
+ ".ms");
}
for (WeakReference ref : sessionCallbackList) {
SonicSessionCallback callback = ref.get();
if (callback != null) {
// 命中缓存的时候
callback.onSessionHitCache();
}
}
return;
}
/**
* createConnectionIntent方法创建intent,填入必要的数据,比如cookie。客户端信息
*/
/**
*
*/
server = new SonicServer(this, createConnectionIntent(sessionData));
// Connect to web server
// 开始连接服务器的h5界面
int responseCode = server.connect();
if (SonicConstants.ERROR_CODE_SUCCESS == responseCode) {
responseCode = server.getResponseCode();
// If the page has set cookie, sonic will set the cookie to kernel.
long startTime = System.currentTimeMillis();
Map> headerFieldsMap = server
.getResponseHeaderFields();
if (SonicUtils.shouldLog(Log.DEBUG)) {
SonicUtils.log(TAG, Log.DEBUG,
"session(" + sId
+ ") connection get header fields cost = "
+ (System.currentTimeMillis() - startTime)
+ " ms.");
}
startTime = System.currentTimeMillis();
//为webview设置头信息
setCookiesFromHeaders(headerFieldsMap,
shouldSetCookieAsynchronous());
if (SonicUtils.shouldLog(Log.DEBUG)) {
SonicUtils.log(TAG, Log.DEBUG,
"session(" + sId + ") connection set cookies cost = "
+ (System.currentTimeMillis() - startTime)
+ " ms.");
}
}
SonicUtils
.log(TAG,
Log.INFO,
"session("
+ sId
+ ") handleFlow_Connection: respCode = "
+ responseCode
+ ", cost "
+ (System.currentTimeMillis() - statistics.connectionFlowStartTime)
+ " ms.");
// Destroy before server response
if (isDestroyedOrWaitingForDestroy()) {
SonicUtils
.log(TAG,
Log.ERROR,
"session("
+ sId
+ ") handleFlow_Connection error: destroy before server response!");
return;
}
/**
* 资源是靠服务端的头决定的,承载资源的头,由服务端确定sonic-link
*/
// when find preload links in headers
String preloadLink = server
.getResponseHeaderField(SonicSessionConnection.CUSTOM_HEAD_FILED_LINK);
// Log.i("huoying", "preloadLink:"+preloadLink);
if (!TextUtils.isEmpty(preloadLink)) {
Log.i("huoying", "加载资源");
preloadLinks = Arrays.asList(preloadLink.split(";"));
//开始开启任务进行资源下载
handleFlow_PreloadSubResource();
}
// When response code is 304
if (HttpURLConnection.HTTP_NOT_MODIFIED == responseCode) {
SonicUtils
.log(TAG,
Log.INFO,
"session("
+ sId
+ ") handleFlow_Connection: Server response is not modified.");
//资源没有过期的话,又命中了缓存
handleFlow_NotModified();
return;
}
// When response code is not 304 nor 200
//如果状态码不正确,那么通知,出错
if (HttpURLConnection.HTTP_OK != responseCode) {
handleFlow_HttpError(responseCode);
SonicEngine.getInstance().getRuntime()
.notifyError(sessionClient, srcUrl, responseCode);
SonicUtils.log(TAG, Log.ERROR, "session(" + sId
+ ") handleFlow_Connection error: response code("
+ responseCode + ") is not OK!");
return;
}
/**
* 得到自定义的响应头 cache-offline true:缓存到磁盘并展示返回内容 false:展示返回内容,无需缓存到磁盘
* store:缓存到磁盘,如果已经加载缓存,则下次加载,否则展示返回内容
* http:容灾字段,如果http表示终端六个小时以内不会采用sonic请求该URL
*/
String cacheOffline = server
.getResponseHeaderField(SonicSessionConnection.CUSTOM_HEAD_FILED_CACHE_OFFLINE);
SonicUtils.log(TAG, Log.INFO, "session(" + sId
+ ") handleFlow_Connection: cacheOffline is " + cacheOffline
+ ".");
// When cache-offline is "http": which means sonic server is in bad
// condition, need feed back to run standard http request.
//如果cache-Offline为http的话,6小时内不采用缓存
if (OFFLINE_MODE_HTTP.equalsIgnoreCase(cacheOffline)) {
//六
if (hasCache) {
// stop loading local sonic cache.
handleFlow_ServiceUnavailable();
}
// 默认的unavailableTime为创建的时候加上6小时
long unavailableTime = System.currentTimeMillis()
+ SonicEngine.getInstance().getConfig().SONIC_UNAVAILABLE_TIME;
//设置失效时间
SonicDataHelper.setSonicUnavailableTime(id, unavailableTime);
for (WeakReference ref : sessionCallbackList) {
SonicSessionCallback callback = ref.get();
if (callback != null) {
callback.onSessionUnAvailable();
}
}
return;
}
/*
* etag:页面内容的唯一标识(哈希值) template-tag:模板唯一标识(哈希值),客户端使用本地校验或服务端使用判断是模板有变更。
* template-change:标记模板是否变更,客户端使用 cache-offline:客户端使用,根据不同类型进行不同行为
*/
// When cacheHtml is empty, run First-Load flow
//没有缓存的时候的处理
if (!hasCache) {
//没有缓存调用第一次加载
handleFlow_FirstLoad();
return;
}
// Handle cache-offline : false or null.
//如果是false清除缓存
if (TextUtils.isEmpty(cacheOffline)
|| OFFLINE_MODE_FALSE.equalsIgnoreCase(cacheOffline)) {
SonicUtils
.log(TAG,
Log.ERROR,
"session("
+ sId
+ ") handleFlow_Connection error: Cache-Offline is empty or false!");
SonicUtils.removeSessionCache(id);
return;
}
String eTag = server
.getResponseHeaderField(SonicSessionConnection.CUSTOM_HEAD_FILED_ETAG);
//标记模板是否变更
String templateChange = server
.getResponseHeaderField(SonicSessionConnection.CUSTOM_HEAD_FILED_TEMPLATE_CHANGE);
// When eTag is empty, run fix logic
if (TextUtils.isEmpty(eTag) || TextUtils.isEmpty(templateChange)) {
SonicUtils.log(TAG, Log.ERROR, "session(" + sId
+ ") handleFlow_Connection error: eTag is ( " + eTag
+ " ) , templateChange is ( " + templateChange + " )!");
SonicUtils.removeSessionCache(id);
return;
}
// When templateChange is false : means data update
//判断模板有没有变更
if ("false".equals(templateChange) || "0".equals(templateChange)) {
//模板没有变更,判断数据是否变更,变更的话,改变数据
handleFlow_DataUpdate(server.getUpdatedData());
} else {
//模板改变的话,改变模板数据
handleFlow_TemplateChange(server.getResponseData(clientIsReload
.get()));
}
}
1、首先通过expiredTime属性判断缓存有没有过期,如果缓存没有过期,则回调命中的方法,然后直接缓存,因为你采用缓存加载数据了,就没有必要重新从服务器上获取了
2、如果缓存过期则创建SonicServer对象用于连接服务器操作
3、通过 server.connect()连接服务器
4、获得响应的头信息,为webview设置头信息中的Cookie。
5、通过 sonic-link响应头信息获得当前h5资源的所有连接,并开启任务进行资源的下载
6、如果和服务器交互不成功直接通知错误并返回
7、交互成功后,获得相关缓存头进行处理,得到自定义的响应头 cache-offline true:缓存到磁盘并展示返回内容 false:展示返回内容,无需缓存到磁盘
* store:缓存到磁盘,如果已经加载缓存,则下次加载,否则展示返回内容
* http:容灾字段,如果http表示终端六个小时以内不会采用sonic请求该URL
8、判断有没有缓存,如果没有缓存则调用handleFlow_FirstLoad实现第一次加载,此时
将h5数据先读取出来,然后重新加载读取的数据,然后进行保存
9、通过响应头template-change判断模板有没有改变(还记得前面说的模板和数据吗),改变的话重新加载模板,并保存
10、如果模板没有改变,判断数据是否改变,数据改变,局部刷新h5
继续,先从server.connect()连接服务器看起,连接服务器分为创建连接、初始化连接、开始连接,创建连接如下,如果url是https的时候,设置证书,其他的都是URLConnection 的用法没啥好看的。
protected URLConnection createConnection() {
String currentUrl = session.srcUrl;
if (TextUtils.isEmpty(currentUrl)) {
return null;
}
URLConnection connection = null;
try {
URL url = new URL(currentUrl);
String dnsPrefetchAddress = intent
.getStringExtra(SonicSessionConnection.DNS_PREFETCH_ADDRESS);
String originHost = null;
/*
* Use the ip value mapped by {@code
* SonicSessionConnection.DNS_PREFETCH_ADDRESS} to avoid the
* cost time of DNS resolution. Meanwhile it can reduce the risk
* from hijacking http session.
*/
if (!TextUtils.isEmpty(dnsPrefetchAddress)) {
originHost = url.getHost();
url = new URL(currentUrl.replace(originHost,
dnsPrefetchAddress));
SonicUtils.log(TAG, Log.INFO,
"create UrlConnection with DNS-Prefetch("
+ originHost + " -> " + dnsPrefetchAddress
+ ").");
}
connection = url.openConnection();
if (connection != null) {
if (connection instanceof HttpURLConnection) {
((HttpURLConnection) connection)
.setInstanceFollowRedirects(false);
}
if (!TextUtils.isEmpty(originHost)) {
/*
* If originHost is not empty, that means connection
* uses the ip value instead of http host. So http
* header need to set the Host and {@link
* com.tencent.sonic.sdk.SonicSessionConnection.
* CUSTOM_HEAD_FILED_DNS_PREFETCH} request property.
*/
connection.setRequestProperty("Host", originHost);
connection
.setRequestProperty(
SonicSessionConnection.CUSTOM_HEAD_FILED_DNS_PREFETCH,
url.getHost());
if (connection instanceof HttpsURLConnection) { // 如果属于https,需要特殊处理,比如支持sni
/*
* If the scheme of url is https, then it needs
* extra processing, such as the sni support.
*/
final String finalOriginHost = originHost;
final URL finalUrl = url;
HttpsURLConnection httpsConnection = (HttpsURLConnection) connection;
httpsConnection
.setSSLSocketFactory(new SonicSniSSLSocketFactory(
SonicEngine.getInstance()
.getRuntime().getContext(),
originHost));
httpsConnection
.setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname,
SSLSession session) {
boolean verifySuccess = false;
long startTime = System
.currentTimeMillis();
if (finalUrl.getHost().equals(
hostname)) {
verifySuccess = HttpsURLConnection
.getDefaultHostnameVerifier()
.verify(finalOriginHost,
session);
SonicUtils
.log(TAG,
Log.DEBUG,
"verify hostname cost "
+ (System
.currentTimeMillis() - startTime)
+ " ms.");
}
return verifySuccess;
}
});
}
}
}
} catch (Throwable e) {
if (connection != null) {
connection = null;
}
SonicUtils.log(TAG, Log.ERROR,
"create UrlConnection fail, error:" + e.getMessage()
+ ".");
}
return connection;
}
protected boolean initConnection(URLConnection connection) {
if (null != connection) {
SonicSessionConfig config = session.config;
connection.setConnectTimeout(config.CONNECT_TIMEOUT_MILLIS);
connection.setReadTimeout(config.READ_TIMEOUT_MILLIS);
/*
* {@link SonicSessionConnection#CUSTOM_HEAD_FILED_ACCEPT_DIFF}
* is need to be set If client accepts incrementally updates.
*
Note: It doesn't support incrementally updated for
* template file.
*/
connection.setRequestProperty(CUSTOM_HEAD_FILED_ACCEPT_DIFF,
config.ACCEPT_DIFF_DATA ? "true" : "false");
String eTag = intent.getStringExtra(CUSTOM_HEAD_FILED_ETAG);
if (null == eTag)
eTag = "";
connection.setRequestProperty(HTTP_HEAD_FILED_IF_NOT_MATCH,
eTag);
String templateTag = intent
.getStringExtra(CUSTOM_HEAD_FILED_TEMPLATE_TAG);
if (null == templateTag)
templateTag = "";
// 又是服务器返回的缓存头信息
connection.setRequestProperty(CUSTOM_HEAD_FILED_TEMPLATE_TAG,
templateTag);
connection.setRequestProperty("method", "GET");
connection.setRequestProperty("Accept-Encoding", "gzip");
connection.setRequestProperty("Accept-Language", "zh-CN,zh;");
connection.setRequestProperty(CUSTOM_HEAD_FILED_SDK_VERSION,
"Sonic/" + SonicConstants.SONIC_VERSION_NUM);
// set custom request headers
if (null != config.customRequestHeaders
&& 0 != config.customRequestHeaders.size()) {
for (Map.Entry entry : config.customRequestHeaders
.entrySet()) {
connection.setRequestProperty(entry.getKey(),
entry.getValue());
}
}
String cookie = intent.getStringExtra(HTTP_HEAD_FIELD_COOKIE);
if (!TextUtils.isEmpty(cookie)) {
connection.setRequestProperty(HTTP_HEAD_FIELD_COOKIE,
cookie);
} else {
SonicUtils.log(TAG, Log.ERROR,
"create UrlConnection cookie is empty");
}
connection.setRequestProperty(HTTP_HEAD_FILED_USER_AGENT,
intent.getStringExtra(HTTP_HEAD_FILED_USER_AGENT));
return true;
}
return false;
}
eTag:页面内容唯一标识
template-tag:模板唯一标识
if-none-match:让服务端判断etag和template-tag是否过期,不过期返回304,不用返回数据
最后就是连接:
protected synchronized int internalConnect() {
if (connectionImpl instanceof HttpURLConnection) {
HttpURLConnection httpURLConnection = (HttpURLConnection) connectionImpl;
try {
httpURLConnection.connect();
return SonicConstants.ERROR_CODE_SUCCESS;
} catch (Throwable e) {
String errMsg = e.getMessage();
SonicUtils.log(TAG, Log.ERROR, "connect error:" + errMsg);
if (e instanceof IOException) {
if (e instanceof SocketTimeoutException) {
return SonicConstants.ERROR_CODE_CONNECT_TOE;
}
if (!TextUtils.isEmpty(errMsg)
&& errMsg.contains("timeoutexception")) {
return SonicConstants.ERROR_CODE_CONNECT_TOE;
}
return SonicConstants.ERROR_CODE_CONNECT_IOE;
}
if (e instanceof NullPointerException) {
return SonicConstants.ERROR_CODE_CONNECT_NPE;
}
}
}
return SonicConstants.ERROR_CODE_UNKNOWN;
}
*/
protected boolean setCookiesFromHeaders(Map> headers,
boolean executeInNewThread) {
if (null != headers) {
final List cookies = headers
.get(SonicSessionConnection.HTTP_HEAD_FILED_SET_COOKIE
.toLowerCase());
if (null != cookies && 0 != cookies.size()) {
if (!executeInNewThread) {
return SonicEngine.getInstance().getRuntime()
.setCookie(getCurrentUrl(), cookies);
} else {
SonicUtils
.log(TAG, Log.INFO,
"setCookiesFromHeaders asynchronous in new thread.");
SonicEngine.getInstance().getRuntime()
.postTaskToThread(new Runnable() {
@Override
public void run() {
SonicEngine
.getInstance()
.getRuntime()
.setCookie(getCurrentUrl(), cookies);
}
}, 0L);
}
return true;
}
}
return false;
}
*/
private void handleFlow_PreloadSubResource() {
if (preloadLinks == null || preloadLinks.isEmpty()) {
return;
}
SonicEngine.getInstance().getRuntime().postTaskToThread(new Runnable() {
@Override
public void run() {
if (resourceDownloaderEngine == null) {
resourceDownloaderEngine = new SonicDownloadEngine(
SonicDownloadCache.getSubResourceCache());
}
resourceDownloaderEngine.addSubResourcePreloadTask(preloadLinks);
}
}, 0);
}
ublic void addSubResourcePreloadTask(List preloadLinks) {
SonicRuntime runtime = SonicEngine.getInstance().getRuntime();
for (final String link : preloadLinks) {
if (!resourceTasks.containsKey(link)) {
resourceTasks.put(link,
download(link,
runtime.getHostDirectAddress(link),
runtime.getCookie(link),
new SonicDownloadClient.SubResourceDownloadCallback(link)
)
);
}
}
protected void handleFlow_NotModified() {
Message msg = mainHandler.obtainMessage(CLIENT_MSG_NOTIFY_RESULT);
msg.arg1 = SONIC_RESULT_CODE_HIT_CACHE;
msg.arg2 = SONIC_RESULT_CODE_HIT_CACHE;
mainHandler.sendMessage(msg);
for (WeakReference ref : sessionCallbackList) {
SonicSessionCallback callback = ref.get();
if (callback != null) {
callback.onSessionHitCache();
}
}
}
也就是最终改变数据库表中存储的命中数量cacheHitCount那个字段。最后判断模板是否改变(模板是什么就是将h5拆分成容易变化的部分我们定义为data数据,不变化部分称为模板),也就是说如果模板发现没有变化,那么会在响应头部返回template-change=false,同时响应包体返回的数据不再是完整的html,而是一段JSON数据,及全部的数据块。我们现在需要跟本地数据进行差分,找出真正的增量数据,如上图中,后台返回了N个数据,实际上仅有一个数据是有变化的,那么我们仅需要将这个变化的数据提交到页面即可。一般场景下,这个差异的数据比全部数据要小很多。如果页面拆分数据得更细,那么页面的变动就更小,这个取决于前端同学对数据块的细化程度。
获得变化数据块(diff_data)后,客户端只需要通知页面页面设置的回调接口(getDiffDataCallback)进行界面元素更新即可。这里javascript的通信方式也可以自由定义(可以使用webview标准的javascript通信方式,也可以使用伪协议的方式),只要页面跟终端协商一致就可以。 假设模板变化了,那么会调用handleFlow_TemplateChange方法,来瞧一瞧这个方法都做了什么?如下:
protected void handleFlow_TemplateChange(String newHtml) {
try {
SonicUtils.log(TAG, Log.INFO, "handleFlow_TemplateChange.");
String htmlString = newHtml;
long startTime = System.currentTimeMillis();
// When serverRsp is empty
if (TextUtils.isEmpty(htmlString)) {
// 得到服务端的字节流
pendingWebResourceStream = server
.getResponseStream(wasOnPageFinishInvoked);
if (pendingWebResourceStream == null) {
SonicUtils
.log(TAG,
Log.ERROR,
"session("
+ sId
+ ") handleFlow_TemplateChange error:server.getResponseStream = null!");
return;
}
htmlString = server.getResponseData(clientIsReload.get());
}
// 得到缓存控制头cache-offline
String cacheOffline = server
.getResponseHeaderField(SonicSessionConnection.CUSTOM_HEAD_FILED_CACHE_OFFLINE);
/**
* 客户端是否已经准备好
*/
if (!clientIsReload.get()) {
// send CLIENT_CORE_MSG_TEMPLATE_CHANGE message
mainHandler.removeMessages(CLIENT_CORE_MSG_PRE_LOAD);
Message msg = mainHandler
.obtainMessage(CLIENT_CORE_MSG_TEMPLATE_CHANGE);
msg.obj = htmlString;
if (!OFFLINE_MODE_STORE.equals(cacheOffline)) {
msg.arg1 = TEMPLATE_CHANGE_REFRESH;
}
mainHandler.sendMessage(msg);
} else {
Message msg = mainHandler
.obtainMessage(CLIENT_MSG_NOTIFY_RESULT);
msg.arg1 = SONIC_RESULT_CODE_TEMPLATE_CHANGE;
msg.arg2 = SONIC_RESULT_CODE_TEMPLATE_CHANGE;
mainHandler.sendMessage(msg);
}
/**
* 回调模板改变的回调接口
*/
for (WeakReference ref : sessionCallbackList) {
SonicSessionCallback callback = ref.get();
if (callback != null) {
callback.onSessionTemplateChanged(htmlString);
}
}
if (SonicUtils.shouldLog(Log.DEBUG)) {
SonicUtils.log(
TAG,
Log.DEBUG,
"session(" + sId + ") read byte stream cost "
+ (System.currentTimeMillis() - startTime)
+ " ms, wasInterceptInvoked: "
+ wasInterceptInvoked.get());
}
// save and separate data,保存和拆分data数据
if (SonicUtils.needSaveData(config.SUPPORT_CACHE_CONTROL,
cacheOffline, server.getResponseHeaderFields())) {
switchState(STATE_RUNNING, STATE_READY, true);
if (!TextUtils.isEmpty(htmlString)) {
postTaskToSaveSonicCache(htmlString);
}
} else if (OFFLINE_MODE_FALSE.equals(cacheOffline)) {
SonicUtils.removeSessionCache(id);
SonicUtils
.log(TAG,
Log.INFO,
"handleClientCoreMessage_TemplateChange:offline mode is 'false', so clean cache.");
} else {
SonicUtils.log(TAG, Log.INFO, "session(" + sId
+ ") handleFlow_TemplateChange:offline->"
+ cacheOffline + " , so do not need cache to file.");
}
} catch (Throwable e) {
SonicUtils.log(TAG, Log.DEBUG, "session(" + sId
+ ") handleFlow_TemplateChange error:" + e.getMessage());
}
}
下面是重新加载模板的方法:
private void handleClientCoreMessage_TemplateChange(Message msg) {
SonicUtils.log(TAG, Log.INFO,
"handleClientCoreMessage_TemplateChange wasLoadDataInvoked = "
+ wasLoadDataInvoked.get() + ",msg arg1 = " + msg.arg1);
if (wasLoadDataInvoked.get()) {
if (TEMPLATE_CHANGE_REFRESH == msg.arg1) {
String html = (String) msg.obj;
if (TextUtils.isEmpty(html)) {
SonicUtils
.log(TAG,
Log.INFO,
"handleClientCoreMessage_TemplateChange:load url with preload=2, webCallback is null? ->"
+ (null != diffDataCallback));
sessionClient.loadUrl(srcUrl, null);
} else {
SonicUtils
.log(TAG, Log.INFO,
"handleClientCoreMessage_TemplateChange:load data.");
/**
* 重新加载本地的代码
*/
sessionClient.loadDataWithBaseUrlAndHeader(srcUrl, html,
"text/html", getCharsetFromHeaders(), srcUrl,
getHeaders());
}
setResult(SONIC_RESULT_CODE_TEMPLATE_CHANGE,
SONIC_RESULT_CODE_TEMPLATE_CHANGE, false);
} else {
SonicUtils.log(TAG, Log.INFO,
"handleClientCoreMessage_TemplateChange:not refresh.");
setResult(SONIC_RESULT_CODE_TEMPLATE_CHANGE,
SONIC_RESULT_CODE_HIT_CACHE, true);
}
} else {
SonicUtils
.log(TAG, Log.INFO,
"handleClientCoreMessage_TemplateChange:oh yeah template change hit 304.");
if (msg.obj instanceof String) {
String html = (String) msg.obj;
sessionClient.loadDataWithBaseUrlAndHeader(srcUrl, html,
"text/html", getCharsetFromHeaders(), srcUrl,
getHeaders());
setResult(SONIC_RESULT_CODE_TEMPLATE_CHANGE,
SONIC_RESULT_CODE_HIT_CACHE, false);
} else {
SonicUtils
.log(TAG, Log.ERROR,
"handleClientCoreMessage_TemplateChange error:call load url.");
sessionClient.loadUrl(srcUrl, null);
setResult(SONIC_RESULT_CODE_TEMPLATE_CHANGE,
SONIC_RESULT_CODE_FIRST_LOAD, false);
}
}
diffDataCallback = null;
mainHandler.removeMessages(CLIENT_MSG_ON_WEB_READY);
}
方法进行数据保存,最终调用到下面这个方法:
protected void doSaveSonicCache(SonicServer sonicServer, String htmlString) {
// if the session has been destroyed, exit directly
if (isDestroyedOrWaitingForDestroy() || server == null) {
SonicUtils
.log(TAG,
Log.ERROR,
"session("
+ sId
+ ") doSaveSonicCache: save session files fail. Current session is destroy!");
return;
}
long startTime = System.currentTimeMillis();
String template = sonicServer.getTemplate();
String updatedData = sonicServer.getUpdatedData();
if (!TextUtils.isEmpty(htmlString) && !TextUtils.isEmpty(template)) {
String newHtmlSha1 = sonicServer
.getResponseHeaderField(SonicSessionConnection.CUSTOM_HEAD_FILED_HTML_SHA1);
if (TextUtils.isEmpty(newHtmlSha1)) {
//得到htmlString的sha1值
newHtmlSha1 = SonicUtils.getSHA1(htmlString);
}
String eTag = sonicServer
.getResponseHeaderField(SonicSessionConnection.CUSTOM_HEAD_FILED_ETAG);
String templateTag = sonicServer
.getResponseHeaderField(SonicSessionConnection.CUSTOM_HEAD_FILED_TEMPLATE_TAG);
Map> headers = sonicServer
.getResponseHeaderFields();
for (WeakReference ref : sessionCallbackList) {
SonicSessionCallback callback = ref.get();
if (callback != null) {
callback.onSessionSaveCache(htmlString, template,
updatedData);
}
}
if (SonicUtils.saveSessionFiles(id, htmlString, template,
updatedData, headers)) {
long htmlSize = new File(SonicFileUtils.getSonicHtmlPath(id))
.length();
//数据库保存
SonicUtils.saveSonicData(id, eTag, templateTag, newHtmlSha1,
htmlSize, headers);
} else {
SonicUtils.log(TAG, Log.ERROR, "session(" + sId
+ ") doSaveSonicCache: save session files fail.");
SonicEngine
.getInstance()
.getRuntime()
.notifyError(sessionClient, srcUrl,
SonicConstants.ERROR_CODE_WRITE_FILE_FAIL);
}
} else {
SonicUtils
.log(TAG,
Log.ERROR,
"session("
+ sId
+ ") doSaveSonicCache: save separate template and data files fail.");
SonicEngine
.getInstance()
.getRuntime()
.notifyError(sessionClient, srcUrl,
SonicConstants.ERROR_CODE_SPLIT_HTML_FAIL);
}
SonicUtils.log(TAG, Log.INFO,
"session(" + sId + ") doSaveSonicCache: finish, cost "
+ (System.currentTimeMillis() - startTime) + "ms.");
}
在保存h5数据的时候,这个方法先把h5里的所有数据切分成易变的
数据块
Data数据和模板数据,这里相当于一个规则,只要前端工程师在h5
界面中加入(通过代码注释的方式,增加了“sonicdiff-xxx”来标注一个数据块的开始与结束)这种标记就认为这是可拆分的h5,只要有
这个标记h5就会被切分成模板部分和数据块部分,最后保存文件的时候,完整的h5保存一份,模板保存一份,数据块保存一份,头信息保存一份
,然后数据库表记录一份,资源各保存一份,资源信息表保存各保存一条,整个存储就大功告成了。
下面看一下数据块改变时候的局部刷新,这样的好处是不会出现再次加载出现闪屏的不友好现象,代码如下:
protected void handleFlow_DataUpdate(String serverRsp) {
SonicUtils.log(TAG, Log.INFO, "session(" + sId
+ ") handleFlow_DataUpdate: start.");
try {
String htmlString = null;
if (TextUtils.isEmpty(serverRsp)) {
serverRsp = server.getResponseData(true);
} else {
htmlString = server.getResponseData(false);
}
if (TextUtils.isEmpty(serverRsp)) {
SonicUtils.log(TAG, Log.ERROR,
"handleFlow_DataUpdate:getResponseData error.");
return;
}
final String eTag = server
.getResponseHeaderField(SonicSessionConnection.CUSTOM_HEAD_FILED_ETAG);
final String templateTag = server
.getResponseHeaderField(SonicSessionConnection.CUSTOM_HEAD_FILED_TEMPLATE_TAG);
String cacheOffline = server
.getResponseHeaderField(SonicSessionConnection.CUSTOM_HEAD_FILED_CACHE_OFFLINE);
long startTime = System.currentTimeMillis();
JSONObject serverRspJson = new JSONObject(serverRsp);
final JSONObject serverDataJson = serverRspJson
.optJSONObject("data");
String htmlSha1 = serverRspJson.optString("html-sha1");
JSONObject diffDataJson = SonicUtils
.getDiffData(id, serverDataJson);
Bundle diffDataBundle = new Bundle();
if (null != diffDataJson) {
diffDataBundle.putString(DATA_UPDATE_BUNDLE_PARAMS_DIFF,
diffDataJson.toString());
} else {
SonicUtils.log(TAG, Log.ERROR,
"handleFlow_DataUpdate:getDiffData error.");
SonicEngine
.getInstance()
.getRuntime()
.notifyError(sessionClient, srcUrl,
SonicConstants.ERROR_CODE_MERGE_DIFF_DATA_FAIL);
}
if (SonicUtils.shouldLog(Log.DEBUG)) {
SonicUtils.log(
TAG,
Log.DEBUG,
"handleFlow_DataUpdate:getDiffData cost "
+ (System.currentTimeMillis() - startTime)
+ " ms.");
}
boolean hasSentDataUpdateMessage = false;
if (wasLoadDataInvoked.get()) {
if (SonicUtils.shouldLog(Log.INFO)) {
SonicUtils
.log(TAG, Log.INFO,
"handleFlow_DataUpdate:loadData was invoked, quick notify web data update.");
}
Message msg = mainHandler
.obtainMessage(CLIENT_CORE_MSG_DATA_UPDATE);
if (!OFFLINE_MODE_STORE.equals(cacheOffline)) {
msg.setData(diffDataBundle);
}
mainHandler.sendMessage(msg);
hasSentDataUpdateMessage = true;
}
startTime = System.currentTimeMillis();
if (TextUtils.isEmpty(htmlString)) {
htmlString = SonicUtils.buildHtml(id, serverDataJson, htmlSha1,
serverRsp.length());
}
if (SonicUtils.shouldLog(Log.DEBUG)) {
SonicUtils.log(
TAG,
Log.DEBUG,
"handleFlow_DataUpdate:buildHtml cost "
+ (System.currentTimeMillis() - startTime)
+ " ms.");
}
if (TextUtils.isEmpty(htmlString)) {
SonicEngine
.getInstance()
.getRuntime()
.notifyError(sessionClient, srcUrl,
SonicConstants.ERROR_CODE_BUILD_HTML_ERROR);
}
if (!hasSentDataUpdateMessage) {
mainHandler.removeMessages(CLIENT_CORE_MSG_PRE_LOAD);
Message msg = mainHandler
.obtainMessage(CLIENT_CORE_MSG_DATA_UPDATE);
msg.obj = htmlString;
mainHandler.sendMessage(msg);
}
for (WeakReference ref : sessionCallbackList) {
SonicSessionCallback callback = ref.get();
if (callback != null) {
callback.onSessionDataUpdated(serverRsp);
}
}
if (null == diffDataJson
|| null == htmlString
|| !SonicUtils.needSaveData(config.SUPPORT_CACHE_CONTROL,
cacheOffline, server.getResponseHeaderFields())) {
SonicUtils.log(TAG, Log.INFO, "session(" + sId
+ ") handleFlow_DataUpdate: clean session cache.");
SonicUtils.removeSessionCache(id);
return;
}
switchState(STATE_RUNNING, STATE_READY, true);
Thread.yield();
startTime = System.currentTimeMillis();
Map> headers = server
.getResponseHeaderFields();
for (WeakReference ref : sessionCallbackList) {
SonicSessionCallback callback = ref.get();
if (callback != null) {
callback.onSessionSaveCache(htmlString, null,
serverDataJson.toString());
}
}
if (SonicUtils.saveSessionFiles(id, htmlString, null,
serverDataJson.toString(), headers)) {
long htmlSize = new File(SonicFileUtils.getSonicHtmlPath(id))
.length();
SonicUtils.saveSonicData(id, eTag, templateTag, htmlSha1,
htmlSize, headers);
SonicUtils
.log(TAG,
Log.INFO,
"session("
+ sId
+ ") handleFlow_DataUpdate: finish save session cache, cost "
+ (System.currentTimeMillis() - startTime)
+ " ms.");
} else {
SonicUtils.log(TAG, Log.ERROR, "session(" + sId
+ ") handleFlow_DataUpdate: save session files fail.");
SonicEngine
.getInstance()
.getRuntime()
.notifyError(sessionClient, srcUrl,
SonicConstants.ERROR_CODE_WRITE_FILE_FAIL);
}
} catch (Throwable e) {
SonicUtils.log(TAG, Log.ERROR, "session(" + sId
+ ") handleFlow_DataUpdate error:" + e.getMessage());
}
}
这个方法也很好理解,首先将h5的数据的数据块通过正则表达式拆出来,然后计算它的sha1值和原来的sha1值做比较,如果相同说明
数据块没有改变,那么就没有必要刷新,如果改变了,则刷新数据值,重新保存数据。数据的改变逻辑最终调用的是SonicSession类的
setResult这个方法,如下
protected void setResult(int srcCode, int finalCode, boolean notify) {
SonicUtils.log(TAG, Log.INFO, "session(" + sId
+ ") setResult: srcCode=" + srcCode + ", finalCode="
+ finalCode + ".");
statistics.originalMode = srcResultCode = srcCode;
statistics.finalMode = finalResultCode = finalCode;
if (!notify)
return;
if (wasNotified.get()) {
SonicUtils.log(TAG, Log.ERROR, "session(" + sId
+ ") setResult: notify error -> already has notified!");
}
if (null == diffDataCallback) {
SonicUtils
.log(TAG,
Log.ERROR,
"session("
+ sId
+ ") setResult: notify fail as webCallback is not set, please wait!");
return;
}
if (this.finalResultCode == SONIC_RESULT_CODE_UNKNOWN) {
SonicUtils
.log(TAG,
Log.ERROR,
"session("
+ sId
+ ") setResult: notify fail finalResultCode is not set, please wait!");
return;
}
wasNotified.compareAndSet(false, true);
final JSONObject json = new JSONObject();
try {
if (finalResultCode == SONIC_RESULT_CODE_DATA_UPDATE) {
JSONObject pendingObject = new JSONObject(pendingDiffData);
if (!pendingObject.has("local_refresh_time")) {
SonicUtils.log(TAG, Log.INFO, "session(" + sId
+ ") setResult: no any updated data. "
+ pendingDiffData);
pendingDiffData = "";
return;
} else {
long timeDelta = System.currentTimeMillis()
- pendingObject.optLong("local_refresh_time", 0);
if (timeDelta > 30 * 1000) {
SonicUtils
.log(TAG,
Log.ERROR,
"session("
+ sId
+ ") setResult: notify fail as receive js call too late, "
+ (timeDelta / 1000.0) + " s.");
pendingDiffData = "";
return;
} else {
if (SonicUtils.shouldLog(Log.DEBUG)) {
SonicUtils
.log(TAG,
Log.DEBUG,
"session("
+ sId
+ ") setResult: notify receive js call in time: "
+ (timeDelta / 1000.0)
+ " s.");
}
if (timeDelta > 0)
json.put("local_refresh_time", timeDelta);
}
}
pendingObject.remove(WEB_RESPONSE_LOCAL_REFRESH_TIME);
json.put(WEB_RESPONSE_DATA, pendingObject.toString());
}
json.put(WEB_RESPONSE_CODE, finalResultCode);
json.put(WEB_RESPONSE_SRC_CODE, srcResultCode);
final JSONObject extraJson = new JSONObject();
if (server != null) {
extraJson
.put(SonicSessionConnection.CUSTOM_HEAD_FILED_ETAG,
server.getResponseHeaderField(SonicSessionConnection.CUSTOM_HEAD_FILED_ETAG));
extraJson
.put(SonicSessionConnection.CUSTOM_HEAD_FILED_TEMPLATE_TAG,
server.getResponseHeaderField(SonicSessionConnection.CUSTOM_HEAD_FILED_TEMPLATE_TAG));
extraJson
.put(SonicSessionConnection.CUSTOM_HEAD_FILED_CACHE_OFFLINE,
server.getResponseHeaderField(SonicSessionConnection.CUSTOM_HEAD_FILED_CACHE_OFFLINE));
}
extraJson.put("isReload", clientIsReload);
json.put(WEB_RESPONSE_EXTRA, extraJson);
} catch (Throwable e) {
e.printStackTrace();
SonicUtils.log(TAG, Log.ERROR, "session(" + sId
+ ") setResult: notify error -> " + e.getMessage());
}
if (SonicUtils.shouldLog(Log.DEBUG)) {
String logStr = json.toString();
if (logStr.length() > 512) {
logStr = logStr.substring(0, 512);
}
SonicUtils.log(TAG, Log.DEBUG, "session(" + sId
+ ") setResult: notify now call jsCallback, jsonStr = "
+ logStr);
}
pendingDiffData = null;
long delta = 0L;
if (clientIsReload.get()) {
delta = System.currentTimeMillis()
- statistics.diffDataCallbackTime;
delta = delta >= 2000 ? 0L : delta;
}
if (delta > 0L) {
delta = 2000L - delta;
SonicEngine.getInstance().getRuntime()
.postTaskToMainThread(new Runnable() {
@Override
public void run() {
if (diffDataCallback != null) {
diffDataCallback.callback(json.toString());
statistics.diffDataCallbackTime = System.currentTimeMillis();
}
}
}, delta);
} else {
diffDataCallback.callback(json.toString());
statistics.diffDataCallbackTime = System.currentTimeMillis();
}
}
添加eTag字段等,最后装饰完数据后,通过js方法调用的方式,将数据传给前端人员,前端人员动态的操作Dom树来实现动态的局部
刷新数据。
public void getDiffData2(final String jsCallbackFunc) {
if (null != sessionClient) {
sessionClient.getDiffData(new SonicDiffDataCallback() {
//将改变的数据交给js处理更新
@Override
public void callback(final String resultData) {
Runnable callbackRunnable = new Runnable() {
@Override
public void run() {
String jsCode = "javascript:" + jsCallbackFunc + "('"+ toJsString(resultData) + "')";
sessionClient.getWebView().loadUrl(jsCode);
}
};
if (Looper.getMainLooper() == Looper.myLooper()) {
callbackRunnable.run();
} else {
new Handler(Looper.getMainLooper()).post(callbackRunnable);
}
}
});
}
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
if (sonicSession != null) {
return (WebResourceResponse) sonicSession.getSessionClient().requestResource(url);
}
return null;
}
});
public final Object onClientRequestResource(String url) {
String currentThreadName = Thread.currentThread().getName();
if (CHROME_FILE_THREAD.equals(currentThreadName)) {
resourceInterceptState.set(RESOURCE_INTERCEPT_STATE_IN_FILE_THREAD);
} else {
resourceInterceptState
.set(RESOURCE_INTERCEPT_STATE_IN_OTHER_THREAD);
if (SonicUtils.shouldLog(Log.DEBUG)) {
SonicUtils.log(TAG, Log.DEBUG,
"onClientRequestResource called in "
+ currentThreadName + ".");
}
}
// url匹配调用onRequestResource,url不匹配的时候调用onRequestSubResource
Object object = isMatchCurrentUrl(url) ? onRequestResource(url)
: (resourceDownloaderEngine != null ? resourceDownloaderEngine
.onRequestSubResource(url, this) : null);
resourceInterceptState.set(RESOURCE_INTERCEPT_STATE_NONE);
return object;
}
可以看出这个方法首先判断加载的url是不是会话绑定的url,如果是的话,将通过
onRequestResource方法来设置资源,如果不是的话
将通过onRequestSubResource来设置资源(此时加载的就相当于该url所包括的资源内容)。onRequestResource方法如下:
/**
* url匹配的话调用它
*/
protected Object onRequestResource(String url) {
// Log.i("huoying", "开始");
// 避免返回的时候再调用,返回直接用webview的缓存机制
if (wasInterceptInvoked.get() || !isMatchCurrentUrl(url)) {
return null;
}
if (!wasInterceptInvoked.compareAndSet(false, true)) {
// Log.i("huoying", "结束");
SonicUtils
.log(TAG,
Log.ERROR,
"session("
+ sId
+ ") onClientRequestResource error:Intercept was already invoked, url = "
+ url);
return null;
}
if (SonicUtils.shouldLog(Log.DEBUG)) {
SonicUtils.log(TAG, Log.DEBUG, "session(" + sId
+ ") onClientRequestResource:url = " + url);
}
long startTime = System.currentTimeMillis();
if (sessionState.get() == STATE_RUNNING) {
synchronized (sessionState) {
try {
if (sessionState.get() == STATE_RUNNING) {
SonicUtils.log(TAG, Log.INFO, "session(" + sId
+ ") now wait for pendingWebResourceStream!");
sessionState.wait(30 * 1000);
}
} catch (Throwable e) {
SonicUtils
.log(TAG,
Log.ERROR,
"session("
+ sId
+ ") wait for pendingWebResourceStream failed"
+ e.getMessage());
}
}
} else {
if (SonicUtils.shouldLog(Log.DEBUG)) {
SonicUtils.log(TAG, Log.DEBUG, "session(" + sId
+ ") is not in running state: " + sessionState);
}
}
SonicUtils.log(TAG, Log.INFO,
"session(" + sId + ") have pending stream? -> "
+ (pendingWebResourceStream != null) + ", cost "
+ (System.currentTimeMillis() - startTime) + "ms.");
if (null != pendingWebResourceStream) {
Log.i("huoying", "pendingWebResourceStream:不为null");
Object webResourceResponse;
if (!isDestroyedOrWaitingForDestroy()) {
String mime = SonicUtils.getMime(srcUrl);
webResourceResponse = SonicEngine
.getInstance()
.getRuntime()
.createWebResourceResponse(mime,
getCharsetFromHeaders(),
pendingWebResourceStream, getHeaders());
} else {
webResourceResponse = null;
SonicUtils
.log(TAG,
Log.ERROR,
"session("
+ sId
+ ") onClientRequestResource error: session is destroyed!");
}
pendingWebResourceStream = null;
return webResourceResponse;
}
return null;
}
数据,那么如果VasSonic框架的异步拉锯数据的引擎已经获取数据完毕,而此时内核刚刚初始化完毕的话,那么将直接拿拉取的流来
创建WebResourceResponse,也就是说,浏览器内核初始化和网页数据的拉取是并行进行的,只要网络已经拉取到数据,不管拉取了
多少,就给浏览器内核多少。避免内核去重新拉取数据,从而提高渲染速度,最后通过下面这个方法创建 WebResourceResponse
WebResourceResponse resourceResponse = new WebResourceResponse(mimeType, encoding, data);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
String cookie = SharedPreferencesUtils.getString(
BaseApplication.getInstance(), "Cookie", null);
headers.put("Set-Cookie", cookie);
resourceResponse.setResponseHeaders(headers);
}
return resourceResponse;
}
这里注意一个细节,如果当前手机系统是5.0及以上的话,必须得为资源设置头信息,否则将会出错。同样的道理,如果url是资源url的
话,将会通过下载引擎去加载资源url,还记得会话连接完服务器之后获得 sonic-link头信息之后,就会去下载资源,如果此时资源存在的话,就会
直接加载资源,如果不存在则通过webview内核去拉取资源。
今天太累了就写到这里,VasSonic框架基本介绍完毕。