[TOC]
資源
- Web Apps
- WebView
- Android4.4 webview實現分析
- Android WebView使用深入淺出
- 深入講解WebView(上) - 互調,緩存,異常處理等
- 深入講解WebView(下) - session,cookie等
- Android WebView Memory Leak WebView內存泄漏 ==! 這個我用leakcanary沒檢測出來
-
PHP、Android、iOS 的恩恩怨怨
從Android 4.4(KitKat)開始,WebView組件是基于開源的Chromium項目.包含V8 js引擎并支持新的web標準,新webView也共享Chrome for Android的渲染引擎,另外,從5.0(Lollipop)開始,WebView被移到獨立的apk中,因此它可以進行單獨更新,可以從 "settings -- Apps -- Android System WebView" 中查看其版本;
用途
默認情況下,webView不啟用js交互,并會忽略頁面錯誤,適用于展示靜態信息;
也可以啟用js功能,實現與用戶的交互
輔助類
- WebChromeClient 當可能影響webView UI的操作發生時會調用到該類,比如進度變化或者js提示框...
- WebViewClient 當可能影響內容渲染的操作發生時會調用到該類,比如錯誤等...另外,可以通過重寫
shouldOverrideUrlLoading()
來中斷url的加載; - WebSettings 功能設置,比如可否允許js代碼;
基本操作
- 訪問網絡的話需要添加網絡權限
<uses-permission android:name="android.permission.INTERNET" />
- 啟用js功能
WebSettings webSettings = myWebView.getSettings();
webSettings.setJavaScriptEnabled(true);
操作localStorage
項目中接到的要求,要傳給H5頁面添加一些token值,方便其發送非url請求的時候調用
P.S. shouldOverrideUrlLoading()
只能攔截url超鏈接請求,對于H5頁面自己發送其他非跳轉請求的話這個方法是沒法攔截的
而 shouldInterceptRequest()
是返回給app端一個response,如果方法返回的是null則走正常網絡請求返回,否則就返回給定的response,
想到的方案是調用js代碼給localstorage中設定一些值,方便h5調用,當然給出一個原生方法給h5調用也一樣
mWebSettings = mWebView.getSettings();
mWebSettings.setJavaScriptEnabled(true);
mWebSettings.setDomStorageEnabled(true);//給權限
mWebView.setWebViewClient(new WebViewClient() {
// 不在 onPageStart() 中去設置是因為設置完以后又loadUrl(url),之前設定的值就無效了
// 當然,在 onPageFinished() 設置的話也得H5中在document.ready()之后才能去獲取
// 或者也可以考慮在 WebChromeClient 的 onProgressChanged() 方法中作設定
@Override
public void onPageFinished(WebView view, String url) {
LogUtils.d("footTest", "onPageFinished " + url);
view.loadUrl(
"javascript:" +
"localStorage.setItem('token', '" + UacDataInstance.getUserTokenWithoutBear() + "');");
}
}
);
設置返回鍵回退功能
mWv.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
// 需要添加 mWv.canGoBack(),不然當返回到初始頁面時,可能無法繼續通過返回鍵關閉頁面
if (keyCode == KeyEvent.KEYCODE_BACK && mWv.canGoBack()) {
mWv.goBack();
return true;
}
return false;
}
});
也可以通過設置所在Activity的onBackPressed()方法來支持webView回退:
@Override
public void onBackPressed() {
if (mWv.canGoBack()) {
mWv.goBack();
} else {
super.onBackPressed();
}
}
設置標題
mWv.setWebChromeClient(new WebChromeClient(){
@Override
public void onReceivedTitle(WebView view, String title) {
// title 是獲取到的網頁title,可以將之設置為webView所在頁面的標題
MainActivity.this.setTitle(title);
}
)};
設置加載進度
@Override
protected void onCreate(Bundle savedInstanceState) {
//requestWindowFeature(Window.FEATURE_PROGRESS);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//getWindow().setFeatureInt(Window.FEATURE_PROGRESS, Window.PROGRESS_VISIBILITY_ON);
......
mProgressDlg = new ProgressDialog(this);
mProgressDlg.setMessage("loading...");
mWv.setWebChromeClient(new WebChromeClient() {
@Override
public void onProgressChanged(WebView view, int newProgress) {
//更新進度條示數
//這種方式我沒看到效果...
//MainActivity.this.setProgress(newProgress);
//使用控件ProgressDialog來顯示進度
//但記得這種方式需要在error發生時也進行取消
if (newProgress <= 90) {
mProgressDlg.setProgress(newProgress);
} else {
mProgressDlg.dismiss();
}
}
});
mWv.setWebViewClient(new WebViewClient() {
@Override
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
super.onReceivedError(view, request, error);
// 加載某些網站的時候會報:ERR_CONNECTION_REFUSED,因此需要在這里取消進度條的顯示
Toast.makeText(MainActivity.this, "error", Toast.LENGTH_SHORT).show();
if (mProgressDlg.isShowing()) {
mProgressDlg.dismiss();
}
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
if (!mProgressDlg.isShowing()) {
mProgressDlg.show();
}
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
if (mProgressDlg.isShowing()) {
mProgressDlg.dismiss();
}
}
});
控制url跳轉
mWv.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
// 這個方法我沒有重寫的話也還是使用webView來加載鏈接
// 而且我這里測試返回的true/false貌似沒什么影響
if (Uri.parse(url).getHost().endsWith("jianshu.com")) {
//若是指定服務器的鏈接則在當前webView中跳轉
view.loadUrl(url);
return false;
} else if (Uri.parse(url).getHost().length() == 0) {
// 本地鏈接的話直接在webView中跳轉
return false;
}
// 其他情況則使用系統瀏覽器打開網址
Uri uri = Uri.parse(url);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);
return true;
}
});
加載頁面
1. 加載本地asset文件
mWv.loadUrl("file:///android_asset/index.html");
2. 加載本地網頁2
//index.html文件放置于 src/main/assets 目錄中
myWebView.loadUrl("file:///android_asset/index.html");
3. 加載網頁
myWebView.loadUrl("http://www.lxweimin.com/users/302253a7ed00/latest_articles");
4. 解析html字符串
String summary = "<!DOCTYPE html>\n" +
"<html lang=\"zh_CN\">\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <title>webViewDemoFromAsset</title>\n" +
" <script src=\"js/basic.js\"></script>\n" +
"</head>\n" +
"<body>\n" +
"<div>\n" +
" <button id=\"btn\" onclick='showToast()'>調用android toast</button>\n" +
"</div>\n" +
"\n" +
"<label id='label'>js android代碼互調測試</label>\n" +
"\n" +
"<br>\n" +
"<a href=\"http://www.lxweimin.com/users/302253a7ed00/latest_articles/\">個人主頁</a>\n" +
"</body>\n" +
"</html>";
// 官網例子給的下面的寫法,但是會出現中文亂碼,
// 原因:http://blog.csdn.net/top_code/article/details/9163597
// mWv.loadData(summary, "text/html", "utf-8");
mWv.loadData(summary, "text/html;charset=UTF-8", null);
使用android studio的話,項目結構中沒有asset目錄,需要手動創建 src/main/assets
目錄即可;
擴展:
- 如果html文件存于sdcard:則加前綴:
content://com.android.htmlfileprovider/sdcard/
另外,content
前綴可能導致異常,直接使用file:///sdcard/
或者file:/sdcard
也可以; - 也可使用
locaData()
,先將文件讀取出來,在傳入字符串到方法中,可以用于展示頁面,但不會引用css,js等文件;
js與andorid互調
- 通過
addJavaScriptInterface()
來設置接口,傳入實例和類名,讓js可以調用;
Note: The object that is bound to your JavaScript runs in another thread and not in the thread in which it was constructed.
允許網頁調用android功能可以存在風險,比如加載其他網頁,默認做法是使用瀏覽器去加載外部其他網頁;
- 自定義的js對應andoird實現類
//通過webView按鈕調用android toast功能
public class BasicJsAppInterface {
private Context cxt;
public BasicJsAppInterface(Context cxt) {
this.cxt = cxt;
}
// 如果targetSDKVersion設置為17以上,這里需要添加該annotation標志
@JavascriptInterface
public void showToast() {
Toast.makeText(this.cxt, "toast in android", Toast.LENGTH_SHORT).show();
}
}
// 實現js調用android功能
WebView mWv = (WebView) findViewById(R.id.wv);
WebSettings wvSettings = mWv.getSettings();
wvSettings.setJavaScriptEnabled(true);
wvSettings.setDefaultTextEncodingName("utf-8");
//傳入實現js功能的android實例 以及 js調用時使用的名稱
mWv.addJavascriptInterface(new BasicJsAppInterface(this), "AndroidApp");
//加載本地asset文件,以 `file:///` 開頭
mWv.loadUrl("file:///android_asset/index.html");
1. js 調用 android 功能
// 在src/main/assets 目錄(不存在則手動創建)中創建該html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>webViewDemoFromAsset</title>
<script src="js/basic.js"></script>
</head>
<body>
<div>
<button id="btn" onclick='showToast()'>調用android toast</button>
</div>
<label id='label'>js android代碼互調測試</label>
<br>
<a >github主頁</a>
<a href="http://www.lxweimin.com/users/302253a7ed00/latest_articles">簡書主頁</a>
<video width="400" controls>
<source src="res/shuai_dan_ge.mp4" type="video/mp4">
<source src="res/gongsi_de_liliang.flv" type="video/flv">
<p>不支持該格式視頻</p>
</video>
</body>
</html>
注意:這里引入的獨立js文件標簽不能簡寫成 <script src="..."/>
,否則解析可能會出錯,參見
- 自閉合標簽;
-
Whe don't self-closing script tags work
webview_js
//在 assets/js/ 目錄下創建js獨立文件basic.js,當然也可以把這些代碼直接嵌入到html中
function setLabel(id, label) {
document.getElementById(id).innerHTML = label;
}
function showToast(){
AndroidApp.showToast(); //也可以寫成window.AndroidApp.showToast();
}
2. android 調用js代碼:
//前綴javascript, `setLabel()是網頁js文件中定義的方法`
mWv.loadUrl("javascript:setLabel('label','通過android調用js代碼')");
緩存/Cookie
webview應用的緩存文件放置于 /data/data/{yourProjectName}/
下面,之前想提取webview緩存的圖片,往上查找的資料大都是通過 webviewcache.db
來獲取圖片對應的緩存文件,但是我在紅米1s4.4以及nexus6p 6.0系統上都沒有再發現這個文件了,新的緩存目錄結構:
從上圖可以發現 Cookies
文件存在,使用16進制編輯器打開查看,也可在程序中獲取:
private String getCookie() {
CookieManager cm = CookieManager.getInstance();
String cookie = cm.getCookie(mUrl);
if (TextUtils.isEmpty(cookie)) {
cookie = "there is no cookie exist";
}
return cookie;
}
另外,圖中紅色方框內的文件就是緩存的文件了,它們名稱是如何跟實際資源文件對應起來的,這個我還沒弄懂,不過還是可以獲取緩存圖片的,我們使用16進制編輯器來查看,可以發現頭部有該圖片的url地址("?g....d64d.png"):
我們刪除該文件的url地址信息,保存后,修改后綴名為png,即可看到實際的圖片:
播放視頻
支持標準MP4,ogg之類的,flash得啟用插件進行播放,不考慮
官網 建議播放視頻的時候開啟硬件加速,不過我在nexus6p上沒有開(默認開了嗎?)也ok的;
全屏播放
頁面適應
Pixel-Perfect UI in the WebView
- 一個針對移動端優化過的頁面帶有如下類似的屬性:
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
系統將頁面顯示在一個虛擬的Viewport中,這個視口通常比屏幕大,這樣網頁就不會被限制在很小的范圍內,用戶可以通過縮放和平移來查看內容;
- 對于無法控制內容的線上網頁,可以通過代碼方式設置ViewPort:
//強制手機使用 desktop-size viewport
wvSettings.setUseWideViewPort(true);
wvSettings.setLoadWithOverviewMode(true);
擴展-響應式
調試
chrome
需要在電腦上安裝Chrome32以上的版本;在電腦上啟動瀏覽器打開網址
chrome://inspect
,-
android啟用webView調試
條件:- android 4.4以上
- 允許遠端調試
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// 官網: https://developer.chrome.com/devtools/docs/remote-debugging#debugging-webviews
// 官網說WebView不受manifest的debuggable標簽的影響,若需要在該標簽啟用時才允許調試,則添加如下條件判斷(注意:盡量不要在manifest中顯式指定debuggable屬性,放空即可,這樣Android Studio會自動在調試時設置成true,在release版本中設置成false)
int debuggable = getApplicationInfo().flags &= ApplicationInfo.FLAG_DEBUGGABLE;
if (0 != debuggable) {
WebView.setWebContentsDebuggingEnabled(true);
}
}
注意事項
-
All WebView methods must be called on the same thread
wv.loadUrl() 方法放在主線程(根據錯誤提示來指定)中執行;
P.S. webview類中有checkThread()方法,跟初始的Lopper.myLooper做比較,想跟蹤setContentView看看,結果源碼不全,斷點跟蹤不知道跟到哪里去了...后續得再補補; - html頁面應用獨立的js文件時,script不能寫成自閉合標簽,否則瀏覽器解析可能會出錯;
- 官方建議WebView的height屬性設置為
match_parent
或者指定值,而非wrap_content
,同事設置為match_parent
后,其各個父容器不允許設置height為wrap_content
,否則可能導致異常發生; - android 4.4對webView做了些變化,可以參考 [這篇文章](Migrating to WebView in Android 4.4);
- 混淆時,需要設置javaScriptInterface不被混淆
# app/proguard-rules.pro
-keep public class org.lynxz.webviewdemo.BasicJsAppInterface{
public <methods>;
}
-keepattributes *Annotation*
-keepattributes *JavascriptInterface*
異常
1. 內存泄露
這個我還沒測試,Android WebView Memory Leak WebView內存泄漏
==! 然后查了內存檢查:
Android最佳性能實踐(二)——分析內存的使用情況
這里有人發現android 5.1也有類似的情況,我沒有嘗試加載很多頁面,先記錄下來:
Android 5.1 Webview 內存泄漏新場景
2. loadData() 中文亂碼
mWv.loadData(yourHtmlString, "text/html;charset=UTF-8", null);
有人說這么設置也可以避免亂碼,但是我在nexus 6p上沒測試成功:
wvSettings.setDefaultTextEncodingName("utf-8");
3. eglCodecCommon: **** ERROR unknown type 0x73000f (glSizeof,80)
Genymotion模擬器不支持硬件加速,關閉即可:
mWv.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
4. html中含有angular.js,數據獲取成功,但是顯示空白:
已啟用js支持:
mWv.getSettings().setJavaScriptEnabled(true);
logCat報錯:
I/xxx: url = http://fep-web.debug.web.nd/#!/report/student/compositive?client=phone&mode=debug&user_id=2079947956
D/dalvikvm: GC_FOR_ALLOC freed 37K, 14% free 21151K/24528K, paused 17ms, total 17ms
I/dalvikvm-heap: Grow heap (frag case) to 25.837MB for 3288976-byte allocation
W/AwContents: nativeOnDraw failed; clearing to background color.
I/Timeline: Timeline: Activity_idle id: android.os.BinderProxy@425bdee8 time:31230137
I/chromium: [INFO:CONSOLE(39)] "Uncaught Error: [$injector:modulerr] http://errors.angularjs.org/1.4.10/$injector/modulerr?p0=app&p1=Error%3A%20%5B%24injector%3Amodulerr%5D%20http%3A%2F%2Ferrors.angularjs.org%2F1.4.10%2F%24injector%2Fmodulerr%3Fp0%3Dapp-theme%26p1%3DTypeError%253...<omitted>...3)", source: http://fep-web.debug.web.nd/bower_components/angular/angular.min.js?v=201604181940 (39)
I/chromium: [INFO:CONSOLE(72)] "error_log:localStorage error", source: http://fep-web.debug.web.nd/js-error.no-ng.js (72)
我也不懂angular.js用到了什么,添加下dom支持就可以了:
settings.setDomStorageEnabled(true);
5. A WebView method was called on thread 'JavaBridge'. All WebView methods must be called on the same thread.
webview加載了網頁后,在html中點擊重新加載網頁,我之前直接在接口類的方法中直接運行,
@JavascriptInterface
public void retriveToUrl(String url) {
mWv.loadUrl(url);
}
需要將其放置在ui線程中運行:
The JavaScript method is executed on a background (i.e. non-UI) thread. You need to call all Android View related methods on the UI thread.
@JavascriptInterface
public void retriveToUrl(String url) {
mWv.post(new Runnable() {
@Override
public void run() {
mWebView.loadUrl(...).
}
});
}
6. "TypeError: Object [object Object] has no method 'callNative' - 混淆
在release版本中,js代碼調用不到我定義的接口類中的方法,從混淆文件中把這個類排除即可;
js interface proguard