VasSonic源码之并行加载

欢迎关注微信公众号: 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等。

今天的车就开到这了,欢迎关注后面分享。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容