懂球帝Android客户端WebView优化之路
一、业务场景
首先我们分析一下一般App中,H5相关的业务场景和这些业务场景中的信息流。H5业务场景里面包含了几个角色:WebView是显示H5的控件;Activity/Fragment等嵌入有WebView的界面、H5页面、JsBridge是H5和客户端互相调用的桥梁,这里面用户直接打交道的是界面和H5页面。基本上,大部分业务场景可以归纳为以下几种
用户与H5交互,需要调用客户端某项功能,比如用户点击了H5的按钮参加某个活动,这时候H5需要获取客户端用户的登录信息。
用户与界面交互,需要调用H5的某项功能,比如用户下拉刷新,这时候需要通知H5去对数据做更新。
界面的某些非用户行为触发H5的相关操作,比如在Acitivty的生命周期中调用WebView或者H5页面进行一些操作。
Webview自身的状态改变需要调用界面,比如在WebView中WebviewClient和WebChromeClient的几个回调
onPageStarted、onPageFinished
等方法,需要分别通知界面和H5进行处理。
二、原有实现逻辑
WebviewManager和BrigeHelper的功能越来越多,越来越多的处理逻辑和代码往这里面堆。
由于通信是双向的,也就意味着WebviewManager和Bridgehelper必须提供双向通信的接口,但是随着业务的增长,接口也臃肿不堪,接口粒度过大导致有些页面只需要接口中的部分信息,也被迫去实现整个接口。而且,一有新的功能就必须要改接口,这也不符合设计规范。
所有业务都糅合在WebviewManager和BrigeHelper中,导致各个业务不好拆分甚至会互相影响。比如登录和支付之后返回当前页面都可能会有刷新页面的需求,这两个业务刷新页面的部分就杂糅在一起,天长日久后,就没人知道这一块业务到底是做什么的。
WebviewManager和BridgeHelper没有任何约束,中间各个对象互相调用,有着千丝万缕的联系。
各个页面对Webview的使用方式也不太一致。
三、重构的方案
把各个业务封装在独立的策略中就可以不互相影响;
策略也可以自由组合,给界面和H5提供不同的功能;
策略模式可以方便扩展,扩展策略接口就好了
Activity/Fragment的生命周期可以从界面的LifeCycle中获得,实现LifeCycleObserver就可以了
Webview的触发事件定义为IWebviewCallback,这里面包含策略里面需要用到的主要方法
public interface IWebviewCallback {
void onPageFinished();
void onPageStart();
boolean shouldOverrideUrlLoading(WebView webView, String url);
WebResourceResponse shouldInterceptRequest(WebView view, final WebResourceRequest webResourceRequest);
boolean onShowFileChooser(ValueCallback<Uri[]> filePathCallback,WebChromeClient.FileChooserParams fileChooserParams);
void onLoadResource(WebView webView, String url);
......
}
JsBridge触发的定义为IBridgeHandler
public interface IBridgeHandler {
/**
* bridge名称
* @return
*/
public String[] getName();
/**
* Birdge触发时调用调用
* @param jsonObject
* @param callBackFunction
*/
public void onHandle(String functionName, JSONObject jsonObject, CallBackFunction callBackFunction);
}
策略自身被加载和卸载时的生命周期的监听
public interface IPluginLifeCycle {
/**
* 插件被添加的时候调用
*/
void onPluginAdded();
/**
* 插件被移除的时候调用
*/
void onPluginRemoved();
}
接下来定义策略的接口IWebviewplugin继承者四个接口
public abstract class IWebviewPlugin implements GenericLifecycleObserver, IPluginLifeCycle, IWebviewCallback, IBridgeHandler {
}
除了以上介绍的几个接口,我们还需要几个接口和类:
WebHostCallback
:由界面实现,可以获取WebView的状态PageInterface
: 由于使用WebView的可能是Activity,也有可能是Fragment甚至是一个ViewGroup,所以需要对界面的功能进行抽象,提供一个可供WebView使用的一些功能,比如打开一个Activity等PluginManager
:策略的管理类,可以动态组合管理策略集合WebviewWrapper
:WebView的包装类,这里采用抽象WebView的一些Api然后包装的形式而不是继承WebView来扩展功能,主要是考虑到使用者可能继承WebView实现自己的一些功能,甚至是未来可能更换系统的WebView采用第三方的内核PluginFactory
:生产策略的工厂
可以看到,使用接口抽象后,WebView并不知道有多少个策略被实现了,它要做的只是调用IWebviewCallback接口,这样就把WebView从业务中抽离出来,WebviewWrapper并不涉及到业务的代码,只需要配置和管理好WebView就可以了 通过PageInterface和WebviewHostCallback的接口,Webview也不需要关心自己是在Activity还是Fragment里面。从使用者的角度来看,继承IWebviewPlugin实现自己的策略就可以了。通过接口抽象后,解耦了原来各个部分强耦合的关系。
四、实现WebView的HttpDns和本地图片、文件的缓存
Html和js、css等文件的加载
图片等素材的下载
异步网络请求,比如一些Ajax请求
在这些网络请求中,我们面临一些问题:
图片在Native中下载过,点进去H5时还得再下一遍,图片无法复用
Ajax异步网络请求不能添加Header或者其他的一些处理逻辑(Webview只能在loadUrl的时候添加header)
Html和js、css反复下载,不能进行可靠的管理
在一些网络环境中DNS服务并不可靠,DNS结果可能拿不到或者被劫持(笔者在项目中就跟过几例在浙江移动网络中文件被劫持成其他文件的情况)。
1.shouldInterceptRequest接口
public WebResourceResponse shouldInterceptRequest(WebView view, final WebResourceRequest webResourceRequest)
Notify the host application of a resource request and allow the application to return the data. If the return value is null, the WebView will continue to load the resource as usual. Otherwise, the return response and data will be used. This callback is invoked for a variety of URL schemes (e.g., http(s):, data:, file:, etc.), not only those schemes which send requests over the network. This is not called for javascript: URLs, blob: URLs, or for assets accessed via file:///android_asset/ or file:///android_res/ URLs. In the case of redirects, this is only called for the initial resource URL, not any subsequent redirect URLs.
这里面有几点需要注意的
这个接口不只是http或者https的请求才出发的,也有可能是data或者file协议,但是不包括javascript、file:///android_asset或者file:///android_res。所以拦截的时候我们要注意只拦截网络请求。
返回值为空时浏览器会按原有的逻辑执行请求,否则就使用指定的数据。
重定向的请求只有第一个链接会调用这个接口,后续跳转链接不会。
public interface WebResourceRequest {
/**
* 请求的url
*/
Uri getUrl();
/**
* 返回该请求是不是主Frame发出的
*/
boolean isForMainFrame();
/**
* 该请求是否是服务端重定向的结果
*/
boolean isRedirect();
/**
* 该请求是否与用户的手势有关,比如点击等
*/
boolean hasGesture();
/**
* 网络的请求Method,比如GET或者POST
*/
String getMethod();
/**
* 网络请求的header
*/
Map<String, String> getRequestHeaders();
}
这里面包含了请求的基本信息,再来看看返回值WebResourceResponse的相关属性
public class WebResourceResponse {
private String mMimeType;//资源的MIME类型,比如text/html
private String mEncoding;//response的编码格式,比如utf-8
private int mStatusCode;//http状态码
private String mReasonPhrase;//状态码描述与,比如200对应的是“OK”,这个值不能为空
private Map<String, String> mResponseHeaders;//response的header
private InputStream mInputStream;//输入流
}
2.实现代理资源请求和HttpDns
Request.Builder builder = new Request.Builder();
//构造请求
builder.url(url).method(webResourceRequest.getMethod(), null);
Map<String, String> requestHeaders = webResourceRequest.getRequestHeaders();
if (Lang.isNotEmpty(requestHeaders)) {
for (Map.Entry<String, String> entry : requestHeaders.entrySet()) {
builder.addHeader(entry.getKey(), entry.getValue());
}
}
Call synCall = mClient.newCall(builder.build());
okhttp3.Response response = synCall.execute();
if (response.body() != null) {
ResponseBody body = response.body();
Map<String, String> map = new HashMap<>();
for (int i = 0; i < response.headers().size(); i++) {
//相应体的header
map.put(response.headers().name(i), response.headers().value(i));
}
//MIME类型
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(url));
String contentType = response.headers().get("Content-Type");
String encoding = "utf-8";
//获取ContentType和编码格式
if (contentType != null && !"".equals(contentType)) {
if (contentType.contains(";")) {
String[] args = contentType.split(";");
mimeType = args[0];
String[] args2 = args[1].trim().split("=");
if (args.length == 2 && args2[0].trim().toLowerCase().equals("charset")) {
encoding = args2[1].trim();
}
} else {
mimeType = contentType;
}
}
WebResourceResponse webResourceResponse = new WebResourceResponse(mimeType, encoding, body.byteStream());
String message = response.message();
int code = response.code();
if (TextUtils.isEmpty(message) && code == 200) {
//message不能为空
message = "OK";
}
webResourceResponse.setStatusCodeAndReasonPhrase(code, message);
webResourceResponse.setResponseHeaders(map);
okClientBuilder.followRedirects(false);
这样,就可以代理网络请求。
结合我们基于OkHttp做的HttpDns,就可以解决WebView的请求Dns的问题。
此外,如有应用对Cookie有需求,可以自行设置Cookie的请求,相关设置可以参考
有赞技术团队的相关文章
3.加载本地缓存
前面介绍过我们把H5页面的资源分成三个部分:
Html和js、css等文件的加载
图片等素材的下载
异步网络请求,比如一些Ajax请求
fileInputStream = new FileInputStream(file);
String type = "text/html";
if (path.contains(".css")) {
type = "text/css";
} else if (path.contains(".js")) {
type = "application/javascript";
}
WebResourceResponse response = new WebResourceResponse(type, "utf-8", fileInputStream);
Map<String, String> map = new HashMap<>();
map.put("Content-Type", type);
//跨域响应头,否则可能会有跨域无法访问的问题
map.put("Access-Control-Allow-Origin", "*");
response.setResponseHeaders(map);
return response;
四、总结与展望
参考文献
【1】如何设计一个优雅健壮的Android WebView?(下)
【2】有赞webview加速平台探索与建设(三)——html加速
【3】《研磨设计模式》,清华大学出版社,陈臣,王斌
【4】Android Developers
【5】Android:手把手教你构建 全面的WebView 缓存机制 & 资源加载方案