美文网首页
WebView系列2--视口、管理 Cookies、缓存、预加载

WebView系列2--视口、管理 Cookies、缓存、预加载

作者: 凯玲之恋 | 来源:发表于2018-12-09 00:27 被阅读36次

    1 视口(viewport)

    https://developer.android.com/guide/webapps/targeting.html
    https://developer.mozilla.org/en-US/docs/Mozilla/Mobile/Viewport_meta_tag
    https://developer.mozilla.org/zh-CN/docs/Web/CSS/@viewport

    视口是一个为网页提供绘图区域的矩形。

    你可以指定数个视口属性,比如尺寸和初始缩放系数(initial scale)。其中最重要的是视口宽度,它定义了网页水平方向的可用像素总数(可用的CSS像素数)。

    多数 Android 上的网页浏览器(包括 Chrome)设置默认视口为一个大尺寸(被称为”wide viewport mode”,宽约 980px)。

    也有许多浏览器默认会尽可能缩小以显示完整的视口宽度(被称为”overview mode“)。

    // 是否支持viewport属性,默认值 false
    // 页面通过`<meta name="viewport" ... />`自适应手机屏幕
    // 当值为true且viewport标签不存在或未指定宽度时使用 wide viewport mode
    settings.setUseWideViewPort(true);
    // 是否使用overview mode加载页面,默认值 false
    // 当页面宽度大于WebView宽度时,缩小使页面宽度等于WebView宽度
    settings.setLoadWithOverviewMode(true);
    

    viewport 语法

    <meta name="viewport"
          content="
              height = [pixel_value | "device-height"] ,
              width = [pixel_value | "device-width"] ,
              initial-scale = float_value ,
              minimum-scale = float_value ,
              maximum-scale = float_value ,
              user-scalable = ["yes" | "no"]
              " />
    

    指定视口宽度精确匹配设备屏幕宽度同时禁用了缩放

    <head>
        <title>Example</title>
        <meta name="viewport" content="width=device-width, user-scalable=no" />
    </head>
    

    通过WebView设置初始缩放(initial-scale)

    // 设置初始缩放百分比
    // 0表示依赖于setUseWideViewPort和setLoadWithOverviewMode
    // 100表示不缩放
    web.setInitialScale(0)
    

    2 管理 Cookies

    https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Cookies

    Cookie 是服务器发送到用户浏览器并保存在浏览器上的一块数据,它会在浏览器下一次发起请求时被携带并发送到服务器上。

    可通过Cookie保存浏览信息来获得更轻松的在线体验,比如保持登录状态、记住偏好设置,并提供本地的相关内容。

    2.1 会话Cookie 与 持久Cookie

    • 会话cookie不需要指定Expires和Max-Age,浏览器关闭之后它会被自动删除。
    • 持久cookie指定了Expires或Max-Age,会被存储到磁盘上,不会因浏览器而失效。

    2.2 第一方Cookie 与 第三方Cookie

    每个Cookie都有与之关联的域,与页面域一样的就是第一方Cookie,不一样的就是第三方Cookie。

    // 设置接收第三方Cookie
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        CookieManager.getInstance().setAcceptThirdPartyCookies(vWeb, true);
    }
    

    2.3 读取/写入/移除 Cookie

    // 获取指定url关联的所有Cookie
    // 返回值使用"Cookie"请求头格式:"name=value; name2=value2; name3=value3"
    CookieManager.getInstance().getCookie(url);
    // 为指定的url设置一个Cookie
    // 参数value使用"Set-Cookie"响应头格式,参考:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Set-Cookie
    CookieManager.getInstance().setCookie(url, value);
    // 移除指定url下的指定Cookie
    CookieManager.getInstance().setCookie(url, cookieName + "=");
    

    2.4 webkit cookie 工具类

    public class WebkitCookieUtil { 
        // 移除指定url关联的所有cookie
        public static void remove(String url) {
            CookieManager cm = CookieManager.getInstance();
            for (String cookie : cm.getCookie(url).split("; ")) {
                cm.setCookie(url, cookie.split("=")[0] + "=");
            }
            flush();
        }
        // sessionOnly 为true表示移除所有会话cookie,否则移除所有cookie
        public static void remove(boolean sessionOnly) {
            CookieManager cm = CookieManager.getInstance();
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                if (sessionOnly) {
                    cm.removeSessionCookies(null);
                } else {
                    cm.removeAllCookies(null);
                }
            } else {
                if (sessionOnly) {
                    cm.removeSessionCookie();
                } else {
                    cm.removeAllCookie();
                }
            }
            flush();
        }
        // 写入磁盘
        public static void flush() {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                CookieManager.getInstance().flush();
            } else {
                CookieSyncManager.getInstance().sync();
            }
        }
    }
    

    2.5 同步系统Cookie 与 Webkit Cookie

    // 将系统级Cookie(比如`new URL(...).openConnection()`的Cookie) 同步到 WebView
    public class WebkitCookieHandler extends CookieHandler {
        private static final String TAG = WebkitCookieHandler.class.getSimpleName();
        private CookieManager wcm;
        public WebkitCookieHandler() {
            this.wcm = CookieManager.getInstance();
        }
        @Override
        public void put(URI uri, Map<String, List<String>> headers) throws IOException {
            if ((uri == null) || (headers == null)) {
                return;
            }
            String url = uri.toString();
            for (String headerKey : headers.keySet()) {
                if ((headerKey == null) || !(headerKey.equalsIgnoreCase("set-cookie2") || headerKey.equalsIgnoreCase("set-cookie"))) {
                    continue;
                }
                for (String headerValue : headers.get(headerKey)) {
                    Log.e(TAG, headerKey + ": " + headerValue);
                    this.wcm.setCookie(url, headerValue);
                }
            }
        }
        @Override
        public Map<String, List<String>> get(URI uri, Map<String, List<String>> headers) throws IOException {
            if ((uri == null) || (headers == null)) {
                throw new IllegalArgumentException("Argument is null");
            }
            String url = uri.toString();
            String cookie = this.wcm.getCookie(url);
            Log.e(TAG, "cookie: " + cookie);
            if (cookie != null) {
                return Collections.singletonMap("Cookie", Arrays.asList(cookie));
            } else {
                return Collections.emptyMap();
            }
        }
    }
    

    3 缓存(Cache)

    设置缓存模式

    • WebSettings.LOAD_DEFAULT 根据cache-control决定是否从网络上取数据
    • WebSettings.LOAD_CACHE_ELSE_NETWORK 无网,离线加载,优先加载缓存(即使已经过期)
    • WebSettings.LOAD_NO_CACHE 仅从网络加载
    • WebSettings.LOAD_CACHE_ONLY 仅从缓存加载
    // 网络正常时根据cache-control决定是否从网络上取数据 
    if (isNetworkConnected(mActivity)) {
        settings.setCacheMode(WebSettings.LOAD_DEFAULT); 
    } else {
        settings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); 
    }
    

    清除缓存

    // 传入true表示同时内存与磁盘,false表示仅清除内存
    // 由于内核缓存是全局的因此这个方法不仅仅针对webview而是针对整个应用程序
    web.clearCache(true);
    

    4 预加载(Preload)

    一个简单的预加载示例(shouldInterceptRequest)
    点击 assets/demo.xml 里的链接”hello”时会加载本地的 assets/hello.html

    assets/demo.xml

    <html>
    <body>
    <a href="http://demo.com/assets/hello.html">hello</a>
    </body>
    </html>
    

    assets/hello.html

    <html>
    <body>
    hello world!
    </body>
    </html>
    

    重载 shouldInterceptRequest

    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
        return preload("assets/", url);
    }
    WebResourceResponse preload(String path, String url) { 
        if (!url.contains(path)) {
            return null;
        }
        String local = url.replaceFirst("^http.*" + path, ""); 
        try {
            InputStream is = getApplicationContext().getAssets().open(local);
            String ext = MimeTypeMap.getFileExtensionFromUrl(local);
            String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
            return new WebResourceResponse(mimeType, "UTF-8", is);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        } 
    }
    

    5 与Javascript交互

    启用Javascript

    // 是否支持Javascript,默认值false
    settings.setJavaScriptEnabled(true);
    

    注入对象到Javascript

    // 注入对象'jsobj',在网页中通过`jsobj.say(...)`调用
    web.addJavascriptInterface(new JSObject(), "jsobj")
    
    

    在API17后支持白名单,只有添加了@JavascriptInterface注解的方法才会注入JS

    public class JSObject {
        @JavascriptInterface
        public void say(String words) {
          // todo
        }
    }
    

    移除已注入Javascript的对象

    web.removeJavascriptInterface("jsobj")
    

    执行JS表达式

    // 弹出提示框
    web.loadUrl("javascript:alert('hello')");
    // 调用注入的jsobj.say方法
    web.loadUrl("javascript:jsobj.say('hello')");
    

    在API19后可异步执行JS表达式,并通过回调返回值

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        vWeb.evaluateJavascript("111+222", new ValueCallback<String>() {
            @Override
            public void onReceiveValue(String value) {
                // value => "333"
            }
        });
    }
    

    6 地理位置(Geolocation)

    https://developer.mozilla.org/zh-CN/docs/Web/API/Geolocation/Using_geolocation

    需要以下权限

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    

    默认可用

    settings.setGeolocationEnabled(true);
    

    当H5调用地理位置API时,会先通过WebChromeClient.onGeolocationPermissionsShowPrompt申请授权

    // 指定源的网页内容在没有设置权限状态下尝试使用地理位置API。 
    @Override
    public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
        boolean allow = true;   // 是否允许origin使用定位API
        boolean retain = false; // 内核是否记住这次制授权
        callback.invoke(origin, true, false);
    }
    // 之前调用 onGeolocationPermissionsShowPrompt() 申请的授权被取消时,隐藏相关的UI。
    @Override
    public void onGeolocationPermissionsHidePrompt() {
    }
    

    注:从API24开始,仅支持安全源(https)的请求,非安全源的请求将自动拒绝且不调用 onGeolocationPermissionsShowPrompt 与 onGeolocationPermissionsHidePrompt

    7 弹框(alert/confirm/prompt/onbeforeunload)

    在javascript中使用 alert/confirm/prompt 会弹出对话框,可通过重载 WebChromeClient 的下列方法控制弹框的交互,比如替换系统默认的对话框或屏蔽这些对话框

    @Override
    public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
        // 这里处理交互逻辑
        // result.cancel(); 表示用户取消了操作(点击了取消按钮)
        // result.confirm(); 表示用户确认了操作(点击了确认按钮)
        // ...
        // 返回true表示自已处理,返回false表示由系统处理
        return false; 
    } 
    @Override
    public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
        return false;
    } 
    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        return false;
    }
    @Override
    public boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result) {
        return false;
    }
    

    8 全屏(Fullscreen)

    Fullscreen API

    https://developer.mozilla.org/zh-CN/docs/DOM/Using_fullscreen_mode

    • 当H5请求全屏时,会回调 WebChromeClient.onShowCustomView 方法
    • 当H5退出全屏时,会回调 WebChromeClient.onHideCustomView 方法
      1.manifest
      自己处理屏幕尺寸方向的变化(切换屏幕方向时不重建activity)
      WebView播放视频需要开启硬件加速
    <activity
        android:name=".WebViewActivity"
        android:configChanges="orientation|screenSize"
        android:hardwareAccelerated="true"
        android:screenOrientation="portrait" />
    

    2.页面布局

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            style="@style/Toolbar.Back"/>
        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <WebView
                android:id="@+id/web"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>
            ...
        </FrameLayout>
    </LinearLayout>
    

    3.处理全屏回调

    CustomViewCallback mCallback;
    View vCustom;
    @Override
    public void onShowCustomView(View view, CustomViewCallback callback) {
        setFullscreen(true);
        vCustom = view;
        mCallback = callback;
        if (vCustom != null) {
            ViewGroup parent = (ViewGroup) vWeb.getParent();
            parent.addView(vCustom);
        }
    }
    @Override
    public void onHideCustomView() {
        setFullscreen(false);
        if (vCustom != null) {
            ViewGroup parent = (ViewGroup) vWeb.getParent();
            parent.removeView(vCustom);
            vCustom = null;
        }
        if (mCallback != null) {
            mCallback.onCustomViewHidden();
            mCallback = null;
        } 
    }
    

    4.设置全屏,切换屏幕方向

    void setFullscreen(boolean fullscreen) { 
        if (fullscreen) {
            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
            getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
            vToolbar.setVisibility(View.GONE);
            vWeb.setVisibility(View.GONE);
        } else {
            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
            getWindow().setFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN, WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
            vToolbar.setVisibility(View.VISIBLE);
            vWeb.setVisibility(View.VISIBLE);
        }
        if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        } else {
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
        }
    }
    

    9 内存泄漏

    直接 new WebView 并传入 application context 代替在 XML 里面声明以防止 activity 引用被滥用,能解决90+%的 WebView 内存泄漏。

    vWeb =  new WebView(getContext().getApplicationContext());
    container.addView(vWeb);
    

    销毁 WebView

    if (vWeb != null) {
        vWeb.setWebViewClient(null);
        vWeb.setWebChromeClient(null);
        vWeb.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
        vWeb.clearHistory();
        ((ViewGroup) vWeb.getParent()).removeView(vWeb);
        vWeb.destroy();
        vWeb = null;
    }
    

    参考

    Android WebView 详解

    相关文章

      网友评论

          本文标题:WebView系列2--视口、管理 Cookies、缓存、预加载

          本文链接:https://www.haomeiwen.com/subject/cvlchqtx.html