从腾讯VasSonic源码剖析到webview优化的思考

提到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自带的缓存)

<span style="color:#333333">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);</span>


开启缓存之后,在调用loadUrl之前判断网络是否连接,如果没有连接的话,直接加载内存里的网页,如果没有缓存网页的话,则会回调

<span style="color:#333333">public void setDataFrom() {
		if (NetWork_Util.isNetworkConnected(context)) {
			getSettings().setCacheMode(WebSettings.LOAD_DEFAULT);
		} else {
			getSettings().setCacheMode(WebSettings.LOAD_CACHE_ONLY);
		}
	}</span>

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渲染的话,接入它很简单,如下

<span style="color:#333333"># 终端接入指引-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;
}
```</span>


如果要总结的话,那么第一步是创建SonicEngine引擎

<span style="color:#333333"> if (!SonicEngine.isGetInstanceAllowed()) {
            SonicEngine.createInstance(new SonicRuntimeImpl(getApplication()), new SonicConfig.Builder().build());
        }
</span>

其中SonicRuntimeImpl需要你自定义,这个类的主要作用就是

1、设置用户浏览器信息:

<span style="color:#333333"> 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";
    }
</span>

2、获取用户id信息:

<span style="color:#333333">   public String getCurrentUserAccount() {
        return "VasSonic-client";
    }</span>

3、设置缓存文件地址:

<span style="color:#333333"> 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();
    }
</span>

4、设置Cookie:

<span style="color:#333333"> public boolean setCookie(String url, List<String> 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;
    }</span>

5、创建资源流(并行现在DOM,将DOM流设置给webView,从而让webView加载并行下载的资源,而不通过内核加载,加快响应时间):

<span style="color:#333333">public Object createWebResourceResponse(String mimeType, String encoding, InputStream data, Map<String, String> 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;
    }
</span>

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());


这里腾讯为这个类赋予了对话的含义,在web端何为对话,对话就是session,何为session?

新开的浏览器窗口会生成新的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<String, String> customRequestHeaders = null;

    /**
     * 给WebResourceResponse设值头信息,5.0以上不设置可能会抛异常
     */
    Map<String, String> 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);
                    }
                });
            }


接下来是配置WebViewClient

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;
	}


这个方法只是将wasOnPageFinishInvoked设置为true,标志网页完全加载完毕,wasOnPageFinishInvoked是原子类型AtomicBoolean,最重要的设置方法为shouldInterceptRequest,这个方法意在刚开始加载网页的时候允许你做拦截处理,如果你返回null,那么webview将走内核的加载策略,如果返回不为null,将会直接加载你提供的资源,也就是说如果本地已经有资源的话,加载本地资源将远远超过webView重新通过网络获取,在回调到这个方法的时候,已经表明手机浏览器内核初始化完毕。然后就是设置从网页回调到App的方法,如下:

  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);


然后为session绑定SonicSessionClientImpl,如下:

  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<String, String> 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<String> remainParamTreeSet = new TreeSet<String>();
                    if (!TextUtils.isEmpty(sonicRemainParams)) {
                        Collections.addAll(remainParamTreeSet, sonicRemainParams.split(SonicConstants.SONIC_REMAIN_PARAMETER_SPLIT_CHAR));
                    }

                    TreeSet<String> parameterNamesTreeSet = new TreeSet<String>(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;
    }


这个方法主要做的事件就是通过url求出md5的值,然后将用户账号和md5拼接形成唯一的sessionId,创建完sessionId后接下来就是判断缓存文件中是否已经有当前的url对应的会话了,如果有则取缓存,如果没有,则创建新的会话并保存到集合中,进入lookupSession方法看一下,如下:

 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;
    }


首先判断当前url对应的会话是否过期,默认过期时间是从会话创建的后的3个小时后,如果过期就从集合删除,并清除session中的信息,并返回null,如果没过期就返回当前的session,如果没有缓存返回null。如果缓存中没有url对应的缓存,那么接下来判断缓存池里面的session的数量是否超过5个,如果没有超过缓存的最大数量,那么创建一个新的session会话,并将它放到缓存中。创建session会话的代码如下:

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;
    }


默认配置文件创建的是QuickSonicSession,添加session状态回调接口,创建session会话完毕后,开始从服务器上拉取数据。继续看一下创建会话时,会话的构造方法都做了些什么?

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 + ".");
		}
	}


首先它创建了一个Handler来接收保存文件的消息,然后判断是否设置了cookie自动化,如果设置了,将cookie取出来先放入intent,用来后面的时候用。接下来开始执行会话的网络拉取,还记得上面提到的并行方案吗,如果这是你的项目第一次用webview的时候,webview不会马上去网络上拉取网页数据,而是先初始化浏览器内核,初始化内核之后才开始拉取网络进行渲染,那么这里会话的作用就是在浏览器初始化过程中先进性拉取网络数据,浏览器内核初始化之后,直接将已经拉取的数据给webview直接加载,从而并行的加快渲染速度。

下面看看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<SonicSessionCallback> 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;


ok,接下来看看会话具体做了哪些事情,下面是runSonicFlow方法,如下:

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.");
		}
	}


这个方法首先从数据库表中获得session会话的相关数据,比如session会话所持有的h5界面的md5值,用来标记当前h5界面有没有变化等,看一下这个方法

static SessionData getSessionData(String sessionId) {
        SQLiteDatabase db = SonicDBHelper.getInstance().getWritableDatabase();
        SessionData sessionData = getSessionData(db, sessionId);
        if (null == sessionData) {
            sessionData = new SessionData();
        }
        return sessionData;
    }


根据会话的id从数据库中获取到当前的会话的一些数据,来看一看数据库都有哪些表,第一张表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" +
            " ); ";


resourceID:资源id,唯一标识
resourceSha1:sha1编码值
resourceSize:资源大下
resourceUpdateTime:资源最后一次更改时间
cacheExpiredTime:缓存过期时间


好,继续走代码流程加载完sessionData后,接下来通过SonicCacheInterceptor.getSonicCacheData获取缓存h5的全字符串,如下代码:

 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<SonicSessionCallback> ref : sessionCallbackList) {
			SonicSessionCallback callback = ref.get();
			if (callback != null) {
				// 回调开始加载缓存
				callback.onSessionLoadLocalCache(cacheHtml);
			}
		}
	}


这个方法的主旨就是,获取的缓存不为null就发送PRE_LOAD_WITH_CACHE的消息,如果为null,那么走webview的路径正常加载网页,来看一下这两个消息的处理,有缓存的时候通过loadDataWithBaseUrlAndHeader加载,直接加载h5界面不用再去服务器下载,省了大量时间。如下:

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<SonicSessionCallback> 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<String, List<String>> 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<SonicSessionCallback> 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.
				 * <br> <p><b>Note: It doesn't support incrementally updated for
				 * template file.</b><p/>
				 */
				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<String, String> 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;
		}


连接服务器成功之后,就是获取响应的头信息,并为webview设置头信息Cookie,来看一下

 */
	protected boolean setCookiesFromHeaders(Map<String, List<String>> headers,
			boolean executeInNewThread) {
		if (null != headers) {
			final List<String> 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;
	}


这个方法没有什么好讲的,就是获得cookie,然后设置给Webview,实现cookie自动化管理。接下来看一下资源下载的方法handleFlow_PreloadSubResource,如下:

 */
	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);
	}


这个方法通过线程池看一个任务创建下载引擎SonicDownloadEngine,将所有资源的链接集合加入进去,最后加载

ublic void addSubResourcePreloadTask(List<String> 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)
                        )
                );
            }
        }


最终通过download方法进行资源的下载,当然这其中也包括判断资源是否过期和缓存里的资源做比较。服务器返回304的时候说明h5文件没有变化,那么就会调用handleFlow_NotModified方法改变命中率,如下:

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<SonicSessionCallback> 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<SonicSessionCallback> 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());
		}
	}


这个方法做的主要工作就是,判断客服端是否准备好(在使用此框架调用sonicSessionClient.clientReady()时已经标记客户端已经准备好),如果准备好的话,先发送消息,重新开始加载模板,也就是h5界面,只不过这个时候直接加载网络拉取的数据就好了,没有必要通过webview重新加载网络,然后根据头信息cache-offline来确定是否需要缓存数据,如果需要缓存数据,如果不需要,删除数据。

下面是重新加载模板的方法:

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);
	}


最重要的就是这句代码 sessionClient.loadDataWithBaseUrlAndHeader(srcUrl, html,
"text/html", getCharsetFromHeaders(), srcUrl,
getHeaders()),直接加载框架拉取的网页数据。然后就是调用postTaskToSaveSonicCache

方法进行数据保存,最终调用到下面这个方法:

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<String, List<String>> headers = sonicServer
					.getResponseHeaderFields();
			for (WeakReference<SonicSessionCallback> 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<SonicSessionCallback> 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<String, List<String>> headers = server
					.getResponseHeaderFields();

			for (WeakReference<SonicSessionCallback> 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();
		}

	}


可以看出,数据块部分这个框架采用的是json格式的数据,而手机端需要为这块数据额外的添加头信息数据,以给前端人员判断,比如

添加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);
                    }
                }
            });
        }
    }


好了,最后再来看一下webview内核加载完毕拦截资源的的回调方法,VasSonic框架都做了些什么,也就是下面这个方法:

  @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;
	}


上面已经提到shouldInterceptRequest只要你返回的不为null,那么数据将加载你返回的,而不再通过webview的内核去重新加载

数据,那么如果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框架基本介绍完毕。

 

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值