欢迎关注微信公众号: JueCode
VasSonic是腾讯开源的一套完整的Hybrid方案,Github地址: ,官方定义是一套轻量级和高性能的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 (WeakReferenceref : 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(), 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的时候优先从内存流中读取,再从网络流读取。
/** * * ASonicSessionStream
obtains input bytes * from amemStream
and anetStream
. *memStream
is read data from network,netStream
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 includememStream
data andnetStream
data */ private ByteArrayOutputStream outputStream; /** *netStream
data completed flag */ private boolean netStreamReadComplete = true; /** *memStream
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); } ... /** * * * 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 (WeakReferenceref : 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 isPRE_LOAD_NO_CACHE
and client did not * initiate request for load url,client will invoke loadUrl method. If the type of this message is *PRE_LOAD_WITH_CACHE
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, HashMapheaders) { 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> 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 sendCLIENT_CORE_MSG_FIRST_LOAD
* 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. * ** 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
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发起资源请求,
//BrowserActivitywebView.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; } });复制代码
//SonicSessionClientpublic Object requestResource(String url) { if (session != null) { return session.onClientRequestResource(url); } return null;}复制代码
//SonicSessionpublic 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等。
今天的车就开到这了,欢迎关注后面分享。