歡迎關注微信公眾號: JueCode
VasSonic是騰訊開源的一套完整的Hybrid方案,Github地址: VasSonic,官方定義是一套輕量級和高效能的Hybrid框架,專注於提升H5首屏載入速度。今天主要分享下其中的一個技術,並行載入技術。在開始之前先了解一個核心概念SonicSession,VasSonic將一次URL請求抽象為SonicSession。SonicSession 在 VasSonic 的設計裡面非常關鍵。其將資源的請求和 WebView 脫離開來,有了 SonicSession,結合 SonicCache,就可以不依賴 WebView 去做資源的請求,這樣就可以實現 WebView 開啟和資源載入並行、資源預載入等加速方案。
下面正式進入並行載入技術分析
並行載入其實主要是兩個方面,一個是在WebView初始化時執行緒池發起網路請求,另外一個就是通過新增中間層 BridgeStream 來連線 WebView 和資料流,中間層 BridgeStream 會先把記憶體的資料讀取返回後,再繼續讀取網路的資料,看一張官方的圖片:
大家知道,客戶端在WebView啟動的時候需要先初始化核心,會有一段白屏的時間,在這段時間網路完全是空閒在等待的,非常浪費,VasSonic採用並行載入模式,初始化核心和發起網路請求並行。
在Demo中BrowserActivity中的onCreate中, 有兩條線,一個是在if (MainActivity.MODE_DEFAULT != mode)中會建立SonicSession,然後線上程池中runSonicFlow
,包括讀取快取,連線LocalServer, 拆分模板和資料等; 另外一個就是主執行緒中初始化WebView,這就實現了並行載入的目的。
@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) {
return new 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 ...
// 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
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);
}
}
複製程式碼
在SonicEngine會構建url對應的SonicSession,其中
sonicSession = SonicEngine.getInstance().createSession(url, sessionConfigBuilder.build());
複製程式碼
跟到SonicEngine中,第一次進來快取中沒有對應的sessionId,所以走到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);
}
return sonicSession;
}
} 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;
}
複製程式碼
在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() {
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);
}
複製程式碼
跟到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);
}
複製程式碼
接著到SonicSessionThreadPool中, 其中執行緒池啟的每個執行緒名稱字首是:"pool-sonic-session-thread-"
/**
* 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);
}
return thread;
}
}
/**
* Constructor and initialize thread pool object
* default one core pool and the maximum number of threads is 6
*
*/
private SonicSessionThreadPool() {
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) {
return sInstance.execute(task);
}
}
複製程式碼
並行處理是可以加快處理速度,如果終端初始化比較快,但是資料還沒有完成返回,這樣核心就會在空等,而核心是支援邊載入邊渲染,所以VasSonic在並行的同時,也利用了核心的這個特性。採用了一箇中間層SonicSessionStream橋接核心和資料,也就是流式攔截:
先看下SonicSessionStream, SonicSessionStream用來橋接兩個流,一個是記憶體流(memStream),一個是網路流(netStream), 在read的時候優先從記憶體流中讀取,再從網路流讀取。
/**
*
* 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 an integer 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);
}
}
return c;
}
}
複製程式碼
再看到前面的`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
,在沒有快取情況下, WebView會呼叫loadUrl先行載入url頁面。
/**
* 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;
}
}
複製程式碼
之後就會呼叫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
中接著往下走,Sonic在post訊息到主執行緒之後會通過SonicSessionConnection建立一個URLConnection,接著通過這個連線獲取伺服器返回的資料。由於獲取網路資料是個耗時的過程,所以在讀取網路資料的過程中會不斷的判斷webView是否發起資源攔截請求(通過SonicSession的wasInterceptInvoked來判斷),如果webview已經發起資源攔截請求,就中斷網路資料的讀取,將已經讀取的資料和未讀取的網路資料拼接成橋接流SonicSessionStream,並將其賦值給SonicSession的pendingWebResourceStream。如果整個網路資料讀取完畢之後webview還沒有初始化完,那麼就會把之前post的CLIENT_CORE_MSG_PRE_LOAD的訊息cancel調。同時post一個CLIENT_CORE_MSG_FIRST_LOAD的訊息到主執行緒。之後再對html內容進行模版分割及資料儲存。
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);
複製程式碼
之後走到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 flow
if (!hasCache) {
handleFlow_FirstLoad();
return;
}
...
}
複製程式碼
到handleFlow_FirstLoad
中,在函式中第一行程式碼server.getResponseStream(wasInterceptInvoked)
,從網路連線中持續讀取資料流到outputstream中,如果WebView發起資源請求,就會置wasInterceptInvoked
為true,這樣在getResponseStream
會構造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 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.getResponseStream(wasInterceptInvoked)會從SonicServer讀取網路資料知道客戶端發起資源請求,到getResponseStream中,可以在函式readServerResponse中看到如果breakCondition為true就會退出while迴圈,然後函式返回true,在getResponseStream中就會return SonicSessionStream,這個就是上面返回的pendingWebResourceStream
.
/**
* 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;
}
複製程式碼
wasInterceptInvoked
是在什麼時候設定為true?在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);
}
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 (!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;
}
複製程式碼
看一張onRequestResource的debug圖:
VasSonic是一個比較完善的Hybrid框架,裡面有很多可以學習的東西,篇幅所限,這次只分析到裡面用到的並行載入技術,後面會有其他分享的內容,比如流式攔截,模板和資料拆分,LocalServer等。
今天的車就開到這了,歡迎關注後面分享。