美文网首页
VasSonic源码之并行加载

VasSonic源码之并行加载

作者: Sophia_dd35 | 来源:发表于2019-09-17 11:53 被阅读0次

在WebView初始化的时候,会有一段白屏的时间,在这段时间网络完全是空闲,VasSonic采用并行加载模式,初始化和网络请求并行。

在Demo的MainActivity的onCreate()中

@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
        ....

       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
           ....
           // create sonic session and run sonic flow
           sonicSession = SonicEngine.getInstance().createSession(url, sessionConfigBuilder.build());
          ...
       }
       // 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
       ....
       // 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());

 /**
     *
     * @param url           url for SonicSession Object
     * @param sessionConfig SSonicSession config
     * @return This method will create and return SonicSession Object when url is legal.
     */
    public synchronized SonicSession createSession(@NonNull String url, @NonNull SonicSessionConfig sessionConfig) {
        if (isSonicAvailable()) {//database没有在升级
            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;
    }

/**
     *
     * @param sessionId possible sessionId
     * @param pick      When {@code pick} is true and there is SonicSession in {@link #preloadSessionPool},
     *                  it will remove from {@link #preloadSessionPool}
     * @return
     *          Return valid SonicSession Object from {@link #preloadSessionPool} if the specified sessionId is a key in {@link #preloadSessionPool}.
     */
    private SonicSession lookupSession(SonicSessionConfig config, String sessionId, boolean pick) {
        if (!TextUtils.isEmpty(sessionId) && config != null) {
            SonicSession sonicSession = preloadSessionPool.get(sessionId);
            if (sonicSession != null) {
                //判断session缓存是否过期,以及sessionConfig是否发生变化
                if (!config.equals(sonicSession.config) ||
                        sonicSession.config.PRELOAD_SESSION_EXPIRED_TIME > 0 && System.currentTimeMillis() - sonicSession.createdTime > sonicSession.config.PRELOAD_SESSION_EXPIRED_TIME) {
                    if (runtime.shouldLog(Log.ERROR)) {
                        runtime.log(TAG, Log.ERROR, "lookupSession error:sessionId(" + sessionId + ") is expired.");
                    }
                    preloadSessionPool.remove(sessionId);
                    sonicSession.destroy();
                    return null;
                }

                if (pick) {
                    preloadSessionPool.remove(sessionId);
                }
            }
            return sonicSession;
        }
        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() {
    ...
   SonicEngine.getInstance().getRuntime().postTaskToSessionThread(new Runnable() {
            @Override
            public void run() {
                runSonicFlow(true);
            }
        });
    ...
    }

跟到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);
    }

并行加载是可以加快处理速度,但如果webview初始化比较快,并且数据还没有完成返回,这样内核就会空等,而内核支持边加载变渲染,所以VasSonic在并行的同时,也利用了内核的这个特性。添加了一个中间层SonicSessionStream来桥接内核和数据,也就是流式拦截。

先看下SonicSessionStream, 是用来桥接两个流,一个是内存流(memStream),一个是网络流(netStream), 在read的时候优先从内存流中读取,再从网络流读取。

public class SonicSessionStream extends InputStream {
    private static final String TAG = "SonicSdk_SonicSessionStream";
    private BufferedInputStream netStream;
    private BufferedInputStream memStream;
    private ByteArrayOutputStream outputStream;
    private boolean netStreamReadComplete = true;
    private boolean memStreamReadComplete = true;
    private final WeakReference<SonicSessionStream.Callback> callbackWeakReference;

    public SonicSessionStream(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();
        }
        this.callbackWeakReference = new WeakReference(callback);
    }
    ...
    public synchronized int read() throws IOException {
        int c = -1;

        try {
            if (null != this.memStream && !this.memStreamReadComplete) {
                c = this.memStream.read();
            }

            if (-1 == c) {
                this.memStreamReadComplete = true;
                if (null != this.netStream && !this.netStreamReadComplete) {
                    c = this.netStream.read();
                    if (-1 != c) {
                        this.outputStream.write(c);
                    } else {
                        this.netStreamReadComplete = true;
                    }
                }
            }

            return c;
        } catch (Throwable var3) {
            SonicUtils.log("SonicSdk_SonicSessionStream", 6, "read error:" + var3.getMessage());
            if (var3 instanceof IOException) {
                throw var3;
            } else {
                throw new IOException(var3);
            }
        }
    }  
      ...
}

再看到前面的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(msg)。

 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中继续执行,会执行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) {
        // create connection for current session
         ...
        server = new SonicServer(this, createConnectionIntent(sessionData));
         ...
        // When cacheHtml is empty, run First-Load flow
        if (!hasCache) {
            handleFlow_FirstLoad();
            return;
        }
      ...
    }

由于是第一次,所以没有缓存,接下来调用 handleFlow_FirstLoad();

 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);
        ...
        //缓存相关操作
        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.getResonseStream(wasInterceptInvoked)会从SonicServer读取网络数据直到客户端发起资源请求。到getResponseStream中,可以在函数readServerResponse中看到如果breakCondition为true就会退出while循环,然后函数返回true,在getResponseStream中就会return SonicSessionStream,这个就是上面返回的pendingWebResourceStream.

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;
        }
    }

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发起资源请求的时候。

//MainActivity
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 (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;
    }

相关文章

网友评论

      本文标题:VasSonic源码之并行加载

      本文链接:https://www.haomeiwen.com/subject/xfifuctx.html