WebView 是 Android 最復(fù)雜以及最強大的一個控件(最多坑) 儡遮, 一大堆的 setting 讓人摸不著頭腦 乳蛾, 很多時候壓根不知道這個設(shè)置有什么用 ,加上 WebViewClient 和 WebChromeClient 做為內(nèi)部類 , 一堆業(yè)務(wù)邏輯 肃叶, 使得 Activity 變得亂糟糟的 蹂随,代碼可讀性更是糟糕透了 , 最后被逼上梁山 因惭, 走上了封裝的道路 岳锁。
WebView 封裝思路
對于 WebView 的封裝 , 相信很多人都是抽象在一個基類里面 蹦魔, 封裝成一個 BaseWebActivity , 或者 BaseWebFragment 激率, 對于這種封裝還是不能滿足像我這種有潔癖有程序員的 , 因為復(fù)用性不高 勿决, 而且容易導(dǎo)致 Activity 或者 Fragment 基類膨脹 乒躺。 下面向大家分享我的封裝思路。
首先讓大家看下我封裝的效果
mAgentWeb = AgentWeb.with(this)//傳入Activity
.setAgentWebParent(mLinearLayout, new LinearLayout.LayoutParams(-1, -1))//傳入AgentWeb 的父控件 低缩,如果父控件為 RelativeLayout 嘉冒, 那么第二參數(shù)需要傳入 RelativeLayout.LayoutParams
.useDefaultIndicator()// 使用默認(rèn)進度條
.defaultProgressBarColor() // 使用默認(rèn)進度條顏色
.setReceivedTitleCallback(mCallback) //設(shè)置 Web 頁面的 title 回調(diào)
.createAgentWeb()//
.ready()
.go("http://www.jd.com");
效果圖
上面已經(jīng)封裝成一個 Web 庫了 , 叫 AgentWeb 咆繁, 歡迎大家使用 讳推。
可以看到里面沒有一句 WebSettings , 甚至 WebChromeClient 和 WebViewClient 都不用配置 玩般, 使用的是簡潔鏈?zhǔn)秸{(diào)用 银觅。
AgentWeb 封裝思路是通過代理 , 將 WebView 從 Activity 或者 Fragment 中代理出來 壤短, 不再需要 Activity 或者 Fragment 內(nèi)部創(chuàng)建和管理 设拟,Activity 管理 WebView 需要通過 AgentWeb , 下面通過 UML 圖來簡單說明下 .
BaseActivity 封裝使用的 UML 關(guān)系圖
AgentWeb 封裝使用的 UML 關(guān)系圖
BaseWebActivity 直接組合 WebView 久脯, 這樣做為什么說復(fù)用性不高呢 纳胧? 主要還是因為 WebView 依附在 BaseWebActivity 身上 ,要別人直接繼承你的 Activity 是很不好的 帘撰,因為 Java 的單繼承關(guān)系 跑慕, 使得使用基類的靈活性受到很大的約束 , 這也是 Effective Java 里面提到的組合優(yōu)先于繼承 摧找。
AgentWeb 則不同 核行, AgentWeb 是一個獨立的庫 , 可以讓你很方便一句話就引入 蹬耘, 不需要依賴 BaseWebActivity 芝雪, 就像上面一樣簡簡單單一句話引入即可。
AgentWeb 把 WebView 代理出來 综苔, 將功能細(xì)分成一個類去管理 惩系, 比如說的 WebCreator 負(fù)責(zé)創(chuàng)建 WebView 以及 進度條 位岔、WebSettings 則是對 WebView 進行統(tǒng)一設(shè)置 , JsEntraceAccess 是對 Javascript 方法訪問進行統(tǒng)一入口 堡牡, 這樣做使得每一個功能獨立 抒抬, 相互不影響 , 也使得 AgentWeb 的結(jié)構(gòu)更清晰 晤柄, 符合單一職責(zé)原則 擦剑。 源碼太多就不貼了 ,下面分享下封裝 WebView 遇到的一些問題 芥颈。
WebView 封裝的一些問題與解決思路
WebView 的封裝可謂真是一波三折啊 惠勒, WebView 實在太多坑了 , 比如說 常見的泄露 浇借, Js 安全 捉撮,低版本跨源問題 , Context 引致的 onJsAlert 失效 妇垢,Android 4.4 不支持文件選擇問題等等巾遭。
內(nèi)存泄露
這個問題在低版本不好解決就算類似下面代碼通過反射制空 sConfigCallback 該字段, 還是有些手機會出現(xiàn)泄露 闯估,對于該問題 灼舍,唯一有效方案解決在 AndroidManifest 里面為 Web Activity 添加 android:process=":web"
屬性 , 然后在 該 Activity onDestroy 里面 執(zhí)行 System.exit(0); 下面可以解決一部分泄露
Field field = WebView.class.getDeclaredField("mWebViewCore");
field = field.getType().getDeclaredField("mBrowserFrame");
field = field.getType().getDeclaredField("sConfigCallback");
field.setAccessible(true);
field.set(null, null);
addJavascriptInterface API 引起的遠(yuǎn)程代碼執(zhí)行漏洞
對于這個問題 涨薪,我相信大家或多或少都有點了解 骑素,問題是由注入類引起 ,從注入類中找到 Runtime 對象 刚夺,可以通過 Runtime 執(zhí)行 shell 命令 献丑。 Google 只針對 Android 4.2.2 版本及以上版本給出了解決方法 ,為了解決兼容 4.2.2 以下版本這個問題 AgentWeb 采用 360 大牛給出的方案 侠姑, 向 Web 頁面注入一段 Js 腳本 创橄,然后通過腳本彈 Prompt 向 Java 通信 ,解決了 4.2.2 以下版本 addJavascriptInterface 安全通信問題 莽红。 下面給出大致實現(xiàn)
比如下面注入類
public class AndroidInterface {
public void callAndroid(final String msg) {
Log.i("Info",""+msg);
}
}
mAgentWebView.addJavascriptInterface("android",new AndroidInterface()); //注意 mAgentWebView 是 WebView 子類 妥畏, 并重寫了 addJavascriptInterface 方法 。
低于 Android 4.2.2 的版本上面的注入對象會經(jīng)過包裝拼接成如下腳本 安吁。
注入的腳本
.....省略N多 Js 代碼
android.callAndroid = function() {
.....省略N多 Js 代碼
var m = prompt('AgentWeb: ' + JSON.stringify({method: l, types: e, args: f}));
var g = JSON.parse(m);
if (g.code != 200) {
throw "Android call error, code:" + g.code + ", message:" + g.result
}
return g.result
};
通過 webView.loadUrl(腳本) 注入上面的腳本 醉蚁。
Js 執(zhí)行下面方法
function sendHelloToAndroid() {
window.android.callAndroid("你好,Android! ");
}
就會執(zhí)行上面的 function 里面的方法體 鬼店,Android 端回調(diào)onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result)
message 參數(shù)里面取出 js 要調(diào)的目標(biāo)方法 网棍, 然后通過反射調(diào)用該目標(biāo)方法 。
對于同源跨域攻擊問題
什么是同源策略嗎 妇智? 同源策略是由 Netscape 提出來的 滥玷,現(xiàn)在主流的瀏覽器都遵循這種策略 捌锭,同源是一般指http://(協(xié)議)www.google.com(主機):8080 (端口) 三要素都相同 ,但實際上并不是那么嚴(yán)格 罗捎, 比如 IE 就會忽略對端口的判斷 。
同源有什么作用嗎 拉盾?
同源的數(shù)據(jù)默認(rèn)為可以安全訪問的 桨菜。 比如說 url http://www.google.com:xxxx/login 登錄后瀏覽器就會把返回來的 cookies 保存起來 , 對于 http://www.google.com:xxxx/index.html 對于這個 url 瀏覽器會默認(rèn)為跟前者同源 捉偏, 那么這個 url 可以無縫的訪問到 login 保存下來的 cookies 倒得。
大家都知道 Android 應(yīng)用之間的文件和數(shù)據(jù)一般情況下是不能相互訪問的 , 但是不正確的使用 WebView 夭禽,會打破這種狀況 霞掺, 比如說以下這種使用
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_webview);
webView = (WebView) findViewById(R.id.webView1);
webView.getSettings().setAllowFileAccess(true);
webView.getSettings().setJavaScriptEnabled(true);
webView.getSettings().setAllowFileAccessFromFileURLs(true);
webView.getSettings().setAllowUniversalAccessFromFileURLs(true);
Intent i = getIntent();
String url = i.getData().toString();
webView.loadUrl(url);
}
將該 Activity 設(shè)置 exported="true" , 其他應(yīng)用就可以通過隱式啟動 讹躯,將 data 作為 url 菩彬,啟動的該應(yīng)用 , 讓該應(yīng)用加載腳本 潮梯,遍歷該應(yīng)用內(nèi)的文件或者私密文件 骗灶, 上傳服務(wù)器 。
這個問題一直存在 Android 手機中 秉馏,這個問題 Google 并沒有修復(fù)它 耙旦, 只是在 4.2 后的版本把 setAllowFileAccessFromFileURLs 以及 mWebSettings.setAllowUniversalAccessFromFileURLs 設(shè)置為 false , 用戶沒有刻意去開啟它 萝究, 高于 Android 4.2 版本默認(rèn)是安全的 免都。 對于該問題 AgentWeb 使用的是以下設(shè)置 。
mWebSettings.setAllowFileAccess(true); //允許file 協(xié)議 帆竹, 加載本地文件
mWebSettings.setAllowFileAccessFromFileURLs(false); //禁止通過 file url 加載的文件執(zhí)行 Javascript 讀取其他的本地文件 .
mWebSettings.setAllowUniversalAccessFromFileURLs(false);//禁止通過 file url 加載的文件執(zhí)行 Javascript 可以訪問其他的源 如 http 绕娘, https 。
Context 引致的 onJsAlert 失效
這個問題根本原因在防止泄露時候創(chuàng)建 new WebView(activity.getApplicationContext());
導(dǎo)致 馆揉, 很正常啊 业舍, 因為你創(chuàng)建 WebView 傳的是 Application , Application 本身是無法彈 Dialog 的 升酣。 所以只能無反應(yīng) 舷暮!這個問題解決方案只要你創(chuàng)建 WebView 時候傳入 Activity , 或者 自己實現(xiàn) onJsAlert 方法即可噩茄。
Android 4.4 文件訪問
Android 4.4 WebView 內(nèi)核正式有 WebKit 替換 為 Chromium 使得很多 Api 都廢棄掉了 下面, 這是 Google 正式宣告拋棄 Webkit 的一個句號 。 所以要兼容 Android 4.4 以下的 WebView 的應(yīng)用特別難受 绩聘。 回到正題 沥割, 4.4 文件訪問 耗啦, 你會發(fā)現(xiàn) 4.4 點擊 input 標(biāo)簽沒反應(yīng) , 瞬間一萬只曹尼瑪在崩騰 机杜, 幸慶的是還有 Js 通信 帜讲,解決方案:可以通能過 Js 訪問 Java 然后打開文件選擇器 , 拿到文件后 椒拗, 將文件轉(zhuǎn)成 Base64 字符串回傳給 Js 似将, 因為拿到的文件路徑是 Content:// 開頭 JS 是無法解析的 。(這個代碼跨度有點大 蚀苛, 就不貼源碼了 在验, 有興趣可以克隆倉庫看下)
WebView 封裝后使用
AgentWeb 在 Fragment 中使用
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mAgentWeb = AgentWeb.with(this)// Fragment 傳入
.setAgentWebParent((LinearLayout) view, new LinearLayout.LayoutParams(-1, -1))// 設(shè)置 AgentWeb 的父控件 , 這里的view 是 LinearLayout 堵未, 那么需要傳入 LinearLayout.LayoutParams
.useDefaultIndicator()// 使用默認(rèn)進度條
.setReceivedTitleCallback(mCallback) //標(biāo)題回調(diào)
.setSecurityType(AgentWeb.SecurityType.strict) //注意這里開啟 strict 模式 腋舌, 設(shè)備低于 4.2 情況下回把注入的 Js 全部清空掉 , 這里推薦使用 onJsPrompt 通信
.createAgentWeb()//
.ready()//
.go(getUrl());
}
跟原先 WebFragment 比簡潔多了 渗蟹。
Js 調(diào)用 块饺。
function callByAndroid(msg1,msg2){
console.log("callByAndroid")
}
沒有經(jīng)過封裝的
mWebView.loadUrl("javascript:callByAndroid("+"\"hello\""+","+"\" js\""+")");
封裝后
mAgentWeb.getJsEntraceAccess().quickCallJs("callByAndroid","Hello","js");