Welcome to follow our wechat official account: JueCode
VasSonic is a Hybrid framework designed to speed up the first screen loading of the H5. VasSonic is a Hybrid framework designed to speed up the first screen loading of the H5. Today I’m going to share one of them, parallel loading. Before we get started, consider the core concept of SonicSession. VasSonic abstracts a URL request into a SonicSession. Sonicsessions are key in VasSonic’s design. It separates the resource request from WebView. With SonicSession and SonicCache, it does not rely on WebView to request resources. In this way, it can realize accelerated schemes such as WebView opening and resource loading parallel and resource preloading.
Now enter the analysis of parallel loading technology
In parallel loading, there are two main aspects. One is when the WebView is initialized, the thread pool initiates a network request, and the other is to connect the WebView to the data stream by adding an intermediate layer BridgeStream. The intermediate layer BridgeStream reads the data from the memory and returns it. Read more of the network’s data and take a look at the official picture:
As we all know, when the client starts WebView, it needs to initialize the kernel first, and there will be a blank screen time, during which the network is completely idle and waiting, which is very wasteful. VasSonic adopts the parallel loading mode, and initializes the kernel and initiates network requests in parallel.
In the Demo, there are two lines in onCreate in BrowserActivity, one is in if (mainActivity.mode_default! = mode) creates a SonicSession and runSonicFlow in the thread pool, including read cache, connect to LocalServer, split template and data, etc. The other is to initialize the WebView in the main thread, which implements the purpose of parallel loading.
@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
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 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
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) {
returnnew OfflinePkgSessionConnection(BrowserActivity.this, session, intent); }}); } // 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 codes to feedback as a default mode.
// throw new UnknownError("create session fail!");
Toast.makeText(this, "create sonic session fail!", Toast.LENGTH_LONG).show();
}
}
// start init flow ...
// inThe 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(newWebViewClient() {
@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);
}
returnnull; }}); WebSettings webSettings = webView.getSettings(); // add java script interface // note:ifAPI 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
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); }}Copy the code
SonicEngine builds the URL corresponding to SonicSession, where
sonicSession = SonicEngine.getInstance().createSession(url, sessionConfigBuilder.build());
Copy the code
SonicEngine doesn’t have a sessionId in the cache the first time, so internalCreateSession,
public synchronized SonicSession createSession(@NonNull String url, @NonNull SonicSessionConfig sessionConfig) {
if (isSonicAvailable()) {
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); }returnsonicSession; }}else {
runtime.log(TAG, Log.ERROR, "createSession fail for sonic service is unavailable!");
}
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;
}
Copy the code
Default in SonicSessionConfig:
/**
* The mode of SonicSession, include{@link QuickSonicSession} and {@link StandardSonicSession}
*/
int sessionMode = SonicConstants.SESSION_MODE_QUICK;
Copy the code
So we’ll use QuickSonicSession as an example to analyze parallel loading techniques, and then start, runSonicFlow(true) in SonicSession will run in the thread pool,
/**
* Start the sonic process
*/
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); }}); notifyStateChange(STATE_NONE, STATE_RUNNING, null); }Copy the code
As to the 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);
}
Copy the code
SonicSessionThreadPool = SonicSessionThreadPool = SonicSessionThreadPool = SonicSessionThreadPool
/**
* SonicSession ThreadPool
*/
class SonicSessionThreadPool {
/**
* Log filter
*/
private final static String TAG = SonicConstants.SONIC_SDK_LOG_PREFIX + "SonicSessionThreadPool";
/**
* Singleton object
*/
private final static SonicSessionThreadPool sInstance = new SonicSessionThreadPool();
/**
* ExecutorService object (Executors.newCachedThreadPool())
*/
private final ExecutorService executorServiceImpl;
/**
* SonicSession ThreadFactory
*/
private static class SessionThreadFactory implements ThreadFactory {
/**
* Thread group
*/
private final ThreadGroup group;
/**
* Thread number
*/
private final AtomicInteger threadNumber = new AtomicInteger(1);
/**
* Thread prefix name
*/
private final static String NAME_PREFIX = "pool-sonic-session-thread-";
/**
* Constructor
*/
SessionThreadFactory() { SecurityManager securityManager = System.getSecurityManager(); this.group = securityManager ! = null ? securityManager.getThreadGroup() : Thread.currentThread().getThreadGroup(); } /** * Constructs a new {@code Thread}. Implementations may also initialize * priority, name, daemon status, {@code ThreadGroup}, etc. * * @param r A runnable to be executed by new thread instance * @return Constructed thread, or {@code null} if the request to
* create a thread is rejected
*/
public Thread newThread(@NonNull Runnable r) {
Thread thread = new Thread(this.group, r, NAME_PREFIX + this.threadNumber.getAndIncrement(), 0L);
if (thread.isDaemon()) {
thread.setDaemon(false);
}
if(thread.getPriority() ! = 5) { thread.setPriority(5); }returnthread; } } /** * Constructor and initialize thread pool object * default one core pool and the maximum number of threads is 6 * */ privateSonicSessionThreadPool() {
executorServiceImpl = new ThreadPoolExecutor(1, 6,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new SessionThreadFactory());
}
/**
* Executes the given command at some time in the future. The command
* may execute in a new thread, in a pooled thread, or in the calling
* thread, at the discretion of the {@code Executor} implementation.
*
* @param task The runnable task
* @return Submit success or not
*/
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;
}
}
/**
* Post an runnable to the pool thread
*
* @param task The runnable task
* @return Submit success or not
*/
static boolean postTask(Runnable task) {
returnsInstance.execute(task); }}Copy the code
Parallel processing can be used to speed up processing. If the terminal initialization is fast but the data has not been returned, the kernel will wait empty. The kernel supports loading and rendering at the same time, so VasSonic also takes advantage of this feature of the kernel. An intermediate layer SonicSessionStream is used to bridge kernel and data, namely stream interception:
SonicSessionStream is used to bridge a memStream and a netStream. SonicSessionStream is used to bridge a memStream and a netStream. SonicSessionStream is used to bridge a memStream and a netStream.
/**
*
* A <code>SonicSessionStream</code> obtains input bytes
* from a <code>memStream</code> and a <code>netStream</code>.
* <code>memStream</code>is read data from network, <code>netStream</code>is unread data from network.
*
*/
public class SonicSessionStream extends InputStream {
/**
* Log filter
*/
private static final String TAG = SonicConstants.SONIC_SDK_LOG_PREFIX + "SonicSessionStream";
/**
* Unread data from network
*/
private BufferedInputStream netStream;
/**
* Read data from network
*/
private BufferedInputStream memStream;
/**
* OutputStream include <code>memStream</code> data and <code>netStream</code> data
*/
private ByteArrayOutputStream outputStream;
/**
* <code>netStream</code> data completed flag
*/
private boolean netStreamReadComplete = true;
/**
* <code>memStream</code> data completed flag
*/
private boolean memStreamReadComplete = true;
/**
* Constructor
*
* @param callback Callback
* @param outputStream Read data from network
* @param netStream Unread data from network
*/
public 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(); } callbackWeakReference = new WeakReference<Callback>(callback); }... /** * * <p> * Reads a single byte from this stream and returns it as aninteger in the
* range from 0 to 255. Returns -1 if the end of the stream has been
* reached. Blocks until one byte has been read, the end of the source
* stream is detected or an exception is thrown.
*
* @throws IOException if the stream is closed or another IOException occurs.
*/
@Override
public synchronized int read() throws IOException {
int c = -1;
try {
if(null ! = memStream && ! memStreamReadComplete) { c = memStream.read(); }if (-1 == c) {
memStreamReadComplete = true;
if(null ! = netStream && ! netStreamReadComplete) { c = netStream.read();if(1! = c) { outputStream.write(c); }else {
netStreamReadComplete = true;
}
}
}
} catch (Throwable e) {
SonicUtils.log(TAG, Log.ERROR, "read error:" + e.getMessage());
if (e instanceof IOException) {
throw e;
} else{//Turn all exceptions to IO exceptions to prevent scenes that the kernel can not capture throw new IOException(e); }}returnc; }}Copy the code
FirstRequest firstRequest=true, then 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 ifexist before connection } boolean hasHtmlCache = ! TextUtils.isEmpty(cacheHtml) || ! firstRequest; final SonicRuntime runtime = SonicEngine.getInstance().getRuntime();if(! runtime.isNetworkValid()) { //Whether the network is availableif(hasHtmlCache && ! TextUtils.isEmpty(config.USE_SONIC_CACHE_IN_BAD_NETWORK_TOAST)) { runtime.postTaskToMainThread(newRunnable() {
@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."); }}Copy the code
For QuickSonicSession, see handleFlow_LoadLocalCache(cacheHtml), which sends a message to the main thread via 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); }}}Copy the code
In 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;
}
Copy the code
Enter handleClientCoreMessage_PreLoad. In the absence of caching, the WebView calls loadUrl to load the URL page first.
/**
* Handle the preload message. If the type of this message is <code>PRE_LOAD_NO_CACHE</code> and client did not
* initiate request for load url,client will invoke loadUrl method. If the type of this message is
* <code>PRE_LOAD_WITH_CACHE</code> and and client did not initiate request for loadUrl,client will load local data.
*
* @param msg The message
*/
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; }}Copy the code
The loadURL of the WebView is then called:
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; }}}Copy the code
This is what the main thread does when it is first loaded with no cache, the child thread continues down in runSonicFlow, Sonic establishes an URLConnection through SonicSessionConnection after the POST message is sent to the main thread, The data returned by the server is then retrieved through this connection. Obtaining network data is a time-consuming process, so during the network data reading, the webView is constantly checked whether the resource interception request is initiated by using the wasInterceptInvoked of SonicSession. If the webView has initiated the resource interception request, To interrupt the read of network data, concatenate read and unread network data into a bridge SonicSessionStream and assign it to the pendingWebResourceStream of SonicSession. If the webView is not initialized after the entire network has been read, cancel the previous POST’s CLIENT_CORE_MSG_PRE_LOAD message. Also post a CLIENT_CORE_MSG_FIRST_LOAD message to the main thread. Then the HTML content template segmentation and data storage.
final SonicRuntime runtime = SonicEngine.getInstance().getRuntime();
if(! runtime.isNetworkValid()) { //Whether the network is availableif(hasHtmlCache && ! TextUtils.isEmpty(config.USE_SONIC_CACHE_IN_BAD_NETWORK_TOAST)) { runtime.postTaskToMainThread(newRunnable() {
@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);
Copy the code
Then go to 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) {
...
server = new SonicServer(this, createConnectionIntent(sessionData));
// Connect to web server
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();
setCookiesFromHeaders(headerFieldsMap, shouldSetCookieAsynchronous());
if (SonicUtils.shouldLog(Log.DEBUG)) {
SonicUtils.log(TAG, Log.DEBUG, "session(" + sId + ") connection set cookies cost = " + (System.currentTimeMillis() - startTime) + " ms."); }}... // When cacheHtml is empty, run First-Load flowif(! hasCache) { handleFlow_FirstLoad();return; }... }Copy the code
In handleFlow_FirstLoad, the first line of code in the function server.getresponseStream (wasInterceptInvoked) keeps reading the data stream from the network connection into the OutputStream, If the WebView initiates a resource request, the wasInterceptInvoked is true so that the getResponseStream constructs the SonicSessionStream
/**
*
* In this case sonic will always read the new data from the server until the client
* initiates a resource interception.
*
* If the server data is read finished, sonic will send <code>CLIENT_CORE_MSG_FIRST_LOAD</code>
* message with the new html content from server.
*
* If the server data is not read finished sonic will split the read and unread data into
* a bridgedStream{@link SonicSessionStream}.When client initiates a resource interception,
* sonic will provide the bridgedStream to the kernel.
*
* <p>
* If need save and separate data, sonic will save the server data and separate the server data
* to template and data.
*
*/
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);
for (WeakReference<SonicSessionCallback> ref : sessionCallbackList) {
SonicSessionCallback callback = ref.get();
if(callback ! = null) { callback.onSessionFirstLoad(htmlString); } } 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 cachein 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."); }}Copy the code
The server. GetResponseStream (wasInterceptInvoked) reads the network data from the SonicServer until the client initiates the resource request. The getResponseStream can be invoked in the function readServerResponse If breakCondition is true it exits the while loop, and then the function returns true, and in getResponseStream it returns SonicSessionStream, This is the returned pendingWebResourceStream above.
/**
* Read all of data from {@link SonicSessionConnection#getResponseStream()} into byte array output stream {@code outputStream} until
* {@code breakCondition} is true when {@code breakCondition} is not null.
* Then return a {@code SonicSessionStream} obtains input bytes
* from {@code outputStream} and a {@code netStream} when there is unread data from network.
*
* @param breakConditions This method won't read any data from {@link SonicSessionConnection#getResponseStream()} if {@code breakCondition} is true. * @return Returns a {@code SonicSessionStream} obtains input bytes * from {@code outputStream} and a {@code netStream} when there is unread data from network. */ 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; } } /** * Read all of data from {@link SonicSessionConnection#getResponseStream()} into byte array output stream {@code outputStream} until * {@code breakCondition} is true if {@code breakCondition} is not null. * And then this method convert outputStream into response string {@code serverRsp} at the end of response stream. * * @param breakCondition This method won't read any data from {@link SonicSessionConnection#getResponseStream()} if {@code breakCondition} is true.
* @return True when read any of data from {@link SonicSessionConnection#getResponseStream()} and write into {@code outputStream}
*/
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;
}
Copy the code
When is the wasInterceptInvoked set to True? Make a resource request in the WebView,
//BrowserActivity
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);
}
returnnull; }});Copy the code
//SonicSessionClient
public Object requestResource(String url) {
if(session ! = null) {return session.onClientRequestResource(url);
}
return null;
}
Copy the code
//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;
}
Copy the code
If the host and path of the resource request are the same as the URL from which the SonicSession was constructed it will go to the onRequestResource in the QuickSonicSession, The webResourceResponse is constructed and returned to the WebView using the pendingWebResourceStream mentioned above.
protected Object onRequestResource(String url) {
if(wasInterceptInvoked.get() || ! isMatchCurrentUrl(url)) {return null;
}
if(! wasInterceptInvoked.compareAndSet(false.true)) {
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) { 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;
}
Copy the code
Take a look at the debug diagram for onRequestResource:
VasSonic is a relatively complete Hybrid framework. There are many things to learn from VasSonic. This time, we will only analyze the parallel loading technology used in VasSonic, and we will share other content such as stream interception, template and data splitting, and LocalServer.
That’s all for today’s car. Welcome to share with us.