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位置信息,否则无法定位。
-
有三种定位方式(GPS、WI-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日记(九)