乐趣区

关于android:安卓Webview网页秒开策略探索

痛点是什么?

网页加载迟缓,白屏,应用卡顿。

为何有这种问题?

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

相干教程

Android 根底系列教程:

Android 根底课程 U - 小结_哔哩哔哩_bilibili

Android 根底课程 UI- 布局_哔哩哔哩_bilibili

Android 根底课程 UI- 控件_哔哩哔哩_bilibili

Android 根底课程 UI- 动画_哔哩哔哩_bilibili

Android 根底课程 -activity 的应用_哔哩哔哩_bilibili

Android 根底课程 -Fragment 应用办法_哔哩哔哩_bilibili

Android 根底课程 - 热修复 / 热更新技术原理_哔哩哔哩_bilibili

本文转自 https://juejin.cn/post/6844903887111979021,如有侵权,请分割删除。

退出移动版