VasSonic原始碼之並行載入

juexingzhe發表於2018-05-04

歡迎關注微信公眾號: 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等。

今天的車就開到這了,歡迎關注後面分享。

相關文章