WebView详解

WebSettings:对WebView进行配置和管理
    WebSettings webSettings = webView.getSettings();
    //如果访问的页面中要与Javascript交互,则webview必须设置支持Javascript
    webSettings.setJavaScriptEnabled(true);

    //设置自适应屏幕,两者合用
    //将图片调整到适合webview的大小
    webSettings.setUseWideViewPort(true); 
    // 缩放至屏幕的大小
    webSettings.setLoadWithOverviewMode(true); 

    //支持内容重新布局 
     webSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);

    //支持缩放,默认为true。是下面那个的前提。
    webSettings.setSupportZoom(true);
    //设置内置的缩放控件。若为false,则该WebView不可缩放
    webSettings.setBuiltInZoomControls(true); 
    //隐藏原生的缩放控件
    webSettings.setDisplayZoomControls(false); 
    //设置文本的缩放倍数,默认为 100
    webSettings.setTextZoom(2);
   
    //提高渲染的优先级
    webSettings.setRenderPriority(WebSettings.RenderPriority.HIGH);  
    /设置 WebView 的字体,默认字体为 "sans-serif"
    webSettings.setStandardFontFamily("");
    //设置 WebView 字体的大小,默认大小为 16
    webSettings.setDefaultFontSize(20);
    //设置 WebView 支持的最小字体大小,默认为 8
    webSettings.setMinimumFontSize(12);

    // 5.1以上默认禁止了https和http混用,以下方式是开启
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
    }
    //设置可以访问文件,当需要选择图片时,需要打开
    webSettings.setAllowFileAccess(true);
    //支持通过JS打开新窗口
    webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
    //支持自动加载图片
    webSettings.setLoadsImagesAutomatically(true); 
    //设置编码格式
    webSettings.setDefaultTextEncodingName("utf-8");
    //允许网页执行定位操作
    webSettings.setGeolocationEnabled(true);
    //设置User-Agent
    webSettings.setUserAgentString("Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0");

    //不允许访问本地文件(不影响assets和resources资源的加载)
    webSettings.setAllowFileAccess(false);
    webSettings.setAllowFileAccessFromFileURLs(false);
    webSettings.setAllowUniversalAccessFromFileURLs(false);

    //缓存模式如下:
    //LOAD_CACHE_ONLY: 不使用网络,只读取本地缓存数据
    //LOAD_DEFAULT: (默认)根据cache-control决定是否从网络上取数据。
    //LOAD_NO_CACHE: 不使用缓存,只从网络获取数据.
    //LOAD_CACHE_ELSE_NETWORK,只要本地有,无论是否过期,或者no-cache,都使用缓存中的数据。

    //优先使用缓存:
    webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
    //不使用缓存:
    webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE)
WebSettings:WebView输入框被遮挡处理

  当我们在WebView中使用输入框往往会出现被遮挡情况

  首先,页面是非全屏模式的情况下,给activity设置adjustPan会失效。
  其次,页面是全屏模式的情况,adjustPan跟adjustResize都会失效。

——解释一下,这里的全屏模式即是页面是全屏的,包括Application或activity使用了Fullscreen主题、使用了『状态色着色』、『沉浸式状态栏』、『Immersive Mode』等等——总之,基本上只要是App自己接管了状态栏的控制,就会产生这种问题。

  详情可参考:AndroidBug5497Workaround

  这种坑我们命名为5467,我们可以使用 AndroidBug5497Workaround 这个类直接操作,
看名字就知道,它是专门用来对付"5497"问题的,使用步骤也是超级简单:

  把AndroidBug5497Workaround类复制到项目中
  在需要填坑的activity的onCreate方法中添加一句AndroidBug5497Workaround.assistActivity(this)即可。

  提供一下这个类:

public class AndroidBug5497Workaround {

    // For more information, see https://code.google.com/p/android/issues/detail?id=5497
    // To use this class, simply invoke assistActivity() on an Activity that already has its content view set.

    public static void assistActivity (Activity activity) {
        new AndroidBug5497Workaround(activity);
    }

    private View mChildOfContent;
    private int usableHeightPrevious;
    private FrameLayout.LayoutParams frameLayoutParams;

    private AndroidBug5497Workaround(Activity activity) {
        FrameLayout content = (FrameLayout) activity.findViewById(android.R.id.content);
        mChildOfContent = content.getChildAt(0);
        mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            public void onGlobalLayout() {
                possiblyResizeChildOfContent();
            }
        });
        frameLayoutParams = (FrameLayout.LayoutParams) mChildOfContent.getLayoutParams();
    }

    private void possiblyResizeChildOfContent() {
        int usableHeightNow = computeUsableHeight();
        if (usableHeightNow != usableHeightPrevious) {
            int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
            int heightDifference = usableHeightSansKeyboard - usableHeightNow;
            if (heightDifference > (usableHeightSansKeyboard/4)) {
                // keyboard probably just became visible
                frameLayoutParams.height = usableHeightSansKeyboard - heightDifference;
            } else {
                // keyboard probably just became hidden
                frameLayoutParams.height = usableHeightSansKeyboard;
            }
            mChildOfContent.requestLayout();
            usableHeightPrevious = usableHeightNow;
        }
    }

    private int computeUsableHeight() {
        Rect r = new Rect();
        mChildOfContent.getWindowVisibleDisplayFrame(r);
        return (r.bottom - r.top);// 全屏模式下: return r.bottom
    }

}

  分析一下:

FrameLayout content = (FrameLayout) activity.findViewById(android.R.id.content);
mChildOfContent = content.getChildAt(0);

  其中,第一行中的android.R.id.content所指的View,是Android所有Activity界面上开发者所能控制的区域的根View。

如果Activity是全屏模式,那么android.R.id.content就是占满全部屏幕区域的。
如果Activity是普通的非全屏模式,那么android.R.id.content就是占满除状态栏之外的所有区域。
其他情况,如Activity是弹窗、或者7.0以后的分屏样式等,android.R.id.content也是弹窗的范围或者分屏所在的半个屏幕——这些情况较少,就暂且不考虑了。

  然后通过

mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener({ 
        possiblyResizeChildOfContent();
});

  对我们的视图进行监听,当软键盘弹出时会触发我们这个事件。
  然后获取我们当前界面可用的高度

 private int computeUsableHeight() {
        Rect rect = new Rect();
        mChildOfContent.getWindowVisibleDisplayFrame(rect);
        // rect.top其实是状态栏的高度,如果是全屏主题,直接 return rect.bottom就可以了
        return (rect.bottom - rect.top);
    }

  View.getWindowVisibleDisplayFrame(Rect rect),这行代码能够获取到的Rect——就是界面除去了标题栏、除去了被软键盘挡住的部分,所剩下的矩形区域.
  所以,最后一步,就是把界面高度置为可用高度——大功告成。

rivate void possiblyResizeChildOfContent() {
        int usableHeightNow = computeUsableHeight();
        if (usableHeightNow != usableHeightPrevious) {
            int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
            int heightDifference = usableHeightSansKeyboard - usableHeightNow;
            if (heightDifference > (usableHeightSansKeyboard/4)) {
                // keyboard probably just became visible
                frameLayoutParams.height = usableHeightSansKeyboard - heightDifference;
            } else {
                // keyboard probably just became hidden
                frameLayoutParams.height = usableHeightSansKeyboard;
            }
            mChildOfContent.requestLayout();
            usableHeightPrevious = usableHeightNow;
        }
    }

总结起来,就是这样:
普通Activity(不带WebView),直接使用adjustpan或者adjustResize
如果带WebView:
a) 如果非全屏模式,可以使用adjustResize
b) 如果是全屏模式,则使用AndroidBug5497Workaround进行处理。

WebView添加Header

  在某些情况下我们可能需要通过给请求的url添加请求头来产生会话,比如app跳转到html需要html快速登录,就可以将token添加在请求头中,下面有俩种方式可以:
  1:如果只需在初始加载的时候添加Header,那么比较简单,只需要这么写即可:

     Map<String, String> header = new HashMap<>();
     header.put("token", "token");
     webView.loadUrl(url, header);

  2: 那如果每个页面都要做Header验证,那么就要重写WebViewClient的shouldInterceptRequest方法了:

private WebViewClient mWebViewClient = new WebViewClient() {

        //API until level 21
        @Override
        public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
            //API 21以下,此处不考虑兼容性
            return super.shouldInterceptRequest(view, url);
        }

        //API 21+
        @TargetApi(Build.VERSION_CODES.LOLLIPOP)
        @Override
        public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
            String url = request.getUrl().toString();
            return getNewResponse(url, request.getRequestHeaders());
        }

        private WebResourceResponse getNewResponse(String url, Map<String, String> headers) {

            try {
                OkHttpClient httpClient = new OkHttpClient();
                Request.Builder builder = new Request.Builder()
                        .url(url.trim())
                        .addHeader("token", "token");

                Set<String> keySet = headers.keySet();
                for (String key : keySet) {
                    builder.addHeader(key, headers.get(key));
                }

                Request request = builder.build();
                final Response response = httpClient.newCall(request).execute();
                String conentType = response.header("Content-Type", response.body().contentType().type());
                String temp = conentType.toLowerCase();
                if (temp.contains("charset=utf-8")) {
                    conentType = conentType.replaceAll("(?i)" + "charset=utf-8", "");//不区分大小写的替换
                }
                if (conentType.contains(";")) {
                    conentType = conentType.replaceAll(";", "");
                    conentType = conentType.trim();
                }
                return new WebResourceResponse(
                        conentType,
                        response.header("Content-Encoding", "utf-8"),
                        response.body().byteStream()
                );

            } catch (Exception e) {
                return null;
            }

        }
    };

  拦截了请求并获取到了全部请求头,再通过builder添加我们需要的header即可,当然WebSettings也提供了一种特殊的头部,我们可以通过获取WebView的WebSettings进行设置,如下:

webSettings.setUserAgentString("user_Agent")

  需要和后台进行约定规则使用即可。

WebView实现上传图片

  这里我推荐大家直接重写WebChromeClient即可,代码如下:

public class MyChromeClient extends WebChromeClient {

    private FileCall fileCall;

    public MyChromeClient(FileCall fileCall) {
        this.fileCall = fileCall;
    }

    /**
     * Android 3.0+
     */
    public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) {
        if (fileCall != null)
            fileCall.fileChoseLow(uploadMsg);
    }

    /**
     * Android < 3.0
     */
    public void openFileChooser(ValueCallback<Uri> uploadMsg) {
        openFileChooser(uploadMsg, "");
    }

    /**
     * Android > 4.1.1
     * */
    public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
        openFileChooser(uploadMsg, acceptType);
    }

    /**
     * Android > 5.0
     * */
    @Override
    public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
        if (fileCall != null)
            fileCall.fileChoseHigh(filePathCallback);
        return true;
    }

    /**
     * 文件选择点击回调
     * */
    public interface FileCall {

        //5.0以下点击网页中File Input标签时回调
        void fileChoseLow(ValueCallback<Uri> uploadMsg);

        //5.0和5.0以上点击网页中File Input标签时回调
        void fileChoseHigh(ValueCallback<Uri[]> uploadMsg);
    }

}

  需要重写多个方法,有的同学可能注意到了,有了方法没有被@Override修饰啊,这是因为这几个方法都是隐藏方法,我们直接写就可以,最终都通过FileCall接口把操作传出去,再看下我们的主类。

public class MyViewActivity extends Activity implements MyChromeClient.FileCall {

    private WebView webView;
    private WebSettings webSettings;
    private ValueCallback<Uri> valueCallback;
    private ValueCallback<Uri[]> valueCallbacks;
    public final static int FILECHOOSER_RESULTCODE = 1;
    public final static int FILECHOOSER_RESULTCODE_FOR_ANDROID_5 = 2;

    //调用系统拍照时,拍照后图片的存放路径
    private String mCameraFilePath;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_web_view);
        initView();
        initData();
    }

    private void initView() {
        webView = findViewById(R.id.web_view);
    }

    private void initData() {
        webSettings = webView.getSettings();
        //选择图片等文件需要js的支持
        webSettings.setJavaScriptEnabled(true);
        //打开本地缓存提供JS调用
        webSettings.setDomStorageEnabled(true);
        //解决图片不显示
        webSettings.setBlockNetworkImage(false);
        //5.0开始WebView默认不允许混合模式,https当中不能加载http资源,需要设置开启
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
            webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
        }
        //避免打开外部浏览器
        webView.setWebViewClient(new WebViewClient());
        //拦截文件选择的操作
        webView.setWebChromeClient(new MyChromeClient(this));
        webView.loadUrl("填写需要上传文件的路径");
    }

    @Override
    public void fileChoseLow(ValueCallback<Uri> uploadMsg) {
        openFileChooserImpl(uploadMsg);
    }

    @Override
    public void fileChoseHigh(ValueCallback<Uri[]> uploadMsg) {
        openFileChooserImplForAndroid5(uploadMsg);
    }

    /**
     * 选择图片回调
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == FILECHOOSER_RESULTCODE) {
            //5.0以下,选择图片后回调
            if (null == valueCallback) return;
            Uri result = null;
            if (resultCode == RESULT_OK) {
                result = data == null ? null : data.getData();
                if (result != null) {
                    //选择图库
                    result = FileSwitchUtil.getUri(this, new File(FileSwitchUtil.getRealFilePath(this, result)));
                } else {
                    //拍照
                    result = FileSwitchUtil.getUri(this, new File(mCameraFilePath));
                }
            }
            //没有选择也要调用一次onReceiveValue方法,否则下次点击上传按钮没反应
            valueCallback.onReceiveValue(result);
            valueCallback = null;
            mCameraFilePath = null;
        } else if (requestCode == FILECHOOSER_RESULTCODE_FOR_ANDROID_5) {
            //5.0或5.0以以上,选择图片后回调
            onActivityResultAboveL(requestCode, resultCode, data);
        }

    }

    /**
     * 5.0或5.0以以上,选择图片后回调
     */
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private void onActivityResultAboveL(int requestCode, int resultCode, Intent intent) {
        if (requestCode != FILECHOOSER_RESULTCODE_FOR_ANDROID_5 || valueCallbacks == null)
            return;
        Uri[] results = null;
        if (resultCode == Activity.RESULT_OK) {
            if (intent != null) {
                String dataString = intent.getDataString();
                ClipData clipData = intent.getClipData();
                if (clipData != null) {
                    results = new Uri[clipData.getItemCount()];
                    for (int i = 0; i < clipData.getItemCount(); i++) {
                        ClipData.Item item = clipData.getItemAt(i);
                        results[i] = item.getUri();
                    }
                }
                if (dataString != null) results = new Uri[]{Uri.parse(dataString)};
            }
        }
        valueCallbacks.onReceiveValue(results);
        valueCallbacks = null;
    }

    /**
     * 5.0以下,调用选择框,选择拍照或者图片
     *
     * @param uploadMsg
     */
    private void openFileChooserImpl(ValueCallback<Uri> uploadMsg) {
        valueCallback = uploadMsg;
        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
        i.addCategory(Intent.CATEGORY_OPENABLE);
        i.setType("image/*");
        Intent chooser = createChooserIntent(createCameraIntent());
        chooser.putExtra(Intent.EXTRA_INTENT, i);
        startActivityForResult(chooser,
                FILECHOOSER_RESULTCODE);
    }

    /**
     * 选择拍照或者图片时需要的最终Intent
     */
    private Intent createChooserIntent(Intent... intents) {
        Intent chooser = new Intent(Intent.ACTION_CHOOSER);
        chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, intents);
        chooser.putExtra(Intent.EXTRA_TITLE, "选择图片");
        return chooser;

    }

    /**
     * 拍照需要的Intent
     */
    @SuppressWarnings("static-access")
    private Intent createCameraIntent() {
        Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        File externalDataDir = Environment
                .getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
        File cameraDataDir = new File(externalDataDir.getAbsolutePath()
                + File.separator + "telecom");
        cameraDataDir.mkdirs();
        mCameraFilePath = cameraDataDir.getAbsolutePath()
                + File.separator + System.currentTimeMillis() + ".jpg";
        cameraIntent.putExtra(MediaStore.Images.Media.ORIENTATION, 0);
        cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT,
                Uri.fromFile(new File(mCameraFilePath)));
        return cameraIntent;

    }

    /**
     * 5.0以上,调用选择框,选择拍照或者图片
     */
    private void openFileChooserImplForAndroid5(ValueCallback<Uri[]> uploadMsg) {
        valueCallbacks = uploadMsg;
        Intent contentSelectionIntent = new Intent(Intent.ACTION_GET_CONTENT);
        contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE);
        contentSelectionIntent.setType("image/*");
        Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER);
        chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent);
        chooserIntent.putExtra(Intent.EXTRA_TITLE, "选择图片");
        startActivityForResult(chooserIntent,
                FILECHOOSER_RESULTCODE_FOR_ANDROID_5);
    }

}

  可以看到我们的回调在这里实现,并且区分了不同版本的设备进行选择图片,最终在onActivityResult回调中获取到了我们选择到的图片,然后valueCallback或valueCallbacks将结果返还给WebView,从而完成上传图片这一功能。
这里我们还用到了一个工具类:

public class FileSwitchUtil {

    public static Uri getUri(Context mContext, File apkFile) {
        Uri contentUri = null;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            //6.0以上uri有变化,需要其他配置下面一行代码才会有作用
            contentUri = FileProvider.getUriForFile(mContext, mContext.getPackageName() + ".provider", apkFile);
        } else {
            contentUri = Uri.fromFile(apkFile);
        }
        return contentUri;
    }
    /**
     * 通过Uri返回File文件
     * 注意:通过相机的是类似
content://media/external/images/media/231321
     * 通过相册选择的:file:///storage/sdcard0/DCIM/Camera/IMG_12312321.jpg
     * 通过查询获取实际的地址
     * @param uri
     * @return
     */
    public static String getRealFilePath(final Context context, final Uri uri ) {
        if ( null == uri ) return null;
        final String scheme = uri.getScheme();
        String data = null;
        if ( scheme == null )
            data = uri.getPath();
        else if ( ContentResolver.SCHEME_FILE.equals( scheme ) ) {
            data = uri.getPath();
        } else if ( ContentResolver.SCHEME_CONTENT.equals( scheme ) ) {
            Cursor cursor = context.getContentResolver().query( uri, new String[] { MediaStore.Images.ImageColumns.DATA }, null, null, null );

            if ( null != cursor ) {
                if ( cursor.moveToFirst() ) {
                    int index = cursor.getColumnIndex( MediaStore.Images.ImageColumns.DATA );
                    if ( index > -1 ) {
                        data = cursor.getString( index );
                    }
                }
                cursor.close();
            }
        }
        return data;
    }

}

  这是因为服务器对文件的后缀有判断,而我们获取的uri可能是这样的:

content://media/external/images/media/231321

截取最后面就没有了图片格式,这是不行的。因此,使用此方法转化一下。
  需要注意的是Mainfest不要忘记添加网络和存储权限:

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

  如果是9.0+设备并且是https的话,建议大家在资源文件res下新建xml包,下面再新建一个network_security_config.xml文件,内容为:

    <?xml version="1.0" encoding="utf-8"?>
    <network-security-config>
    <base-config cleartextTrafficPermitted="true" />
    </network-security-config>

  最后在Manifest的Application里面配置:

    android:networkSecurityConfig="@xml/network_security_config"

  即可大功告成.
  布局就不上传了,只有一个WebView,大家自行添加尝试一下。

WebView同步Cookie实现

  Cookie可以让我们的在客户端调用Web页面的时候避免重复登录,一次登录成功后存储服务端返回给我们的Cookie,然后下次进来的时候,可以提前设置上,就可以避免重复登录。
  这里需要说一下基础概念,Cookie本身是放在WebView发起请求的header中的,有的话就带上,没的话就不发送,当我们调用成功一次url过后,Cookie的值会通过服务端的一个字段返回,名为:

Set-Cookie

  感兴趣的同学可以重写WebViewClient的shouldInterceptRequest方法。
  方法1 : 获取和设置Cookie的方法

  使用网络框架获取服务端返回的内容,我们就可以看到这个字段,下面看一下我重写WebViewClient的写法:

        private WebResourceResponse getNewResponse(String url, Map<String, String> headers) {

            try {
                OkHttpClient httpClient = new OkHttpClient();
                httpClient.cookieJar();
                Request.Builder builder = new Request.Builder()
                        .url(url.trim());

                Set<String> keySet = headers.keySet();
                for (String key : keySet) {
                    builder.addHeader(key, headers.get(key));
                }

                Request request = builder.build();

                final Response response = httpClient.newCall(request).execute();

                String conentType = response.header("Content-Type", response.body().contentType().type());
                String temp = conentType.toLowerCase();
                if (temp.contains("charset=utf-8")) {
                    conentType = conentType.replaceAll("(?i)" + "charset=utf-8", "");//不区分大小写的替换
                }
                if (conentType.contains(";")) {
                    conentType = conentType.replaceAll(";", "");
                    conentType = conentType.trim();
                }

                return new WebResourceResponse(
                        conentType,
                        response.header("Content-Encoding", "utf-8"),
                        response.body().byteStream()
                );

            } catch (Exception e) {
                return null;
            }

        }

        @Override
        public void onPageFinished(WebView view, String url) {
            super.onPageFinished(view, url);
            CookieControl.getInstance().save(url);
        }
    };

通过

        webView.setWebViewClient(mWebViewClient);

  给我们的WebView设置上,然后打断点,看一下我访问我们公司登录url返回的内容,如下图:


1-1

  可以看到服务端返回的key为

Set-Cookie

  value为

zqmy.id=89c8dae4-da20-418f-bed5-44c98834b83c; Path=/; HttpOnly

  那我们只需要把这个值给存下来,然后下次请求的时候放到请求头里面就可以了,需要注意的是,我们发给服务端时不是Set-Cookie,而是Cookie,这点需要注意,比如我们可以直接这样写:

Request.Builder builder = new Request.Builder()
                        .url(url.trim())
                        .addHeader("Cookie","zqmy.id=6b8c537a-dcb1-49a6-99dd-9e0121ccd644; Path=/; HttpOnly");

  当然了,是不是用Cookie作为key可以和后台商议决定,反正都是请求头。

  方法2 : 获取和设置Cookie的方法

  但是这种设置Cookie方式比较麻烦,我们可以也通过下面方法获取我们目标url返回的Set-Cookie,系统已经给我们封装了一个去设置Cookie和取Cookie的类,CookieManager,访问过url后我们获取Cookie可以通过:

        CookieManager cookieManager = CookieManager.getInstance();
        String cookie = cookieManager.getCookie(url);

  所以一般Cookie获取我们放在重写WebViewClient类的onPageFinished方法中。那如何设置Cookie呢,通过以下代码:

   public void sync(Context context, String url) {
        String cookie = get(url);
        CookieManager cookieManager = CookieManager.getInstance();
        cookieManager.setAcceptCookie(true);
        if (cookie == null || "".equal(cookie)) {
            return;
        }
        cookieManager.setCookie(url, cookie);
        //通过setCookie设置上后还需要同步刷新WebView
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            cookieManager.flush();
        } else {
            CookieSyncManager.createInstance(context);
            CookieSyncManager.getInstance().sync();
        }
    }

  那么为了避免重复登录,设置Cookie的代码通常放在:

webView.loadUrl()

  之前调用即可,这里还要提一点,比如你是这样写入的cookie:

String cookie = "key=value;time=content";
setCookie("host",cookie);

  那其实只把key=value写入到了[host]这个域名下,为什么time=content没有写入呢,因为分号“;”是cookie默认的分割符,cookie认为出现“;”当前的cookie的值就结束了。

那能不能不使用“;”去;连接字符串呢?
最好不要这样做,因为大家都认为cookie的分隔符就是“;”,如果你换成了另外一种,当有的同事不知道的话,就出问题了。
正确的方式应该是怎么样的呢?
使用base64转码一下就可以了,这样做你还能把信息加密一次,当然需要你跟h5的同学沟通一下,他那边拿到cookie的值需要base64一下,这样就完美了。

  方法一是为了便于大家理解Cookie的最终来源和设置,开发中通常使用第二种方式,下面提供第二种方式的工具类:

/**
 * 用于WebView的Cookie存取的工具
 * 操作方式:
 * 1:在webView.loadUrl()前调用CookieControl.getInstance().sync()
 * 2:在onPageFinished里面调用CookieControl.getInstance().save()即可
 **/
public class CookieControl {

    private CookieCache cookieCache;
    private static volatile CookieControl cookieControl;
    private static HashMap<String, String> cookies = new HashMap<>();

    private CookieControl(Context context) {
        cookieCache = CookieCache.getInstance(context);
    }

    public static CookieControl getInstance(Context context) {
        if (cookieControl == null) {
            synchronized (CookieControl.class) {
                if (cookieControl == null) {
                    cookieControl = new CookieControl(context);
                }
            }
        }
        return cookieControl;
    }

    /**
     * 保存url对应的Cookie
     */
    public void save(String url) {
        if (isEmpty(url)) {
            return;
        }
        CookieManager cookieManager = CookieManager.getInstance();
        String cookie = cookieManager.getCookie(url);
        put(url, cookie);
    }

    /**
     * 同步url对应的Cookie
     */
    public void sync(Context context, String url) {
        String cookies = get(url);
        CookieManager cookieManager = CookieManager.getInstance();
        cookieManager.setAcceptCookie(true);
        //删除全部的Cookie,不建议这样做,因为会导致其他全部的Cookie也失效
        //cookieManager.removeAllCookie();
        if (cookies == null) {
            return;
        }
        cookieManager.setCookie(url, cookies);
        //通过setCookie设置上后还需要同步刷新WebView
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            cookieManager.flush();
        } else {
            CookieSyncManager.createInstance(context);
            CookieSyncManager.getInstance().sync();
        }
    }

    private void put(String url, String newCookie) {
        if (newCookie == null) {
            return;
        }
        cookies.put(url, newCookie);
        cookieCache.put(url, newCookie);
    }

    private String get(String url) {
        String cookie = cookies.get(url);
        if (isEmpty(cookie)) {
            cookie = cookieCache.get(url);
            cookies.put(url, cookieCache.get(url));
        }
        return cookie == null ? "" : cookie;
    }

    private boolean isEmpty(String value) {
        if (value == null || "".equals(value)) {
            return true;
        }
        return false;
    }

}

  以及:

/**
 * 使用SharedPreferences轻量级缓存Cookie即可
 */
public class CookieCache {

    private static volatile CookieCache cookieCache;
    private String cookieName = "CookieCache_Name";
    private SharedPreferences cookieShared;
    private SharedPreferences.Editor cookieEditor;

    private CookieCache(Context context) {
        cookieShared = context.getSharedPreferences(cookieName, Context.MODE_PRIVATE);
        cookieEditor = cookieShared.edit();
    }

    public static CookieCache getInstance(Context context) {
        if (cookieCache == null) {
            synchronized (CookieCache.class) {
                if (cookieCache == null) {
                    cookieCache = new CookieCache(context);
                }
            }
        }
        return cookieCache;
    }

    /**
     * 存储Cookie
     */
    public boolean put(String url, String cookie) {
        if (isEmpty(url) || isEmpty(cookie)) {
            return false;
        }
        cookieEditor.putString(url, cookie);
        return cookieEditor.commit();
    }

    /**
     * 获取Cookie
     */
    public String get(String url) {
        if (isEmpty(url)) {
            return "";
        }
        return cookieShared.getString(url, "");
    }

    private boolean isEmpty(String value) {
        if (value == null || "".equals(value)) {
            return true;
        }
        return false;
    }

}

  使用起来也很简单,只需要:
1:在webView.loadUrl()前调用CookieControl.getInstance(content).sync()
2:在onPageFinished里面调用CookieControl.getInstance(context).save()

未完待续...

推荐阅读更多精彩内容