Exploring strategies for opening Android Webview pages instantly

1 person liked this article

What are the pain points?

The webpage loads slowly, the screen is white, and the use is laggy.

Why is this a problem?

1. The web page loading process will only start when the loadUrl() method is called. 2. JS bloat problem. 3. Loading too many pictures. 4. Problems with the webview itself.

How does webiew load web pages?

webview initialization->DOM download→DOM parsing→CSS request + download→CSS parsing→rendering→drawing→synthesis

What is the optimization direction?

1.Optimize webview itself

  • Early kernel initialization code:
public class App extends Application {

    private WebView mWebView ;
    @Override
    public void onCreate() {
        super.onCreate();
        mWebView = new WebView(new MutableContextWrapper(this));
    }
}

Effect: See the picture below

  • webview reuse pool code:
public class WebPools {
    private final Queue<WebView> mWebViews;
    private Object lock = new Object();
    private static WebPools mWebPools = null;
    private static final AtomicReference<WebPools> mAtomicReference = new AtomicReference<>();
    private static final String TAG=WebPools.class.getSimpleName();

    private WebPools() {
        mWebViews = new LinkedBlockingQueue<>();
    }
    public static WebPools getInstance() {
        for (; ; ) {
            if (mWebPools != null)
                return mWebPools;
            if (mAtomicReference.compareAndSet(null, new WebPools()))
                return mWebPools=mAtomicReference.get();
        }
    }
    public void recycle(WebView webView) {
        recycleInternal(webView);
    }
    public WebView acquireWebView(Activity activity) {
        return acquireWebViewInternal(activity);
    }
    private WebView acquireWebViewInternal(Activity activity) {
        WebView mWebView = mWebViews.poll();
        LogUtils.i(TAG,"acquireWebViewInternal  webview:"+mWebView);
        if (mWebView == null) {
            synchronized (lock) {
                return new WebView(new MutableContextWrapper(activity));
            }
        } else {
            MutableContextWrapper mMutableContextWrapper = (MutableContextWrapper) mWebView.getContext();
            mMutableContextWrapper.setBaseContext(activity);
            return mWebView;
        }
    }
    private void recycleInternal(WebView webView) {
        try {
            if (webView.getContext() instanceof MutableContextWrapper) {
                MutableContextWrapper mContext = (MutableContextWrapper) webView.getContext();
             mContext.setBaseContext(mContext.getApplicationContext());
                LogUtils.i(TAG,"enqueue  webview:"+webView);
                mWebViews.offer(webView);
            }
            if(webView.getContext() instanceof  Activity){
                //throw new RuntimeException("leaked");
                LogUtils.i(TAG,"Abandon this webview  , It will cause leak if enqueue !");
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

Problems caused by: memory leaks and the effects of using pre-created and reused pools

  • Independent process, process preloading code:
<service
            android:name=".PreWebService"
            android:process=":web"/>
        <activity
            android:name=".WebActivity"
            android:process=":web"/>

Before starting the webview page, start PreWebService to create the [web] process. When starting WebActivity, the system finds that the [web] process already exists, so there is no need to spend time forking a new [web] process.

  • Use x5 kernel to directly use Tencent’s x5 kernel to replace the native browser kernel.
  • Effect:
    • Open for the first time

    • Second opening

  • Other solutions: 1. Set webview cache 2. Load animation/finally let the image download 3. Turn off image loading during rendering 4. Set timeout 5. Enable software and hardware acceleration

2. Optimization when loading resources. This kind of optimization mostly uses third parties, which are introduced below.

3. Optimization of the web page The front-end engineers of the web page optimize the web page, or work with the mobile terminal to achieve incremental and dynamic updates of the web page. The app has built-in css, js files and version control

Note: If you hope to speed up the loading speed of web pages only through webview settings, you will be disappointed. Just modifying the settings can do very little improvement. Therefore, this article will focus on analyzing and comparing the advantages and disadvantages of the third-party webview frameworks that can be used now.


VasSonic

//导入 Tencent/VasSonic
    implementation 'com.tencent.sonic:sdk:3.1.0'

STEP2:

//创建一个类继承SonicRuntime
//SonicRuntime类主要提供sonic运行时环境,包括Context、用户UA、ID(用户唯一标识,存放数据时唯一标识对应用户)等等信息。以下代码展示了SonicRuntime的几个方法。
public class TTPRuntime extends SonicRuntime
{
    //初始化
    public TTPRuntime( Context context )
    {
        super(context);
    }
    
    @Override
    public void log(
            String tag ,
            int level ,
            String message )
    {
        //log设置
    }
    
    //获取cookie
    @Override
    public String getCookie( String url )
    {
        return null;
    }
    
    //设置cookid
    @Override
    public boolean setCookie(
            String url ,
            List<String> cookies )
    {
        return false;
    }
    
    //获取用户UA信息
    @Override
    public String getUserAgent()
    {
        return "Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Mobile Safari/537.36";
    }
    
    //获取用户ID信息
    @Override
    public String getCurrentUserAccount()
    {
        return "ttpp";
    }
    
    //是否使用Sonic加速
    @Override
    public boolean isSonicUrl( String url )
    {
        return true;
    }
    
    //创建web资源请求
    @Override
    public Object createWebResourceResponse(
            String mimeType ,
            String encoding ,
            InputStream data ,
            Map<String, String> headers )
    {
        return null;
    }
    
    //网络属否允许
    @Override
    public boolean isNetworkValid()
    {
        return true;
    }
    
    @Override
    public void showToast(
            CharSequence text ,
            int duration )
    { }
    
    @Override
    public void postTaskToThread(
            Runnable task ,
            long delayMillis )
    { }
    
    @Override
    public void notifyError(
            SonicSessionClient client ,
            String url ,
            int errorCode )
    { }
    
    //设置Sonic缓存地址
    @Override
    public File getSonicCacheDir()
    {
        return super.getSonicCacheDir();
    }
}

STEP3:

//创建一个类继承SonicSessionClien
//SonicSessionClient主要负责跟webView的通信,比如调用webView的loadUrl、loadDataWithBaseUrl等方法。
public class WebSessionClientImpl extends SonicSessionClient
{
    private WebView webView;
    
    //绑定webview
    public void bindWebView(WebView webView) {
        this.webView = 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 )
    {
        if( headers.isEmpty() )
        {
            webView.loadDataWithBaseURL( baseUrl, data, mimeType, encoding, historyUrl );
        }
        else
        {
            webView.loadUrl( baseUrl,headers );
        }
    }
}

STEP4:

//创建activity
public class WebActivity extends AppCompatActivity
{
    private String url = "http://www.baidu.com";
    private SonicSession sonicSession;
    
    @Override
    protected void onCreate( @Nullable Bundle savedInstanceState )
    {
        super.onCreate( savedInstanceState );
        setContentView( R.layout.activity_web);
        initView();
    }
    
    private void initView()
    {
        getWindow().addFlags( WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
        
        //初始化 可放在Activity或者Application的onCreate方法中
        if( !SonicEngine.isGetInstanceAllowed() )
        {
            SonicEngine.createInstance( new TTPRuntime( getApplication() ),new SonicConfig.Builder().build() );
        }
        //设置预加载
        SonicSessionConfig config = new SonicSessionConfig.Builder().build();
        SonicEngine.getInstance().preCreateSession( url,config );
        
        WebSessionClientImpl client = null;
        //SonicSessionConfig  设置超时时间、缓存大小等相关参数。
        //创建一个SonicSession对象,同时为session绑定client。session创建之后sonic就会异步加载数据了
        sonicSession = SonicEngine.getInstance().createSession( url,config );
        if( null!= sonicSession )
        {
            sonicSession.bindClient( client = new WebSessionClientImpl() );
        }
        //获取webview
        WebView webView = (WebView)findViewById( R.id.webview_act );
        webView.setWebViewClient( new WebViewClient()
        {
            @Override
            public void onPageFinished(
                    WebView view ,
                    String url )
            {
                super.onPageFinished( view , url );
                if( sonicSession != null )
                {
                    sonicSession.getSessionClient().pageFinish( url );
                }
            }
            
            @Nullable
            @Override
            public WebResourceResponse shouldInterceptRequest(
                    WebView view ,
                    WebResourceRequest request )
            {
                return shouldInterceptRequest( view, request.getUrl().toString() );
            }
            //为clinet绑定webview,在webView准备发起loadUrl的时候通过SonicSession的onClientReady方法通知sonicSession: webView ready可以开始loadUrl了。这时sonic内部就会根据本地的数据情况执行webView相应的逻辑(执行loadUrl或者loadData等)
            @Nullable
            @Override
            public WebResourceResponse shouldInterceptRequest(
                    WebView view ,
                    String url )
            {
                if( sonicSession != null )
                {
                    return (WebResourceResponse)sonicSession.getSessionClient().requestResource( url );
                }
                return null;
            }
        });
        //webview设置
        WebSettings webSettings = webView.getSettings();
        webSettings.setJavaScriptEnabled(true);
        webView.removeJavascriptInterface("searchBoxJavaBridge_");
        //webView.addJavascriptInterface(new SonicJavaScriptInterface(sonicSessionClient, intent), "sonic");
        webSettings.setAllowContentAccess(true);
        webSettings.setDatabaseEnabled(true);
        webSettings.setDomStorageEnabled(true);
        webSettings.setAppCacheEnabled(true);
        webSettings.setSavePassword(false);
        webSettings.setSaveFormData(false);
        webSettings.setUseWideViewPort(true);
        webSettings.setLoadWithOverviewMode(true);

        //为clinet绑定webview,在webView准备发起loadUrl的时候通过SonicSession的onClientReady方法通知sonicSession: webView ready可以开始loadUrl了。这时sonic内部就会根据本地的数据情况执行webView相应的逻辑(执行loadUrl或者loadData等)。
        if( client != null )
        {
            client.bindWebView( webView );
            client.clientReady();
        }
        else
        {
            webView.loadUrl( url );
        }
    }
    
    @Override
    public void onBackPressed()
    {
        super.onBackPressed();
    }
    
    @Override
    protected void onDestroy()
    {
        if( null != sonicSession )
        {
            sonicSession.destroy();
            sonicSession = null;
        }
        super.onDestroy();
    }
}

A brief analysis of its core ideas: Parallel, making full use of the webview initialization time to process some data. When the activity containing the webview is started, the initialization logic of the webview will be performed while the sonic logic will be executed in parallel. This sonic logic is the preloading principle of web pages:

  • Quick mode mode classification:
  1. No cache mode process:

The webview process on the left: After the webview is initialized, the onClientReady method of SonicSession is called to inform the webview that it has been initialized.

client.clientReady();

The sonic process on the right:

  1. Create SonicEngine object
  2. Get locally cached url data through SonicCacheInterceptor
  3. If the data is empty, send a CLIENT\_CORE\_MSG\_PRE\_LOAD message to the main thread.
  4. Establish a URLConnection through SonicSessionConnection
  5. The connection obtains the data returned by the server, and continuously determines whether the webview initiates a resource interception request when reading network data. If it is sent, interrupt the reading of network data, splice the read and unread data into the bridge stream SonicSessionStream and assign it to the pendingWebResourceStream of SonicSession. If the webview has not been initialized after the network reading is completed, it will cancel. Drop the CLIENT\_CORE\_MSG\_PRE\_LOAD message and send the CLIENT\_CORE\_MSG\_FIRST\_LOAD message at the same time
  6. Then template split the html content and save the data.
  7. If webview processes the CLIENT\_CORE\_MSG\_PRE\_LOAD message, it will call webview's loadUrl, and then webview will call its own resource interception method. In this method, the previously saved pendingWebResourceStream will be returned to webview for it to parse rendering,
  8. If the webview processes the CLIENT\_CORE\_MSG\_FIRST\_LOAD message, if the webview has not loadedUrl, it will call the loadDataWithBaseUrl method to load the previously read network data, so that the webview can directly parse and render.

2. Complete cache process with cache mode: The process of webview on the left is consistent with that of no cache. The process of sonic on the right will obtain whether the local data is empty through SonicCacheInterceptor. If it is not empty, a CLIENT\_CORE\_MSG\_PRE\_LOAD message will occur. After that, webview will use loadDataWithBaseUrl to load the web page for rendering.

  • Effect
    • Open for the first time

    • Second opening


TBS Tencent browsing service

For the integration method, please follow the instructions on the official website. Let’s just put the effect picture after use here.


Baidu app solution

Let’s take a look at Baidu app’s solution for webview processing

  1. Back-end direct out Back-end direct out - page static direct out The back-end server obtains all the first screen content of html, including the content and styles required for the first screen display. In this way, when the client obtains the entire web page and loads it, the kernel can render it directly. Here the server needs to provide an interface for the client to obtain the entire content of the web page. Moreover, some variables in the obtained web pages that need to be used on the client are replaced with macros. When the client loads the web page, they are replaced with specific content, which has been adapted to different user settings, such as font size, page color, etc. However, there are still some problems with this solution, which is that the network images are not processed, and it still takes time to obtain the images.

2. Intelligent prefetching - network requests in advance. Get part of the landing page html from the network in advance and cache it locally. When the user clicks to view it, it only needs to be loaded from the cache.

3. Universal interception - cache sharing, request parallelism. Direct out solves the problem of text display speed, but the image loading and rendering speed is not ideal yet. Through the kernel's shouldInterceptRequest callback, the landing page image request is intercepted, the client calls the image download framework to download, and fills it into the kernel's WebResourceResponse in a pipeline manner. That is to say, intercept all URLs in shouldInterceptRequest, and then only target image resources with .PNG/.JPG suffixes. Use a third-party image download tool similar to Fresco to download and return an InputStream.

Summarize:

  • Do it ahead of time: including pre-creating WebView and prefetching data
  • Parallel execution: including direct image outgoing and interception of loading, starting asynchronous threads to prepare data during the framework initialization phase, etc.
  • Lightweight: For the front end, it is necessary to reduce the page size as much as possible and delete unnecessary JS and CSS. This can not only shorten the network request time, but also improve the kernel parsing time.
  • Simplification: For simple information display pages and scenarios that do not require high dynamic content, you can consider using direct out instead of hybrid. The displayed content can be rendered directly without the need for JS asynchronous loading.
    • *

Toutiao plan

So how did Toutiao deal with it? 1. The css/js and other files of the article details page are preset in the assets folder, and version control can be performed. 2. When the webview is pre-created, an html spliced ​​using JAVA code is pre-loaded to parse the js/css resources in advance. . 3. The article details page uses a pre-created webview. This webview has preloaded HTML, and then calls js to set the page content. 3. For image resources, use ContentProvider to obtain them, and images are downloaded using Fresco.

content://com.xposed.toutiao.provider.ImageProvider/getimage/origin/eJy1ku0KwiAUhm8l_F3qvuduJSJ0mRO2JtupiNi9Z4MoWiOa65cinMeX57xXVDda6QPKFld0bLQ9UckbJYlR-UpX3N5Smfi5x3JJ934YxWlKWZhEgbeLhBB-QNFyYUfL1s6uUQFgMkKMtwLA4gJSVwrndUWmUP8CC5xhm87izlKY7VDeTgLXZUtOlJzjkP6AxXfiR5eMYdMCB9PHneGHBzh-VzEje7AzV3ZvHYpjJV599w-uZWXvWadQR_vlAhtY_Bn2LKuzu_GGOscc1MfZ4veyTyNuuu4G1giVqQ==/6694469396007485965/3

Let’s sort out the ideas of these major manufacturers. Purpose: Open web pages instantly. Strategy:

  • For the client 1. Pre-create (when application onCreate) webview 1.1 Load the html text with css/js while pre-creating 2. Webview reuse pool 3. Webview setting setting 4. Prefetch the web page and cache it, and get the html in advance And cache it locally, you just need to load it from the cache. 5. Resource interception is loaded in parallel, and kernel initialization and resource loading are performed at the same time.
  • For the server side 1. Directly assemble the web page, get all the content of the web page on the server side, and load it directly after getting it on the client side 2. Version control of local html resources on the client side
  • For the web front-end 1. Delete unnecessary js/css 2. Use VasSonic with the client to update and download the page only for specific content.
    • *

My own opinion:

  1. The demand for web pages to be opened in seconds, if it is only done by the client, it feels like it is only half done. It is best to work together to optimize the front-end and back-end.
  2. But it is also possible to only optimize the client side. The author actually tested it and found that through prefetching, the web page can indeed be opened in seconds.
  3. 5G will be available this year. It is possible that under the 5G network, web page loading will not be a problem at all.
    • *

Tips

Fix the white screen phenomenon: When the system is processing view drawing, there is an attribute setDrawDuringWindowsAnimating. This attribute is used to control whether the window can be drawn normally during the animation process. It happens that between Android 4.2 and Android N, the system switches components in order to For process considerations, this field is false. We can use reflection to manually modify this attribute.

/**
     * 让 activity transition 动画过程中可以正常渲染页面
     */
    private void setDrawDuringWindowsAnimating(View view) {
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M
                || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
            // 1 android n以上  & android 4.1以下不存在此问题,无须处理
            return;
        }
        // 4.2不存在setDrawDuringWindowsAnimating,需要特殊处理
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
            handleDispatchDoneAnimating(view);
            return;
        }
        try {
            // 4.3及以上,反射setDrawDuringWindowsAnimating来实现动画过程中渲染
            ViewParent rootParent = view.getRootView().getParent();
            Method method = rootParent.getClass()
                    .getDeclaredMethod("setDrawDuringWindowsAnimating", boolean.class);
            method.setAccessible(true);
            method.invoke(rootParent, true);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
     * android4.2可以反射handleDispatchDoneAnimating来解决
     */
    private void handleDispatchDoneAnimating(View paramView) {
        try {
            ViewParent localViewParent = paramView.getRootView().getParent();
            Class localClass = localViewParent.getClass();
            Method localMethod = localClass.getDeclaredMethod("handleDispatchDoneAnimating");
            localMethod.setAccessible(true);
            localMethod.invoke(localViewParent);
        } catch (Exception localException) {
            localException.printStackTrace();
        }
    }

VasSonic preload partial source code analysis

The previous article has explained the main idea of ​​Sonic and the main caching logic process. Let's take a look at how it operates the preloading function based on the source code.

SonicSessionConfig.Builder sessionConfigBuilder = new SonicSessionConfig.Builder();
sessionConfigBuilder.setSupportLocalServer(true);

// 预先加载
boolean preloadSuccess = SonicEngine.getInstance().preCreateSession(DEMO_URL, sessionConfigBuilder.build());

EnterpreCreateSessionHow to see

public synchronized boolean preCreateSession(@NonNull String url, @NonNull SonicSessionConfig sessionConfig) {
    //数据库是否准备好
    if (isSonicAvailable()) {
        //根据url以及RunTime中设置的账号,生成唯一的sessionId
        String sessionId = makeSessionId(url, sessionConfig.IS_ACCOUNT_RELATED);
        if (!TextUtils.isEmpty(sessionId)) {
            SonicSession sonicSession = lookupSession(sessionConfig, sessionId, false);
            if (null != sonicSession) {
                runtime.log(TAG, Log.ERROR, "preCreateSession:sessionId(" + sessionId + ") is already in preload pool.");
                    return false;
                }
       //判断预载池是否满了
       if (preloadSessionPool.size() < config.MAX_PRELOAD_SESSION_COUNT) {
          //网络判断
          if (isSessionAvailable(sessionId) && runtime.isNetworkValid()) {
              //创建sonicSession去进行预载
              sonicSession = internalCreateSession(sessionId, url, sessionConfig);
              if (null != sonicSession) {
                  //放到池子里
                  preloadSessionPool.put(sessionId, sonicSession);
                            return true;
                        }
                    }
                } else {
                    runtime.log(TAG, Log.ERROR, "create id(" + sessionId + ") fail for preload size is bigger than " + config.MAX_PRELOAD_SESSION_COUNT + ".");
                }
            }
        } else {
            runtime.log(TAG, Log.ERROR, "preCreateSession fail for sonic service is unavailable!");
        }
        return false;
    }

Analysis: This method only needs to create the sonic session. But it will only be created if the size of the preload pool (preloadSessionPool) is less than MAX_PRELOAD_SESSION_COUNT. Let's move on to the next methodinternalCreateSession to see how it is created

private SonicSession internalCreateSession(String sessionId, String url, SonicSessionConfig sessionConfig) {
        //预载的sessionId不在已经运行的Session的map中
        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);
            }
            //session状态变化监听
            sonicSession.addSessionStateChangedCallback(sessionCallback);
            
            //默认为true启动session
            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;
    }

This method is to create different cache type sessions based on the sessionMode type in sessionConfig. QuickSonicSession and StandardSonicSession types. Finally, start the session to perform preloading work. Let's continue reading from sonicSession.start().

public void start() {
       ...

        for (WeakReference<SonicSessionCallback> ref : sessionCallbackList) {
            SonicSessionCallback callback = ref.get();
            if (callback != null) {
                //回调启动状态
                callback.onSonicSessionStart();
            }
        }
        ...
        //在session线程中运行预载网页方法
        SonicEngine.getInstance().getRuntime().postTaskToSessionThread(new Runnable() {
            @Override
            public void run() {
                runSonicFlow(true);
            }
        });
     ...
    }

The most important method isrunSonicFlow(true)This method performs network request operations in sonic's dedicated thread pool.

private void runSonicFlow(boolean firstRequest) {
        ...

        //首次请求
        if (firstRequest) {
            //获取html缓存 首次为空
            cacheHtml = SonicCacheInterceptor.getSonicCacheData(this);
            statistics.cacheVerifyTime = System.currentTimeMillis();
            SonicUtils.log(TAG, Log.INFO, "session(" + sId + ") runSonicFlow verify cache cost " + (statistics.cacheVerifyTime - statistics.sonicFlowStartTime) + " ms");
            //发送消息CLIENT_CORE_MSG_PRE_LOAD   arg1:PRE_LOAD_NO_CACHE
            handleFlow_LoadLocalCache(cacheHtml);
        }
        
        boolean hasHtmlCache = !TextUtils.isEmpty(cacheHtml) || !firstRequest;

        final SonicRuntime runtime = SonicEngine.getInstance().getRuntime();
        if (!runtime.isNetworkValid()) {
            //网络不存在
            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();
        }

        ...
    }

Analysis: In the first request, calling the handleFlow_LoadLocalCache method actually calls the previously created QuickSonicSession or StandardSonicSession handleFlow_LoadLocalCacheThe main function is to send a message CLIENT\_CORE\_MSG\_PRE\_LOAD and whether it contains cache. Then call the handleFlow_Connection(hasHtmlCache, sessionData) method to perform the request when the network exists. Next, enter the handleFlow_Connection method to see how to establish the connection.

protected void handleFlow_Connection(boolean hasCache, SonicDataHelper.SessionData sessionData) {
        ...
        //创建网络请求
        server = new SonicServer(this, createConnectionIntent(sessionData));
        ...
}

The method is very long. Let’s look at it part by part. First, this creates a SonicServer object, which is created through SonicSessionConnection URLConnection

public SonicServer(SonicSession session, Intent requestIntent) {
        this.session = session;
        this.requestIntent = requestIntent;
        connectionImpl = SonicSessionConnectionInterceptor.getSonicSessionConnection(session, requestIntent);
    }
public static SonicSessionConnection getSonicSessionConnection(SonicSession session, Intent intent) {
        SonicSessionConnectionInterceptor interceptor = session.config.connectionInterceptor;
        //是否有拦截
        if (interceptor != null) {
            return interceptor.getConnection(session, intent);
        }
        return new SonicSessionConnection.SessionConnectionDefaultImpl(session, intent);
    }
public SessionConnectionDefaultImpl(SonicSession session, Intent intent) {
            super(session, intent);
            //创建URLConnection
            connectionImpl = createConnection();
            initConnection(connectionImpl);
        }

After , return tohandleFlow_Connection. Now that it is createdURLConnection, you can connect to request data.

int responseCode = server.connect();
protected int connect() {
        long startTime = System.currentTimeMillis();
        // 连接是否正常返回码
        int resultCode = connectionImpl.connect();
        ...

        if (SonicConstants.ERROR_CODE_SUCCESS != resultCode) {
            return resultCode; // error case
        }

        startTime = System.currentTimeMillis();
        //连接请求返回码
        responseCode = connectionImpl.getResponseCode(); 
        ...

        // When eTag is empty
        if (TextUtils.isEmpty(eTag)) {
            readServerResponse(null);
            if (!TextUtils.isEmpty(serverRsp)) {
                eTag = SonicUtils.getSHA1(serverRsp);
                addResponseHeaderFields(getCustomHeadFieldEtagKey(), eTag);
                addResponseHeaderFields(CUSTOM_HEAD_FILED_HTML_SHA1, eTag);
            } else {
                return SonicConstants.ERROR_CODE_CONNECT_IOE;
            }

            if (requestETag.equals(eTag)) { // 304 case
                responseCode = HttpURLConnection.HTTP_NOT_MODIFIED;
                return SonicConstants.ERROR_CODE_SUCCESS;
            }
        }

        // When templateTag is empty
        String templateTag = getResponseHeaderField(CUSTOM_HEAD_FILED_TEMPLATE_TAG);
        if (TextUtils.isEmpty(templateTag)) {
            if (TextUtils.isEmpty(serverRsp)) {
                readServerResponse(null);
            }
            if (!TextUtils.isEmpty(serverRsp)) {
                separateTemplateAndData();
                templateTag = getResponseHeaderField(CUSTOM_HEAD_FILED_TEMPLATE_TAG);
            } else {
                return SonicConstants.ERROR_CODE_CONNECT_IOE;
            }
        }

        //check If it changes template or update data.
        String requestTemplateTag = requestIntent.getStringExtra(SonicSessionConnection.CUSTOM_HEAD_FILED_TEMPLATE_TAG);
        if (requestTemplateTag.equals(templateTag)) {
            addResponseHeaderFields(SonicSessionConnection.CUSTOM_HEAD_FILED_TEMPLATE_CHANGE, "false");
        } else {
            addResponseHeaderFields(SonicSessionConnection.CUSTOM_HEAD_FILED_TEMPLATE_CHANGE, "true");
        }

        return SonicConstants.ERROR_CODE_SUCCESS;
    }

Let’s mainly look at this methodreadServerResponse. What it does is to get the returned data stream and splice it into a string.

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

Let’s go back to thehandleFlow_Connectionmethod

// When cacheHtml is empty, run First-Load flow
        if (!hasCache) {
            handleFlow_FirstLoad();
            return;
        }

The end of sonic processing

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.");
        }
    }

Create a ResponseStream to return when the webview loads resources, remove the CLIENT\_CORE\_MSG\_PRE\_LOAD message, send the CLIENT\_CORE\_MSG\_FIRST\_LOAD message, and save the data like this , all the data of the web page is obtained locally. Just wait for the webview to start loading the url, and return the saved pendingWebResourceStream at shouldInterceptRequest to achieve fast loading.

Override
            public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
                if (sonicSession != null) {
                    //返回预载时的数据流
                    return (WebResourceResponse) sonicSession.getSessionClient().requestResource(url);
                }
                return null;
            }

Guess you like

Origin blog.csdn.net/qq_33209777/article/details/130621108