痛點(diǎn)是什么养葵?
網(wǎng)頁(yè)加載緩慢,白屏邦尊,使用卡頓背桐。
為何有這種問(wèn)題?
1.調(diào)用loadUrl()方法的時(shí)候蝉揍,才會(huì)開(kāi)始網(wǎng)頁(yè)加載流程
2.js臃腫問(wèn)題
3.加載圖片太多
4.webview本身問(wèn)題
webiew是怎么加載網(wǎng)頁(yè)的呢链峭?
webview初始化->DOM下載→DOM解析→CSS請(qǐng)求+下載→CSS解析→渲染→繪制→合成
優(yōu)化方向是?
1.webview本身優(yōu)化
- 提前內(nèi)核初始化
代碼:
public class App extends Application {
private WebView mWebView ;
@Override
public void onCreate() {
super.onCreate();
mWebView = new WebView(new MutableContextWrapper(this));
}
}
效果:初次內(nèi)核初始化大概2000ms又沾,第二次50ms以內(nèi)
- webview復(fù)用池
代碼:
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();
}
}
}
帶來(lái)的問(wèn)題:內(nèi)存泄漏
- 獨(dú)立進(jìn)程,進(jìn)程預(yù)加載
代碼:
<service
android:name=".PreWebService"
android:process=":web"/>
<activity
android:name=".WebActivity"
android:process=":web"/>
啟動(dòng)webview頁(yè)面前杖刷,先啟動(dòng)PreWebService把[web]進(jìn)程創(chuàng)建了励饵,當(dāng)啟動(dòng)WebActivity時(shí),系統(tǒng)發(fā)發(fā)現(xiàn)[web]進(jìn)程已經(jīng)存在了滑燃,就不需要花費(fèi)時(shí)間Fork出新的[web]進(jìn)程了役听。
使用x5內(nèi)核
直接使用騰訊的x5內(nèi)核,替換原生的瀏覽器內(nèi)核其他的解決方案:
1.設(shè)置webview緩存
2.加載動(dòng)畫(huà)/最后讓圖片下載
3.渲染時(shí)關(guān)掉圖片加載
4.設(shè)置超時(shí)時(shí)間
5.開(kāi)啟軟硬件加速
2.加載資源時(shí)的優(yōu)化
這種優(yōu)化多使用第三方表窘,下面有介紹
3.網(wǎng)頁(yè)端的優(yōu)化
由網(wǎng)頁(yè)的前端工程師優(yōu)化網(wǎng)頁(yè)典予,或者說(shuō)是和移動(dòng)端一起,將網(wǎng)頁(yè)實(shí)現(xiàn)增量更新乐严,動(dòng)態(tài)更新瘤袖。app內(nèi)置css,js文件并控制版本
注意:如果你寄希望于只通過(guò)webview的setting來(lái)加速網(wǎng)頁(yè)的加載速度,那你就要失望了麦备。只修改設(shè)置孽椰,能做的提升非常少。所以本文就著重分析比較下凛篙,現(xiàn)在可以使用的第三方webview框架的優(yōu)缺點(diǎn)黍匾。
現(xiàn)在大廠的方法有以下幾種:
VasSonic
參考文章:
https://blog.csdn.net/tencent__open/article/details/77324952
接入方法:
STEP1:
//導(dǎo)入 Tencent/VasSonic
implementation 'com.tencent.sonic:sdk:3.1.0'
STEP2:
//創(chuàng)建一個(gè)類(lèi)繼承SonicRuntime
//SonicRuntime類(lèi)主要提供sonic運(yùn)行時(shí)環(huán)境,包括Context呛梆、用戶UA锐涯、ID(用戶唯一標(biāo)識(shí),存放數(shù)據(jù)時(shí)唯一標(biāo)識(shí)對(duì)應(yīng)用戶)等等信息填物。以下代碼展示了SonicRuntime的幾個(gè)方法纹腌。
public class TTPRuntime extends SonicRuntime
{
//初始化
public TTPRuntime( Context context )
{
super(context);
}
@Override
public void log(
String tag ,
int level ,
String message )
{
//log設(shè)置
}
//獲取cookie
@Override
public String getCookie( String url )
{
return null;
}
//設(shè)置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;
}
//創(chuàng)建web資源請(qǐng)求
@Override
public Object createWebResourceResponse(
String mimeType ,
String encoding ,
InputStream data ,
Map<String, String> headers )
{
return null;
}
//網(wǎng)絡(luò)屬否允許
@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 )
{ }
//設(shè)置Sonic緩存地址
@Override
public File getSonicCacheDir()
{
return super.getSonicCacheDir();
}
}
STEP3:
//創(chuàng)建一個(gè)類(lèi)繼承SonicSessionClien
//SonicSessionClient主要負(fù)責(zé)跟webView的通信,比如調(diào)用webView的loadUrl滞磺、loadDataWithBaseUrl等方法升薯。
public class WebSessionClientImpl extends SonicSessionClient
{
private WebView webView;
//綁定webview
public void bindWebView(WebView webView) {
this.webView = webView;
}
//加載網(wǎng)頁(yè)
@Override
public void loadUrl(String url, Bundle extraData) {
webView.loadUrl(url);
}
//加載網(wǎng)頁(yè)
@Override
public void loadDataWithBaseUrl(String baseUrl, String data, String mimeType, String encoding,
String historyUrl) {
webView.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl);
}
//加載網(wǎng)頁(yè)
@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:
//創(chuàng)建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() );
}
//設(shè)置預(yù)加載
SonicSessionConfig config = new SonicSessionConfig.Builder().build();
SonicEngine.getInstance().preCreateSession( url,config );
WebSessionClientImpl client = null;
//SonicSessionConfig 設(shè)置超時(shí)時(shí)間、緩存大小等相關(guān)參數(shù)击困。
//創(chuàng)建一個(gè)SonicSession對(duì)象涎劈,同時(shí)為session綁定client广凸。session創(chuàng)建之后sonic就會(huì)異步加載數(shù)據(jù)了
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準(zhǔn)備發(fā)起loadUrl的時(shí)候通過(guò)SonicSession的onClientReady方法通知sonicSession: webView ready可以開(kāi)始loadUrl了蛛枚。這時(shí)sonic內(nèi)部就會(huì)根據(jù)本地的數(shù)據(jù)情況執(zhí)行webView相應(yīng)的邏輯(執(zhí)行l(wèi)oadUrl或者loadData等)
@Nullable
@Override
public WebResourceResponse shouldInterceptRequest(
WebView view ,
String url )
{
if( sonicSession != null )
{
return (WebResourceResponse)sonicSession.getSessionClient().requestResource( url );
}
return null;
}
});
//webview設(shè)置
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準(zhǔn)備發(fā)起loadUrl的時(shí)候通過(guò)SonicSession的onClientReady方法通知sonicSession: webView ready可以開(kāi)始loadUrl了。這時(shí)sonic內(nèi)部就會(huì)根據(jù)本地的數(shù)據(jù)情況執(zhí)行webView相應(yīng)的邏輯(執(zhí)行l(wèi)oadUrl或者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();
}
}
簡(jiǎn)單分析下它的核心思想:
并行扭吁,充分利用webview初始化的時(shí)間進(jìn)行一些數(shù)據(jù)的處理。在包含webview的activity啟動(dòng)時(shí)會(huì)一邊進(jìn)行webview的初始化邏輯盲镶,一邊并行的執(zhí)行sonic的邏輯侥袜。這個(gè)sonic邏輯就是網(wǎng)頁(yè)的預(yù)加載
原理:
-
Quick模式
模式分類(lèi):
1.無(wú)緩存模式
流程:
左邊的webview流程:webview初始化后調(diào)用SonicSession的onClientReady方法,告知
webview已經(jīng)初始化完畢徒河。
client.clientReady();
右邊的sonic流程:
1.創(chuàng)建SonicEngine對(duì)象
2.通過(guò)SonicCacheInterceptor獲取本地緩存的url數(shù)據(jù)
3.數(shù)據(jù)為空就發(fā)送一個(gè)CLIENT_CORE_MSG_PRE_LOAD的消息到主線程
4.通過(guò)SonicSessionConnection建立一個(gè)URLConnection
5.連接獲取服務(wù)器返回的數(shù)據(jù)系馆,并在讀取網(wǎng)絡(luò)數(shù)據(jù)的時(shí)候不斷判斷webview是否發(fā)起資源攔截請(qǐng)求。如果發(fā)了顽照,就中斷網(wǎng)絡(luò)數(shù)據(jù)的讀取,把已經(jīng)讀取的和未讀取的數(shù)據(jù)拼接成橋接流SonicSessionStream并賦值給SonicSession的pendingWebResourceStream闽寡,如果網(wǎng)絡(luò)讀取完成后webview還沒(méi)有初始化完成代兵,就會(huì)cancel掉CLIENT_CORE_MSG_PRE_LOAD消息,同時(shí)發(fā)送CLIENT_CORE_MSG_FIRST_LOAD消息
6.之后再對(duì)html內(nèi)容進(jìn)行模版分割及數(shù)據(jù)保存
7.如果webview處理了CLIENT_CORE_MSG_PRE_LOAD這個(gè)消息爷狈,它就會(huì)調(diào)用webview的loadUrl,之后webview會(huì)調(diào)用自身的資源攔截方法植影,在這個(gè)方法中,會(huì)將之前保存的pendingWebResourceStream返回給webview讓其解析渲染涎永,
8.如果webview處理的是CLIENT_CORE_MSG_FIRST_LOAD消息思币,webview如果沒(méi)有l(wèi)oadUrl過(guò)就會(huì)調(diào)用loadDataWithBaseUrl方法加載之前讀取的網(wǎng)絡(luò)數(shù)據(jù),這樣webview就可以直接做解析渲染了羡微。
2.有緩存模式
完全緩存流程:
左邊webview的流程跟無(wú)緩存一致谷饿,右邊sonic的流程會(huì)通過(guò)SonicCacheInterceptor獲取本地?cái)?shù)據(jù)是否為空,不為空就會(huì)發(fā)生CLIENT_CORE_MSG_PRE_LOAD消息妈倔,之后webview就會(huì)使用loadDataWithBaseUrl加載網(wǎng)頁(yè)進(jìn)行渲染了
TBS騰訊瀏覽服務(wù)
官網(wǎng)
集成方法博投,請(qǐng)按照官網(wǎng)的來(lái)操作即可。這里直接放上使用后的效果圖吧
百度app方案
來(lái)看下百度app對(duì)webview處理的方案
- 后端直出
后端直出-頁(yè)面靜態(tài)直出
后端服務(wù)器獲取html所有首屏內(nèi)容盯蝴,包含首屏展現(xiàn)所需的內(nèi)容和樣式毅哗。這樣客戶端獲取整個(gè)網(wǎng)頁(yè)并加載時(shí),內(nèi)核可以直接進(jìn)行渲染捧挺。
這里服務(wù)端要提供一個(gè)接口給客戶端取獲取網(wǎng)頁(yè)的全部?jī)?nèi)容虑绵。而且
獲取的網(wǎng)頁(yè)中一些需要使用客戶端的變量的使用宏替換,在客戶端加載網(wǎng)頁(yè)的時(shí)候替換成特定的內(nèi)容闽烙,已適應(yīng)不同用戶的設(shè)置翅睛,例如字體大小、頁(yè)面顏色等等。
但是這個(gè)方案還有些問(wèn)題就是網(wǎng)絡(luò)圖片沒(méi)有處理宏所,還是要花費(fèi)時(shí)間起獲取圖片酥艳。
2.智能預(yù)取-提前化網(wǎng)絡(luò)請(qǐng)求
提前從網(wǎng)絡(luò)中獲取部分落地頁(yè)html,緩存到本地爬骤,當(dāng)用戶點(diǎn)擊查看時(shí)充石,只需要從緩存中加載即可。
3.通用攔截-緩存共享霞玄、請(qǐng)求并行
直出解決了文字展現(xiàn)的速度問(wèn)題骤铃,但是圖片加載渲染速度還不理想。
借由內(nèi)核的shouldInterceptRequest回調(diào)坷剧,攔截落地頁(yè)圖片請(qǐng)求惰爬,由客戶端調(diào)用圖片下載框架進(jìn)行下載,并以管道方式填充到內(nèi)核的WebResourceResponse中惫企。就是說(shuō)在shouldInterceptRequest攔截所有URL撕瞧,之后只針對(duì)后綴是.PNG/.JPG等圖片資源,使用第三方圖片下載工具類(lèi)似于Fresco進(jìn)行下載并返回一個(gè)InputStream狞尔。
總結(jié):
- 提前做:包括預(yù)創(chuàng)建WebView和預(yù)取數(shù)據(jù)
- 并行做:包括圖片直出&攔截加載丛版,框架初始化階段開(kāi)啟異步線程準(zhǔn)備數(shù)據(jù)等
- 輕量化:對(duì)于前端來(lái)說(shuō),要盡量減少頁(yè)面大小偏序,刪減不必要的JS和CSS页畦,不僅可以縮短網(wǎng)絡(luò)請(qǐng)求時(shí)間,還能提升內(nèi)核解析時(shí)間
- 簡(jiǎn)單化:對(duì)于簡(jiǎn)單的信息展示頁(yè)面研儒,對(duì)內(nèi)容動(dòng)態(tài)性要求不高的場(chǎng)景豫缨,可以考慮使用直出替代hybrid,展示內(nèi)容直接可渲染端朵,無(wú)需JS異步加載
今日頭條方案
那今日頭條是怎么處理的呢好芭?
1.assets文件夾內(nèi)預(yù)置了文章詳情頁(yè)面的css/js等文件,并且能進(jìn)行版本控制
2.webview預(yù)創(chuàng)建的同時(shí)逸月,預(yù)先加載一個(gè)使用JAVA代碼拼接的html栓撞,提前對(duì)js/css資源進(jìn)行解析。
3.文章詳情頁(yè)面使用預(yù)創(chuàng)建的webview碗硬,這個(gè)webview已經(jīng)預(yù)加載了html瓤湘,之后就調(diào)用js來(lái)設(shè)置頁(yè)面內(nèi)容
3.對(duì)于圖片資源,使用ContentProvider來(lái)獲取恩尾,而圖片則是使用Fresco來(lái)下載的
content://com.xposed.toutiao.provider.ImageProvider/getimage/origin/eJy1ku0KwiAUhm8l_F3qvuduJSJ0mRO2JtupiNi9Z4MoWiOa65cinMeX57xXVDda6QPKFld0bLQ9UckbJYlR-UpX3N5Smfi5x3JJ934YxWlKWZhEgbeLhBB-QNFyYUfL1s6uUQFgMkKMtwLA4gJSVwrndUWmUP8CC5xhm87izlKY7VDeTgLXZUtOlJzjkP6AxXfiR5eMYdMCB9PHneGHBzh-VzEje7AzV3ZvHYpjJV599w-uZWXvWadQR_vlAhtY_Bn2LKuzu_GGOscc1MfZ4veyTyNuuu4G1giVqQ==/6694469396007485965/3
整理下這幾個(gè)大廠的思路
目的:網(wǎng)頁(yè)秒開(kāi)
策略:
- 針對(duì)客戶端
1.預(yù)創(chuàng)建(application onCreate 時(shí))webview
1.1預(yù)創(chuàng)建的同時(shí)加載帶有css/js的html文本
2.webview復(fù)用池
3.webview setting的設(shè)置
4.預(yù)取網(wǎng)頁(yè)并緩存弛说,預(yù)先獲取html并緩存本地,需要是從緩存中加載即可
5.資源攔截并行加載翰意,內(nèi)核初始化和資源加載同時(shí)進(jìn)行木人。 - 針對(duì)服務(wù)端
1.直出網(wǎng)頁(yè)的拼裝信柿,服務(wù)端時(shí)獲取網(wǎng)頁(yè)的全部?jī)?nèi)容,客戶端獲取后直接加載
2.客戶端本地html資源的版本控制 - 針對(duì)網(wǎng)頁(yè)前端
1.刪減不必要的js/css
2.配合客戶端使用VasSonic醒第,只對(duì)特定的內(nèi)容進(jìn)行頁(yè)面更新與下載渔嚷。
自己的想法:
1.網(wǎng)頁(yè)秒開(kāi)的這個(gè)需求,如果如果只是客戶端來(lái)做稠曼,感覺(jué)只是做了一半形病,最好還是前后端一起努力來(lái)優(yōu)化。
2.但是只做客戶端方面的優(yōu)化也是可以的霞幅,筆者實(shí)際測(cè)試了下漠吻,通過(guò)預(yù)取的方式,的確能做到秒開(kāi)網(wǎng)頁(yè)司恳。
3.今年就上5G了途乃,有可能在5G的網(wǎng)絡(luò)下,網(wǎng)頁(yè)加載根本就不是問(wèn)題了呢扔傅。
小技巧
修復(fù)白屏現(xiàn)象:系統(tǒng)處理view繪制的時(shí)候耍共,有一個(gè)屬性setDrawDuringWindowsAnimating,這個(gè)屬性是用來(lái)控制window做動(dòng)畫(huà)的過(guò)程中是否可以正常繪制猎塞,而恰好在Android 4.2到Android N之間划提,系統(tǒng)為了組件切換的流程性考慮,該字段為false邢享,我們可以利用反射的方式去手動(dòng)修改這個(gè)屬性
/**
* 讓 activity transition 動(dòng)畫(huà)過(guò)程中可以正常渲染頁(yè)面
*/
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以下不存在此問(wèn)題,無(wú)須處理
return;
}
// 4.2不存在setDrawDuringWindowsAnimating淡诗,需要特殊處理
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
handleDispatchDoneAnimating(view);
return;
}
try {
// 4.3及以上骇塘,反射setDrawDuringWindowsAnimating來(lái)實(shí)現(xiàn)動(dòng)畫(huà)過(guò)程中渲染
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來(lái)解決
*/
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();
}
}