WebView相关问题总结

在进行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方法生成请求  
});

推荐阅读更多精彩内容