一: WebView 基本配置及使用
1:WebSetting 管理 WebView 状态配置
WebSettings常用配置:
WebSettings webSetting = this.getSettings();
// 支持js脚本
webSetting.setJavaScriptEnabled(true);
// 设置是否允许 WebView 使用 File 协议 设置为true,即允许在 File 域下执行任意 JavaScript 代码 有安全问题
// webSetting.setAllowFileAccess(true);
// 设置 WebView 底层的布局算法 1 NARROW_COLUMNS:可能的话使所有列的宽度不超过屏幕宽度. 2 NORMAL:正常显示不做任何渲染. 3 SINGLE_COLUMN:把所有内容放大 WebView 等宽的一列中
webSetting.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NARROW_COLUMNS);
//设置自适应屏幕,两者合用
webSetting.setUseWideViewPort(true);// 将图片调整到适合 WebView 的大小
webSetting.setLoadWithOverviewMode(true);// 缩放至屏幕的大小
// 存储
webSetting.setAppCacheEnabled(true);// 设置 Application 缓存 API 是否开启 setAppCachePath
webSetting.setAppCacheMaxSize(Long.MAX_VALUE);
webSetting.setDatabaseEnabled(true);// 设置是否开启数据库存储 API 权限 setDatabasePath
webSetting.setDomStorageEnabled(true);// 设置是否开启 DOM 存储 API 权限
if(shouldCache){
webSetting.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
}else {
webSetting.setCacheMode(WebSettings.LOAD_NO_CACHE);
}
// 插件
webSetting.setPluginsEnabled(true);
webSetting.setPluginState(WebSettings.PluginState.ON_DEMAND);
// 设置渲染优先级
webSetting.setRenderPriority(WebSettings.RenderPriority.HIGH);
// 是否可访问 Content Provider 的资源,默认值 true
webSetting.setAllowContentAccess(true);
// 启用地理定位
webSetting.setGeolocationEnabled(true);
// UserAgentInfo
String defaultUA = webSetting.getUserAgentString();
webSetting.setUserAgentString(getAgent(defaultUA));
webSetting.setDefaultTextEncodingName("UTF-8");// 设置编码格式
webSetting.setTextSize(WebSettings.TextSize.NORMAL);
// 缩放操作
webSetting.setSupportZoom(true); // 支持缩放,默认为true。是下面那个的前提。
webSetting.setBuiltInZoomControls(true); // 设置内置的缩放控件。若为false,则该WebView不可缩放
webSetting.setDisplayZoomControls(false); // 隐藏 WebView 缩放按钮
// webSetting.setSupportMultipleWindows(true); //打开新窗口
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// 允许从 http 加载资源
webSetting.setMixedContentMode(android.webkit.WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
// 自动加载图片
webSettings.setLoadsImagesAutomatically(true);
2:WebViewClient 处理 WebView 的各种事件、回调
// 加载页面资源时会回调,每一个资源产生的一次网络加载,除非本地有当前缓存
public void onLoadResource(WebView view, String url)
// 开始加载页面时回调,一次Frame加载对应一次回调
public void onPageStarted(WebView view, String url, Bitmap favicon)
// 完成加载页面时回调,一次Frame加载对应一次回调
public void onPageFinished(WebView view, String url)
// 是否在 WebView 内加载页面
public boolean shouldOverrideUrlLoading(WebView view, String url)
// 可以拦截某一次的 request 来返回我们自己加载的数据
public WebResourceResponse shouldInterceptRequest
// 访问 url 出错
public void onReceivedError
// http 请求错误信息
public void onReceivedHttpError
// ssl 访问证书出错,handler.cancel()取消加载,handler.proceed()对然错误也继续加载
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error)
3:WebChromeClient 处理 WebView 对 Javascript 的对话框,网站图标,网站 title,加载进度 等
// 加载 web 进度
public void onProgressChanged(WebView view, int newProgress)
// js 调用 alert对话框
public boolean onJsAlert(WebView view, String url, String message, JsResult result)
// js 中的 Confirm 对话框
public boolean onJsConfirm
// js console
public boolean onConsoleMessage(ConsoleMessage consoleMessage)
// js Prompt对话框
public boolean onJsPrompt
// 接收 web 页面的 title
public void onReceivedTitle(WebView view, String title)
4:js/java 交互
js -> java
1:addJavascriptInterface 注入 java 对象 到 js window 下
2:shouldOverrideUrlLoading 制定url协议
3:WebChromeClient 的 console/alert/confirm/prompt 拦截 也需要定制协议
java -> js
1:loadUrl 会刷新页面
2:evaluateJavascript Android 4.4 后才可使用,有回调并不会刷新页面
5:常见问题
1:交互的安全问题addJavascriptInterface 接口会引起远程代码执行漏洞
解决方案:addJavascriptInterface 加@ 注解 并 WebView 初始化时调用 removeJavascriptInterfaces 移除系统 bridge 注入对象
@TargetApi(11)
private static final void removeJavascriptInterfaces(WebView webView) {
try {
if (Build.VERSION.SDK_INT >= 11 && Build.VERSION.SDK_INT < 17) {
webView.removeJavascriptInterface("searchBoxJavaBridge_");
webView.removeJavascriptInterface("accessibility");
webView.removeJavascriptInterface("accessibilityTraversal");
}
} catch (Throwable tr) {
tr.printStackTrace();
}
}
2:301/302重定向问题 WebView.HitTestResult 判断是否是重定向,从而决定是否自己加载url
fun isRedirectUrl(view: WebView): Boolean {
val hitTestResult = view.hitTestResult
if (hitTestResult == null) {
LogUtil.d("111", "HitTestResult null")
return true
}
if (hitTestResult.type == WebView.HitTestResult.UNKNOWN_TYPE) {
LogUtil.d("111", "HitTestResult unknow type")
return true
}
return false
}
三: WebView 源码设计(Framework层)
大致画了下类关系图,先看下 WebView Framework 层类之间的关系找个入口,从 WebView 的 loadUrl 方法会调用 mWebViewCore.sendMessage 方法,发送一个 EventHub.LOAD_URL 的消息, WebViewCore 的 mHandler 收到消息并处理 -> 调用到 mBrowserFrame.loadUrl(url, extraHeaders); 在 BrowserFrame 里 loadUrl 方法,如果是 url 有字符 ‘javascript:’ 就调用 JNI 方法 stringByEvaluatingJavaScriptFromString 否则调用 nativeLoadUrl
public void loadUrl(String url, Map extraHeaders) { mLoadInitFromJava = true; if (URLUtil.isJavaScriptUrl(url)) { // strip off the scheme and evaluate the string stringByEvaluatingJavaScriptFromString( url.substring("javascript:".length())); } else { nativeLoadUrl(url, extraHeaders); } mLoadInitFromJava = false; }
之后 JNI 回调到 BrowserFrame 的 loadStarted 再调用 mCallbackProxy 对象 mCallbackProxy.onPageStarted(url, favicon); 完成 WebViewClient 的回调
综上分析:
1:WebViewCore 是承接 WebView 功能的代理类,功能的初始化在 WebCoreThread 完成
2:BrowserFrame 负责主要 Web 功能对应 C 层 WebKit::Frame 是 native 和 java 回调的工具类
3:CallbackProxy 是其他线程调用功能回调的通道,负责把收到的消息具体分发到 WebView,WebViewClient,WebChromeClient中。
native 层代码可以扫下这2个目录:
// c++ /external/webkit/WebCore/bridge/jni/jni_jsobject.mm
// jni /external/webkit/WebKit/android/jni/WebCoreJni.cpp
四:WebView 在项目中的封装
我们对一个模块进行封装前一定要设计几个小功能,对外提供 api 调用,对内模块聚合。先想了几个点,哈哈,那就试着尝试一下:
1:WebView 不能在线程初始化,但是在 Application 中初始化会耗时造成 App 启动慢 于是我们可以在 provider 里
// manifest 里
// WebPreInitContentProvider 进行 WebView 初始化
class WebPreInitContentProvider : ContentProvider() {
override fun onCreate(): Boolean {
if(AWebProxy.isPreInitWebView()){
val webView = AWebView(MutableContextWrapper(context))
Log.e("AWebView","-> init")
if(AWebProxy.getPreInitUrl().isNotEmpty()){
webView.loadUrl(AWebProxy.getPreInitUrl())
Log.e("AWebView","-> url = " + AWebProxy.getPreInitUrl())
}
WebViewPools.recycle(webView)
}
return true
}
2:WebView 回收利用
// 回收池:“先进先出” 的数据结构
object WebViewPools {
private var mWebViews: Queue? = null
init {
mWebViews = LinkedBlockingQueue()
}
fun recycle(webView: IWebView) {
try {
mWebViews?.offer(webView)
} catch (e: Exception) {
e.printStackTrace()
}
}
@MainThread
fun acquireWebView(context: Context?): WebView? {
if(mWebViews?.size==0){
return AWebView(context)
}
val poll = mWebViews?.poll()
return try {
poll as? WebView
} catch (e: Exception) {
null
}
}
使用:
class MyWebViewActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my_web_view)
// get WebView
val webView:WebView? = WebViewPools.acquireWebView(this)
webView?.let {
rootView.addView(it)
it.loadUrl("https://www.baidu.com")
}
}
}
3:提供清晰的模块初始化入口
public class App extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
AWebProxy.INSTANCE
//.setWebSettings(null)
.setPreInitUrl("https://www.baidu.com")
.isPrintPerformance(true);
}
有同学注意到了 attachBaseContext 初始化 是因为我们想在入口通过变量控制是否 ContentProvider里 运行初始化 WebView 及预加载 url 的代码,而 Application 的 onCreate 是慢于 Providers 的 onCreate 方法的。
我们可以看下源码 providers 的注册及 application 的绑定
// handleBindApplication
if (!data.restrictedBackupMode) {
List providers = data.providers;
if (providers != null) {
// 注册并调用 provide 的 onCreate
installContentProviders(app, providers);
// For process that contains content providers, we want to
// ensure that the JIT is enabled "at some point".
mH.sendEmptyMessageDelayed(H.ENABLE_JIT, 10*1000);
}
}
// Do this after providers, since instrumentation tests generally start their
// test thread at this point, and we don't want that racing.
try {
mInstrumentation.onCreate(data.instrumentationArgs);
}
catch (Exception e) {
throw new RuntimeException(
"Exception thrown in onCreate() of "
+ data.instrumentationName + ": " + e.toString(), e);
}
try {
// Application 的 onCreate() 方法
mInstrumentation.callApplicationOnCreate(app);
} catch (Exception e) {
if (!mInstrumentation.onException(app, e)) {
throw new RuntimeException(
"Unable to create application " + app.getClass().getName()
+ ": " + e.toString(), e);
}
}
下面是封装的模块 AWebProxy :
AWebView:
class AWebView : WebView, IWebView {
constructor(context: Context?) : this(context, null)
constructor(context: Context?, attributeSet: AttributeSet?) : super(context, attributeSet)
init {
onInit()
}
override fun onInit() {
var webSettings = AWebProxy.getWebSettings()
if (webSettings == null) {
webSettings = CustomWebSettings()
}
webSettings.configWebSettings(this)
removeJavascriptInterfaces(this)
}
override fun onDestroy() {}
@TargetApi(11)
private fun removeJavascriptInterfaces(webView: WebView) {
try {
webView.removeJavascriptInterface("searchBoxJavaBridge_")
webView.removeJavascriptInterface("accessibility")
webView.removeJavascriptInterface("accessibilityTraversal")
} catch (tr: Throwable) {
tr.printStackTrace()
}
}
override fun drawChild(canvas: Canvas, child: View?, drawingTime: Long): Boolean {
val ret = super.drawChild(canvas, child, drawingTime)
Log.e("AWebView", "start show Performance")
if (AWebProxy.isPrintPerformance()) {
// do print on Screen
}
return ret
}
}
模块接口:
interface IWebSettings{
fun configWebSettings(webView: WebView): WebSettings?
}
interface IWebView{
fun onInit()
fun onDestroy()
}
interface IWebProxy{
// 模块内部 api
interface Inner{
fun getWebSettings(): IWebSettings?
fun isPrintPerformance():Boolean
fun isPreInitWebView():Boolean
fun getPreInitUrl():String
}
// 模块外部暴漏的接口
interface Outer{
fun setWebSettings(webSettings: IWebSettings?):Outer
fun isPrintPerformance(isPrintPerformance: Boolean): Outer
fun isPreInitWebView(isPreInitWebView: Boolean): Outer
fun setPreInitUrl(url: String) :Outer
}
}
AProxy.kt:代码
object AWebProxy : IWebProxy.Inner, IWebProxy.Outer {
private var webSettings: IWebSettings? = null
private var isPrintPerformance = false
private var isPreInitWebView = false
private var preInitUrl = ""
init {
// some check
}
override fun setWebSettings(webSettings: IWebSettings?): IWebProxy.Outer {
this.webSettings = webSettings
return this
}
override fun isPrintPerformance(isPrintPerformance: Boolean): IWebProxy.Outer {
this.isPrintPerformance = isPrintPerformance
return this
}
override fun isPreInitWebView(isPreInitWebView: Boolean): IWebProxy.Outer {
this.isPreInitWebView = isPreInitWebView
return this
}
override fun setPreInitUrl(url: String): IWebProxy.Outer {
this.isPreInitWebView = true
this.preInitUrl = url
return this
}
override fun getWebSettings(): IWebSettings? = this.webSettings
override fun isPrintPerformance(): Boolean = this.isPrintPerformance
override fun isPreInitWebView(): Boolean = this.isPreInitWebView
override fun getPreInitUrl(): String = this.preInitUrl
}
感谢,感谢 ^ ^