安卓webview经验汇总

2016-04-13 fishedee 前端

1 概述

安卓webview经验汇总

2 引擎

2.1 自带

安卓自带有一个webview引擎,优点是安装包比较小,缺点是兼容性有很多问题,而且还各种bug,4.2以下的addJavascriptInterface的注入攻击。

2.2 Crosswalk

Intel赞助的一个项目,将chromium移植到手机上,安装包上包含的就是一整个chromium的webview引擎,所以安装包比较大,22m,优点是快,兼容性好,稳定性好,缺点是大,炒鸡大。

后来出了Crosswalk-Lite,说是将Chromium中的webgl等不必要的部分删掉,精简成一个只包含常用功能的webview引擎,有用过,可是比较不稳定,而且第一次启动时还会因为解压压缩包的原因导致超级慢,不推荐使用。

2.3 X5

QQ家的项目,由于微信与QQ中内置了一个自带的浏览器内核,所以开放出浏览器内核X5引擎,当用户装有微信或QQ时,会自动切换为X5内核的浏览器,否则会使用自带的浏览器。支持的html5特性没有Crosswalk标准而且全面,但胜在微信与QQ的安装率很高,所以在对安装包有严格限制的情况下,非常推荐使用X5引擎。另外,X5引擎自带有video全屏,图片上传等功能,这点比Crosswalk要好。

2.4 总结

如果是纯粹的hybrid项目,是比较建议使用Crosswalk的,质量很好,我们线上的app平均崩溃率只有0.2%。如果对安装包大小有要求,或者webview只是app中嵌套的一个小功能,则建议使用X5引擎。

3 webview通信

安卓webview通信,安卓的webview与javascript之间如何安全可靠高效的通信,这个看似简单的问题,其实很头疼。

3.1 原始方法

//设置支持javascript 
setting.setJavaScriptEnabled(true); 
//javascript->Java
webView.addJavascriptInterface(new Object(){   
    public void startPhone(String num){ 
        Intent intent=new Intent(); 
                       
        intent.setAction(Intent.ACTION_CALL); 
        intent.setData(Uri.parse("tel:"+num)); 
        startActivity(intent); 
    }
}, "demo"); 
//Java->javascript
webView.loadUrl("javascript:alert(123)"); 

使用webview下的addJavascriptInterface就能实现javascript通信给java,而loadUrl就能实现java通信给javascript。这个方案优点在于简单可靠,缺点是无法传入javascript中的闭包参数,而且4.2版本下的安卓还有安全性问题

3.2 拦截url

public WebResourceResponse shouldInterceptRequest(WebView view,String url) {
    return m_jsInterface.shouldInterceptLoadRequest(url);
}

安卓的setWebViewClient中提供了可以拦截url请求的方法,如果我们将所有javascript对java的接口调用改为ajax请求,然后返回时像服务器一样将数据写入WebResourceResponse不就可以了。

public WebResourceResponse shouldInterceptLoadRequest(String url) {
    try{
        //校验协议
        final Uri urlInfo = Uri.parse(url);
        if( urlInfo.getScheme().equals("http") == false )
            return null;
        //校验pathInfo
        List<String> pathInfo = urlInfo.getPathSegments();
        if( pathInfo.size() != 3 ){
            return null;
        }
        final String schemaName = pathInfo.get(0);
        final String objectName = pathInfo.get(1);
        final String methodName = pathInfo.get(2);
        if( schemaName.equals("crossapi") == false )
            return null;
        Log.d("SafeWebView","crossapi request "+url);

        final PipedOutputStream out = new PipedOutputStream();
        PipedInputStream in = new PipedInputStream(out);

        //调用接口
        invokeBridge(objectName,methodName,new SafeWebViewJsInferfaceUriBridge(urlInfo,out));
        return new WebResourceResponse("text/plain","utf-8",in);
    }catch(Exception e){
        e.printStackTrace();
        return null;
    }
}

写法还是蛮简单的,要注意的是使用PipedOutputStream来输出数据,避免在shouldInterceptLoadRequest长时间执行,导致UI线程阻塞

$.get("/crossapi/device/waitResponse",{test:"你好"},function(data){
     console.log(JSON.stringify(data));
 });

像ajax一样调用远程请求,来实现javascript调用java即可,java回调javascript则通过的是WebResponse写入。这种方法用很好的安全性,而且兼容性也很好,而且也支持javascript的闭包参数传入。

我们将这种办法也在线上沿用了很久,基本也没什么问题。可是,在做友盟推送接口发现,这种做法的系统消耗比较大,而且如果一个接口的阻塞比较久不返回,就会连累其他接口不及时返回。每次回调的次数只能是一次,不能说javascript请求一次java后,java能无限次地回调javascript接口,这个限制使得在做推送接口时也显得比较麻烦。

3.3 拦截confirm+loadUrl调用

@Override
public boolean onJsConfirm(WebView view, String url, String message,
                           JsResult result) {
    boolean isHandle = m_jsInterface.onJsConfirm(view, message);
    if( isHandle ){
        result.confirm();
        return true;
    }
    return super.onJsConfirm(view, url, message, result);
}

安卓的setWebChromeClient中提供了可以拦截confirm,alert和prompt,如果我们在这里将请求拦截,然后返回时使用loadUrl来注入javascript不就可以返回了。问题是,loadUrl只支持基本类型的回调,不支持javascript的闭包回调,怎么办?

var CrossapiCallbackPool = [];
var Crossapi = function(url,argv,callback){
    CrossapiCallbackPool.push(callback);
    var callbackId = CrossapiCallbackPool.length - 1;
    var bundle = {
        url:url,
        argv:argv,
        callback:callbackId,
    };
    var prefix = "CrossApiBridge:"+JSON.stringify(bundle);
    confirm(prefix);
};

解决办法时,传入confirm前,将闭包数据转换为CallbackPool上一个整数ID就可以了,这样回调时使用

window.CrossapiCallbackPool[callbackId](xxxx)

就能实现java对javascript数据的闭包回调了

public boolean onJsConfirm(WebView webView,String message){
    try{
        //校验协议
        String prefix = "CrossApiBridge:";
        if( message.startsWith(prefix) == false ){
            return false;
        }
        JSONTokener tokener = new JSONTokener(message.substring(prefix.length()));
        JSONObject invokeInfo = (JSONObject)tokener.nextValue();
        String url = invokeInfo.getString("url");
        Object argv = invokeInfo.get("argv");
        int callbackId = invokeInfo.getInt("callback");

        String[] splitInfoTemp = url.split("/");
        List<String> splitInfo = new ArrayList<String>();
        for( String singleSplit : splitInfoTemp){
            if( singleSplit.trim().length() == 0 ){
                continue;
            }
            splitInfo.add(singleSplit.trim());
        }
        if( splitInfo.size() < 2 ){
            return false;
        }
        if( argv instanceof JSONObject == false ){
            return false;
        }
        final String objectName = splitInfo.get(0);
        final String methodName = splitInfo.get(1);

        //调用接口
        invokeBridge(objectName,methodName,new SafeWebViewJsInferfacePromptBridge((JSONObject)argv,webView,callbackId));
        return true;
    }catch(Exception e){
        e.printStackTrace();
        return false;
    }
}

写法还是挺简单的,要注意的是,这种方法需要预先在javascript环境中注入代码,不然回调中用不了。可以指定用户加载某个javascript文件,而这个文件默认在放在安卓本地环境,这样的话javascript用户用起来比较方便。

Crossapi("/device/waitResponse",{test:"你好"},function(data){
    console.log(data);
});

调用时直接调用即可,这种方法较好地实现了,安全,高效,支持闭包传递的特性,解决了拦截url的多请求阻塞问题。我们也在逐步迁移到这种webviewbridge的手法上。这也是目前较为流行的做法

3.4 总结

怎样建立一个可靠的webviewbridge,这个问题其实也不简单噢,需要好好考虑一下。

4 配置

4.1 硬件加速

<application
    android:hardwareAccelerated="true"
    android:allowBackup="false"
    android:label="@string/app_name"
    android:icon="@mipmap/ic_launcher"
    android:theme="@style/AppTheme"
    tools:replace="android:allowBackup">
</application>

在AndroidManifest中设置android:hardwareAccelerated为true,开启硬件加速模式

4.2 javascript

this.getSettings().setJavaScriptEnabled(true);

开启webview的javascript,不然网页中的javascript代码默认是不启动的。

4.3 视频

全屏播放视频有两种模式,分别是webkit播放模式和x5播放模式

4.3.1 webkit播放模式

webkit播放模式是默认的播放模式,自带浏览器,Crosswalk浏览器和X5内核浏览器都支持的播放模式。其原理为在遇到需要全屏播放视频的页面时,webkit会将播放视频的view传递给开发者,开发者将播放view替代webview即可。

WebChromeClient chromeClient = new WebChromeClient() {
    View myVideoView;
    View myNormalView;
    CustomViewCallback callback;

    @Override
    public void onShowCustomView(View view, CustomViewCallback customViewCallback) {
        View normalView = X5WebView.this;
        ViewGroup viewGroup = (ViewGroup) normalView.getParent();
        viewGroup.removeView(normalView);
        viewGroup.addView(view);
        myVideoView = view;
        myNormalView = normalView;
        callback = customViewCallback;
    }

    @Override
    public void onHideCustomView() {
        if (callback != null) {
            callback.onCustomViewHidden();
            callback = null;
        }
        if (myVideoView != null) {
            ViewGroup viewGroup = (ViewGroup) myVideoView.getParent();
            viewGroup.removeView(myVideoView);
            viewGroup.addView(myNormalView);
        }
    }
};
this.setWebChromeClient(chromeClient);

4.3.2 X5内核

X5浏览器支持三种播放视频的模式,分别是webkit播放模式与x5播放模式,x5播放模式是将视频文件跳转到一个特别的activity来单独播放,体验更好,速度也更流畅,推荐使用。但要注意的是,x5播放模式需要WebView的Context必须为Activity类型,我们试过在react-native中嵌套X5浏览器,传入webview的context是ReactContext,发现视频一直都是黑屏,坑爹呀。

<activity
    android:name="com.tencent.smtt.sdk.VideoActivity"
    android:alwaysRetainTaskState="true"
    android:configChanges="orientation|screenSize|keyboardHidden"
    android:exported="false"
    android:launchMode="singleTask" >
    <intent-filter>
        <action android:name="com.tencent.smtt.tbs.video.PLAY" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>

在AndroidManifest中加上这个activity

if(this.getX5WebViewExtension()!=null){
    Bundle data = new Bundle();
    data.putBoolean("standardFullScreen", false);//true表示标准全屏,false表示X5全屏;不设置默认false,
    data.putBoolean("supportLiteWnd", false);//false:关闭小窗;true:开启小窗;不设置默认true,
    data.putInt("DefaultVideoScreen", 2);//1:以页面内开始播放,2:以全屏开始播放;不设置默认:1
    this.getX5WebViewExtension().invokeMiscMethod("setVideoParams", data);
}

在webview加上以上配置,那么在遇到带有视频的网页时,点击播放按钮就会跳转到com.tencent.smtt.sdk.VideoActivity来全屏播放视频了,非常方便。

4.4 对话框

有时候,我们需要更改console,alert,confirm,prompt这几种原生的对话框样式,我们可以这么做。

WebChromeClient chromeClient = new WebChromeClient() {
    @Override
    public boolean onConsoleMessage(ConsoleMessage var1) {
        return super.onConsoleMessage(var1);
    }

    @Override
    public boolean onJsConfirm(WebView arg0, String arg1, String arg2, JsResult arg3) {
        //TODO
        return super.onJsConfirm(arg0, arg1, arg2, arg3);
    }

    @Override
    public boolean onJsAlert(WebView arg0, String arg1, String arg2, JsResult arg3) {
        //TODO
        return super.onJsAlert(null, "www.baidu.com", "aa", arg3);
    }

    @Override
    public boolean onJsPrompt(WebView arg0, String arg1, String arg2, String arg3, JsPromptResult arg4) {
        //TODO
        return super.onJsPrompt(arg0, arg1, arg2, arg3, arg4);
    }
};
this.setWebChromeClient(chromeClient);

修改一下WebChromeClient里面的回调就可以了,挺简单的。

4.5 缓存

webSetting.setAppCacheEnabled(true);
webSetting.setDatabaseEnabled(true);
webSetting.setDomStorageEnabled(true);
webSetting.setAppCacheMaxSize(Long.MAX_VALUE);
webSetting.setCacheMode(WebSettings.LOAD_NO_CACHE);

开启html5的各种缓存模式,database,appcache,以及domstorage,而cacheMode是用来配置http协议上的缓存。

4.6 拦截请求

4.6.1 页面加载

WebViewClient webviewClient = new WebViewClient() {
    public void onPageFinished(WebView view, String url) {
        //TODO
    }

    public void onPageStarted(WebView view, String url, Bitmap favicon) {
        //TODO
    }
}
this.setWebViewClient(webViewClient);

WebChromeClient webChromeClient = new WebChromeClient() {
    public void onProgressChanged(WebView var1, int var2) {
        //TODO
    }
}
this.setWebChromeClient(webChromeClient);

webViewClient与webChromeClient中一起提供了页面开始加载,加载中,加载完成这三个事件,这也是大多数浏览器中实现顶部进度条的原理。

4.6.2 页面内请求

WebViewClient webviewClient = new WebViewClient() {
    public WebResourceResponse shouldInterceptRequest (WebView view,
                                                       WebResourceRequest request) {
        //TODO
        return super.shouldInterceptRequest(view,request);
    }

    public WebResourceResponse shouldInterceptRequest(WebView view,
                                                      String url) {
        //TODO
        return super.shouldInterceptRequest(view,url);
    }
}
this.setWebViewClient(webViewClient);

拦截页面中的ajax,文件等各种各样的请求,也有被用来做webview通信的。

4.6.3 页面外请求

WebViewClient webviewClient = new WebViewClient() {
    /**
     * 防止加载网页时调起系统浏览器
     */
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        view.loadUrl(url);
        return true;
    }
}
this.setWebViewClient(webViewClient);

拦截页面中跳转到其他页面的请求,一般不做拦截的话,安卓浏览器中会弹出提示框,提示你是否跳转到系统浏览器中看这个页面。所以,要么是在重用本地的webview来加载新页面,要么是新开一个activity的webview来加载新页面,看业务了。

4.6.4 新窗口请求

WebChromeClient webChromeClient = new WebChromeClient() {
    public boolean onCreateWindow(WebView var1, boolean var2, boolean var3, Message var4) {
        return super.onCreateWindow(var1,var2,var3,var4);
    }
}
this.setWebChromeClient(webChromeClient);

拦截用javascript来启动新窗口的请求,这个一般是不拦截的,不允许javascript来开启新窗口。

4.7 安全

this.getSettings().setAllowFileAccess(true);
this.getSettings().setAllowFileAccessFromFileURLs(true);
this.getSettings().setAllowUniversalAccessFromFileURLs(true);

设置file域名下页面的安全性,一般file域名是设置为无跨域的,简单暴力。

4.8 图片选择器

this.setWebChromeClient(new WebChromeClient(){
  @Override
  public void openFileChooser(final ValueCallback<Uri> var1, String var2, String var3) {
      if( m_fileChooseListener == null ){
          return;
      }
      m_fileChooseListener.onChoose(new FinishListener<Uri>() {
          @Override
          public void onFinish(int code, String message, Uri data) {
              if( code != 0 ){
                  var1.onReceiveValue(null);
                  return;
              }
              Log.d("CrossWebView","onChoose "+data);
              var1.onReceiveValue(data);
          }
      },var2);

  }

  @Override
  public boolean onShowFileChooser(WebView var1, final ValueCallback<Uri[]> var2, WebChromeClient.FileChooserParams var3) {
      if( m_fileChooseListener == null ){
          return true;
      }
      String[] acceptTypes = var3.getAcceptTypes();
      m_fileChooseListener.onChoose(new FinishListener<Uri>() {
          @Override
          public void onFinish(int code, String message, Uri data) {
              if( code != 0 ){
                  var2.onReceiveValue(null);
                  return;
              }
              Log.d("CrossWebView","onChoose "+data);
              var2.onReceiveValue(new Uri[]{data});
          }
      },acceptTypes[0]);
      return true;
  }
});

在setChromeClient中设置openFileChooser和onShowFileChooser两个回调就可以了

m_crossWebView.setOnFileChooserListener(new CrossWebView.OnFileChooserListener() {
@Override
public void onChoose(final FinishListener<Uri> callback, String accept) {
 try {
     JSONObject args = new JSONObject();
     args.put("format", "url");
     FinishListener<Object> argsCallback = new FinishListener<Object>() {
         @Override
         public void onFinish(int code, String msg, Object data) {
             if (code != 0) {
                 callback.onFinish(code, msg, null);
                 return;
             }
             String dataStr = (String) data;
             Uri uri = Uri.fromFile(new File(dataStr));
             callback.onFinish(0, "", uri);
         }
     };
     m_imageApi.preview(new CrossWebViewJsContextStub(args, argsCallback));
 }catch (Exception e){
     callback.onFinish(1, e.getMessage(), null);
 }
}

要注意的是,我们打开图片浏览器后,获取的String类型的url最好用Uri.fromFile来转换成Uri后再吐给webview的回调就可以了

5 启动

5.1 loadUrl启动

webView.loadUrl("file:///android_asset/webpage/fullscreenVideo.html");
webView.loadUrl("http://www.baidu.com");

要么是加载本地资源上的文件,要么是加载网络上的文件,都可以。

5.2 loadData启动

view.loadDataWithBaseURL(view.getBaseUrl(), html, "text/html", view.getCharset(), null);

传入一段指定的html数据,让webview跑起来,这个还是比较简单的。

<!--错误演示-->
<div>HelloWorld</div>
<!--正确演示-->
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
    </head>
    <body>
        <div>HelloWorld</div>
    </body>
</html>

但是要注意的是,很多时候,我们是用这个loadData来展示富文本内容的,所以就直接将div扔进去loadData里面,这样会造成很多兼容性问题,所以,请务必在富文本前后加上恰当的html内容后才扔进webview内部

6 优化

6.1 加载优化

  • 并发加载优化
  • 先拉缓存的md5,检查是否需要更新,然后使用
  • 直接拉缓存,缓存更新由服务器更新
  • 直接旧缓存,然后再检查是否需要更新,然后再刷新

7 FAQ

7.1 webview高度自适应

this.addJavascriptInterface(new Object(){
    @JavascriptInterface
    public void getContentHeight(double height){
        //TODO height
    }
},"NativeAndroid");

WebChromeClient webChromeClient = new WebChromeClient() {
    public void onPageFinished(WebView view, String url) {
        view.loadUrl("javascript:(function() { " + "NativeAndroid.getContentHeight(document.documentElement.scrollHeight);" + "})();");
    }
}
this.setWebChromeClient(webChromeClient);

有时候需要webview的高度自适应其内部内容的高度。目前并没有什么好办法,就是在onPageFinished后执行一段javascript代码,让页面的高度用javascriptInterface的方法传递到native端。

7.2 视频在关闭后仍然播放

webView.getClass().getMethod("onPause").invoke(webView,(Object[])null);
webView.getClass().getMethod("onResume").invoke(webView,(Object[])null);
webView.removeAllViews()
webView.destroy()

安卓自带的webview有很多问题,其中一个大bug,就是关闭webview后,仍然会有视频的声音放出。解决办法是将页面onResume与onPause时调用webview的onPause与onRusume,在页面的onDestroy时调用webview的removeAllViews与destroy来彻底关闭视频。

7.3 弹出提示跳转浏览器

this.setWebViewClient(new WebViewClient(){
  @Override
  public boolean shouldOverrideUrlLoading(WebView var1,String var2){
      CrossWebView.this.loadUrl(var2);
      return true;
  }
}

打开webview,弹出了一个对话框,让你选择什么浏览器来打开页面。解决办法很简单,覆盖setWebViewClient的shouldOverrideUrlLoading方法即可,让他默认使用原来的webview来加载网页,并且返回true代表你已经处理了这种情况。

7.4 支持下拉刷新的功能

@Override
public boolean canScrollVertically( int direction ){
   if( direction < 0 ){
       return this.getWebScrollY() > 0;
   }else{
       return 0;
   }
}

不像ListView,ScrollView,这些原生的组件默认就与各大的下拉刷新组件相兼容,WebView默认与下拉组件不兼容,导致webview组件往下拉时没有触发下拉刷新。

7.5 onPageFinished与onRecieveError多次触发

@Override
public void onPageFinished(WebView var1, String var2) {
     if( m_isLoadError == true ){
         return;
     }
     postDelayed(new Runnable() {
         @Override
         public void run() {
             if( m_isLoadError == true ){
                 return;
             }
             m_finishListener.onFinish();
         }
     },300);
}

@Override
public void onReceivedError(WebView var1, int var2, String var3, String var4) {
     m_isLoadError = true;
     m_errorListener.onError();
}

安卓的onPageFinished与onReceiveError触发时机有问题。例如,如果打开的页面是一个404页面,安卓会触发onReceiveError事件,然后触发onPageFinished。然后,如果你调用reload()来重新加载页面,安卓又会触发onPageFinished事件,然后触发onReceiveError事件。而我们希望的是,页面出错时只触发onReceiveError事件,页面正常打开时触发onPageFinished事件。所以,解决办法只能像上面这样了。注意,每次loadUrl与reload后,默认设置m_isLoadError为false。

7.6 内存泄漏

安卓与ios的webview中有严重的内存泄漏问题,页面关掉了以后,页面的webview仍然占据着内存空间

7.6.1 onDestry

mWebview.removeAllViews();
mWebview.destroy();

在Page的onDestroy回调中,手动调用webview的removeAllViews与destroy来回收垃圾。回收能力一般,不能解决全部问题

public void setConfigCallback(WindowManager windowManager) {
    try {
        Field field = WebView.class.getDeclaredField("mWebViewCore");
        field = field.getType().getDeclaredField("mBrowserFrame");
        field = field.getType().getDeclaredField("sConfigCallback");
        field.setAccessible(true);
        Object configCallback = field.get(null);

        if (null == configCallback) {
            return;
        }

        field = field.getType().getDeclaredField("mWindowManager");
        field.setAccessible(true);
        field.set(configCallback, windowManager);
    } catch(Exception e) {
    }
}

部分同学表示还需要用反射强制将webview的成员变量设置为null

webView.loadUrl('about:blank');

强制将webview加载到一个空页面,将原网页的图片,js等资源释放掉

7.6.2 杀死进程

将webview所在的activity放到一个单独的进程中,然后关闭activity时强制System.exit(0)来将进程关掉。暴力,好用,还能避免webview挂掉导致原进程挂了,目前微信和QQ打开网页用的就是这个办法,局限在于webview只有一个的场景,多webview的场景无能为力

7.6.3 webview池

@Override
public void onDestroy(){
    super.onDestroy();
    mWebView.loadUrl('about:blank');
    pool.add(mWebView);
}

将用完的webview放入到一个池里面,下一次新建webView时优先从池里面取出,这样就能将新建的webView控制到一个可控的范围内,避免内存无限增大,但依然会有少许的内存泄漏。

7.7 cookie持久化

webView中的cookie不能很好地持久化到磁盘上,导致用户登录后,下次启动app还需要重新登录

7.7.1 强制持久化

@Override
public void onPause() {
   super.onPause();
   CookieStore.store(this);
}

在Activity的onPause回调中,将webview的cookie池全部写入到磁盘中

public class MyApplication extends Application{
    @Override
    public void onCreate(){
        super.onCreate();
        CookieStore.load(this);
    }
}

在Application启动时,将磁盘的cookie读取到webview的cookie池中

public class CookieStore {
    private static String getDomain(){
        HomePageInfo.Setting setting = HomePageInfo.getSetting();
        String url = setting.m_menuSetting.get(0).m_url;
        Uri uri = Uri.parse(url);
        return uri.getHost();
    }

    public static void store(Context context){
        //获取cookie
        String cookie = CookieManager.getInstance().getCookie("http://"+getDomain()+"/");
        if( cookie == null ){
            Log.d("mm_fish","storeCookie null");
            return;
        }
        Log.d("mm_fish","storeCookie "+cookie);
        Store.set(context,"cookie",cookie);
        //同步cookie
        CookieManager.getInstance().flush();
        CookieSyncManager.getInstance().sync();
    }

    private static int loadSize = 0;
    public static void load(Context context,final FinishListener<Object> listener){
        //开启cookie
        context = context.getApplicationContext();
        CookieSyncManager.createInstance(context);
        final CookieManager cookieManager = CookieManager.getInstance();
        cookieManager.setAcceptCookie(true);
        //设置cookie
        String cookie = Store.get(context,"cookie");
        if( cookie.isEmpty() ){
            Log.d("mm_fish","loadCookie null");
            listener.onFinish(0,"",null);
            return;
        }
        Log.d("mm_fish","loadCookie "+cookie);
        ValueCallback<Boolean> callback = new ValueCallback<Boolean>() {
            @Override
            public void onReceiveValue(Boolean aBoolean) {
                Log.d("mm_fish2","setCookie "+aBoolean);
                loadSize--;
                if( loadSize == 0 ){
                    cookieManager.flush();
                    CookieSyncManager.getInstance().sync();
                    listener.onFinish(0,"",null);
                }
            }
        };
        String[] cookies = cookie.split(";");
        loadSize = cookies.length;
        for( String singleCookie : cookies ){
            singleCookie += "; Max-Age=62208000; path=/; domain="+getDomain();
            cookieManager.setCookie("http://"+getDomain()+"/", singleCookie,callback);
        }
    }

cookie的存储落地采用的是SharedPreference

7.7.2 x5与自带webview冲突

QbSdk.preInit(context.getApplicationContext(), new QbSdk.PreInitCallback() {
     @Override
     public void onCoreInitFinished() {

     }

     @Override
     public void onViewInitFinished() {
         listener.onFinish(0,"",new InitResult());
     }
 });

如果你是使用x5内核浏览服务,第一次启动时是用自带的webview,cookie写入到自带的cookie池中。第二次启动时是用的x5内核,使用的cookie池是x5的cookie池,导致第一次写入的cookie都丢失了。解决办法是,第一次启动就强制使用x5内核加载浏览服务。

7.7.3 接管网络请求

@Override
public WebResourceResponse shouldInterceptRequest(WebView var1, String var2) {
    return m_jsBridge.shouldInterceptLoadRequest(var2);
}

@Override
public WebResourceResponse shouldInterceptRequest(WebView var1, WebResourceRequest var2) {
    return m_jsBridge.shouldInterceptLoadRequest(var2.getUrl().toString());
}

@Override
public WebResourceResponse shouldInterceptRequest(WebView var1, WebResourceRequest var2, Bundle var3) {
    return m_jsBridge.shouldInterceptLoadRequest(var2.getUrl().toString());
}

在WebViewClient中强制将所有的网络请求都走okhttp或volley,让cookie都给okhttp等的网络框架来做持久化的行为,安全可靠。唯一不足是,API 21以下的机器,这样做会导致post,put请求都丢失了,因为API 21以下的拦截网路请求中只给予了url,没有body体。

7.8 图片选择器拍照上传的延迟问题

自定义图片选择器以后,会发现当选择拍照上传的图片,webview不能马上读取到,需要延迟一会儿(大概1s)才能读取到。这个问题有点蛋疼,因为拍照时调用的是系统的拍照app,这个app在拍摄完后没有将数据及时落地到磁盘,导致了其他进程获取这个文件时会不齐全。但是,当你使用java的读文件接口来获取数据,却能避免这个情况。解决方法有两个:

  • 接管webview中关于外部磁盘文件的读取工作,使得读取文件都是用Java的方式来读取,避免读取延迟的问题。
  • 拍摄类的照片,加上5s的读取延迟

7.9 html页面禁止缓存

怎么做到只是html页面不缓存,而js,css等是走缓存的呢。实现办法是,在loadUrl上做手脚,让它带上随机数来避免缓存,但要注意的是随机数的参数必须加在query的地方,不要加在hash的地方,不然会影响跨页面的喵点跳转

private String getNoCacheUrl(String url){
   Date date = new Date();
   int queryIndex = url.indexOf('?');
   int hashIndex = url.indexOf('#');
   String query = "";
   String hash = "";
   String base = "";
   if( queryIndex != -1 && hashIndex != -1 ){
       if( queryIndex < hashIndex ){
           base = url.substring(0,queryIndex);
           query = url.substring(queryIndex+1,hashIndex);
           hash = url.substring(hashIndex+1);
       }else{
           base = url.substring(0,hashIndex);
           hash = url.substring(hashIndex+1,queryIndex);
           query = url.substring(queryIndex+1);
       }
   }else if( queryIndex != -1 ){
       base = url.substring(0,queryIndex);
       query = url.substring(queryIndex+1);
       hash = "";
   }else if( hashIndex != -1 ){
       base = url.substring(0,hashIndex);
       hash = url.substring(hashIndex+1);
       query = "";
   }else{
       base = url;
       query = "";
       hash = "";
   }
   if( query.isEmpty() == false ){
       query += "&";
   }
   query += "_crossapinocache="+date.getTime();
   String result = base + "?" + query+"#"+hash;
   Log.d("CrossWebView","url "+url+","+" noCacheUrl: "+result);
   return result;
}

相关文章