首个hybird商业项目踩坑总结
该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列。该系列引用了《Android开发艺术探索》以及《深入理解Android 卷Ⅰ,Ⅱ,Ⅲ》中的相关知识,另外也借鉴了其他的优质博客,在此向各位大神表示感谢,膜拜!!!
前言
前段时间做了首个hybird商业上面,hybird虽然私下里有些了解,并且写了些demo,但是做正式的商业项目还是首次,这一篇也算是自己首个hybird项目的反思与总结吧。
注:该项目涉及到的技术大概分为以下几个方面,1,微信登录 2,WebView与原生代码的交互 3,WebView的优化,下面也分这几个大方面进行一一说明
微信登录
微信登录的准备
准备什么,自然是开发者账号以及认证开发者资质,然后创建应用,认证开发者资质需要300人民币,并且填写一系列资料,接着走一系列流程,这些本应该是公司应该提前准备好的事情,不过我遇到的并不是这样,拿到这些准备的东西可能是整个开发环节中最费劲的事情。
微信登录的断点调试
我们在微信开放平台创建移动应用时,需要填入应用签名以及应用包名,如下图
其实我们如果想要断点调试WXEntryActivity类,那么我们只需要Debug包的签名与上面的应用签名保持一致,那么我们便能以Debug的方式运行安装包,断点调试微信登录、分享之类的功能
WebView的基本信息
除去WebView外,在开发中我们还经常用到其他的WebView工具类
WebSettings
对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");//设置编码格式
WebClient
处理各种通知 & 请求事件
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);    //可做其他处理
        }
    }
WebChromeClient
辅助 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);
   }
}
WebView与原生代码的交互
Java->JS
loadUrl
//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() 回调之后才能调用,否则不会调用。
evaluateJavascript
- 该方法的执行不会使页面刷新,而第一种方法(loadUrl )的执行则会。所以该方法比第一种方法效率更高。
 
- Android 4.4 后才可使用
 
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方法返回结果
        }
    });
}
JS->Java
通过WebView的addJavascriptInterface()方法
这种方法是我们最常用的方法,使用方法如下
//添加映射对象以及命名空间
 mWebView.addJavascriptInterface(new JsInteration(), "android");
private class JsInteration {
        @JavascriptInterface
        public void hello(String messsage) {
        }
    }
上面的java代码对应的js代码是
//
//注意android是上面定义的命名空间
window.android.hello(message)
通过WebViewClient 的shouldOverrideUrlLoading()方法回调
这个我们已经在上面的代码里写过了,比如你可以自己维护一些特殊的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;
        }
通过WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回调
这种方法跟上面的没有本质差异,也是在回调函数中进行Java代码操作,目前我在项目中用到的地方较少,主要用来做一些比较特殊的功能,例如检测到Alert弹框中的内容符合条件进行Java代码。
三种方法优劣比较
- 通过WebView的addJavascriptInterface()方法比较简单,并且也更为常见,不过其存在不小的安全隐患。
 - 通过WebViewClient 的shouldOverrideUrlLoading()方法回调这个使用起来也比较简单,也不存在方式1的安全隐患,不过JS获取Android方法的返回值复杂。
 
如果JS想要得到Android方法的返回值,只能通过 WebView 的 loadUrl ()去执行 JS 方法把返回值传递回去
WebView的文件上传
当在网页里有文件上传组件时,我们惊奇的发现Android端这个文件上传组件并没有起作用。原因何在呢?因为Android 中的 WebView是不能直接打开文件选择弹框的。
接下来我讲简单提供一下解决方案,先说一下思路
接收WebView打开文件选择器的通知,收到通知后,打开文件选择器等待用户选择需要上传的文件
在onActivityResult中得到用户选择的文件的Uri
然后把Uri传递给Html5
这样就完成了一次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);
}
WebView的优化
WebView的addJavascriptInterface()方法的安全隐患
上面已经稍微说了一下,该方法只能在Android4.4以上安全使用,那么我们来看一下Android 系统占比,Google公布的数据:截止 2018 .6 .28 ,Android4.4 之下占有约5%,具体占比如下图
现在Android4.4 之下的Android手机已经占比非常少了,不过有兴趣的同学可参看你不知道的 Android WebView 使用漏洞,该篇文章比较详细的解析了如何解决该安全隐患
WebView的内存泄露
WebView的内存泄露问题已经是个老生常谈的问题了,现在只要用到WebView的开发者都得注意到这个问题。
现在流行的有以下两种解决方案
独立进程法
独立进程法顾名思义是让包含WebView的Acitivy以android:process=":web"的形式指定单独进程,然后在需要退出的时候使用System.exit(0)结束整个进程,内存自然回收了。该方法简单暴力,并有以下优点
- 每个独立的进程都能分配独立的内存,这样的话,你的app可以获得双倍的内存,其中一半给Webview吃。增大Webview获得的内存,变相的减小内存泄露产生OOM的概率。
 - 在适当时机直接杀掉Webview独立进程,什么内存泄露,内存占用巨大的问题都见鬼去吧。要问什么时机?比如退出app时,检测到没有Webview页面时。
 - Webview发生崩溃时不会导致app闪退,就像第二点说的,因为Webview是在独立进程中,如果发生崩溃,主进程还安然无事,app还在运行中,没有闪退,不闪的才是健康的。
 
源码解决法
这个方法就是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();
}
X5WebView
尽管有了上述的一些优化,不过原生WebView的一些不足,如兼容性、流量消耗、以及性能等诸多方面还是不能达到要求,不过腾讯提供的X5WebView算是目前比较好的解决方案了,关于X5WebView详情读者看参看腾讯官网腾讯浏览服务
本篇总结
本篇呢是首个hybird的项目的踩坑总结,有什么不足之处还请不吝赐教,以后在开发过程中遇到的更多的WebView的坑也会继续追加更新。
此致,敬礼
首个hybird商业项目踩坑总结的更多相关文章
- electron项目踩坑--A JavaScript error occurred in the main process:document is not defined
		
前言 记录electron-vue项目开发中遇到的一个错误,运行时报错如图: 控制台报错如下: ReferenceError: document is not defined at Object.&l ...
 - html2canvas在Vue项目踩坑-生成图片偏移不完整
		
背景 最近做一个Vue项目需求是用户长按保存图片,页面的数据是根据不同id动态生成的,页面渲染完生成内容图片让用户长按保存的时候,把整个页面都保存起来. 在项目遇到的坑是图片能生成,可是生成的图片总是 ...
 - Vue(项目踩坑)_解决vue中axios请求跨域的问题
		
一.前言 今天在做项目的时候发现axios不能请求跨域接口 二.主要内容 1.之前直接用get方式请求聚合数据里的接口报错如下 2.当前请求的代码 3.解决方法 (1)在项目目录中依次找到:confi ...
 - mpvue微信小程序项目踩坑记录
		
1.mpvue入门教程, http://mpvue.com/mpvue/quickstart.html # . 先检查下 Node.js 是否安装成功 $ node -v v8.9.0 $ npm - ...
 - 在IIS上部署 .Net Core 3.0 项目踩坑实录
		
在IIS上部署 .Net Core 3.0 项目的主要流程有: 安装并启用IIS 安装AspNetCoreModuleV2 添加.配置网站 设置应用程序池 通过VS发布 一.安装并启用IIS: 安装了 ...
 - web项目踩坑过程
		
sql函数设计: 一开始本来是直接用Java的jdbc直接传输操作语句的.但后来学了存储过程发现存储过程可以提高不少的效率.就重构了自己对数据库的操作代码.包括:开启,查找,修改,关闭. 开启:直接使 ...
 - nuxt项目踩坑
		
1.window or document is not undefined // .vue 页面 if (process.browser) { var Distpicker = require('v- ...
 - vue-cli 项目踩坑  npm install 时出错
		
1.报错如下: 2.此时你执行npm run dev / npm run build 会报错如下 npm ERR! code ELIFECYCLEnpm ERR! errno 1npm ERR! v ...
 - php小项目踩坑以及其中的注意点(第二篇)
		
用户登录页面 1.通过数据库验证用户名和密码(可以将里面要用到的数据库信息,放入到一个config文件中) <?php define('DB_HOST','localhost'); define ...
 
随机推荐
- 333. Largest BST Subtree节点数最多的bst子树
			
[抄题]: Given a binary tree, find the largest subtree which is a Binary Search Tree (BST), where large ...
 - 396. Rotate Function 移动加权求和,取最大值
			
[抄题]: Given an array of integers A and let n to be its length. Assume Bk to be an array obtained by ...
 - laravel简书(2)
			
用户注册 public function register() { //验证 $this->validate(\request(),[ ' ...
 - redis安全删key脚本(模糊匹配,长list,大set等)
			
两种情况: 1.删除指定前缀开头的rediskey ,扫描和删除过程中对线上无感知 2.删除一个大的list,set,zset,hash,这种得分批次减少大小,一直缩到0再删 第一种情况:只要知道线上 ...
 - Netsharp平台工具常见问题(FAQ)
			
1. 请问EntityId如何填? 回答:Netsharp中EntityId是经常需要输入的一个字段,因为Netsharp工具一般的源头是实体元数据,也就是一般常说的所谓模型驱动.所以很多工具都需要E ...
 - Mac 电脑设置显示路径
			
# 设置 defaults write com.apple.finder _FXShowPosixPathInTitle -bool TRUE;killall Finder # 删除 defaults ...
 - 2019.02.11 bzoj1568: [JSOI2008]Blue Mary开公司(线段树)
			
传送门 题意简述:维护整体加一条线段,求单点极值. 思路: 直接上李超线段树维护即可. 代码: #include<bits/stdc++.h> #define ri register in ...
 - st-link调试和下载程序(待写)
			
st-link调试只用三根线 GND SWCLK SWDIO
 - pop
			
package com.example.hellopopupwindow; import android.os.Bundle; import android.app.Activity; import ...
 - Beta冲刺 (6/7)
			
Part.1 开篇 队名:彳艮彳亍团队 组长博客:戳我进入 作业博客:班级博客本次作业的链接 Part.2 成员汇报 组员1:(组长)柯奇豪 过去两天完成了哪些任务 部分代码的整合 编辑及标注的提交操 ...