安卓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网页秒开策略探索
文章图片

  • webview复用池 代码:
public class WebPools { private final Queue mWebViews; private Object lock = new Object(); private static WebPools mWebPools = null; private static final AtomicReference 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,"acquireWebViewInternalwebview:"+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,"enqueuewebview:"+webView); mWebViews.offer(webView); } if(webView.getContext() instanceofActivity){ //throw new RuntimeException("leaked"); LogUtils.i(TAG,"Abandon this webview, It will cause leak if enqueue !"); } }catch (Exception e){ e.printStackTrace(); } } }

带来的问题:内存泄漏 使用预先创建以及复用池后的效果
安卓Webview网页秒开策略探索
文章图片

  • 独立进程,进程预加载 代码:

启动webview页面前,先启动PreWebService把[web]进程创建了,当启动WebActivity时,系统发发现[web]进程已经存在了,就不需要花费时间Fork出新的[web]进程了。
  • 使用x5内核 直接使用腾讯的x5内核,替换原生的浏览器内核
  • 效果:
    • 首次打开
      安卓Webview网页秒开策略探索
      文章图片

    • 二次打开
      安卓Webview网页秒开策略探索
      文章图片

  • 其他的解决方案: 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 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 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 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流程: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加载网页进行渲染了
  • 效果
    • 首次打开
      安卓Webview网页秒开策略探索
      文章图片

    • 二次打开
      安卓Webview网页秒开策略探索
      文章图片

TBS腾讯浏览服务 集成方法,请按照官网的来操作即可。这里直接放上使用后的效果图吧
安卓Webview网页秒开策略探索
文章图片

百度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 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_LOADarg1: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 = https://www.it610.com/article/!TextUtils.isEmpty(htmlString); SonicUtils.log(TAG, Log.INFO,"session(" + sId + ") handleFlow_FirstLoad:hasCompletionData="https://www.it610.com/article/+ 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 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
【安卓Webview网页秒开策略探索】本文转自 https://juejin.cn/post/6844903887111979021,如有侵权,请联系删除。

    推荐阅读