Skip to content

5.2 webView优化2

杨充 edited this page May 17, 2020 · 1 revision

优化汇总目录介绍

  • 5.1.6 WebView判断断网和链接超时
  • 5.1.7 @JavascriptInterface注解方法注意点
  • 5.1.8 使用onJsPrompt实现js通信注意点
  • 5.1.9 Cookie同步场景和具体操作
  • 5.2.0 shouldOverrideUrlLoading处理多类型
  • 5.2.1 WebView独立进程解决方案
  • 5.2.2 截取WebView屏幕的整个可视区域
  • 5.2.3 截取WebView屏幕长图效果
  • 5.2.4 Android P加载X5内核失败优化
  • 5.2.5 WebView流量轻量优化
  • 5.2.6 onReceivedTitle执行多次
  • 5.2.7 WebView自定义长按选择弹窗
  • 5.2.8 关于下拉刷新重新加载web页面优化

5.1.6 WebView判断断网和链接超时

  • 第一种方法,可以直接使用,在WebViewClient中的onReceivedError方法中捕获
    @Override
    public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
        super.onReceivedError(view, errorCode, description, failingUrl);
        // 断网或者网络连接超时
        if (errorCode == ERROR_HOST_LOOKUP || errorCode == ERROR_CONNECT || errorCode == ERROR_TIMEOUT) {
            // 避免出现默认的错误界面
            view.loadUrl("about:blank"); 
            //view.loadUrl(mErrorUrl);
        }
    }
    
  • 第二种方法,只是提供思路,但是不建议使用,既然是请求url,那么可不可以通过HttpURLConnection来获取状态码,是不是回到了最初学习的时候,哈哈
    • 注意一定是要开启子线程,因为网络请求是耗时操作,就不解释这多呢
    new Thread(new Runnable() {
        @Override
        public void run() {
            int responseCode = getResponseCode(mUrl);
            if (responseCode == 404 || responseCode == 500) {
                Message message = mHandler.obtainMessage();
                message.what = responseCode;
                mHandler.sendMessage(message);
            }
        }
    }).start();
    
    • 在主线程中处理消息
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            int code = msg.what;
            if (code == 404 || code == 500) {
                System.out.println("handler = " + code);
                mWebView.loadUrl(mErrorUrl);
            }
        }
    };
    
    • 那么看看getResponseCode(mUrl)是如何操作的
    /**
     * 获取请求状态码
     * @param url
     * @return 请求状态码
     */
    private int getResponseCode(String url) {
        try {
            URL getUrl = new URL(url);
            HttpURLConnection connection = (HttpURLConnection) getUrl.openConnection();
            connection.setRequestMethod("GET");
            connection.setReadTimeout(5000);
            connection.setConnectTimeout(5000);
            return connection.getResponseCode();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return -1;
    }
    

5.1.7 @JavascriptInterface注解方法注意点

  • 在js调用Android原生方法时,会用@JavascriptInterface注解标注那些需要被调用的Android原生方法,那么思考一下,这些原生方法是否可以执行耗时操作,如果有会阻塞通信吗?
  • JS会阻塞等待当前原生函数(耗时操作的那个)执行完毕再往下走,所以 @JavascriptInterface注解的方法里面最好也不要做耗时操作,最好利用Handler封装一下,让每个任务自己处理,耗时的话就开线程自己处理,这样是最好的。
  • JavascriptInterface注入的方法被js调用时,可以看做是一个同步调用,虽然两者位于不同线程,但是应该存在一个等待通知的机制来保证,所以Native中被回调的方法里尽量不要处理耗时操作,否则js会阻塞等待较长时间。
  • 那么具体的实践案例,可以看lib中的WvWebView类,就是通过handler处理消息的。具体的代码可以看项目中的案例……

5.1.8 使用onJsPrompt实现js通信注意点

  • 在WvWebView类中,就是使用onJsPrompt实现js调用Android通信,具体可以看一下代码。
  • 在js调用​window.alert​,​window.confirm​,​window.prompt​时,​会调用WebChromeClient​对应方法,可以此为入口,作为消息传递通道,考虑到开发习惯,一般不会选择alert跟confirm,​通常会选promopt作为入口,在App中就是onJsPrompt作为jsbridge的调用入口。由于onJsPrompt是在UI线程执行,所以尽量不要做耗时操作,可以借助Handler灵活处理。对于回调的处理跟上面的addJavascriptInterface的方式一样即可
    @Override
    public boolean onJsPrompt(WebView view, String url, final String message,
                              String defaultValue, final JsPromptResult result) {
        if(Build.VERSION.SDK_INT<= Build.VERSION_CODES.JELLY_BEAN){
            String prefix="_wvjbxx";
            if(message.equals(prefix)){
                Message msg = mainThreadHandler.obtainMessage(HANDLE_MESSAGE, defaultValue);
                mainThreadHandler.sendMessage(msg);
            }
            return true;
        }
        //省略部分代码……
        return true;
    };
    
  • 然后主线程中处理接受的消息,这里仅仅展示部分代码,具体逻辑请下载demo查看。
    @SuppressLint("HandlerLeak")
    private class MyHandler extends Handler {
    
        private WeakReference<Context> mContextReference;
    
        MyHandler(Context context) {
            super(Looper.getMainLooper());
            mContextReference = new WeakReference<>(context);
        }
    
        @Override
        public void handleMessage(Message msg) {
            final Context context = mContextReference.get();
            if (context != null) {
                switch (msg.what) {
                    case HANDLE_MESSAGE:
                        WvWebView.this.handleMessage((String) msg.obj);
                        break;
                    default:
                        break;
                }
            }
        }
    }
    

5.1.9 Cookie同步场景和具体操作

  • 场景:C/S->B/S Cookie同步
    • 在Android混合应用开发中,一般来说,有些页面通过Native方式实现,有些通过WebView实现。对于登录而已,假设我们通过Native登录,需要把SessionID传递给WebView,这种情况需要同步。
  • 场景:不同域名之间的Cookie同步
    • 对于分布式应用,或者灰度发布的应用,他的缓存服务器是同一个,那么域名的切换时会导致获取不到sessionID,因此,不同域名也需要特别处理。
    public static void synCookies(Context context, String url) {
        try {
            if (!UserManager.hasLogin()) {  
                //判断用户是否已经登录
                setCookie(context, url, "SESSIONID", "invalid");
            } else {
                setCookie(context, url, "SESSIONID", SharePref.getSessionId());
    
            }
        } catch (Exception e) {
        }
    }
    
    private static void setCookie( Context context,String url, String key, String value) {
        if (Build.VERSION.SDK_INT < 21) {
            CookieSyncManager.createInstance(context);
        }
        CookieManager cookieManager = CookieManager.getInstance();
        cookieManager.setAcceptCookie(true);  //同样允许接受cookie
        URL pathInfo = new URL(url);
        String[] whitelist = new String{".abc.com","abc.cn","abc.com.cn"}; //白名单
        String domain = null;
    
        for(int i=0;i<whitelist.length;i++){
            if(pathInfo.getHost().endsWith(whitelist[i])){
                domain  = whitelist[i];
                break;
            }
        }
        if(TextUtils.isEmpty(domain)) return; //不符合白名单的不同步cookie
        StringBuilder sbCookie = new StringBuilder();
        sbCookie.append(String.format("%s=%s", key, value));
        sbCookie.append(String.format(";Domain=%s", domain));
        sbCookie.append(String.format(";path=%s", "/"));
        String cookieValue = sbCookie.toString();
        cookieManager.setCookie(url, cookieValue);
        if (Build.VERSION.SDK_INT < 21) {
            CookieSyncManager.getInstance().sync();
        } else {
            CookieManager.getInstance().flush();
        }
    }
    
    • 调用位置:shouldOverrideUrlLoading中调用
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        //同步,当然还可以优化,如果前一个域名和后一个域名不同时我们再同步
        syncCookie(view.getContext(),url);   
        //省略代码……
    }
    
  • 场景三:从B/S->C/S
    • 这种情况多发生于第三方登录,但是获取cookie通过CookieManager即可,但是的好时机是页面加载完成。
    • 注意:在页面加载之前一定要调用cookieManager.setAcceptCookie(true); 允许接受cookie,否则可能产生问题。
    public void onPageFinished(WebView view, String url) {  
        CookieManager cookieManager = CookieManager.getInstance();  
        String CookieStr = cookieManager.getCookie(url);  
        LogUtil.i("Cookies", "Cookies = " + CookieStr);  
        super.onPageFinished(view, url);  
    } 
    

5.2.0 shouldOverrideUrlLoading处理多类型

  • 这个方法很强大,平时一般很少涉及处理多类型,比如电子邮件类型,地图类型,选中的文字类型等等。我在X5WebViewClient中的shouldOverrideUrlLoading方法中只是处理了未知类型
    • 还是很可惜,没遇到过复杂的h5页面,需要处理各种不同类型。这里只是简单介绍一下,知道就可以呢。
    /**
     * 这个方法中可以做拦截
     * 主要的作用是处理各种通知和请求事件
     * 返回值是true的时候控制去WebView打开,为false调用系统浏览器或第三方浏览器
     * @param view                              view
     * @param url                               链接
     * @return                                  是否自己处理,true表示自己处理
     */
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        //页面关闭后,直接返回,不要执行网络请求和js方法
        boolean activityAlive = X5WebUtils.isActivityAlive(context);
        if (!activityAlive){
            return false;
        }
        if (TextUtils.isEmpty(url)) {
            return false;
        }
        try {
            url = URLDecoder.decode(url, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        WebView.HitTestResult hitTestResult = null;
        if (url.startsWith("http:") || url.startsWith("https:")){
            hitTestResult = view.getHitTestResult();
        }
        if (hitTestResult == null) {
            return false;
        }
        //HitTestResult 描述
        //WebView.HitTestResult.UNKNOWN_TYPE 未知类型
        //WebView.HitTestResult.PHONE_TYPE 电话类型
        //WebView.HitTestResult.EMAIL_TYPE 电子邮件类型
        //WebView.HitTestResult.GEO_TYPE 地图类型
        //WebView.HitTestResult.SRC_ANCHOR_TYPE 超链接类型
        //WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE 带有链接的图片类型
        //WebView.HitTestResult.IMAGE_TYPE 单纯的图片类型
        //WebView.HitTestResult.EDIT_TEXT_TYPE 选中的文字类型
        if (hitTestResult.getType() == WebView.HitTestResult.UNKNOWN_TYPE) {
            return false;
        }
        return super.shouldOverrideUrlLoading(view, url);
    }
    

5.2.1 WebView独立进程解决方案

5.2.2 截取WebView屏幕的整个可视区域

  • 有两种方法,第一种是截取activity的可见区域的视图;第二种是根据View的宽高去draw视图
  • 第一种是截取activity的可见区域的视图
    /**
     * 截屏,截取activity的可见区域的视图
     * @param activity
     * @return
     */
    public static Bitmap activityShot(Activity activity) {
        /*获取windows中最顶层的view*/
        View view = activity.getWindow().getDecorView();
        //允许当前窗口保存缓存信息
        view.setDrawingCacheEnabled(true);
        view.buildDrawingCache();
        //获取状态栏高度
        Rect rect = new Rect();
        view.getWindowVisibleDisplayFrame(rect);
        int statusBarHeight = rect.top;
        WindowManager windowManager = activity.getWindowManager();
        //获取屏幕宽和高
        DisplayMetrics outMetrics = new DisplayMetrics();
        windowManager.getDefaultDisplay().getMetrics(outMetrics);
        int width = outMetrics.widthPixels;
        int height = outMetrics.heightPixels;
        //去掉状态栏
        Bitmap bitmap = Bitmap.createBitmap(view.getDrawingCache(), 0, statusBarHeight, width, height - statusBarHeight);
        //销毁缓存信息
        view.destroyDrawingCache();
        view.setDrawingCacheEnabled(false);
        return bitmap;
    }
    
  • 第二种是根据View的宽高去draw视图,这里提供截取RelativeLayout代码。其他的可以看项目demo。
    /**
     * 截取RelativeLayout
     **/
    public static Bitmap getRelativeLayoutBitmap(RelativeLayout relativeLayout) {
        int h = 0;
        Bitmap bitmap;
        for (int i = 0; i < relativeLayout.getChildCount(); i++) {
            h += relativeLayout.getChildAt(i).getHeight();
        }
        // 创建对应大小的bitmap
        bitmap = Bitmap.createBitmap(relativeLayout.getWidth(), h, Bitmap.Config.ARGB_8888);
        final Canvas canvas = new Canvas(bitmap);
        relativeLayout.draw(canvas);
        return bitmap;
    }
    

5.2.3 截取WebView屏幕长图效果

  • 直接提供代码,如下所示:实际开发中建议用这种去截取长图,不仅仅使用webView,还适用LinearLayout,RelativeLayout等。
    /**
     * 计算view的大小
     */
    public static Bitmap measureSize(Activity activity, View view) {
        //将布局转化成view对象
        View viewBitmap = view;
        WindowManager manager = activity.getWindowManager();
        DisplayMetrics outMetrics = new DisplayMetrics();
        manager.getDefaultDisplay().getMetrics(outMetrics);
        int width = outMetrics.widthPixels;
        int height = outMetrics.heightPixels;
        //然后View和其内部的子View都具有了实际大小,也就是完成了布局,相当与添加到了界面上。
        //接着就可以创建位图并在上面绘制
        return layoutView(viewBitmap, width, height);
    }
    
    /**
     * 填充布局内容
     */
    private static  Bitmap layoutView(final View viewBitmap, int width, int height) {
        // 整个View的大小 参数是左上角 和右下角的坐标
        viewBitmap.layout(0, 0, width, height);
        int measuredWidth = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
        int measuredHeight = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.UNSPECIFIED);
        viewBitmap.measure(measuredWidth, measuredHeight);
        viewBitmap.layout(0, 0, viewBitmap.getMeasuredWidth(), viewBitmap.getMeasuredHeight());
        viewBitmap.setDrawingCacheEnabled(true);
        viewBitmap.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);
        viewBitmap.setDrawingCacheBackgroundColor(Color.WHITE);
        // 把一个View转换成图片
        Bitmap cachebmp = viewConversionBitmap(viewBitmap);
        viewBitmap.destroyDrawingCache();
        return cachebmp;
    }
    
    /**
     * view转bitmap
     */
    private static Bitmap viewConversionBitmap(View v) {
        int w = v.getWidth();
        int h = 0;
        if (v instanceof LinearLayout){
            LinearLayout linearLayout = (LinearLayout) v;
            for (int i = 0; i < linearLayout.getChildCount(); i++) {
                h += linearLayout.getChildAt(i).getHeight();
            }
        } else if (v instanceof RelativeLayout){
            RelativeLayout relativeLayout = (RelativeLayout) v;
            for (int i = 0; i < relativeLayout.getChildCount(); i++) {
                h += relativeLayout.getChildAt(i).getHeight();
            }
        } else {
            h = v.getHeight();
        }
        Bitmap bmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(bmp);
        //如果不设置canvas画布为白色,则生成透明
        c.drawColor(Color.WHITE);
        v.layout(0, 0, w, h);
        v.draw(c);
        return bmp;
    }
    

5.2.4 Android P加载X5内核失败优化

  • 关于Android 9:绝不多数手机是可以直接初始化成功的,但是到了Android 9,有人反馈部分手机初始化直接失败。
  • 具体原因呢是因为从Android 6.0开始引入了对Https的推荐支持,与以往不同,Android P的系统上面默认所有Http的请求都被阻止了。
  • 如何修改,代码如下所示。
    <application
        ...
        android:usesCleartextTraffic="true"
        ...>
        ...
    </application>
    

5.2.5 WebView流量轻量优化

5.2.6 onReceivedTitle执行多次

  • onReceivedTitle(WebView view, String title)拿标题
    • 这个方法在网页回退时是无法拿到正确的上一级标题的,网上的处理方法是自己维护一个List去缓存标题,在执行完webView.goBack()后,移除List的最后一条,再将新的最后一条设置给标题栏。
    • 而且onReceivedTitle方法在一个页面打开时并不是仅调用一次,而是多次调用。
    /**
     * 获取标题
     * @param view                  view
     * @param title                 title
     */
    private void getWebTitle(WebView view, String title){
        /*if (mBasisView!=null){
            mBasisView.setTitle(title);
        }*/
        LogUtils.i("-------onReceivedTitle-----1--"+title);
        WebBackForwardList forwardList = view.copyBackForwardList();
        WebHistoryItem item = forwardList.getCurrentItem();
        if (item != null) {
            if (mBasisView!=null){
                mBasisView.setTitle(item.getTitle());
                LogUtils.i("-------onReceivedTitle----2---"+item.getTitle());
            }
        }
    }
    

5.2.7 WebView自定义长按选择弹窗

5.2.8 关于下拉刷新重新加载web页面优化

  • webView滑动到顶部,并且web已经加载结束后才能触发下拉刷新重新加载页面
    mSwipeRefreshContainer.setOnRefreshListener(new OnRefreshListener() {
        @Override
        public void onRefresh() {
            if (NetworkUtils.isNetworkAvailable()){
                //个人觉得下拉刷新更新web页面体验不太友好,参考qq浏览器,百度浏览器等,是点击触发刷新按钮刷新web
                //先结束下拉刷新,然后在加载web页面。
                //建议下拉刷新结束后加载web,不建议下拉刷新动画中就加载web
                if (mWebView!=null && mWebView.isTop() && mWebView.getWebViewClient().isLoadFinish()){
                    mSwipeRefreshContainer.setRefreshing(false);
                    mAgentWeb.reLoad();
                }
                //mSwipeRefreshContainer.setRefreshing(false);
            } else {
                mPaddingView.setViewState(NET);
            }
        }
    });