android日记(八)

上一篇:android日记(七)

1.file协议为什么是三个斜杠

  • 一个http协议地址,比如https://i.cnblogs.com/posts/edit;postId=14145949,scheme后面都是两个斜杠。
  • 一个file协议地址,file:///android_asset/web/helloWorld.html,scheme后面是三个斜杠。
  • URI的标准结构如下,
    scheme://[user:password@]host[:port]/path?query#fragment
  • 因为file协议没有host,于是就直接变成了file:///path/结构了,也就是scheme后面三个斜杠。

2.webview访问本地html file

  • 访问磁盘资源的file地址,
    file:///sdcard/xy_data/helloworld.html
  • 访问本地资源的file地址
    file:///android_asset/web/helloWorld.html

     在assets目录下,建立web文件夹,放入html文件。加载时使用file///android_asset/web/。

  • 在webview中打开html file,url就是上面的file地址
    mCorpWebView.loadUrl(url);
  • webview关闭file访问权限,默认为ture允许访问file,当设为false时禁止访问file。
     WebSettings settings = getSettings();
     settings.setAllowFileAccess(false);
  • 当关闭file访问权限后,访问sdcard磁盘中的文件,会报错。但是仍然可以访问本地assets资源下的file。

3.查看CookieManager中的cookie

  • 服务端为了对客户端进行鉴权,又不能让客户端反复键入凭证,于是有了cookie机制,一种将凭证保存到浏览器本地存储中的机制。
  • cookie可以有服务端下发给客户端,客户端收到cookie后,自己做解析存储。
  • 为了更方便的存储cookie,服务端可以通过HTTP的set-cookie指令,实现客户端自动存储cookie,并且在下一次请求时,取出同域下的cookie。具体的,服务端在返回response header中,指定set-cookie的内容和有效期等信息,格式可查阅HTTP官方文档

      

  • Android Webview查看cookie,使用AndroidStudio Device Flie Explorer工具, data/data/com.example.xxx/app_webview/Default目录下,可以看到Cookies文件,将其导出,并带上.db后缀。

      

  • 使用数据库工具查看Cookies.db文件,可以看到各个域名下的具体cookie信息。

      

  • 当webview向服务端发起某个域名下的请求时,webview会自动去CookieManager中寻找该域名下的cookie,并在请求头中携带,传给服务端。
  • webview实现自动cookie ,需要做以下配置

    //接受服务端通过response头中的set-cookie指令添加cookie
    CookieManager.getInstance().acceptCookie();
    CookieManager.getInstance().setAcceptCookie(true);
    CookieManager.setAcceptFileSchemeCookies(true);
    CookieManager.getInstance().setAcceptThirdPartyCookies(this, true);  

4.OkHttp一次性流导致的java.lang.IIIegalStateException: closed

  • 某次http请求的onReponse()回调如下
    public void onResponse(Call call, okhttp3.Response response) throws IOException {
                        if (response.isSuccessful() && response.body() != null) {
                            Log.d("tag",response.body().string());
                            handleResponse(response.body().string());
                        }
                    }
  • 上面代码在执行到handleResponse()方法里的response.body.string()时,抛出异常
    01-12 14:57:34.583 23912 25469 E AndroidRuntime: java.lang.IllegalStateException: closed
    01-12 14:57:34.583 23912 25469 E AndroidRuntime:        at okio.RealBufferedSource.rangeEquals(RealBufferedSource.kt:402)
    01-12 14:57:34.583 23912 25469 E AndroidRuntime:        at okio.RealBufferedSource.rangeEquals(RealBufferedSource.kt:393)
    01-12 14:57:34.583 23912 25469 E AndroidRuntime:        at okhttp3.internal.Util.bomAwareCharset(Util.java:471)
    01-12 14:57:34.583 23912 25469 E AndroidRuntime:        at okhttp3.ResponseBody.string(ResponseBody.java:175)

    也就是说上面执行response.body.string()两次过程中,前一次正常,后一次却异常了。

  • 查看esponse.body.string()源码,可以发现,string()方法在执行完后,会自动调用closeQuietly(source)关闭资源,
    public final String string() throws IOException {
        BufferedSource source = source();
        try {
          Charset charset = Util.bomAwareCharset(source, charset());
          return source.readString(charset);//读取资源
        } finally {
          Util.closeQuietly(source);//读取完关闭资源
        }
      }
    public static void closeQuietly(Closeable closeable) {
        if (closeable != null) {
          try {
            closeable.close();//RealBufferedSource
          } catch (RuntimeException rethrown) {
            throw rethrown;
          } catch (Exception ignored) {
          }
        }
      }
    @Override public void close() throws IOException {
        if (closed) return;
        closed = true;
        source.close();
        buffer.clear();//关闭并清理资源
      }
  • 正是因为reponse.body().string()这种调用自动关闭资源的机制,或者叫做一次性资源机制,使得重复调用string()方法时遭遇异常,具体源码可以看出,
    public final String string() throws IOException {
        BufferedSource source = source();
        try {
          Charset charset = Util.bomAwareCharset(source, charset());
          return source.readString(charset);
        } finally {
          Util.closeQuietly(source);
        }
      }
    public static Charset bomAwareCharset(BufferedSource source, Charset charset) throws IOException {
        if (source.rangeEquals(0, UTF_8_BOM)) {
          source.skip(UTF_8_BOM.size());
          return UTF_8;
        }
        if (source.rangeEquals(0, UTF_16_BE_BOM)) {
          source.skip(UTF_16_BE_BOM.size());
          return UTF_16_BE;
        }
        if (source.rangeEquals(0, UTF_16_LE_BOM)) {
          source.skip(UTF_16_LE_BOM.size());
          return UTF_16_LE;
        }
        if (source.rangeEquals(0, UTF_32_BE_BOM)) {
          source.skip(UTF_32_BE_BOM.size());
          return UTF_32_BE;
        }
        if (source.rangeEquals(0, UTF_32_LE_BOM)) {
          source.skip(UTF_32_LE_BOM.size());
          return UTF_32_LE;
        }
        return charset;
      }
     @Override public void skip(long byteCount) throws IOException {
        if (closed) throw new IllegalStateException("closed");
        while (byteCount > 0) {
          if (buffer.size == 0 && source.read(buffer, Segment.SIZE) == -1) {
            throw new EOFException();
          }
          long toSkip = Math.min(byteCount, buffer.size());
          buffer.skip(toSkip);
          byteCount -= toSkip;
        }
      }

    在source.readString()之前,会先检查资源,如果资源已经关闭,就抛出抛出IIlegalArgumentException("closed")异常了。

  • 这种一次性资源的机制,有助于尽可能减少资源占用的开销,阅后即焚,腾出空间。

5.Kotlin读取流文件

  • 在java读取一个流文件mInputStream的标准姿势,
    private void readBuffer(String mInputStream) {
            final StringBuilder sb = new StringBuilder();
            BufferedReader osReader = new BufferedReader(new InputStreamReader(mInputStream));
            String result = null;
            try {
                while ((result = osReader.readLine()) != null) {
                    sb.append(result + "\n");
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    将输入流包装成InputStreamReader,再包装成BufferReader,最后读取流文件。

  • 遍历行,一行一行读取。遍历条件中有一句关键的赋值代码,读取行的同时进行赋值。
    while ((result = osReader.readLine()) != null)
  • 上面的逻辑如果换做kotlin写法,while条件为报错:“Assignments are not expressions, and only expressions are allowed in this context”,这里只允许出现表达式,等式不是一个表达式。
    private fun readBuffer(mInputStream: InputStream) {
            val sb = java.lang.StringBuilder()
            val osReader = BufferedReader(InputStreamReader(mInputStream))
            var result: String? = null
            try {
                while ((result = osReader.readLine()) != null) {//报错
                    sb.append("$result".trimIndent())
                }
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
  • 借助kotlin的also扩展函数来解决,
    var result: String? = null
    while (osReader.readLine().also { result = it } != null) {
          sb.append("$result".trimIndent())
    }

6.webview的xss漏洞

  • WebView提供了addJavaScriptInterface()方法用于向webview中注入jsbridge对象。
  • 使用有个前提,需要设置setJavaScriptEnable(true),然后设为true后,lint会报警告,XSS漏洞风险。
    Using setJavaScriptEnabled can introduce XSS vulnerabilities into your application, review carefully
  • 那什么是XSS漏洞呢?简单来说就是,js端恶意地调用native代码,造成功能异常和信息窃取。
  • js在获得注入的对象后,可以借助对象的类加载器,恶意地反射加载和调用Android项目和系统方法。
  • 比如,反射调用Runtime.exec()方法,执行查询手机存储中数据的命令。
    <!--Web端会利用Android端提供的原生实例对象,利用Java反射机制执行任意Android原生代码-->
    function illegalInvokeJavaMethod(android){
      var clz = android.getClass().getClassLoader().loadClass("java.lang.Runtime");
      clz.getDeclaredMethod("exec").invoke("sh");  
    }
  • Android4.4开始,google修复了漏洞,只有通过@JavascriptInterface注解过的方法,才能被js调用。

7.不注入对象实现JS与Native通信

  • 由于通过addJavaScriptInterface()接口注入对象的方式,在Android4.4以下存在XSS漏洞,为了兼容Android4.4以下的系统,就需要考虑其他jsbridge方式。
  • 通常来说有两种交互方式,一是js通过loction.href()触发WebViewClient().shouldOverrideUrlLoading()方法,二是通过js的alert()、prompt()、confirm()方法分别调用WebChromClient的onJsAlert()、onJsPrompt()、onJsConfirm()。
  • 约定一种通信协议,在js端invoke时,将协议传入到native端,native解析协议,获知invoke意图后完成具体的调用。
    jsbridge://className/functionName?params=xx
  • 以prompt为例,js调native
     <!--调用Native入口方法-->    
     function invoke(object, func, params) {
        window.prompt("jsbridge://" + object + "/"+ func + "?" + "params="+JSON.stringify(params));
     }

    native接收并解析协议

    private void initWebViewClient() {
            this.setWebChromeClient(new WebChromeClient() {
                @Override
                public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
                    result.cancel();
                    parseInvokeUrl(message);//解析协议
                    return true;
                }
            }
        }
  • native调用js,有下面两种方式
    webview.loadUrl("javascript:jsfunction()")
    webView.evaluateJavascript("javascript:(function() {jsfunction();)()", null);

8.webview拦截请求

  • 有时会因为各种各样的原因,需要篡改webview的请求或者响应,一个标准的拦截请求的姿势如下,
    public class MyWebViewClient extends WebViewClient {
        @Override
        public WebResourceResponse shouldInterceptRequest(WebView view, final WebResourceRequest request) {
            String url = request.getUrl().toString();
            //your target url
            if (url.equals(targetUrl))) {
                try {
                    OkHttpClient okHttpClient = HTTPClientManager.getInstance().getOkHttpClient();
                    Request.Builder builder = new Request.Builder();
                    //do you want to do for builder
                    Request req = builder.url(url).get().build();
                    Response response = okHttpClient.newCall(req).execute();
                    //do you want to do for response
                    WebResourceResponse webResourceResponse = new WebResourceResponse("text/html", "utf-8", new ByteArrayInputStream(response.body().string().getBytes()));
                    return webResourceResponse;
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return super.shouldInterceptRequest(view, request);
            }
    }
  • 然而,这样写存在一些坑。
  • 坑1: Request缺少cookie。WebView请求不被拦截的话,请求时会自动携带CookieManager中当前domain下的cookie,然而拦截后请求通过OkHttpClient代发,就需要手动去加回cookie。
    Request req = new Request.Builder().url(url).get().headers(HttpApis.createHeaders(url)).build();
    String cookie = CookieUtils.getCookiesByUrl(url);
    if (!TextUtils.isEmpty(cookie)) {
        req = req.newBuilder().addHeader("cookie", cookie).build();
    }
    Response response = okHttpClient.newCall(req).execute();
  • 坑2: Response header丢失。当把OkHttp的Response重新组装回WebResourceResponse返回时,只组装了Response的body部分,而丢失了Response Header,需要手动加回。并且由于Response.Header的数据类型与WebResorceResponse接收的headers类型不一致,还需要手动做一层转换。
    Headers headers = response.headers();
    if (headers != null) {
       Map<String, List<String>> headersMap = headers.toMultimap();
       if (headersMap.size() > 0) {
         for (String name : headers.names()) {
                for (String value : headersMap.get(name)) {
                       responseHeader.put(name, value);
                     }
                }
          }
          webResourceResponse.setResponseHeaders(responseHeader);
     }
  • 坑3: Response set-cookie指令失效。虽然手动向WebResourceResponse中加回了Response Header,但Response的set-cookie指令却没生效。也就是说当Response Header中存在set-cookie,却没能写入对应的cookie数据。这里暂不清楚为什么,有谁知道的,还请多多赐教。于是,还需要手动向CookieManager中设置服务端返回的cookie数据。
    //重组response会导致set-cookie不生效,需要手动添加cookie到CookieManager
     List<String> responseCookies = headersMap.get("set-cookie");
     if (responseCookies != null && !responseCookies.isEmpty()) {
          for (String cookie : responseCookies) {
               CookieUtils.addCookie(url, cookie);
          }
    }
  • 坑4: 302重定向失败。当拦截请求返回的Response设置了302重定向指令时,有时候无法生效,常见于POST请求,这时候需要手动重定向,并且解析prioriResponse设置cookie。
    if (response.priorResponse() != null
          && response.priorResponse().code() == 302
          && !response.request().url().toString().equals(loadUrl)) {
              //跟坑3中一样,需要手动组装header,并设置cookie        
              handleResponseHeader(response.priorResponse());
              ThreadUtils.runOnUIThread(() -> loadUrl(response.request().url().toString()));
      }
  • 坑5: webview.reload()场景导致拦截失败。demo中的需求是拦截页面dom请求,而不会去拦截资源请求(css,png,js等),也就是说只有当请求的url是当前webview加载的loadUrl时才会进行拦截,并且某些时候,webview需要reload,比如加载失败后触发reload重试。问题在于,reload请求返回的url有时候会被转义编码,导致与原loadUrl在字符串上并不相等。比如:
    原url:
    https://host/path?backurl=https://host/m/Management/Management
    reload后url:
    https://host/path?backurl=https%3A%2F%2Fhost%2Fm%2FManagement%2FManagement

    可以发现,当url携带的参数中又包含一个url字串,存在特殊字符,webview reload请求时,返回的url会将特殊字符进行编码,从而url.equals(loadUrl)返回了false,导致请求被过滤掉了,没能成功拦截。解决办法是显而易见的,要么对loadUrl也进行同样的编码处理,要么避免使用reload。

    webview.loadUrl(loadUrl) 
    替换 
    webview.reload()

9.打开WiFi开关会有助于提升定位精度吗

  • 会的。因为手机会通过WiFi定位方式来进行定位,通过扫描能够探测到的WiFi热点信息,解析出热点的IP地址,并转换为地理位置。

  • 因此,手机只要能够感测到WiFi信号,就能获取当前位置,打开WiFi开关有助于提升定位精度。 

  • 在室外尽管没有连接上WiFi,但是手机只要能够感测到WiFi信号,就能获取当前位置,所以打开WiFi开关也有助于提升定位精度。

10.android定位精度分析

  • android所有定位需要获得定位权限以及开启gps位置信息,否则无法定位。

  • 有三种定位方式(GPSWI-FI,基站),会根据实际的环境选择不同的定位方式。

  • 三种方式的定位精度,GPS>WIFI>基站,GPS定位精度达到10米,Wi-Fi定位精度达24米,基站定位精度达107米。

  • 所有设备都有gps模块,gps定位在室内或者城市街区信号会比较弱,从而无法工作。

  • WI-FI定位需要开启设备wi-fi,工作时不需要连上具体的wi-fi,其工作原理是根据设备扫描到的wi-fi信号,解析出热点地址。

  • 基站定位指的是手机sim连接的运营商基站(通常为移动、联通、电信),也就是说没插卡的手机不存在基站定位。

  • gps信号弱时,wi-fi和基站定位(统称网络定位)会辅助改善定位精度。

  • 精度在100米以上,不代表一定会出现100米以上的定位误差,就是表明定位结果有潜在的误差和可能出现的误差范围。根据实际测试发现,基本上即使精度不理想,定位结果也是无误的。

  • 总结:开启位置信息在室外使用gps定位(10米)> wi-fi开启并扫描到有效信号使用wi-fi定位(24米)手机插了sim卡会使用基站定位(107米)。

下一篇:android日记(九)

posted @ 2021-02-01 20:09  是个写代码的  阅读(259)  评论(0编辑  收藏  举报