在WebView初始化的时候,会有一段白屏的时间,在这段时间网络完全是空闲,VasSonic采用并行加载模式,初始化和网络请求并行。
在Demo的MainActivity的onCreate()中
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
String url = intent.getStringExtra(PARAM_URL);
int mode = intent.getIntExtra(PARAM_MODE, -1);
if (TextUtils.isEmpty(url) || -1 == mode) {
finish();
return;
}
getWindow().addFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
// init sonic engine if necessary, or maybe u can do this when application created
....
SonicSessionClientImpl sonicSessionClient = null;
// if it's sonic mode , startup sonic session at first time
if (MainActivity.MODE_DEFAULT != mode) { // sonic mode
SonicSessionConfig.Builder sessionConfigBuilder = new SonicSessionConfig.Builder();
sessionConfigBuilder.setSupportLocalServer(true);
// if it's offline pkg mode, we need to intercept the session connection
....
// create sonic session and run sonic flow
sonicSession = SonicEngine.getInstance().createSession(url, sessionConfigBuilder.build());
...
}
// 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);
FloatingActionButton btnFab = (FloatingActionButton) findViewById(R.id.btn_refresh);
btnFab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (sonicSession != null) {
sonicSession.refresh();
}
}
});
// 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 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
....
// webview is ready now, just tell session client to bind
if (sonicSessionClient != null) {
sonicSessionClient.bindWebView(webView);
sonicSessionClient.clientReady();
} else { // default mode
webView.loadUrl(url);
}
}
SonicEngine会为url构建对应的SonicSession
sonicSession = SonicEngine.getInstance().createSession(url, sessionConfigBuilder.build());
/**
*
* @param url url for SonicSession Object
* @param sessionConfig SSonicSession config
* @return This method will create and return SonicSession Object when url is legal.
*/
public synchronized SonicSession createSession(@NonNull String url, @NonNull SonicSessionConfig sessionConfig) {
if (isSonicAvailable()) {//database没有在升级
String sessionId = makeSessionId(url, sessionConfig.IS_ACCOUNT_RELATED);
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;
}
/**
*
* @param sessionId possible sessionId
* @param pick When {@code pick} is true and there is SonicSession in {@link #preloadSessionPool},
* it will remove from {@link #preloadSessionPool}
* @return
* Return valid SonicSession Object from {@link #preloadSessionPool} if the specified sessionId is a key in {@link #preloadSessionPool}.
*/
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;
}
/**
* Create sonic session internal
*
* @param sessionId session id
* @param url origin url
* @param sessionConfig session config
* @return Return new SonicSession if there was no mapping for the sessionId in {@link #runningSessionHashMap}
*/
private SonicSession internalCreateSession(String sessionId, String url, SonicSessionConfig sessionConfig) {
if (!runningSessionHashMap.containsKey(sessionId)) {
SonicSession sonicSession;
if (sessionConfig.sessionMode == SonicConstants.SESSION_MODE_QUICK) {
sonicSession = new QuickSonicSession(sessionId, url, sessionConfig);
} else {
sonicSession = new StandardSonicSession(sessionId, url, sessionConfig);
}
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;
}
在SonicSessionConfig中默认:
/**
* The mode of SonicSession, include{@link QuickSonicSession} and {@link StandardSonicSession}
*/
int sessionMode = SonicConstants.SESSION_MODE_QUICK;
所以后面我们以QuickSonicSession为例分析并行加载技术,接着到SonicSession中的start, runSonicFlow(true)会在线程池中运行,
/**
* Start the sonic process
*/
public void start() {
...
SonicEngine.getInstance().getRuntime().postTaskToSessionThread(new Runnable() {
@Override
public void run() {
runSonicFlow(true);
}
});
...
}
跟到SonicRuntime中
/**
* Post a task to session thread(a high priority thread is better)
*
* @param task A runnable task
*/
public void postTaskToSessionThread(Runnable task) {
SonicSessionThreadPool.postTask(task);
}
并行加载是可以加快处理速度,但如果webview初始化比较快,并且数据还没有完成返回,这样内核就会空等,而内核支持边加载变渲染,所以VasSonic在并行的同时,也利用了内核的这个特性。添加了一个中间层SonicSessionStream来桥接内核和数据,也就是流式拦截。
先看下SonicSessionStream, 是用来桥接两个流,一个是内存流(memStream),一个是网络流(netStream), 在read的时候优先从内存流中读取,再从网络流读取。
public class SonicSessionStream extends InputStream {
private static final String TAG = "SonicSdk_SonicSessionStream";
private BufferedInputStream netStream;
private BufferedInputStream memStream;
private ByteArrayOutputStream outputStream;
private boolean netStreamReadComplete = true;
private boolean memStreamReadComplete = true;
private final WeakReference<SonicSessionStream.Callback> callbackWeakReference;
public SonicSessionStream(SonicSessionStream.Callback callback, ByteArrayOutputStream outputStream, BufferedInputStream netStream) {
if (null != netStream) {
this.netStream = netStream;
this.netStreamReadComplete = false;
}
if (outputStream != null) {
this.outputStream = outputStream;
this.memStream = new BufferedInputStream(new ByteArrayInputStream(outputStream.toByteArray()));
this.memStreamReadComplete = false;
} else {
this.outputStream = new ByteArrayOutputStream();
}
this.callbackWeakReference = new WeakReference(callback);
}
...
public synchronized int read() throws IOException {
int c = -1;
try {
if (null != this.memStream && !this.memStreamReadComplete) {
c = this.memStream.read();
}
if (-1 == c) {
this.memStreamReadComplete = true;
if (null != this.netStream && !this.netStreamReadComplete) {
c = this.netStream.read();
if (-1 != c) {
this.outputStream.write(c);
} else {
this.netStreamReadComplete = true;
}
}
}
return c;
} catch (Throwable var3) {
SonicUtils.log("SonicSdk_SonicSessionStream", 6, "read error:" + var3.getMessage());
if (var3 instanceof IOException) {
throw var3;
} else {
throw new IOException(var3);
}
}
}
...
}
再看到前面的runSonicFlow
中, 第一次发起请求firstRequest=true,之后会进入handleFlow_LoadLocalCache(cacheHtml)
private void runSonicFlow(boolean firstRequest) {
...
if (firstRequest) {
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();
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
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.");
}
}
以QuickSonicSession为例,接着会调用handleFlow_LoadLocalCache(cacheHtml),会通过mainHandler给主线程发消息CLIENT_CORE_MSG_PRE_LOAD
/**
* Handle load local cache of html if exist.
* This handle is called before connection.
*
* @param cacheHtml local cache of html
*/
@Override
protected void handleFlow_LoadLocalCache(String cacheHtml) {
Message msg = mainHandler.obtainMessage(CLIENT_CORE_MSG_PRE_LOAD);
if (!TextUtils.isEmpty(cacheHtml)) {
msg.arg1 = PRE_LOAD_WITH_CACHE;
msg.obj = cacheHtml;
} else {
SonicUtils.log(TAG, Log.INFO, "session(" + sId + ") runSonicFlow has no cache, do first load flow.");
msg.arg1 = PRE_LOAD_NO_CACHE;
}
mainHandler.sendMessage(msg);
for (WeakReference<SonicSessionCallback> ref : sessionCallbackList) {
SonicSessionCallback callback = ref.get();
if (callback != null) {
callback.onSessionLoadLocalCache(cacheHtml);
}
}
}
在handlerMessage中:
@Override
public boolean handleMessage(Message msg) {
// fix issue[https://github.com/Tencent/VasSonic/issues/89]
if (super.handleMessage(msg)) {
return true; // handled by super class
}
if (CLIENT_CORE_MSG_BEGIN < msg.what && msg.what < CLIENT_CORE_MSG_END && !clientIsReady.get()) {
pendingClientCoreMessage = Message.obtain(msg);
SonicUtils.log(TAG, Log.INFO, "session(" + sId + ") handleMessage: client not ready, core msg = " + msg.what + ".");
return true;
}
switch (msg.what) {
case CLIENT_CORE_MSG_PRE_LOAD:
handleClientCoreMessage_PreLoad(msg);
break;
case CLIENT_CORE_MSG_FIRST_LOAD:
handleClientCoreMessage_FirstLoad(msg);
break;
case CLIENT_CORE_MSG_CONNECTION_ERROR:
handleClientCoreMessage_ConnectionError(msg);
break;
case CLIENT_CORE_MSG_SERVICE_UNAVAILABLE:
handleClientCoreMessage_ServiceUnavailable(msg);
break;
case CLIENT_CORE_MSG_DATA_UPDATE:
handleClientCoreMessage_DataUpdate(msg);
break;
case CLIENT_CORE_MSG_TEMPLATE_CHANGE:
handleClientCoreMessage_TemplateChange(msg);
break;
case CLIENT_MSG_NOTIFY_RESULT:
setResult(msg.arg1, msg.arg2, true);
break;
case CLIENT_MSG_ON_WEB_READY: {
diffDataCallback = (SonicDiffDataCallback) msg.obj;
setResult(srcResultCode, finalResultCode, true);
break;
}
default: {
if (SonicUtils.shouldLog(Log.DEBUG)) {
SonicUtils.log(TAG, Log.DEBUG, "session(" + sId + ") can not recognize refresh type: " + msg.what);
}
return false;
}
}
return true;
}
由源码可以看出会调用 handleClientCoreMessage_PreLoad(msg)。
private void handleClientCoreMessage_PreLoad(Message msg) {
switch (msg.arg1) {
case PRE_LOAD_NO_CACHE: {
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.");
}
}
break;
case PRE_LOAD_WITH_CACHE: {
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());
} else {
SonicUtils.log(TAG, Log.ERROR, "session(" + sId + ") handleClientCoreMessage_PreLoad:wasLoadDataInvoked = true.");
}
}
break;
}
}
接着会调用webview的loadUrl
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) {
webView.loadUrl(url);
}
@Override
public void loadDataWithBaseUrl(String baseUrl, String data, String mimeType, String encoding, String historyUrl) {
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;
}
}
}
以上就是加载无缓存的情况下的操作,子线程在runSonicFlow
中继续执行,会执行handleFlow_Connection(hasHtmlCache,sessionData)
/**
* Initiate a network request to obtain server data.
*
* @param hasCache Indicates local sonic cache is exist or not.
* @param sessionData SessionData holds eTag templateTag
*/
protected void handleFlow_Connection(boolean hasCache, SonicDataHelper.SessionData sessionData) {
// create connection for current session
...
server = new SonicServer(this, createConnectionIntent(sessionData));
...
// When cacheHtml is empty, run First-Load flow
if (!hasCache) {
handleFlow_FirstLoad();
return;
}
...
}
由于是第一次,所以没有缓存,接下来调用 handleFlow_FirstLoad();
protected void handleFlow_FirstLoad() {
pendingWebResourceStream = server.getResponseStream(wasInterceptInvoked);
if (null == pendingWebResourceStream) {
SonicUtils.log(TAG, Log.ERROR, "session(" + sId + ") handleFlow_FirstLoad error:server.getResponseStream is null!");
return;
}
String htmlString = server.getResponseData(false);
boolean hasCompletionData = !TextUtils.isEmpty(htmlString);
SonicUtils.log(TAG, Log.INFO, "session(" + sId + ") handleFlow_FirstLoad:hasCompletionData=" + hasCompletionData + ".");
mainHandler.removeMessages(CLIENT_CORE_MSG_PRE_LOAD);
Message msg = mainHandler.obtainMessage(CLIENT_CORE_MSG_FIRST_LOAD);
msg.obj = htmlString;
msg.arg1 = hasCompletionData ? FIRST_LOAD_WITH_DATA : FIRST_LOAD_NO_DATA;
mainHandler.sendMessage(msg);
...
//缓存相关操作
String cacheOffline = server.getResponseHeaderField(SonicSessionConnection.CUSTOM_HEAD_FILED_CACHE_OFFLINE);
if (SonicUtils.needSaveData(config.SUPPORT_CACHE_CONTROL, cacheOffline, server.getResponseHeaderFields())) {
if (hasCompletionData && !wasLoadUrlInvoked.get() && !wasInterceptInvoked.get()) { // Otherwise will save cache in com.tencent.sonic.sdk.SonicSession.onServerClosed
switchState(STATE_RUNNING, STATE_READY, true);
postTaskToSaveSonicCache(htmlString);
}
} else {
SonicUtils.log(TAG, Log.INFO, "session(" + sId + ") handleFlow_FirstLoad:offline->" + cacheOffline + " , so do not need cache to file.");
}
}
server.getResonseStream(wasInterceptInvoked)会从SonicServer读取网络数据直到客户端发起资源请求。到getResponseStream中,可以在函数readServerResponse中看到如果breakCondition为true就会退出while循环,然后函数返回true,在getResponseStream中就会return SonicSessionStream,这个就是上面返回的pendingWebResourceStream.
public synchronized InputStream getResponseStream(AtomicBoolean breakConditions) {
if (readServerResponse(breakConditions)) {
BufferedInputStream netStream = !TextUtils.isEmpty(serverRsp) ? null : connectionImpl.getResponseStream();
return new SonicSessionStream(this, outputStream, netStream);
} else {
return null;
}
}
private boolean readServerResponse(AtomicBoolean breakCondition) {
if (TextUtils.isEmpty(serverRsp)) {
BufferedInputStream bufferedInputStream = connectionImpl.getResponseStream();
if (null == bufferedInputStream) {
SonicUtils.log(TAG, Log.ERROR, "session(" + session.sId + ") readServerResponse error: bufferedInputStream is null!");
return false;
}
try {
byte[] buffer = new byte[session.config.READ_BUF_SIZE];
int n = 0;
while (((breakCondition == null) || !breakCondition.get()) && -1 != (n = bufferedInputStream.read(buffer))) {
outputStream.write(buffer, 0, n);
}
if (n == -1) {
serverRsp = outputStream.toString(session.getCharsetFromHeaders());
}
} catch (Exception e) {
SonicUtils.log(TAG, Log.ERROR, "session(" + session.sId + ") readServerResponse error:" + e.getMessage() + ".");
return false;
}
}
return true;
}
wasInterceptInvoked是什么时候设置为true的呢?是在webview发起资源请求的时候。
//MainActivity
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;
}
});
//SonicSessionClient
public Object requestResource(String url) {
if (session != null) {
return session.onClientRequestResource(url);
}
return null;
}
//SonicSession
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 + ".");
}
}
Object object = isMatchCurrentUrl(url)
? onRequestResource(url)
: (resourceDownloaderEngine != null ? resourceDownloaderEngine.onRequestSubResource(url, this) : null);
resourceInterceptState.set(RESOURCE_INTERCEPT_STATE_NONE);
return object;
}
发起资源请求的host和path如果都和构造SonicSession的url一直就会走到QuickSonicSession中的onRequestResource,这里主要是将pendingWebResourceStream
构造成 webResourceResponse
返回给Webview。
protected Object onRequestResource(String url) {
if (wasInterceptInvoked.get() || !isMatchCurrentUrl(url)) {
return null;
}
...
if (null != pendingWebResourceStream) {
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;
}
网友评论