不知不覺(jué)图仓,Hybird App已經(jīng)成了目前比較主流的一種開(kāi)發(fā)方式亮元。
對(duì)于用戶(hù)體驗(yàn)要求較高或者與硬件交互較多的功能我們一般都會(huì)采用Native原生的方式來(lái)實(shí)現(xiàn)跳芳。
而用戶(hù)交互少洲脂,偏展示類(lèi),活動(dòng)類(lèi)的功能我們則通常采用H5的方式來(lái)實(shí)現(xiàn)励饵,
例如新聞?lì)惖腶pp,詳情展示頁(yè)一般就是H5的頁(yè)面
- 一方面圖文排版上web有著先天的優(yōu)勢(shì)滑燃,同時(shí)純展示類(lèi)的頁(yè)面在目前的移動(dòng)設(shè)備上役听,性能體驗(yàn)已經(jīng)很難讓用戶(hù)分辨是網(wǎng)頁(yè)還是原生了;
- 另一方面,H5的頁(yè)面跨平臺(tái)表窘,方便在原生客戶(hù)端上實(shí)現(xiàn)分享功能典予,擁有較強(qiáng)的傳播性,我們平時(shí)常見(jiàn)的活動(dòng)頁(yè)面也擁有這樣的優(yōu)勢(shì)乐严,所以你看到的活動(dòng)頁(yè)面也基本都是H5,只需輕輕一點(diǎn)就能分享到各個(gè)平臺(tái);
- 同時(shí)瘤袖,H5的頁(yè)面開(kāi)發(fā)降低了開(kāi)發(fā)成本,一套代碼昂验,web捂敌,android,ios都能訪問(wèn)既琴。(然而實(shí)際開(kāi)發(fā)過(guò)程中占婉,H5的適配也都是各種淚)
既然Hybird App有這么多優(yōu)勢(shì),那在Android中我們通過(guò)什么樣的方式在原生項(xiàng)目中嵌入H5頁(yè)面呢甫恩?
那就不得不提到我們的WebVew了逆济,作為官方唯一用來(lái)顯示web的組件,
展示網(wǎng)頁(yè)這樣的任務(wù)也只能交給它了磺箕。
A View that displays web pages. This class is the basis upon which you can roll your own web browser or simply display some online content within your Activity. It uses the WebKit rendering engine to display web pages and includes methods to navigate forward and backward through a history, zoom in and out, perform text searches and more.
引用官方文檔的一句話:
WebView是一個(gè)用來(lái)在Activity中顯示我們網(wǎng)頁(yè)的視圖組件奖慌,它通過(guò)webkit渲染引擎渲染和顯示我們的web頁(yè)面,并且包含了web的歷史導(dǎo)航操法松靡,頁(yè)面放大縮小简僧,文本搜索等方法。
我們首先來(lái)看一下WebView的基本用法:
WebView的基本用法
關(guān)于WebView的基本用法击困,大部分人也是輕車(chē)熟路涎劈,
本來(lái)也是寫(xiě)了一部分广凸,無(wú)意中發(fā)現(xiàn)有位博主的博客對(duì)WebView的介紹實(shí)在太過(guò)詳細(xì),像我這樣的懶人蛛枚,有更好的文章是不會(huì)自己去寫(xiě)的谅海,
所以刪了自己寫(xiě)的,將大牛博主的博客分享出來(lái)蹦浦,感興趣同學(xué)的可以一起看一看:
Android WebView 開(kāi)發(fā)詳解(一)
Android WebView 開(kāi)發(fā)詳解(二)
Android WebView 開(kāi)發(fā)詳解(三)
了解完WebView的基本用法扭吁,那就來(lái)總結(jié)下最近項(xiàng)目中遇到的關(guān)于WebView的坑
項(xiàng)目中使用WebView遇到的問(wèn)題
WebView界面的原生標(biāo)題設(shè)置
如圖所示,
一般情況下盲镶,我們WebView所在界面由頂部帶標(biāo)題的原生導(dǎo)航欄跟WebView的內(nèi)容部分組成侥袜,
而WebView中的界面可能在點(diǎn)擊后還會(huì)再跳其他Web頁(yè)面(如圖點(diǎn)擊請(qǐng)假會(huì)在當(dāng)前WebView跳轉(zhuǎn)請(qǐng)假的Web頁(yè)面)。
由于點(diǎn)擊內(nèi)容的不確定性溉贿,所以通常情況下枫吧,最簡(jiǎn)單的做法就是捕獲h5頁(yè)面的 <title> 標(biāo)簽來(lái)進(jìn)行標(biāo)題設(shè)置。
對(duì)于捕獲 <title> 標(biāo)簽內(nèi)容的方式宇色,WebView也很好地提供了支持,我們可以通過(guò)繼承WebChromeClient的onReceivedTitle來(lái)進(jìn)行獲取:
private class WebViewChromeClient extends WebChromeClient {
@Override
public void onReceivedTitle(WebView view, String title) {
super.onReceivedTitle(view, title);
mTitleText.setTitle(String.valueOf(view.getTitle()));
}
}
然而這樣的方式在實(shí)際使用中有一個(gè)問(wèn)題:
當(dāng)通過(guò) webView.goBack() 方式返回上一級(jí)Web頁(yè)面的時(shí)候不會(huì)觸發(fā)這個(gè)方法九杂,因此會(huì)導(dǎo)致標(biāo)題無(wú)法跟隨歷史記錄返回上一級(jí)頁(yè)面。
所以在項(xiàng)目中宣蠕,
我們可以通過(guò)重寫(xiě) WebViewClient 的 [onPageFinished](https://developer.android.google.cn/reference/android/webkit/WebViewClient.html#onPageFinished(android.webkit.WebView, java.lang.String)) 方法例隆,在 [onPageFinished](https://developer.android.google.cn/reference/android/webkit/WebViewClient.html#onPageFinished(android.webkit.WebView, java.lang.String)) 中對(duì)界面標(biāo)題進(jìn)行設(shè)置。
因?yàn)椴还苁菤v史記錄的返回還是點(diǎn)擊跳轉(zhuǎn)都會(huì)觸發(fā)頁(yè)面加載抢蚀,
當(dāng)頁(yè)面加載完成時(shí)(不包括js動(dòng)態(tài)創(chuàng)建以及img圖片加載完畢)都會(huì)觸發(fā) [onPageFinished](https://developer.android.google.cn/reference/android/webkit/WebViewClient.html#onPageFinished(android.webkit.WebView, java.lang.String)) 這個(gè)方法镀层,
此時(shí)我們?nèi)カ@取 <title> 的標(biāo)題內(nèi)容不會(huì)有任何問(wèn)題,可以確保在頁(yè)面返回時(shí)能夠獲取到正確的標(biāo)題皿曲。
mWebView.setWebViewClient(new WebViewClient(){
//Web頁(yè)面每次加載并完成時(shí)會(huì)觸發(fā)該方法
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
mToolbar.setTitle(String.valueOf(view.getTitle()));
Log.i (LOG_TAG, "onPageFinished");
}
});
注: 這種做法有一個(gè)缺陷唱逢,就是返回上一個(gè)界面的時(shí)候,等頁(yè)面加載完成的時(shí)候標(biāo)題才會(huì)顯示出來(lái)屋休,為了更好地優(yōu)化惶我,我們可以創(chuàng)建一個(gè)集合用來(lái)保存我們的標(biāo)題,加載url的時(shí)候把標(biāo)題添加進(jìn)集合博投,當(dāng)返回上一級(jí)頁(yè)面的時(shí)候绸贡,從集合中取出標(biāo)題進(jìn)行顯示,同時(shí)從集合中移除標(biāo)題毅哗。
WebView中的Web頁(yè)面存在<input type='file'>標(biāo)簽時(shí)無(wú)法打開(kāi)文件選擇器
在我們的手機(jī)瀏覽器中听怕,當(dāng)web頁(yè)面中有 <input type='file'> 按鈕標(biāo)簽的時(shí)候點(diǎn)擊會(huì)自動(dòng)打開(kāi)系統(tǒng)的文件選擇器,
然而這個(gè)功能在主流系統(tǒng)的WebView中沒(méi)有被默認(rèn)實(shí)現(xiàn)虑绵,
因此尿瞭,為了讓 <input type='file'> 點(diǎn)擊時(shí)能夠打開(kāi)系統(tǒng)的文件選擇器,
我們必須通過(guò)重寫(xiě) WebChromeClient 來(lái)實(shí)現(xiàn)點(diǎn)擊<input type='file'> 打開(kāi)系統(tǒng)文件選擇器翅睛。
代碼如下:
public class MainActivity extends AppCompatActivity {
/** Android 5.0以下版本的文件選擇回調(diào) */
protected ValueCallback<Uri> mFileUploadCallbackFirst;
/** Android 5.0及以上版本的文件選擇回調(diào) */
protected ValueCallback<Uri[]> mFileUploadCallbackSecond;
protected static final int REQUEST_CODE_FILE_PICKER = 51426;
protected String mUploadableFileTypes = "image/*";
private WebView mWebView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initWebView();
}
private void initWebView() {
mWebView = (WebView) findViewById(R.id.my_webview);
mWebView.loadUrl("file:///android_asset/index.html");
mWebView.setWebChromeClient(new OpenFileChromeClient());
}
private class OpenFileChromeClient extends WebChromeClient {
// Android 2.2 (API level 8)到Android 2.3 (API level 10)版本選擇文件時(shí)會(huì)觸發(fā)該隱藏方法
@SuppressWarnings("unused")
public void openFileChooser(ValueCallback<Uri> uploadMsg) {
openFileChooser(uploadMsg, null);
}
// Android 3.0 (API level 11)到 Android 4.0 (API level 15))版本選擇文件時(shí)會(huì)觸發(fā)声搁,該方法為隱藏方法
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) {
openFileChooser(uploadMsg, acceptType, null);
}
// Android 4.1 (API level 16) -- Android 4.3 (API level 18)版本選擇文件時(shí)會(huì)觸發(fā)黑竞,該方法為隱藏方法
@SuppressWarnings("unused")
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
openFileInput(uploadMsg, null, false);
}
// Android 5.0 (API level 21)以上版本會(huì)觸發(fā)該方法,該方法為公開(kāi)方法
@SuppressWarnings("all")
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) {
if (Build.VERSION.SDK_INT >= 21) {
final boolean allowMultiple = fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE;//是否支持多選
openFileInput(null, filePathCallback, allowMultiple);
return true;
}
else {
return false;
}
}
}
@SuppressLint("NewApi")
protected void openFileInput(final ValueCallback<Uri> fileUploadCallbackFirst, final ValueCallback<Uri[]> fileUploadCallbackSecond, final boolean allowMultiple) {
//Android 5.0以下版本
if (mFileUploadCallbackFirst != null) {
mFileUploadCallbackFirst.onReceiveValue(null);
}
mFileUploadCallbackFirst = fileUploadCallbackFirst;
//Android 5.0及以上版本
if (mFileUploadCallbackSecond != null) {
mFileUploadCallbackSecond.onReceiveValue(null);
}
mFileUploadCallbackSecond = fileUploadCallbackSecond;
Intent i = new Intent(Intent.ACTION_GET_CONTENT);
i.addCategory(Intent.CATEGORY_OPENABLE);
if (allowMultiple) {
if (Build.VERSION.SDK_INT >= 18) {
i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
}
}
i.setType(mUploadableFileTypes);
startActivityForResult(Intent.createChooser(i, "選擇文件"), REQUEST_CODE_FILE_PICKER);
}
public void onActivityResult(final int requestCode, final int resultCode, final Intent intent) {
if (requestCode == REQUEST_CODE_FILE_PICKER) {
if (resultCode == Activity.RESULT_OK) {
if (intent != null) {
//Android 5.0以下版本
if (mFileUploadCallbackFirst != null) {
mFileUploadCallbackFirst.onReceiveValue(intent.getData());
mFileUploadCallbackFirst = null;
}
else if (mFileUploadCallbackSecond != null) {//Android 5.0及以上版本
Uri[] dataUris = null;
try {
if (intent.getDataString() != null) {
dataUris = new Uri[] { Uri.parse(intent.getDataString()) };
}
else {
if (Build.VERSION.SDK_INT >= 16) {
if (intent.getClipData() != null) {
final int numSelectedFiles = intent.getClipData().getItemCount();
dataUris = new Uri[numSelectedFiles];
for (int i = 0; i < numSelectedFiles; i++) {
dataUris[i] = intent.getClipData().getItemAt(i).getUri();
}
}
}
}
}
catch (Exception ignored) { }
mFileUploadCallbackSecond.onReceiveValue(dataUris);
mFileUploadCallbackSecond = null;
}
}
}
else {
//這里mFileUploadCallbackFirst跟mFileUploadCallbackSecond在不同系統(tǒng)版本下分別持有了
//WebView對(duì)象疏旨,在用戶(hù)取消文件選擇器的情況下很魂,需給onReceiveValue傳null返回值
//否則WebView在未收到返回值的情況下,無(wú)法進(jìn)行任何操作檐涝,文件選擇器會(huì)失效
if (mFileUploadCallbackFirst != null) {
mFileUploadCallbackFirst.onReceiveValue(null);
mFileUploadCallbackFirst = null;
}
else if (mFileUploadCallbackSecond != null) {
mFileUploadCallbackSecond.onReceiveValue(null);
mFileUploadCallbackSecond = null;
}
}
}
}
}
注:當(dāng)用戶(hù)點(diǎn)擊input file彈出文件選擇器后遏匆,點(diǎn)擊取消或者返回按鈕沒(méi)有執(zhí)行選擇時(shí),必須在onActivityResult里給valueCallback的onReceiveValue傳null谁榜,因?yàn)関alueCallback持有的是WebView幅聘,在onReceiveValue沒(méi)有回傳值的情況下,WebView無(wú)法進(jìn)行下一步操作窃植,會(huì)導(dǎo)致取消選擇文件后帝蒿,點(diǎn)擊input file不會(huì)再響應(yīng):
if (mFileUploadCallbackFirst != null) {
mFileUploadCallbackFirst.onReceiveValue(null);
mFileUploadCallbackFirst = null;
}
else if (mFileUploadCallbackSecond != null) {
mFileUploadCallbackSecond.onReceiveValue(null);
mFileUploadCallbackSecond = null;
}
示例demo地址:
https://github.com/cjpx00008/FileChooser4WebViewDemo
WebView中的web頁(yè)面調(diào)用系統(tǒng)選擇器或者相機(jī)導(dǎo)致app進(jìn)入后臺(tái)被系統(tǒng)釋放
眾所周知,WebView基于webkit內(nèi)核來(lái)渲染web頁(yè)面巷怜,因此使用起來(lái)相當(dāng)于一個(gè)小型瀏覽器陵叽,即使頁(yè)面內(nèi)容不復(fù)雜,只要使用WebView也會(huì)占用大量的內(nèi)存丛版。
而Android的內(nèi)存回收機(jī)制,在系統(tǒng)內(nèi)存不足的情況下會(huì)優(yōu)先釋放內(nèi)存占用較大的app從而回收內(nèi)存資源偏序,此時(shí)正在使用WebView的運(yùn)行在后臺(tái)的app肯定是首當(dāng)其沖被回收的页畦。
因此,當(dāng)WebView通過(guò)input file調(diào)用系統(tǒng)文件選擇器研儒,或者通過(guò)文件選擇器調(diào)用了相機(jī)時(shí)豫缨,我們的app就進(jìn)入了后臺(tái),在部分低端Android設(shè)備(尤其紅米這類(lèi)手機(jī)端朵,默認(rèn)的神隱模式會(huì)在app進(jìn)入后臺(tái)的時(shí)候較大概率的釋放app)或者系統(tǒng)內(nèi)存資源不足的情況下好芭,我們的app就會(huì)優(yōu)先被釋放掉,導(dǎo)致文件選擇完畢后冲呢,回到上一界面時(shí)舍败,app的界面重新走了onCreate,web頁(yè)面也因此重建了敬拓。
對(duì)于部分需要填寫(xiě)大量表單的web頁(yè)面來(lái)說(shuō)邻薯,用戶(hù)填寫(xiě)的數(shù)據(jù)會(huì)隨著界面的銷(xiāo)毀重建而丟失,而選擇的文件也因?yàn)轫?yè)面的重建而無(wú)法回傳給input file,這對(duì)于用戶(hù)的體驗(yàn)來(lái)說(shuō)肯定是不友好的乘凸。
也許你會(huì)說(shuō)厕诡,重寫(xiě)onSaveInstance保存數(shù)據(jù)就是啦。
這也是我一開(kāi)始考慮的营勤,
我們的WebView也提供了 saveState 以及 restoreState 來(lái)保存狀態(tài)灵嫌。
然而悲催的是壹罚,這兩個(gè)方法并不會(huì)保存web頁(yè)面內(nèi)的數(shù)據(jù),它只保存了WebView加載的頁(yè)面寿羞,前進(jìn)后退的歷史狀態(tài)等數(shù)據(jù)猖凛。
引用官方文檔的描述:
Saves the state of this WebView used in onSaveInstanceState(Bundle)
. Please note that this method no longer stores the display data for this WebView. The previous behavior could potentially leak files if restoreState(Bundle)
was never called.
Please note that this method no longer stores the display data for this WebView
WebView的saveState并不會(huì)保存界面的數(shù)據(jù)。
所以稠曼,對(duì)于表單數(shù)據(jù)的恢復(fù)形病,我們只能自己想辦法了,我們這里采用了兩套方案:
- 通過(guò)WebView與JS交互霞幅,在onSaveInstance的時(shí)候觸發(fā)界面保存數(shù)據(jù)漠吻,保存數(shù)據(jù)的方式也大體分為兩種,
一種使用H5自帶的localStorage來(lái)進(jìn)行數(shù)據(jù)存儲(chǔ)司恳,頁(yè)面銷(xiāo)毀重建的時(shí)候H5頁(yè)面判斷本地localStorage數(shù)據(jù)是否有值途乃,有就將值重新填充到頁(yè)面表單,提交數(shù)據(jù)后清除本地localStorage的數(shù)據(jù)扔傅。
這種方式需要給WebView開(kāi)啟對(duì)localStorage的支持耍共。
WebSettings settings = mWebView.getSettings();
settings.setDomStorageEnabled(true);
另一種則提供JS接口將數(shù)據(jù)傳遞給原生,通過(guò)原生代碼將數(shù)據(jù)保存到本地猎塞,在頁(yè)面重建渲染完成時(shí)试读,web頁(yè)面通過(guò)JS接口調(diào)用原生方法拉取數(shù)據(jù)判斷是否有值,有則填充表單荠耽,無(wú)則不做操作钩骇,提交數(shù)據(jù)后調(diào)用JS接口調(diào)用原生方法清空本地?cái)?shù)據(jù)。
- 由web端自己處理铝量,在表單頁(yè)面文本輸入失去焦點(diǎn)時(shí)自動(dòng)保存數(shù)據(jù)倘屹,頁(yè)面銷(xiāo)毀重建時(shí),自己拉取數(shù)據(jù)進(jìn)行判斷慢叨。
這種方式對(duì)原生的依賴(lài)較低纽匙,個(gè)人更傾向這種方式,當(dāng)然最終由于項(xiàng)目的特殊情況拍谐,我們還是采用了第一種方式烛缔。
以上是表單數(shù)據(jù)的恢復(fù)方案,
而對(duì)于從系統(tǒng)文件選擇器選擇的文件web頁(yè)面是無(wú)法直接接收并處理了轩拨,這里我們提供了一個(gè)JS接口在web頁(yè)面加載完成時(shí)力穗,進(jìn)行觸發(fā),并將數(shù)據(jù)傳遞給web頁(yè)面气嫁。
說(shuō)到這里当窗,不得不提另外一個(gè)問(wèn)題
WebView調(diào)用服務(wù)端頁(yè)面如何訪問(wèn)本地文件
上面我們提到了通過(guò)JS接口將選擇的文件數(shù)據(jù)傳遞給web頁(yè)面,
然而由于安全原因寸宵,WebView限制了遠(yuǎn)程url頁(yè)面訪問(wèn)本地文件崖面,
如果我們加載的url是服務(wù)端的頁(yè)面元咙,那我們沒(méi)有任何辦法直接通過(guò)文件地址來(lái)訪問(wèn)客戶(hù)端本地的文件
我們知道,WebView用來(lái)加載網(wǎng)頁(yè)的方式主要有三種:
loadUrl(String url)
loadUrl(String url, Map<String, String> additionalHttpHeaders)
loadData(String data, String mimeType, String encoding)
loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl)
[loadData()](https://developer.android.google.cn/reference/android/webkit/WebView.html#loadData(java.lang.String, java.lang.String, java.lang.String)) 和 [loadDataWithBaseURL()](https://developer.android.google.cn/reference/android/webkit/WebView.html#loadDataWithBaseURL(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String)) 都是直接將數(shù)據(jù)加載進(jìn)WebView中巫员,相當(dāng)于顯示的一個(gè)本地Web
loadUrl也可以通過(guò)訪問(wèn)本地的文件地址(例如本地asset目錄下的存放了index.html頁(yè)面庶香,可以通過(guò)loadUrl("file:///android_asset/index.html")
來(lái)顯示web頁(yè)面)
對(duì)于這樣的三種加載本地內(nèi)容的方式,我們可以使用多種方式來(lái)傳遞路徑供web頁(yè)面?zhèn)鬟f简识,這里以圖片為例(相冊(cè)目錄下test/IMG_20170105_093405.jpg):
- 直接通過(guò)文件的絕對(duì)地址來(lái)提供給頁(yè)面顯示:
<img src = 'file:///storage/emulated/0/dcim/test/IMG_20170105_093405.jpg' />
- 通過(guò)媒體庫(kù)查詢(xún)出來(lái)的content uri地址展示
<img src = 'content://media/external/images/media/102610' />
- 通過(guò)FileProvider轉(zhuǎn)換的content uri地址展示
<img src = 'content://com.test.myfileprovider/dcim/test/IMG_20170105_093405.jpg'/>
可當(dāng)你使用loadUrl(String url)加載服務(wù)端的http地址時(shí)赶掖,以上三種方法將均無(wú)法使用,經(jīng)過(guò)各種嘗試七扰,目前找到兩種方案來(lái)提供給web端進(jìn)行圖片顯示:
由原生代碼處理奢赂,將文件流轉(zhuǎn)換為Base64之后通過(guò)JS接口回傳給web;
重寫(xiě)WebViewClient里的shouldInterceptRequest方法颈走,每當(dāng)頁(yè)面發(fā)生資源請(qǐng)求的時(shí)候就會(huì)觸發(fā)這個(gè)方法膳灶,我們可以過(guò)濾請(qǐng)求,判斷請(qǐng)求是否為本地文件立由,通過(guò)攔截請(qǐng)求轉(zhuǎn)換為二進(jìn)制流回傳回去轧钓,
示例代碼如下:
mWebView.setWebViewClient(new WebViewClient(){
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
if (url.startsWith("http://")&&url.endWith(".jpg") {
return getWebResourceResponse("/storage/emulated/0/dcim/trinaic/IMG_20170105_093405.jpg", "image/jpeg", ".jpg");
}
return super.shouldInterceptRequest(view, url);
}
}
private WebResourceResponse getWebResourceResponse(String url, String mime, String style) {
WebResourceResponse response = null;
try {
response = new WebResourceResponse(mime, "UTF-8", new FileInputStream(new File(url)));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return response;
}
WebView JS注入漏洞
要想讓原生跟JS進(jìn)行交互,按照官方提供的方法就得使用addJavaScriptInterface
class JsObject {
@JavascriptInterface
public String toString() { return "injectedObject"; }
}
webView.addJavascriptInterface(new JsObject(), "injectedObject");
webView.loadData("", "text/html", null);
webView.loadUrl("javascript:alert(injectedObject.toString())");
Injects the supplied Java object into this WebView. The object is injected into the JavaScript context of the main frame, using the supplied name. This allows the Java object's methods to be accessed from JavaScript. For applications targeted to API level JELLY_BEAN_MR1
and above, only public methods that are annotated with JavascriptInterface
can be accessed from JavaScript. For applications targeted to API level JELLY_BEAN
or below, all public methods (including the inherited ones) can be accessed, see the important security note below for implications.
引用官方api的說(shuō)明锐膜,在Android 4.2以下毕箍,會(huì)有被注入的風(fēng)險(xiǎn),4.2以上版本可以通過(guò)@JavascriptInterface
的注解來(lái)處理這個(gè)問(wèn)題道盏。
具體的注入方式而柑,我找了篇博客,如果有不清楚的同學(xué)可以了解下:
Android WebView的Js對(duì)象注入漏洞解決方案
在之前烏云平臺(tái)報(bào)出的漏洞中,
android/webkit/webview中默認(rèn)內(nèi)置的一個(gè)searchBoxJavaBridge_ 接口同時(shí)存在遠(yuǎn)程代碼執(zhí)行漏洞
在于android/webkit/AccessibilityInjector.java中捞奕,調(diào)用了此組件的應(yīng)用在開(kāi)啟輔助功能選項(xiàng)中第三方服務(wù)的安卓系統(tǒng)中會(huì)造成遠(yuǎn)程代碼執(zhí)行漏洞。這兩個(gè)接口分別是"accessibility" 和"accessibilityTraversal" 拄轻,此漏洞原理與searchBoxJavaBridge_接口遠(yuǎn)程代碼執(zhí)行相似颅围,均為未移除不安全的默認(rèn)接口,不過(guò)此漏洞需要用戶(hù)啟動(dòng)系統(tǒng)設(shè)置中的第三方輔助服務(wù)恨搓,利用條件較復(fù)雜院促。
因此,一般情況下我們通過(guò)removeJavaScripteInterface來(lái)移除這幾個(gè)接口
if (Build.VERSION.SDK_INT < 17) {
mAdvanceWebView.removeJavascriptInterface("searchBoxJavaBridge_");
mAdvanceWebView.removeJavascriptInterface("accessibility");
mAdvanceWebView.removeJavascriptInterface("accessibilityTraversal");
}
除此之外也有通過(guò)onJsPrompt的方式來(lái)實(shí)現(xiàn)WebView原生跟JS交互功能的斧抱,github上的開(kāi)源項(xiàng)目JSBridge就是采用這種方法:
https://github.com/lzyzsd/JsBridge
之前拜讀過(guò)大名鼎鼎的cordova的源碼常拓,它內(nèi)部的原生JS交互也是采用onJsPrompt的方式,不過(guò)在此基礎(chǔ)上做了更強(qiáng)大的封裝辉浦。
WebView后臺(tái)耗電問(wèn)題
當(dāng)我們的WebView的web頁(yè)面在解析或者播放視頻再或者有js定時(shí)器在執(zhí)行的時(shí)弄抬,
如果我們把應(yīng)用退到后臺(tái),不做任何處理的情況下宪郊,以上的操作還會(huì)在后臺(tái)繼續(xù)執(zhí)行掂恕,導(dǎo)致WebView在后臺(tái)持續(xù)耗電拖陆,因此一般我們會(huì)做以下處理
@Override
protected void onPause() {
super.onPause();
mWebView.onPause();//暫停部分可安全處理的操作,如動(dòng)畫(huà)懊亡,定位依啰,視頻播放等
mWebView.pauseTimers();//暫停所有WebView的頁(yè)面布局、解析以及JavaScript的定時(shí)器操作
}
@Override
protected void onResume() {
super.onResume();
mWebView.onResume();
mWebView.resumeTimers();
}
對(duì)于WebView的使用店枣,在處理問(wèn)題的過(guò)程中發(fā)現(xiàn)一個(gè)不錯(cuò)的開(kāi)源庫(kù):
https://github.com/delight-im/Android-AdvancedWebView
基本上上面我提到的或者沒(méi)提到的問(wèn)題它都做了一定的封裝處理速警,并且考慮了一些版本適配的問(wèn)題,可以直接拿來(lái)使用鸯两,也可以拿來(lái)參考學(xué)習(xí)闷旧。
如果你覺(jué)得問(wèn)題還是太多的話也可以考慮使用騰訊瀏覽服務(wù),基于QQ瀏覽器X5內(nèi)核甩卓,適配了Android全部主流平臺(tái)鸠匀,可以在所有Android手機(jī)上使用Blink的技術(shù)能力,具有更好的H5/CSS3支持和性能逾柿,目前微信缀棍、qq都在使用它。
唯一的缺陷就是它不提供打包內(nèi)核版的SDK机错,第一次使用時(shí)爬范,它會(huì)自動(dòng)到騰訊服務(wù)端去下載內(nèi)核,下載完畢后會(huì)彈窗提示用戶(hù)是否重啟app弱匪,重啟之后就能正常使用x5瀏覽服務(wù)了青瀑,如果你不介意這樣的用戶(hù)體驗(yàn),可以考慮直接使用騰訊瀏覽服務(wù)萧诫。
(補(bǔ)充)
WebView混淆問(wèn)題
如果app打包混淆之后發(fā)現(xiàn)提供給web頁(yè)面的JS接口失效了斥难,記得檢查是否添加了JavaScriptInterface的混淆配置:
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
紅米WebView內(nèi)部Web頁(yè)面的div自身滾動(dòng)條問(wèn)題
紅米上WebView內(nèi)部的Web頁(yè)面的div由于內(nèi)容高度大于div,產(chǎn)生了基于div的滾動(dòng)條(WebView滾動(dòng)條已禁用的情況下)帘饶,通過(guò)設(shè)置div的css樣式來(lái)禁用div滾動(dòng)條
Html dom元素ID或class:: -webkit-scrollbar {display:none}
WebView內(nèi)部web頁(yè)面px跟dp的關(guān)系
經(jīng)測(cè)試發(fā)現(xiàn)哑诊,WebView內(nèi)部web頁(yè)面的px值會(huì)在內(nèi)部自動(dòng)轉(zhuǎn)換為dp,且1px=1dp及刻,跟ppi值無(wú)關(guān)镀裤,這點(diǎn)跟原生開(kāi)發(fā)中的1dp = 設(shè)備ppi/160 * px換算關(guān)系nveou