在进行APP+H5混合开发的时候,一些功能是用native方法实现的,如登陆,一些功能是用H5实现的。所以往往需要将在native方法登陆的状态同步到H5中避免再次登陆。这种情况在Android开发中比较常见,下面总结一下webview在项目中遇到的坑,顺便给即将入坑的小伙伴一点小小的建议,希望有所作用,相互学习进步(大佬绕过……)
WebView常见需注意的API:
WebChromeClient是辅助WebView处理Javascript的对话框,网站图标,网站title,加载进度等 :
onCloseWindow(关闭WebView)
onCreateWindow()
onJsAlert (WebView上alert是弹不出来东西的,需要定制你的WebChromeClient处理弹出)
onJsPrompt
onJsConfirm
onProgressChanged
onReceivedIcon
onReceivedTitle
WebView 调取本地文件上传,部分代码如下:
/**
*
* 继承WebChromeClient可以做些其他的工作
*/
class ChromeClient extends WebChromeClient {
.
.
.
}
private ChromeClient mWebChromeClient = new ChromeClient() {
// android 5.0 这里需要使用android5.0 sdk
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback,
WebChromeClient.FileChooserParams fileChooserParams) {
Log.d("", "onShowFileChooser");
if (mUploadMessageForAndroid5 != null) {
mUploadMessageForAndroid5.onReceiveValue(null);
}
mUploadMessageForAndroid5 = filePathCallback;
/**
* 标准意图,被发送到相机应用程序捕获一个图像,并返回它。通过一个额外的extra_output控制这个图像将被写入。
* 如果extra_output是不存在的,
* 那么一个小尺寸的图像作为位图对象返回。如果extra_output是存在的,那么全尺寸的图像将被写入extra_output
* URI值。
*/
takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
File photoFile = null;
try {
// 设置MediaStore.EXTRA_OUTPUT路径,相机拍照写入的全路径
photoFile = createImageFile();
takePictureIntent.putExtra("PhotoPath", mCameraPhotoPath);
} catch (Exception ex) {
Log.e("WebViewSetting", "Unable to create Image File", ex);
}
if (photoFile != null) {
// cameraUri = Uri.fromFile(photoFile);
cameraUri = getUriForFile(context, photoFile);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, cameraUri);
System.out.println(mCameraPhotoPath);
} else {
takePictureIntent = null;
}
}
showChioceDialog();
return true;
}
// 针对 3.0--
public void openFileChooser(ValueCallback<Uri> uploadMsg) {
Log.d("asdf", "openFileChooser1");
openFileChooserImpl(uploadMsg);
}
// 针对 3.0+
public void openFileChooser(ValueCallback uploadMsg, String acceptType) {
Log.d("asdf", "openFileChooser2");
openFileChooserImpl(uploadMsg);
}
// For Android 4.1
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
Log.d("asdf", "openFileChooser3");
openFileChooserImpl(uploadMsg);
}
};
兼容Android 7.0访问权限
/**
* 适配7.0及以上
*
* @param context
* @param file
* @return
*/
private static Uri getUriForFile(Context context, File file) {
if (context == null || file == null) {
throw new NullPointerException();
}
Uri uri;
if (Build.VERSION.SDK_INT >= 24) {
uri = FileProvider.getUriForFile(context.getApplicationContext(), "com.****.fileprovider", file);
} else {
uri = Uri.fromFile(file);
}
return uri;
}
文件选择回调上传处理:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
UMShareAPI.get(this).onActivityResult(requestCode, resultCode, data);
if (requestCode == FILECHOOSER_RESULTCODE) {
if (null == mUploadMessage && null == mUploadMessageForAndroid5)
return;
if (mUploadMessage != null) {
Uri result = data == null || resultCode != RESULT_OK ? null : data.getData();
mUploadMessage.onReceiveValue(result);
mUploadMessage = null;
} else if (mUploadMessageForAndroid5 != null) {
Uri result = (data == null || resultCode != RESULT_OK) ? null : data.getData();
if (result != null) {
mUploadMessageForAndroid5.onReceiveValue(new Uri[] { result });
} else {
mUploadMessageForAndroid5.onReceiveValue(new Uri[] {});
}
mUploadMessageForAndroid5 = null;
}
}
if (requestCode == INPUT_FILE_REQUEST_CODE) {
if (null == mUploadMessage && null == mUploadMessageForAndroid5)
return;
afterOpenCamera();
Uri uri = cameraUri;
Uri[] uris = new Uri[1];
uris[0] = uri;
if (mUploadMessageForAndroid5 != null) {
mUploadMessageForAndroid5.onReceiveValue(uris);
mUploadMessageForAndroid5 = null;
} else {
mUploadMessage.onReceiveValue(uri);
mUploadMessage = null;
}
}
}
WebView 图片延迟加载
有些页面如果包含网络图片,在移动设备上我们等待加载图片的时间可能会很长,所以我们需要让图片延时加载,这样不影响我们加载页面的速度,同样代码说话:
定义变量:
boolean blockLoadingNetworkImage=false;
在WebView初始化的时候设置,就是这么简单就可以了:
blockLoadingNetworkImage = true;
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
//返回值是true的时候控制去WebView打开,为false调用系统浏览器或第三方浏览器
return true;
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
if (!blockLoadingNetworkImage){
webView.getSettings().setBlockNetworkImage(true);
}
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
if (blockLoadingNetworkImage){
webView.getSettings().setBlockNetworkImage(false);
}
}
});
JS调用native
js调用原生大概有两种方法
截取url,获取指定的url,例如页面上有个拨打电话的调用,我们就可以在wenview中这样截取:
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
//返回值是true的时候控制去WebView打开,为false调用系统浏览器或第三方浏览器
Log.e("zhaogl", url);
if (url.startsWith("tel:")){//拨打电话
String[] ss = url.split(":");
if (ss.length > 1 && ss[1] != null){
CommonUtil.call(WebViewActivity.this,ss[1]);
}
}else if (url.equalsIgnoreCase("http://www.baidu.com/")){
WebViewActivity.this.finish();
}else {
view.loadUrl(url);
}
return true;
}
});
调用Java写好的方法:
首先我们要定义一个方法给js调用,这里我把这个方法封装到一个类中:
public class InJavaScript {
private static InJavaScript instance;
public InJavaScript() {
}
public static InJavaScript getInstance() {
if (instance == null){
instance = new InJavaScript();
}
return instance;
}
/**
* 分享
* @param str 内容
* @param targetUrl url
*/
@JavascriptInterface
public void runOnAndroidShare(String str,String targetUrl) {
WebViewEvent event = new WebViewEvent();
event.isShare = true;
event.content = str;
event.url = targetUrl;
EventBus.getDefault().post(event);
}
/**
* 关闭 window.close 不起作用,代替之
*/
@JavascriptInterface
public void closeWindowForAndroid(){
WebViewEvent event = new WebViewEvent();
event.isCloseWindow = true;
EventBus.getDefault().post(event);
}
}
写好的java类及方法如果想让js调用,还需要有如下设置:
webView.addJavascriptInterface(InJavaScript.getInstance(), "injs");
js中就可以用以下方式调用:
function sendToAndroid(){
window.injs.runOnAndroidShare(str,str);//调用android的函数
}
注意,第二中方法在4.2以下版本存在js安全漏洞,但是目前市场上的安卓大部分都已经在4.2以上了,所以如果你的项目安全要求不是那么高,可以正常使用。现在有很多第三方的框架解决这个问题,大家可以去自己查找。
解决部分手机支持webview显示空白
因为由印象笔记备注总结过后的,参考博客没有备注,如有雷同敬请留言添加
LAYER_TYPE_SOFTWARE
:
无论硬件加速是否打开,都会有一张Bitmap(software layer),并在上面对WebView进行软渲染。
好处:
在进行动画,使用software可以只画一次View树,很省。
什么时候不要用:
View树经常更新时不要用。尤其是在硬件加速打开时,每次更新消耗的时间更多。因为渲染完这张Bitmap后还需要再把这张Bitmap渲染到hardware layer上面去。
LAYER_TYPE_HARDWARE
:
硬件加速关闭时,作用同software。
硬件加速打开时会在FBO(Framebuffer Object)上面做渲染,在进行动画时,View树也只需要画一次。
两者区别:
1、一个是渲染到Bitmap,一个是渲染到FB上。
2、hardware可能会有一些操作不支持。
两者相同:
都是开了一个buffer,把View画到这个buffer上面去。
LAYER_TYPE_NONE
:
这个就比较简单了,不为这个View树建立单独的layer
PS:GLSurfaceView和WebView默认Layertype都是none。
GLSurfaceView
:
给GLSurfaceView设置为software或者hardware后,发现什么也画不出来了。得出结论:GLSurfaceView的Layer type只能是none
WebView
:
以前使用WebView时碰到过一个问题,如果在WebView上面使用Animation,WebView的绘画区域不动。当时的解决方案是在进行动画之前对WebView进行截屏(drawingcache)。按上面的道理试了一下,设置一个hardware或者software的layer就OK了。
现在又碰到了另外一个问题,打开硬件加速后,在一些机器上面(我的是3.2)WebView有时会出现某一块区域白屏的问题。默认的layer type是none,改为hardware也不行,设置为software就解决了。当然关闭硬件加速也好了,可是那样的话程序整体就比较慢了。所以最终方案是整体硬件加速,出问题的WebView设置software
- 可以让3D的绘制更快一些:getHolder().setType(SurfaceHolder.SURFACE_TYPE_HARDWARE);
- 在硬件加速开启的情况下GLSurfaceView一旦被从View树上摘下来,会使整个窗口背景变黑,即使设置layer type为software也不管用。
经过两天的排查,发现了原因,我的程序是在C层由drawFrame(属于GLThread线程)来驱动进行绘画,当GLSurfaceView被摘下来时,GLSurfaceView的destroy方法被调用,我在destroy方法(属于UI线程)中直接调用 了GLThread线程的结束方法。而GLSurfaceView.creat,sizeChanged,destroyed在UI线程,Render.create,sizeChanged,drawFrame在GLThread线程。因此,出现了UI线程直接调用GLThread线程的方法的问题。最终通过GLSurfaceView.queueEvent向GLThread线程发送Runnable,问题得到解决。
看来,还是软渲染的容错能力比较强,一开硬件加速,底层就比较脆弱了。
结论:一定要搞清楚哪个是UI线程,哪个是GLThread线程。 - hardware acclerator是对整个窗口进行加速,在硬件加速打开时View.isHardwareAcclerator返回true。但每个View可能被渲染到的Canvas是不同的,比如View可能被通过setLayer设置了Layer,这时,Canvas.isHardwareAccelerator返回false
- Android提供了三种硬件加速是否打开的控制级别,分别是Application,Activity,Window,View。
WebView的cookie机制相关问题(后续继续补充……)
webview cookie同步
因为android不会自动同步cookie到WebView。做iOS开发则不用担心这个问题,因为ios内部已经实现了cookie同步。本文将会介绍两种cookie同步的方式,并重点分析WebView的cookie机制。在开始之前先讲一下基于session的登录验证。
Cookie的重要作用是回话识别(SeesionId)和状态长期保持(在浏览器保存需要长期保持的数据)。
Cookie在安卓中的使用方式--标示会话,附加信息
- 通过Session标示一次会话,举个例子:注册时,判断客户端注册错误次数(注册次数已经超过限制,显示验证码)
- 传递附加数据,举个例子:传递单点登陆的token。
这里需要注意的是在设置cookie之后,是不能设置以下属性的,否则cookie是无效的(不只是这些属性,这里只是举例,最好的方式是在执行loadurl之前再设置cookie)
mWvSignUp.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);
mWvSignUp.getSettings().setJavaScriptEnabled(true);
mWvSignUp.getSettings().setDatabaseEnabled(true);
mWvSignUp.getSettings().setDomStorageEnabled(true);
一些ajax请求需要带入cookie怎么办?
Android 5.0以下
:
mWvSignUp.setWebViewClient(new WebViewClient() {
/**
* 5.0以下
* @param view
* @param url
* @return
*/
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
syncCookie(url);
return super.shouldInterceptRequest(view, url);//将加好cookie的url传给父类继续执行
}
});
Android 5.0以上
:
mWvSignUp.setWebViewClient(new WebViewClient() {
@SuppressLint("NewApi")
@Override
public WebResourceResponse shouldInterceptRequest(WebViewview, WebResourceRequest request) {
String url = request.getUrl().toString();
syncCookie(url);
return super.shouldInterceptRequest(view, url);//因为跟5.0以下的方法返回值是同一个类,所以这里偷懒直接调动4.0方法生成请求
});