该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽可能按照先易后难的顺序进行编写该系列。该系列引用了《Android开发艺术探索》以及《深刻理解Android 卷Ⅰ,Ⅱ,Ⅲ》中的相关知识,另外也借鉴了其余的优质博客,在此向各位大神表示感谢,膜拜!!!javascript
前段时间作了首个hybird商业上面,hybird虽然私下里有些了解,而且写了些demo,可是作正式的商业项目仍是首次,这一篇也算是本身首个hybird项目的反思与总结吧。
注:该项目涉及到的技术大概分为如下几个方面,1,微信登陆 2,WebView与原生代码的交互 3,WebView的优化,下面也分这几个大方面进行一一说明html
准备什么,天然是开发者帐号以及认证开发者资质,而后建立应用,认证开发者资质须要300人民币,而且填写一系列资料,接着走一系列流程,这些本应该是公司应该提早准备好的事情,不过我遇到的并非这样,拿到这些准备的东西多是整个开发环节中最费劲的事情。java
咱们在微信开放平台建立移动应用时,须要填入应用签名以及应用包名,以下图android
其实咱们若是想要断点调试WXEntryActivity类,那么咱们只须要Debug包的签名与上面的应用签名保持一致,那么咱们便能以Debug的方式运行安装包,断点调试微信登陆、分享之类的功能web
除去WebView外,在开发中咱们还常常用到其余的WebView工具类浏览器
对WebView进行配置和管理
//若是访问的页面中要与Javascript交互,则webview必须设置支持Javascript webSettings.setJavaScriptEnabled(true); // 若加载的 html 里有JS 在执行动画等操做,会形成资源浪费(CPU、电量) // 在 onStop 和 onResume 里分别把 setJavaScriptEnabled() 给设置成 false 和 true 便可 //支持插件 webSettings.setPluginsEnabled(true); //设置自适应屏幕,二者合用 webSettings.setUseWideViewPort(true); //将图片调整到适合webview的大小 webSettings.setLoadWithOverviewMode(true); // 缩放至屏幕的大小 //缩放操做 webSettings.setSupportZoom(true); //支持缩放,默认为true。是下面那个的前提。 webSettings.setBuiltInZoomControls(true); //设置内置的缩放控件。若为false,则该WebView不可缩放 webSettings.setDisplayZoomControls(false); //隐藏原生的缩放控件 //其余细节操做 webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); //关闭webview中缓存 webSettings.setAllowFileAccess(true); //设置能够访问文件 webSettings.setJavaScriptCanOpenWindowsAutomatically(true); //支持经过JS打开新窗口 webSettings.setLoadsImagesAutomatically(true); //支持自动加载图片 webSettings.setDefaultTextEncodingName("utf-8");//设置编码格式
处理各类通知 & 请求事件
mWebView.setWebViewClient(new FNWebViewClient()); private class FNWebViewClient extends WebViewClient { //复写shouldOverrideUrlLoading()方法,使得打开网页时不调用系统浏览器, 而是在本WebView中显示 @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { // 特定的url调到native 页面进行处理 返回true if (LinkHandleUtils.handle(FNWebPageActivity.this, url, true)) { return true; } mCurUrl = url; return false; } //开始载入页面调用的,咱们能够设定一个loading的页面,告诉用户程序在等待网络响应。 @Override public void onPageStarted(WebView webView, String s, Bitmap bitmap) { super.onPageStarted(webView, s, bitmap); } //在页面加载结束时调用。咱们能够关闭loading 条,切换程序动做 @Override public void onPageFinished(WebView webView, String s) { super.onPageFinished(webView, s); } //在加载页面资源时会调用,每个资源(好比图片)的加载都会调用一次。 @Override public void onLoadResource(WebView webView, String s) { super.onLoadResource(webView, s); } //加载页面的服务器出现错误时(如404)调用 @Override public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { super.onReceivedError(view, errorCode, description, failingUrl); } //处理https请求 @Override public void onReceivedSslError(WebView webView, SslErrorHandler sslErrorHandler, SslError sslError) { sslErrorHandler.proceed(); //表示等待证书响应 // sslErrorHandler.cancel(); //表示挂起链接,为默认方式 // sslErrorHandler.handleMessage(null); //可作其余处理 } }
辅助 WebView 处理 Javascript 的对话框,网站图标,网站标题等等。
setWebChromeClient(new ProgressWebChromeClient()); private class ProgressWebChromeClient extends WebChromeClient { //得到网页的加载进度并显示 @Override public void onProgressChanged(com.tencent.smtt.sdk.WebView webView, int newProgress) { if (newProgress <= 100 && mProgressBar != null) { if (GONE == mProgressBar.getVisibility()) { mProgressBar.setVisibility(VISIBLE); } startProgressAnimation(newProgress); } super.onProgressChanged(webView, newProgress); } //获取Web页中的标题 @Override public void onReceivedTitle(WebView webView, String title) { super.onReceivedTitle(webView, title); if (mCallback != null && StringUtils.isNotBlank(title)) { mCallback.setTitle(title); } } //支持javascript的警告框 @Override public boolean onJsAlert(WebView webView, String url, String message, final JsResult result) { new AlertDialog.Builder(getContext()) .setTitle("JsAlert") .setMessage(message) .setPositiveButton("OK", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { result.confirm(); } }) .setCancelable(false) .show(); return true; } //支持javascript的确认框 @Override public boolean onJsConfirm(WebView webView, String url, String message, final JsResult jsResult) { new AlertDialog.Builder(getContext()) .setTitle("JsConfirm") .setMessage(message) .setPositiveButton("OK", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { jsResult.confirm(); } }) .setNegativeButton("Cancel", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { jsResult.cancel(); } }) .setCancelable(false) .show(); // 返回布尔值:判断点击时确认仍是取消 // true表示点击了确认;false表示点击了取消; return true; } //支持javascript输入框 @Override public boolean onJsPrompt(WebView webView, String url, String message, String defaultValue, final JsPromptResult result) { return super.onJsPrompt(webView, s, s1, s2, jsPromptResult); } }
//mJSMethodName对应js方法名 //result对应js方法参数 mWebView.loadUrl("javascript:" + mJSMethodName + "(\" " + param + "\")");
对应的html文件以下缓存
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> // JS代码 <script> // Android须要调用的方法 function mJSMethodName(){ alert("Android调用了JS的mJSMethodName方法"); } </script> </head> </html>
特别注意:JS代码调用必定要在 onPageFinished() 回调以后才能调用,不然不会调用。安全
- 该方法的执行不会使页面刷新,而第一种方法(loadUrl )的执行则会。因此该方法比第一种方法效率更高。
mWebView.evaluateJavascript("javascript:" + mJSMethodName + "(\" " + param + "\")", new ValueCallback<String>() { @Override public void onReceiveValue(String result) { //result为js方法返回结果 } });
注:上面两种方法各有优劣,建议根据Android版本混合使用,服务器
// Android版本变量 final int version = Build.VERSION.SDK_INT; // 由于该方法在 Android 4.4 版本才可以使用,因此使用时需进行版本判断 if (version < 18) { mWebView.loadUrl("javascript:" + mJSMethodName + "(\" " + param + "\")"); } else { mWebView.evaluateJavascript("javascript:" + mJSMethodName + "(\" " + param + "\")", new ValueCallback<String>() { @Override public void onReceiveValue(String result) { //result为js方法返回结果 } }); }
这种方法是咱们最经常使用的方法,使用方法以下微信
//添加映射对象以及命名空间 mWebView.addJavascriptInterface(new JsInteration(), "android"); private class JsInteration { @JavascriptInterface public void hello(String messsage) { } }
上面的java代码对应的js代码是
// //注意android是上面定义的命名空间 window.android.hello(message)
这个咱们已经在上面的代码里写过了,好比你能够本身维护一些特殊的URL以及处理这些URL的Activity,而后复写shouldOverrideUrlLoading(),在该方法中拦截特定URL转到特定的Activity进行处理。也能达到JS->Java的目的。而且这种形式也是比较常见的处理方式。
//复写shouldOverrideUrlLoading()方法,使得打开网页时不调用系统浏览器, 而是在本WebView中显示 @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { // 特定的url调到native 页面进行处理 返回true if (LinkHandleUtils.handle(FNWebPageActivity.this, url, true)) { return true; } mCurUrl = url; return false; }
这种方法跟上面的没有本质差别,也是在回调函数中进行Java代码操做,目前我在项目中用到的地方较少,主要用来作一些比较特殊的功能,例如检测到Alert弹框中的内容符合条件进行Java代码。
若是JS想要获得Android方法的返回值,只能经过 WebView 的 loadUrl ()去执行 JS 方法把返回值传递回去
当在网页里有文件上传组件时,咱们惊奇的发现Android端这个文件上传组件并无起做用。缘由何在呢?由于Android 中的 WebView是不能直接打开文件选择弹框的。
接下来我讲简单提供一下解决方案,先说一下思路
这样就完成了一次H5选择文件的过程,下面我把代码贴出来看一下
1.当H5在调用上传文件的Api的时候,WebView会回调 openFileChooser和onShowFileChooser 方法来通知咱们,那咱们就得重写了
须要注意的是openFileChooser在不一样的Android版本上是形参不一样的,
private class ProgressWebChromeClient extends WebChromeClient { //支持文件选择上传 @Override public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> valueCallback, FileChooserParams fileChooserParams) { return super.onShowFileChooser(webView, valueCallback, fileChooserParams); } // Android > 4.1.1 调用这个方法 public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) { if (mFileUploadSupportListener == null) return; //调用传入的接口进行回调 mFileUploadSupportListener.call(uploadMsg); } // 3.0 + 调用这个方法 public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) { if (mFileUploadSupportListener == null) return; mFileUploadSupportListener.call(uploadMsg); } // Android < 3.0 调用这个方法 public void openFileChooser(ValueCallback<Uri> uploadMsg) { if (mFileUploadSupportListener == null) return; mFileUploadSupportListener.call(uploadMsg); } }
2.注入接口
//注入接口 mWebView.setFileUploadSupportListener(new IFileUploadSupportListener() { @Override public void call(ValueCallback<Uri> valueCallback) { mUploadMessage = valueCallback; chooseFile(); } }); //选择文件 private void chooseFile() { PhotoPicker.builder() .setPhotoCount(1) .setShowCamera(true) .setShowGif(true) .setPreviewEnabled(false) .start(FNWebPageActivity.this, PhotoPicker.REQUEST_CODE); }
3.进行回传
if (null == mUploadMessage) { return; } if (resultCode == RESULT_OK && requestCode == PhotoPicker.REQUEST_CODE) { ArrayList<String> photos = data.getStringArrayListExtra(PhotoPicker.KEY_SELECTED_PHOTOS); Uri result = Uri.parse(photos.get(0)); mUploadMessage.onReceiveValue(result); mUploadMessage = null; } else { mUploadMessage.onReceiveValue(null); }
上面已经稍微说了一下,该方法只能在Android4.4以上安全使用,那么咱们来看一下Android 系统占比,Google公布的数据:截止 2018 .6 .28 ,Android4.4 之下占有约5%,具体占好比下图
如今Android4.4 之下的Android手机已经占比很是少了,不过有兴趣的同窗可参看[你不知道的 Android WebView 使用漏洞
](https://www.jianshu.com/p/3a3...,该篇文章比较详细的解析了如何解决该安全隐患
WebView的内存泄露问题已是个老生常谈的问题了,如今只要用到WebView的开发者都得注意到这个问题。
如今流行的有如下两种解决方案
独立进程法顾名思义是让包含WebView的Acitivy以android:process=":web"的形式指定单独进程,而后在须要退出的时候使用System.exit(0)结束整个进程,内存天然回收了。该方法简单暴力,并有如下优势
这个方法就是RTFSC(Read The Fucking Source Code),从LeakCannary分析得出内存泄露在 org.chromium.android_webview.AwContents 类
//org.chromium.android_webview.AwContents 类的onAttachedToWindow() 和 onDetachedFromWindow()方法 @Override public void onAttachedToWindow() { if (isDestroyed()) return; if (mIsAttachedToWindow) { Log.w(TAG, "onAttachedToWindow called when already attached. Ignoring"); return; } mIsAttachedToWindow = true; mContentViewCore.onAttachedToWindow(); nativeOnAttachedToWindow(mNativeAwContents, mContainerView.getWidth(), mContainerView.getHeight()); updateHardwareAcceleratedFeaturesToggle(); if (mComponentCallbacks != null) return; mComponentCallbacks = new AwComponentCallbacks(); mContext.registerComponentCallbacks(mComponentCallbacks); } @Override public void onDetachedFromWindow() { if (isDestroyed()) return;//注意这里 if (!mIsAttachedToWindow) { Log.w(TAG, "onDetachedFromWindow called when already detached. Ignoring"); return; } mIsAttachedToWindow = false; hideAutofillPopup(); nativeOnDetachedFromWindow(mNativeAwContents); mContentViewCore.onDetachedFromWindow(); updateHardwareAcceleratedFeaturesToggle(); if (mComponentCallbacks != null) { mContext.unregisterComponentCallbacks(mComponentCallbacks); mComponentCallbacks = null; } mScrollAccessibilityHelper.removePostedCallbacks(); }
通常状况下,咱们的activity退出的时候,都会主动调用 WebView.destroy() 方法,通过分析,destroy()的执行时间在onDetachedFromWindow以前,因此就会致使不能正常进行unregister(),从而形成内存泄露。
知道缘由了,那么解决办法也就来了。
在Activity的onDestroy里方法里以下代码
@Override protected void onDestroy() { if (mWebView != null) { try { ViewGroup parent = (ViewGroup) mWebView.getParent(); if (parent != null) { parent.removeView(mWebView); } mWebView.removeAllViews(); mWebView.destroy(); } catch (Exception e) { e.printStackTrace(); } } super.onDestroy(); }
尽管有了上述的一些优化,不过原生WebView的一些不足,如兼容性、流量消耗、以及性能等诸多方面仍是不能达到要求,不过腾讯提供的X5WebView算是目前比较好的解决方案了,关于X5WebView详情读者看参看腾讯官网腾讯浏览服务
本篇呢是首个hybird的项目的踩坑总结,有什么不足之处还请不吝赐教,之后在开发过程当中遇到的更多的WebView的坑也会继续追加更新。
此致,敬礼