安卓Webview网页秒开策略探索

news2024/11/26 11:44:23

1 人赞同了该文章

痛点是什么?

网页加载缓慢,白屏,使用卡顿。

为何有这种问题?

1.调用loadUrl()方法的时候,才会开始网页加载流程 2.js臃肿问题 3.加载图片太多 4.webview本身问题

webiew是怎么加载网页的呢?

webview初始化->DOM下载→DOM解析→CSS请求+下载→CSS解析→渲染→绘制→合成

优化方向是?

1.webview本身优化

  • 提前内核初始化 代码:
public class App extends Application {

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

效果:见下图

  • webview复用池 代码:
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();
        }
    }
}

带来的问题:内存泄漏 使用预先创建以及复用池后的效果

  • 独立进程,进程预加载 代码:
<service
            android:name=".PreWebService"
            android:process=":web"/>
        <activity
            android:name=".WebActivity"
            android:process=":web"/>

启动webview页面前,先启动PreWebService把[web]进程创建了,当启动WebActivity时,系统发发现[web]进程已经存在了,就不需要花费时间Fork出新的[web]进程了。

  • 使用x5内核 直接使用腾讯的x5内核,替换原生的浏览器内核
  • 效果:
    • 首次打开

    • 二次打开

  • 其他的解决方案: 1.设置webview缓存 2.加载动画/最后让图片下载 3.渲染时关掉图片加载 4.设置超时时间 5.开启软硬件加速

2.加载资源时的优化 这种优化多使用第三方,下面有介绍

3.网页端的优化 由网页的前端工程师优化网页,或者说是和移动端一起,将网页实现增量更新,动态更新。app内置css,js文件并控制版本

注意:如果你寄希望于只通过webview的setting来加速网页的加载速度,那你就要失望了。只修改设置,能做的提升非常少。所以本文就着重分析比较下,现在可以使用的第三方webview框架的优缺点。


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

简单分析下它的核心思想: 并行,充分利用webview初始化的时间进行一些数据的处理。在包含webview的activity启动时会一边进行webview的初始化逻辑,一边并行的执行sonic的逻辑。这个sonic逻辑就是网页的预加载 原理:

  • Quick模式 模式分类:
  1. 无缓存模式 流程:

左边的webview流程:webview初始化后调用SonicSession的onClientReady方法,告知 webview已经初始化完毕。

client.clientReady();

右边的sonic流程:

  1. 创建SonicEngine对象
  2. 通过SonicCacheInterceptor获取本地缓存的url数据
  3. 数据为空就发送一个CLIENT\_CORE\_MSG\_PRE\_LOAD的消息到主线程
  4. 通过SonicSessionConnection建立一个URLConnection
  5. 连接获取服务器返回的数据,并在读取网络数据的时候不断判断webview是否发起资源拦截请求。如果发了,就中断网络数据的读取,把已经读取的和未读取的数据拼接成桥接流SonicSessionStream并赋值给SonicSession的pendingWebResourceStream,如果网络读取完成后webview还没有初始化完成,就会cancel掉CLIENT\_CORE\_MSG\_PRE\_LOAD消息,同时发送CLIENT\_CORE\_MSG\_FIRST\_LOAD消息
  6. 之后再对html内容进行模版分割及数据保存
  7. 如果webview处理了CLIENT\_CORE\_MSG\_PRE\_LOAD这个消息,它就会调用webview的loadUrl,之后webview会调用自身的资源拦截方法,在这个方法中,会将之前保存的pendingWebResourceStream返回给webview让其解析渲染,
  8. 如果webview处理的是CLIENT\_CORE\_MSG\_FIRST\_LOAD消息,webview如果没有loadUrl过就会调用loadDataWithBaseUrl方法加载之前读取的网络数据,这样webview就可以直接做解析渲染了。

2.有缓存模式 完全缓存流程: 左边webview的流程跟无缓存一致,右边sonic的流程会通过SonicCacheInterceptor获取本地数据是否为空,不为空就会发生CLIENT\_CORE\_MSG\_PRE\_LOAD消息,之后webview就会使用loadDataWithBaseUrl加载网页进行渲染了

  • 效果
    • 首次打开

    • 二次打开


TBS腾讯浏览服务

集成方法,请按照官网的来操作即可。这里直接放上使用后的效果图吧


百度app方案

来看下百度app对webview处理的方案

  1. 后端直出 后端直出-页面静态直出 后端服务器获取html所有首屏内容,包含首屏展现所需的内容和样式。这样客户端获取整个网页并加载时,内核可以直接进行渲染。 这里服务端要提供一个接口给客户端取获取网页的全部内容。而且 获取的网页中一些需要使用客户端的变量的使用宏替换,在客户端加载网页的时候替换成特定的内容,已适应不同用户的设置,例如字体大小、页面颜色等等。 但是这个方案还有些问题就是网络图片没有处理,还是要花费时间起获取图片。

2.智能预取-提前化网络请求 提前从网络中获取部分落地页html,缓存到本地,当用户点击查看时,只需要从缓存中加载即可。

3.通用拦截-缓存共享、请求并行 直出解决了文字展现的速度问题,但是图片加载渲染速度还不理想。 借由内核的shouldInterceptRequest回调,拦截落地页图片请求,由客户端调用图片下载框架进行下载,并以管道方式填充到内核的WebResourceResponse中。就是说在shouldInterceptRequest拦截所有URL,之后只针对后缀是.PNG/.JPG等图片资源,使用第三方图片下载工具类似于Fresco进行下载并返回一个InputStream。

总结:

  • 提前做:包括预创建WebView和预取数据
  • 并行做:包括图片直出&拦截加载,框架初始化阶段开启异步线程准备数据等
  • 轻量化:对于前端来说,要尽量减少页面大小,删减不必要的JS和CSS,不仅可以缩短网络请求时间,还能提升内核解析时间
  • 简单化:对于简单的信息展示页面,对内容动态性要求不高的场景,可以考虑使用直出替代hybrid,展示内容直接可渲染,无需JS异步加载
    • *

今日头条方案

那今日头条是怎么处理的呢? 1.assets文件夹内预置了文章详情页面的css/js等文件,并且能进行版本控制 2.webview预创建的同时,预先加载一个使用JAVA代码拼接的html,提前对js/css资源进行解析。 3.文章详情页面使用预创建的webview,这个webview已经预加载了html,之后就调用js来设置页面内容 3.对于图片资源,使用ContentProvider来获取,而图片则是使用Fresco来下载的

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

整理下这几个大厂的思路 目的:网页秒开 策略:

  • 针对客户端 1.预创建(application onCreate 时)webview 1.1预创建的同时加载带有css/js的html文本 2.webview复用池 3.webview setting的设置 4.预取网页并缓存,预先获取html并缓存本地,需要是从缓存中加载即可 5.资源拦截并行加载,内核初始化和资源加载同时进行。
  • 针对服务端 1.直出网页的拼装,服务端时获取网页的全部内容,客户端获取后直接加载 2.客户端本地html资源的版本控制
  • 针对网页前端 1.删减不必要的js/css 2.配合客户端使用VasSonic,只对特定的内容进行页面更新与下载。
    • *

自己的想法:

  1. 网页秒开的这个需求,如果如果只是客户端来做,感觉只是做了一半,最好还是前后端一起努力来优化。
  2. 但是只做客户端方面的优化也是可以的,笔者实际测试了下,通过预取的方式,的确能做到秒开网页。
  3. 今年就上5G了,有可能在5G的网络下,网页加载根本就不是问题了呢。
    • *

小技巧

修复白屏现象:系统处理view绘制的时候,有一个属性setDrawDuringWindowsAnimating,这个属性是用来控制window做动画的过程中是否可以正常绘制,而恰好在Android 4.2到Android N之间,系统为了组件切换的流程性考虑,该字段为false,我们可以利用反射的方式去手动修改这个属性

/**
     * 让 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 预加载部分源码分析

前文已经说明了sonic的主体思想以及主要的缓存逻辑流程,下面就结合源码一起来看看它是怎么运作预加载这个功能的吧。

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

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

进入preCreateSession方法看看

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

分析:这个方法只要是做了sonic session的创建工作。但是只有满足预载池(preloadSessionPool)的大小小于MAX_PRELOAD_SESSION_COUNT时才会创建。 我们继续进入下一个方法internalCreateSession去看看是怎么创建的

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

这个方法就是根据sessionConfig中的sessionMode类型,来创建不同的缓存类型session。 QuickSonicSession以及StandardSonicSession类型。最后再启动session进行预载工作。我们从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);
            }
        });
     ...
    }

其中最主要的方法就是runSonicFlow(true)这个方法在sonic的专门的线程池中执行网络请求操作。

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

        ...
    }

分析:在首次请求的时候,调用handleFlow_LoadLocalCache方法实际是调用之前创建的QuickSonicSession或者StandardSonicSessionhandleFlow_LoadLocalCache主要作用是发送一则消息CLIENT\_CORE\_MSG\_PRE\_LOAD以及是否含有cache。之后网络存在的情况下调用handleFlow_Connection(hasHtmlCache, sessionData)方法进行请求工作。 接下来进入handleFlow_Connection方法看下是如何建立连接的。

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

方法很长我们一部分一部分看,首先这个创建SonicServer对象,其中通过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);
        }

之后回到handleFlow_Connection,既然创建好了URLConnection那么接下来就可以连接去请求数据了。

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

主要看下readServerResponse这个方法,它做的就是获取返回数据流并拼接成字符串。

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

让我们再次回到handleFlow_Connection方法

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

sonic处理的最后

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

创建ResponseStream用于在webview加载资源的时候进行返回,并且移除CLIENT\_CORE\_MSG\_PRE\_LOAD消息,发送CLIENT\_CORE\_MSG\_FIRST\_LOAD消息,并进行数据的保存 这样,网页的数据就全部获取到本地了,只等待webview开始加载url时,在shouldInterceptRequest时返回保存的pendingWebResourceStream就可以实现快速加载了。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/513572.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

品优购项目学习记录--01公共模块制作

文章目录 一、品优购项目规划1.1 开发工具以及技术栈1.1.1 开发工具1.1.2 技术栈 1.2 品优购项目搭建工作1.2.1 相关文件夹以及文件创建1.2.2 模块化开发1.2.3 网站favicon图标1.2.4 网站TDK三大标签SEO优化 二、品优购首页制作2.1 常用模块类名命名2.2 快捷导航shortcut制作2.…

传统的二次开发有哪些痛点问题?低代码平台帮你解决

一、什么是二次开发呢&#xff1f; 简单的来讲&#xff0c;二次开发就是在原有得软件中进行功能等方面得修改或者扩展&#xff0c;但是不改变原有系统的内核。 二、传统的二次开发有哪些痛点问题&#xff1f; 很多企业在业务发展的过程中会产生各种各样不同得需求&#xff0…

融云亮相「中国信息技术应用创新大会」,入选数字化转型优秀方案集

4 月 27 日&#xff0c;以“全栈创新 从可用到好用”为主题的“2023 第六届中国信息技术应用创新大会”在京顺利召开。移步【融云全球互联网通信云】回复“地图”限量免费领《社交泛娱乐出海作战地图》 大会以“论坛展示展览”的方式&#xff0c;全面、深入地反映信创产业的最新…

基于Android studio的机票管理app设计与开发案例

一 功能介绍 1. 用户模式功能&#xff1a; 用户注册登录功能&#xff08;账号、密码&#xff09;&#xff1b;航班信息&#xff08;航班号、起飞时间、登机时间、起点、终点、登机口&#xff09;&#xff1b;购买机票机票信息&#xff08;航班号、审核状态、乘客姓名、联系方…

Windows平台Qt超详细安装——5.9.6版本以及5.9都差不多,(仔细看,一定学会,学不会怪我)

目录 一、Qt 开发环境&#xff08;Windows&#xff09; 二、设置 QtCreator 编译路径 三、设置 Qt 源码路径 四、QtCreator 介绍 一、Qt 开发环境&#xff08;Windows&#xff09; ①官网下载地址&#xff1a;&#xff08;对应版本&#xff0c;可以在这个网址下面自己找&a…

字节给的比我想的还多?网友看完:打死也要去

曾经的互联网是PC的时代&#xff0c;随着智能手机的普及&#xff0c;移动互联网开始飞速崛起。而字节跳动抓住了这波机遇&#xff0c;2015年&#xff0c;字节跳动全面加码短视频&#xff0c;从那以后&#xff0c;抖音成为了字节跳动用户、收入和估值的最大增长引擎。 自从字节…

【2023考研】双非末流二本非科班一战上岸杭电经验帖

一、作者背景 我本科就读于黑龙江省某末流二本双非大学的物联网工程专业&#xff0c;专业排名一般就在20&#xff05;左右&#xff0c;在校期间无科班竞赛经验&#xff0c;只参加过大学生数学竞赛和杭电要求的PAT考核乙级&#xff0c;项目方面只参加过大创比赛以及专业的课程设…

短视频矩阵多账号系统应用技术开发.源码

抖音矩阵号管理系统是一款用于管理抖音矩阵号的软件&#xff0c;具有如下特点&#xff1a; 1. 用户管理&#xff1a;支持多用户管理&#xff0c;可以指定每个用户对应的抖音矩阵号。 2. 粉丝管理&#xff1a;支持实时查看粉丝数量、关注者数量、点赞数量等统计数据&#xff0c…

学系统集成项目管理工程师(中项)系列20_变更管理

1. 许多项目失败的原因就是由于对变更的处理不当 2. 变更管理是为了使项目实际执行情况和项目基准相一致而对项目变更进行管理&#xff0c;其可能的结果是拒绝变更或调整基准 3. 分类 3.1. 性质 3.1.1. 重大变更 3.1.2. 重要变更 3.1.3. 一般变更 3.1.4. 通过不同审批权…

【Linux】Linux权限,shell命令以及运行原理

之前我们一直敲得命令和口口声声说的shell到底是什么呢&#xff1f;命令行提示符和输入的指令并且可以执行都是通过命令行解释器来实现的&#xff0c;那么命令行解释器就是我们常说的shell&#xff0c;具体我们看下面&#xff01; 目录 一、shell命令以及运行原理 二、Linux权…

百亿量化私募高薪急招C++

百亿量化私募高薪急招C&#xff01; 秋招/校招/社招&#xff0c;21/22/23届可 base北上杭深 现招岗位:C量化系统开发工程师 年base40-80万bonus 通过这里找到我 slam_pan 工作职责 1. 参与交易平台的设计、开发与测试&#xff0c;实现交易策略、风控等需求&#xff1b; 2. 开发…

【DahO安装及使用】

1 安装 Step 1&#xff1a;官网注册帐号 注册地址在这里&#xff08;注册完毕默认登录&#xff09; Step 2&#xff1a;点击下载链接&#xff0c;下载你需要的版本 这里&#xff0c;我下载的是DashO的Windows版本。 下载完毕后官方会向您发送邮件&#xff0c;复制其中的&qu…

WMS和ERP的区别是什么

WMS和ERP是企业常用的两种管理软件&#xff0c;虽然它们都是管理企业资源的软件&#xff0c;但它们的功能和使用场景有所不同。 那么问题来了&#xff1a;很多工厂的管理者都知道自己已经购买或者计划购买的ERP中包含仓储管理模块&#xff0c;那么WMS和ERP系统中的仓储管理模…

node:常用的文件操作fs模块

文章目录 背景读写创建复制移动&#xff08;重命名&#xff09;是否存在删除示例1、文件内容替换2、文件夹内的所有文件内容替换 封装 背景 我们日常工作中&#xff0c;想要对前端做一些自动化操作的时候&#xff0c;免不了使用 node 的文件读写操作&#xff0c;今天来总结一下…

JVM 堆

堆的核心概述 一个 JVM 实例只存在一个堆内存&#xff0c;堆也是 Java 内存管理的核心区域Java 堆区在 JVM 启动的时候即被创建&#xff0c;其空间大小也就确定了。是 JVM 管理的最大一块内存空间堆可以处于物理上不连续的内存空间中&#xff0c;但是在逻辑上它应该被视为连续…

久戴不痛的蓝牙耳机有哪些?久戴不痛的蓝牙耳机推荐

现如今的都市青年离不开什么数码产品&#xff1f;抛开手机这一答案&#xff0c;耳机肯定是第一项&#xff0c;遇到嘈杂的车流声&#xff0c;轰鸣的地铁&#xff0c;安静的图书馆&#xff0c;蓝牙耳机可以可以让人更加沉浸在自己的世界里&#xff0c;很多烧友在选购时候除了对音…

MySQL基础(二十七)性能分析工具的使用

1. 数据库服务器的优化步骤 当我们遇到数据库调优问题的时候&#xff0c;该如何思考呢&#xff1f;这里把思考的流程整理成下面这张图。 整个流程划分成了观察&#xff08;Show status&#xff09;和行动&#xff08;Action&#xff09;两个部分。字母 S 的部分代表观察&…

全网最详细,性能测试场景模型分析,从0到1实施性能测试...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 性能测试&#xf…

资深测试总结,Python接口自动化测试-数据依赖解决(详全)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 Python自动化测试&…

MyBatis学习 (一) 配置文件解析流程

MyBatis源码学习 最近在学习MyBatis的代码。记录下 首先下载下源码&#xff1a; https://github.com/mybatis/parent https://github.com/mybatis/mybatis-3 parent为父控依赖。也需要下载。 入口 InputStream inputStream null; try {// 获取配置文件inputStream Reso…