欢迎关注微信公众号: JueCode
VasSonic是腾讯开源的一套完整的Hybrid方案,Github地址: VasSonic,官方定义是一套轻量级和高性能的Hybrid框架,专注于提升H5首屏加载速度。今天主要分享下其中的一个技术,并行加载技术。在开始之前先了解一个核心概念SonicSession,VasSonic将一次URL请求抽象为SonicSession。SonicSession 在 VasSonic 的设计里面非常关键。其将资源的请求和 WebView 脱离开来,有了 SonicSession,结合 SonicCache,就可以不依赖 WebView 去做资源的请求,这样就可以实现 WebView 打开和资源加载并行、资源预加载等加速方案。
下面正式进入并行加载技术分析
并行加载其实主要是两个方面,一个是在WebView初始化时线程池发起网络请求,另外一个就是通过添加中间层 BridgeStream 来连接 WebView 和数据流,中间层 BridgeStream 会先把内存的数据读取返回后,再继续读取网络的数据,看一张官方的图片:
SonicSessionStream.png
大家知道,客户端在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图:
onRequestResource.PNG
VasSonic是一个比较完善的Hybrid框架,里面有很多可以学习的东西,篇幅所限,这次只分析到里面用到的并行加载技术,后面会有其他分享的内容,比如流式拦截,模板和数据拆分,LocalServer等。
今天的车就开到这了,欢迎关注后面分享。
网友评论