跟着代码看一看豆瓣开源的混合开发框架Rexxaar
// 初始化rexxar
Rexxar.initialize(this);
Rexxar.setDebug(BuildConfig.DEBUG);
// 设置并刷新route
RouteManager.getInstance().setRouteApi("https://raw.githubusercontent.com/douban/rexxar-web/master/example/dist/routes.json");
RouteManager.getInstance().refreshRoute(null);
// 设置需要代理的资源
ResourceProxy.getInstance().addProxyHosts(PROXY_HOSTS);
// 设置local api
RexxarContainerAPIHelper.registerAPIs(FrodoContainerAPIs.sAPIs);
// 设置自定义的OkHttpClient
Rexxar.setOkHttpClient(new OkHttpClient().newBuilder()
.retryOnConnectionFailure(true)
.addNetworkInterceptor(new AuthInterceptor())
.build());
Rexxar.setHostUserAgent(" Rexxar/1.2.x com.douban.frodo/4.3 ");
application里面做初始化,Rexaar这个类主要保存了OkHttpClient以及管理UA
AppContext.init(context);
RouteManager.getInstance();
ResourceProxy.getInstance();
同时做了RouteManager和ResourceProxy的初始化
RouteManager主要为请求路由做处理。
ResourceProxy负责资源管理,比如获取缓存的资源,写入缓存资源,请求线上资源。
后面设置了route地址。这个链接
的数据是这样的
{
“items”: [
{
“deploy_time”: “Sun, 09 Oct 2016 05:54:22 GMT”,
“remote_file”: “https://raw.githubusercontent.com/douban/rexxar-web/master/example/dist/rexxar/demo-252452ae58.html“,
“uri”: “douban://douban.com/rexxar_demo[/]?.*”
}
],
“partial_items”: [
{
“deploy_time”: “Sun, 09 Oct 2016 05:54:22 GMT”,
“remote_file”: “https://raw.githubusercontent.com/douban/rexxar-web/master/example/dist/rexxar/demo-252452ae58.html“,
“uri”: “douban://partial.douban.com/rexxar_demo/_.*”
}
],
“deploy_time”: “Sun, 09 Oct 2016 05:54:22 GMT”
}
暂时认为将上述两个http请求路由到了douban://开头的Uri,实际应该对应着本地文件。
接着refreshRoute(null),最终会走到remoteFile中,在子线程中将结果转换成String类型,这里由于callback为null不会在回调里处理,那么意义就在于利用OkHttp的DiskLruCache,将这个文件结果先缓存下来。
以下是请求的response header
Accept-Ranges:bytes
Access-Control-Allow-Origin:*
Cache-Control:max-age=300
Connection:keep-alive
Content-Encoding:gzip
Content-Length:241
Content-Security-Policy:default-src ‘none’; style-src ‘unsafe-inline’
Content-Type:text/plain; charset=utf-8
Date:Tue, 11 Oct 2016 07:29:40 GMT
ETag:”bab04fe56197eb4382311b3d56dad9c32b21c2f3”
Expires:Tue, 11 Oct 2016 07:34:40 GMT
Source-Age:0
Strict-Transport-Security:max-age=31536000
Vary:Authorization,Accept-Encoding
Via:1.1 varnish
X-Cache:MISS
X-Cache-Hits:0
X-Content-Type-Options:nosniff
X-Fastly-Request-ID:eec0cdd87b37b984f5f917ffbae0515798994004
X-Frame-Options:deny
X-Geo-Block-List:
X-GitHub-Request-Id:67F5E01A:095A:1C39816:57FC94E4
X-Served-By:cache-itm7420-ITM
X-XSS-Protection:1; mode=block
Okhttp缓存说明
接下来的一行设置了需要代理的Host,这里是raw.githubusercontent.com
然后在RexxarContainerAPIHelper中注册了native api,目前认为这个类负责管理natvie api,具体怎么管理的后面分析。
最后设置UA。
接下来看一下使用的部分,在MainActivity中主要是页面跳转,这里插一句看一下CacheHelper这个类,这个类对html文件单独处理,写入指定文件夹缓存,对js,css,png等资源使用DiskLruCache缓存,文件命名采用MD5进行hash然后存储。
具体这些文件是怎么缓存下来的,还需要继续看webview的处理。
假设我们点了完全版的Rexxaar页面。那么就看一下RexxarWebView的实现。
private void init() {
LayoutInflater.from(getContext()).inflate(R.layout.view_rexxar_webview, this, true);
mSwipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_refresh_layout);
mCore = (RexxarWebViewCore) findViewById(R.id.webview);
mErrorView = (RexxarErrorView) findViewById(R.id.rexxar_error_view);
mProgressBar = (ProgressBar) findViewById(R.id.progress_bar);
BusProvider.getInstance().register(this);
}
初始化语句中初始化了几个控件,然后注册了一下EventBus,这里没有直接EventBus.getDefault是比较好的设计。避免了使用Bus的地方和具体的Bus实现直接耦合。
布局是SwipeRefreshLayout里面套自己实现的RexxarWebViewCore,这个是真正的WebView,也包括ErrorView和ProgressBar的封装。
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<com.douban.rexxar.view.SwipeRefreshLayout
android:id="@+id/swipe_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<com.douban.rexxar.view.RexxarWebViewCore
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</com.douban.rexxar.view.SwipeRefreshLayout>
<com.douban.rexxar.view.RexxarErrorView
android:id="@+id/rexxar_error_view"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:background="@android:color/white"
android:visibility="gone"
/>
<ProgressBar
android:id="@+id/progress_bar"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
/>
</merge>
SwipeRefreshLayout拒绝捕获横向的滑动手势,交给子布局处理
// adapted from http://stackoverflow.com/questions/23989910/horizontalscrollview-inside-swiperefreshlayout
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mPrevX = MotionEvent.obtain(event)
.getX();
break;
case MotionEvent.ACTION_MOVE:
final float eventX = event.getX();
float xDiff = Math.abs(eventX - mPrevX);
if (xDiff > mTouchSlop) {
return false;
}
}
return super.onInterceptTouchEvent(event);
}
接下来继续看RxxarWebView,这里先是封装了一些WebView代理方法,然后是提供了默认的load回调处理,默认是显示关闭进度条或者显示错误页,也提供了对外的回调处理接口。也包括对Visibility的处理和EventBus解注册。
package com.douban.rexxar.view;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.webkit.WebView;
import android.widget.FrameLayout;
import android.widget.ProgressBar;
import com.douban.rexxar.Constants;
import com.douban.rexxar.R;
import com.douban.rexxar.utils.BusProvider;
import java.lang.ref.WeakReference;
import java.util.Map;
/**
* pull-to-refresh
* error view
*
* Created by luanqian on 16/4/7.
*/
public class RexxarWebView extends FrameLayout implements RexxarWebViewCore.UriLoadCallback{
public static final String TAG = "RexxarWebView";
/**
* Classes that wish to be notified when the swipe gesture correctly
* triggers a refresh should implement this interface.
*/
public interface OnRefreshListener {
void onRefresh();
}
private SwipeRefreshLayout mSwipeRefreshLayout;
private RexxarWebViewCore mCore;
private RexxarErrorView mErrorView;
private ProgressBar mProgressBar;
private String mUri;
private boolean mUsePage;
private WeakReference<RexxarWebViewCore.UriLoadCallback> mUriLoadCallback = new WeakReference<RexxarWebViewCore.UriLoadCallback>(null);
public RexxarWebView(Context context) {
super(context);
init();
}
public RexxarWebView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public RexxarWebView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
LayoutInflater.from(getContext()).inflate(R.layout.view_rexxar_webview, this, true);
mSwipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_refresh_layout);
mCore = (RexxarWebViewCore) findViewById(R.id.webview);
mErrorView = (RexxarErrorView) findViewById(R.id.rexxar_error_view);
mProgressBar = (ProgressBar) findViewById(R.id.progress_bar);
BusProvider.getInstance().register(this);
}
/**
* 设置下拉刷新监听
* @param listener
*/
public void setOnRefreshListener(final OnRefreshListener listener) {
if (null != listener) {
mSwipeRefreshLayout.setOnRefreshListener(new android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
listener.onRefresh();
}
});
}
}
/**
* 下拉刷新颜色
*
* @param color
*/
public void setRefreshMainColor(int color) {
if (color > 0) {
mSwipeRefreshLayout.setMainColor(color);
}
}
/**
* 启用/禁用 下拉刷新手势
*
* @param enable
*/
public void enableRefresh(boolean enable) {
mSwipeRefreshLayout.setEnabled(enable);
}
/**
* 设置刷新
* @param refreshing
*/
public void setRefreshing(boolean refreshing) {
mSwipeRefreshLayout.setRefreshing(refreshing);
}
public WebView getWebView() {
return mCore;
}
/***************************设置RexxarWebViewCore的一些方法代理****************************/
public void setWebViewClient(RexxarWebViewClient client) {
mCore.setWebViewClient(client);
}
public void setWebChromeClient(RexxarWebChromeClient client) {
mCore.setWebChromeClient(client);
}
public void loadUri(String uri) {
mCore.loadUri(uri);
this.mUri = uri;
this.mUsePage = true;
}
public void loadUri(String uri, final RexxarWebViewCore.UriLoadCallback callback) {
this.mUri = uri;
this.mUsePage = true;
if (null != callback) {
this.mUriLoadCallback = new WeakReference<RexxarWebViewCore.UriLoadCallback>(callback);
}
mCore.loadUri(uri, this);
}
public void loadPartialUri(String uri) {
mCore.loadPartialUri(uri);
this.mUri = uri;
this.mUsePage = false;
}
public void loadPartialUri(String uri, final RexxarWebViewCore.UriLoadCallback callback) {
this.mUri = uri;
this.mUsePage = false;
if (null != callback) {
this.mUriLoadCallback = new WeakReference<RexxarWebViewCore.UriLoadCallback>(callback);
}
mCore.loadPartialUri(uri, this);
}
@Override
public boolean onStartLoad() {
post(new Runnable() {
@Override
public void run() {
if (null == mUriLoadCallback.get() || !mUriLoadCallback.get().onStartLoad()) {
mProgressBar.setVisibility(View.VISIBLE);
}
}
});
return true;
}
@Override
public boolean onStartDownloadHtml() {
post(new Runnable() {
@Override
public void run() {
if (null == mUriLoadCallback.get() || !mUriLoadCallback.get().onStartDownloadHtml()) {
mProgressBar.setVisibility(View.VISIBLE);
}
}
});
return true;
}
@Override
public boolean onSuccess() {
post(new Runnable() {
@Override
public void run() {
if (null == mUriLoadCallback.get() || !mUriLoadCallback.get().onSuccess()) {
mProgressBar.setVisibility(View.GONE);
}
}
});
return true;
}
@Override
public boolean onFail(final RexxarWebViewCore.RxLoadError error) {
post(new Runnable() {
@Override
public void run() {
if (null == mUriLoadCallback.get() || !mUriLoadCallback.get().onFail(error)) {
mProgressBar.setVisibility(View.GONE);
mErrorView.show(error.messsage);
}
}
});
return true;
}
public void destroy() {
mSwipeRefreshLayout.removeView(mCore);
mCore.destroy();
mCore = null;
}
public void loadUrl(String url) {
mCore.loadUrl(url);
}
public void loadData(String data, String mimeType, String encoding) {
mCore.loadData(data, mimeType, encoding);
}
public void loadUrl(String url, Map<String, String> additionalHttpHeaders) {
mCore.loadUrl(url, additionalHttpHeaders);
}
public void loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding,
String historyUrl) {
mCore.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl);
}
public void onPause() {
mCore.onPause();
}
public void onResume() {
mCore.onResume();
}
@Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
if (visibility == View.VISIBLE) {
onPageVisible();
} else {
onPageInvisible();
}
}
/**
* 自定义url拦截处理
*
* @param widget
*/
public void addRexxarWidget(RexxarWidget widget) {
if (null == widget) {
return;
}
mCore.addRexxarWidget(widget);
}
public void onPageVisible() {
mCore.loadUrl("javascript:window.Rexxar.Lifecycle.onPageVisible()");
}
public void onPageInvisible() {
mCore.loadUrl("javascript:window.Rexxar.Lifecycle.onPageInvisible()");
}
@Override
protected void onDetachedFromWindow() {
BusProvider.getInstance().unregister(this);
super.onDetachedFromWindow();
}
public void onEventMainThread(BusProvider.BusEvent event) {
if (event.eventId == Constants.EVENT_REXXAR_RETRY) {
mErrorView.setVisibility(View.GONE);
reload();
} else if (event.eventId == Constants.EVENT_REXXAR_NETWORK_ERROR) {
boolean handled = false;
RexxarWebViewCore.RxLoadError error = RexxarWebViewCore.RxLoadError.UNKNOWN;
if (null != event.data) {
int errorType = event.data.getInt(Constants.KEY_ERROR_TYPE);
error = RexxarWebViewCore.RxLoadError.parse(errorType);
}
if (null != mUriLoadCallback && null != mUriLoadCallback.get()) {
handled = mUriLoadCallback.get().onFail(error);
}
if (!handled) {
mProgressBar.setVisibility(View.GONE);
mErrorView.show(error.messsage);
}
}
}
/**
* 重新加载页面
*/
public void reload() {
if (mUsePage) {
mCore.loadUri(mUri, this);
} else {
mCore.loadPartialUri(mUri, this);
}
}
}
接下来看真正的RexxarWebViewCore,它继承自SafeWebView
package com.douban.rexxar.view;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.AttributeSet;
import android.webkit.WebView;
import com.douban.rexxar.utils.Utils;
/**
* 解决Android 4.2以下的WebView注入Javascript对象引发的安全漏洞
*
* Created by luanqian on 15/10/28.
*/
public class SafeWebView extends WebView {
public SafeWebView(Context context) {
super(context);
removeSearchBoxJavaBridgeInterface();
}
public SafeWebView(Context context, AttributeSet attrs) {
super(context, attrs);
removeSearchBoxJavaBridgeInterface();
}
public SafeWebView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
removeSearchBoxJavaBridgeInterface();
}
@SuppressLint("NewApi")
private void removeSearchBoxJavaBridgeInterface() {
if (Utils.hasHoneycomb() && !Utils.hasJellyBeanMR1()) {
removeJavascriptInterface("searchBoxJavaBridge_");
}
}
}
这个地方有意思。之前只知道addJavascriptInterface会有漏洞,没想到原生注入了一个java对象,细思极恐,先给他remove掉。
接下来看真正的RexxarWebViewCore,首先定义了UriLoadCallback
public interface UriLoadCallback {
/**
* 开始load uri
*/
boolean onStartLoad();
/**
* 开始下载html
*/
boolean onStartDownloadHtml();
/**
* load成功
*/
boolean onSuccess();
/**
* load失败
* @param error
*/
boolean onFail(RxLoadError error);
}
接着定义了几种LoadError类型,后面是初始化代码,为WebView设置了RexxarWebViewClient和RexxarWebChromeClient,处理WebView回调,后面会细看。
/**
* 自定义url拦截处理
*
* @param widget
*/
public void addRexxarWidget(RexxarWidget widget) {
if (null == widget) {
return;
}
mWebViewClient.addRexxarWidget(widget);
}
@Override
public void setWebViewClient(WebViewClient client) {
if (!(client instanceof RexxarWebViewClient)) {
throw new IllegalArgumentException("client must inherit RexxarWebViewClient");
}
if (null != mWebViewClient) {
for (RexxarWidget widget : mWebViewClient.getRexxarWidgets()) {
if (null != widget) {
((RexxarWebViewClient) client).addRexxarWidget(widget);
}
}
}
mWebViewClient = (RexxarWebViewClient) client;
super.setWebViewClient(client);
}
@Override
public void setWebChromeClient(WebChromeClient client) {
if (!(client instanceof RexxarWebChromeClient)) {
throw new IllegalArgumentException("client must inherit RexxarWebViewClient");
}
mWebChromeClient = (RexxarWebChromeClient) client;
super.setWebChromeClient(client);
}
自定义WebViewClient的时候,把前一个client的RexxarWidget复制出来设置给新的。
接下来是loadUri操作,看一看瞧一瞧。
private void loadUri(final String uri, final UriLoadCallback callback, boolean page) {
LogUtils.i(TAG, "loadUri , uri = " + (null != uri ? uri : "null"));
if (TextUtils.isEmpty(uri)) {
throw new IllegalArgumentException("[RexxarWebView] [loadUri] uri can not be null");
}
final Route route;
if (page) {
route = RouteManager.getInstance().findRoute(uri);
} else {
route = RouteManager.getInstance().findPartialRoute(uri);
}
if (null == route) {
LogUtils.i(TAG, "route not found");
if (null != callback) {
callback.onFail(RxLoadError.ROUTE_NOT_FOUND);
}
return;
}
if (null != callback) {
callback.onStartLoad();
}
CacheEntry cacheEntry = null;
// 如果禁用缓存,则不读取缓存内容
if (CacheHelper.getInstance().cacheEnabled()) {
cacheEntry = CacheHelper.getInstance().findHtmlCache(route.getHtmlFile());
}
if (null != cacheEntry && cacheEntry.isValid()) {
// show cache
doLoadCache(uri, route);
if (null != callback) {
callback.onSuccess();
}
} else {
if (null != callback) {
callback.onStartDownloadHtml();
}
HtmlHelper.prepareHtmlFile(route.getHtmlFile(), new Callback() {
@Override
public void onFailure(Call call, IOException e) {
if (null != callback) {
callback.onFail(RxLoadError.HTML_DOWNLOAD_FAIL);
}
}
@Override
public void onResponse(Call call, final Response response) throws IOException {
mMainHandler.post(new Runnable() {
@Override
public void run() {
if (response.isSuccessful()) {
LogUtils.i(TAG, "download success");
final CacheEntry cacheEntry = CacheHelper.getInstance().findHtmlCache(route.getHtmlFile());
if (null != cacheEntry && cacheEntry.isValid()) {
// show cache
doLoadCache(uri, route);
if (null != callback) {
callback.onSuccess();
}
}
} else {
if (null != callback) {
callback.onFail(RxLoadError.HTML_DOWNLOAD_FAIL);
}
}
}
});
}
});
}
}
看看流程,先回去匹配Route,那么看看RouteManager这个类,在构造函数中调用了loadCachedRoutes,这个函数先去读把本地文件缓存中的routes文件,没有读到就去assets里面读取预设的routes文件,那么初始化的时候,就把Routes的List读进去了,两个分别对应了两种Item,虽然并不知道这两种分开的item逻辑上有什么区别。(What the fuck?)
看到这里有点迷,讲道理初始化时读到了本地缓存之后发请求就是为了刷新这个数据,然而demo里面只发了请求没有添加任何逻辑,也许是因为只是demo吧。
好,现在Routes里面有数据了,那么会拿uri去route里面匹配,匹配到了就返回route对象,否则在回调中报错。然后会拿着route信息去CacheHelper匹配缓存,否则就是请求,缓存,再显示。
分析到这里,html的加载就这样了,固定了要套的模板。接下来看看其他资源的缓存。
package com.douban.rexxar.view;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.webkit.MimeTypeMap;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import com.douban.rexxar.Constants;
import com.douban.rexxar.Rexxar;
import com.douban.rexxar.resourceproxy.ResourceProxy;
import com.douban.rexxar.resourceproxy.cache.CacheEntry;
import com.douban.rexxar.resourceproxy.cache.CacheHelper;
import com.douban.rexxar.utils.BusProvider;
import com.douban.rexxar.utils.LogUtils;
import com.douban.rexxar.utils.MimeUtils;
import com.douban.rexxar.utils.Utils;
import com.douban.rexxar.utils.io.IOUtils;
import org.apache.http.conn.ConnectTimeoutException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import okhttp3.FormBody;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.Buffer;
import okio.GzipSource;
/**
* Created by luanqian on 15/10/28.
*/
public class RexxarWebViewClient extends WebViewClient {
static final String TAG = RexxarWebViewClient.class.getSimpleName();
private List<RexxarWidget> mWidgets = new ArrayList<>();
/**
* 自定义url拦截处理
*
* @param widget
*/
public void addRexxarWidget(RexxarWidget widget) {
if (null != widget) {
mWidgets.add(widget);
}
}
public List<RexxarWidget> getRexxarWidgets() {
return mWidgets;
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
LogUtils.i(TAG, "[shouldOverrideUrlLoading] : url = " + url);
if (url.startsWith(Constants.CONTAINER_WIDGET_BASE)) {
boolean handled;
for (RexxarWidget widget : mWidgets) {
if (null != widget) {
handled = widget.handle(view, url);
if (handled) {
return true;
}
}
}
}
return super.shouldOverrideUrlLoading(view, url);
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
if (Utils.hasLollipop()) {
return handleResourceRequest(view, request.getUrl().toString());
} else {
return super.shouldInterceptRequest(view, request);
}
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
return handleResourceRequest(view, url);
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
LogUtils.i(TAG, "onPageStarted");
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
LogUtils.i(TAG, "onPageFinished");
}
@Override
public void onLoadResource(WebView view, String url) {
super.onLoadResource(view, url);
LogUtils.i(TAG, "onLoadResource : " + url);
}
/**
* 拦截资源请求,部分资源需要返回本地资源
* <p>
* <p>
* html,js资源直接渲染进程返回,图片等其他资源先返回空的数据流再异步向流中写数据
* <p>
* <p>
* <note>这个方法会在渲染线程执行,如果做了耗时操作会block渲染</note>
*/
private WebResourceResponse handleResourceRequest(WebView webView, String requestUrl) {
if (!shouldIntercept(requestUrl)) {
return super.shouldInterceptRequest(webView, requestUrl);
}
LogUtils.i(TAG, "[handleResourceRequest] url = " + requestUrl);
// html直接返回
if (Helper.isHtmlResource(requestUrl)) {
// decode resource
if (requestUrl.startsWith(Constants.FILE_AUTHORITY)) {
requestUrl = requestUrl.substring(Constants.FILE_AUTHORITY.length());
}
final CacheEntry cacheEntry = CacheHelper.getInstance().findHtmlCache(requestUrl);
if (null == cacheEntry) {
// 没有cache,显示错误界面
showError(RexxarWebViewCore.RxLoadError.HTML_NO_CACHE.type);
return super.shouldInterceptRequest(webView, requestUrl);
} else if (!cacheEntry.isValid()) {
// 有cache但无效,显示错误界面且清除缓存
showError(RexxarWebViewCore.RxLoadError.HTML_NO_CACHE.type);
CacheHelper.getInstance().removeHtmlCache(requestUrl);
} else {
LogUtils.i(TAG, "cache hit :" + requestUrl);
String data = "";
try {
data = IOUtils.toString(cacheEntry.inputStream);
// hack 检查cache是否完整
if (TextUtils.isEmpty(data) || !data.endsWith("</html>")) {
showError(RexxarWebViewCore.RxLoadError.HTML_CACHE_INVALID.type);
CacheHelper.getInstance().removeHtmlCache(requestUrl);
}
} catch (IOException e) {
e.printStackTrace();
// hack 检查cache是否完整
showError(RexxarWebViewCore.RxLoadError.HTML_CACHE_INVALID.type);
CacheHelper.getInstance().removeHtmlCache(requestUrl);
}
return new WebResourceResponse(Constants.MIME_TYPE_HTML, "utf-8", IOUtils.toInputStream(data));
}
}
// js直接返回
if (Helper.isJsResource(requestUrl)) {
final CacheEntry cacheEntry = CacheHelper.getInstance().findCache(requestUrl);
if (null == cacheEntry) {
// 后面逻辑会通过network去加载
// 加载后再显示
} else if (!cacheEntry.isValid()){
// 后面逻辑会通过network去加载
// 加载后再显示
// 清除缓存
CacheHelper.getInstance().removeInternalCache(requestUrl);
} else {
String data = "";
try {
data = IOUtils.toString(cacheEntry.inputStream);
if (TextUtils.isEmpty(data) || (cacheEntry.length > 0 && cacheEntry.length != data.length())) {
showError(RexxarWebViewCore.RxLoadError.JS_CACHE_INVALID.type);
CacheHelper.getInstance().removeInternalCache(requestUrl);
}
} catch (IOException e) {
e.printStackTrace();
showError(RexxarWebViewCore.RxLoadError.JS_CACHE_INVALID.type);
CacheHelper.getInstance().removeInternalCache(requestUrl);
}
LogUtils.i(TAG, "cache hit :" + requestUrl);
return new WebResourceResponse(Constants.MIME_TYPE_HTML, "utf-8", IOUtils.toInputStream(data));
}
}
// 图片等其他资源使用先返回空流,异步写数据
String fileExtension = MimeTypeMap.getFileExtensionFromUrl(requestUrl);
String mimeType = MimeUtils.guessMimeTypeFromExtension(fileExtension);
try {
LogUtils.i(TAG, "start load async :" + requestUrl);
final PipedOutputStream out = new PipedOutputStream();
final PipedInputStream in = new PipedInputStream(out);
WebResourceResponse xResponse = new WebResourceResponse(mimeType, "UTF-8", in);
if (Utils.hasLollipop()) {
Map<String, String> headers = new HashMap<>();
headers.put("Access-Control-Allow-Origin", "*");
xResponse.setResponseHeaders(headers);
}
final String url = requestUrl;
webView.post(new Runnable() {
@Override
public void run() {
new Thread(new ResourceRequest(url, out, in)).start();
}
});
return xResponse;
} catch (IOException e) {
e.printStackTrace();
LogUtils.e(TAG, "url : " + requestUrl + " " + e.getMessage());
return super.shouldInterceptRequest(webView, requestUrl);
} catch (Throwable e) {
e.printStackTrace();
LogUtils.e(TAG, "url : " + requestUrl + " " + e.getMessage());
return super.shouldInterceptRequest(webView, requestUrl);
}
}
/**
* html或js加载错误,页面无法渲染,通知{@link RexxarWebView}显示错误界面,重新加载
*
* @param errorType 错误类型
*/
public void showError(int errorType) {
Bundle bundle = new Bundle();
bundle.putInt(Constants.KEY_ERROR_TYPE, errorType);
BusProvider.getInstance().post(new BusProvider.BusEvent(Constants.EVENT_REXXAR_NETWORK_ERROR, bundle));
}
/**
* @param requestUrl
* @return
*/
private boolean shouldIntercept(String requestUrl) {
if (TextUtils.isEmpty(requestUrl)) {
return false;
}
// file协议需要替换,用于html
if (requestUrl.startsWith(Constants.FILE_AUTHORITY)) {
return true;
}
// rexxar container api,需要拦截
if (requestUrl.startsWith(Constants.CONTAINER_API_BASE)) {
return true;
}
// 非合法uri,不拦截
Uri uri = null;
try {
uri = Uri.parse(requestUrl);
} catch (Exception e) {
e.printStackTrace();
}
if (null == uri) {
return false;
}
// 非合法host,不拦截
String host = uri.getHost();
if (TextUtils.isEmpty(host)) {
return false;
}
// 不能拦截的uri,不拦截
Pattern pattern;
Matcher matcher;
for (String interceptHostItem : ResourceProxy.getInstance().getProxyHosts()) {
pattern = Pattern.compile(interceptHostItem);
matcher = pattern.matcher(host);
if (matcher.find()) {
return true;
}
}
return false;
}
private static class Helper {
/**
* 是否是html文档
*
* @param requestUrl
* @return
*/
public static boolean isHtmlResource(String requestUrl) {
if (TextUtils.isEmpty(requestUrl)) {
return false;
}
String fileExtension = MimeTypeMap.getFileExtensionFromUrl(requestUrl);
return TextUtils.equals(fileExtension, Constants.EXTENSION_HTML)
|| TextUtils.equals(fileExtension, Constants.EXTENSION_HTM);
}
/**
* 是否是js文档
*
* @param requestUrl
* @return
*/
public static boolean isJsResource(String requestUrl) {
if (TextUtils.isEmpty(requestUrl)) {
return false;
}
String fileExtension = MimeTypeMap.getFileExtensionFromUrl(requestUrl);
return TextUtils.equals(fileExtension, Constants.EXTENSION_JS);
}
/**
* 构建网络请求
*
* @param requestUrl
* @return
*/
public static Request buildRequest(String requestUrl) {
if (TextUtils.isEmpty(requestUrl)) {
return null;
}
Request.Builder builder = new Request.Builder()
.url(requestUrl);
Uri uri = Uri.parse(requestUrl);
String method = uri.getQueryParameter(Constants.KEY_METHOD);
// 如果没有值则视为get
if (Constants.METHOD_POST.equalsIgnoreCase(method)) {
FormBody.Builder formBodyBuilder = new FormBody.Builder();
Set<String> names = uri.getQueryParameterNames();
for (String key : names) {
formBodyBuilder.add(key, uri.getQueryParameter(key));
}
builder.method("POST", formBodyBuilder.build());
} else {
builder.method("GET", null);
}
builder.addHeader("User-Agent", Rexxar.getUserAgent());
return builder.build();
}
}
/**
* {@link #shouldInterceptRequest(WebView, String)} 异步拦截
* <p>
* 先返回一个空的InputStream,然后再通过异步的方式向里面写数据。
*/
private class ResourceRequest implements Runnable {
// 请求地址
String mUrl;
// 输出流
PipedOutputStream mOut;
// 输入流
PipedInputStream mTarget;
public ResourceRequest(String url, PipedOutputStream outputStream, PipedInputStream target) {
this.mUrl = url;
this.mOut = outputStream;
this.mTarget = target;
}
@Override
public void run() {
try {
// read cache first
CacheEntry cacheEntry = null;
if (CacheHelper.getInstance().cacheEnabled()) {
cacheEntry = CacheHelper.getInstance().findCache(mUrl);
}
if (null != cacheEntry && cacheEntry.isValid()) {
byte[] bytes = IOUtils.toByteArray(cacheEntry.inputStream);
LogUtils.i(TAG, "load async cache hit :" + mUrl);
mOut.write(bytes);
return;
}
// request network
Response response = ResourceProxy.getInstance().getNetwork()
.handle(Helper.buildRequest(mUrl));
// write cache
if (response.isSuccessful()) {
InputStream inputStream = null;
if (CacheHelper.getInstance().checkUrl(mUrl) && null != response.body()) {
CacheHelper.getInstance().saveCache(mUrl, IOUtils.toByteArray(response.body().byteStream()));
cacheEntry = CacheHelper.getInstance().findCache(mUrl);
if (null != cacheEntry && cacheEntry.isValid()) {
inputStream = cacheEntry.inputStream;
}
}
if (null == inputStream && null != response.body()) {
inputStream = response.body().byteStream();
}
// write output
if (null != inputStream) {
mOut.write(IOUtils.toByteArray(inputStream));
LogUtils.i(TAG, "load async completed :" + mUrl);
}
} else {
LogUtils.i(TAG, "load async failed :" + mUrl);
if (Helper.isJsResource(mUrl)) {
showError(RexxarWebViewCore.RxLoadError.JS_CACHE_INVALID.type);
return;
}
// return request error
byte[] result = wrapperErrorResponse(response);
if (Rexxar.DEBUG) {
LogUtils.i(TAG, "Api Error: " + new String(result));
}
try {
mOut.write(result);
} catch (IOException e1) {
e1.printStackTrace();
}
}
} catch (SocketTimeoutException e) {
try {
byte[] result = wrapperErrorResponse(e);
if (Rexxar.DEBUG) {
LogUtils.i(TAG, "SocketTimeoutException: " + new String(result));
}
mOut.write(result);
} catch (IOException e1) {
e1.printStackTrace();
}
} catch (ConnectTimeoutException e) {
byte[] result = wrapperErrorResponse(e);
if (Rexxar.DEBUG) {
LogUtils.i(TAG, "ConnectTimeoutException: " + new String(result));
}
try {
mOut.write(result);
} catch (IOException e1) {
e1.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
LogUtils.i(TAG, "load async exception :" + mUrl + " ; " + e.getMessage());
if (Helper.isJsResource(mUrl)) {
showError(RexxarWebViewCore.RxLoadError.JS_CACHE_INVALID.type);
return;
}
byte[] result = wrapperErrorResponse(e);
if (Rexxar.DEBUG) {
LogUtils.i(TAG, "Exception: " + new String(result));
}
try {
mOut.write(result);
} catch (IOException e1) {
e1.printStackTrace();
}
} finally {
try {
mOut.flush();
mOut.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private boolean responseGzip(Map<String, String> headers) {
for (Map.Entry<String, String> entry : headers.entrySet()) {
if (entry.getKey()
.toLowerCase()
.equals(Constants.HEADER_CONTENT_ENCODING.toLowerCase())
&& entry.getValue()
.toLowerCase()
.equals(Constants.ENCODING_GZIP.toLowerCase())) {
return true;
}
}
return false;
}
private byte[] parseGzipResponseBody(ResponseBody body) throws IOException{
Buffer buffer = new Buffer();
GzipSource gzipSource = new GzipSource(body.source());
while (gzipSource.read(buffer, Integer.MAX_VALUE) != -1) {
}
gzipSource.close();
return buffer.readByteArray();
}
private byte[] wrapperErrorResponse(Exception exception){
if (null == exception) {
return new byte[0];
}
try {
// generate json response
JSONObject result = new JSONObject();
result.put(Constants.KEY_NETWORK_ERROR, true);
return (Constants.ERROR_PREFIX + result.toString()).getBytes();
} catch (Exception e) {
e.printStackTrace();
}
return new byte[0];
}
private byte[] wrapperErrorResponse(Response response){
if (null == response) {
return new byte[0];
}
try {
// read response content
Map<String, String> responseHeaders = new HashMap<>();
for (String field : response.headers()
.names()) {
responseHeaders.put(field, response.headers()
.get(field));
}
byte[] responseContents = new byte[0];
if (null != response.body()) {
if (responseGzip(responseHeaders)) {
responseContents = parseGzipResponseBody(response.body());
} else {
responseContents = response.body().bytes();
}
}
// generate json response
JSONObject result = new JSONObject();
result.put(Constants.KEY_RESPONSE_CODE, response.code());
String apiError = new String(responseContents, "utf-8");
try {
JSONObject content = new JSONObject(apiError);
result.put(Constants.KEY_RESPONSE_ERROR, content);
} catch (Exception e) {
e.printStackTrace();
result.put(Constants.KEY_RESPONSE_ERROR, apiError);
}
return (Constants.ERROR_PREFIX + result.toString()).getBytes();
} catch (Exception e) {
e.printStackTrace();
}
return new byte[0];
}
}
}
shouldOverrideUrlLoading回调在新的url访问时,给所有Widgets一个处理机会,如果有控件处理,相当于拦截了这个请求。
shouldInterceptRequest这个回调会在所有的数据请求的时候回调到。对html资源,直接从本地缓存返回,对js资源也是试图从本地资源返回。否则会发请求去取,这一段非常巧妙。
// 图片等其他资源使用先返回空流,异步写数据
String fileExtension = MimeTypeMap.getFileExtensionFromUrl(requestUrl);
String mimeType = MimeUtils.guessMimeTypeFromExtension(fileExtension);
try {
LogUtils.i(TAG, "start load async :" + requestUrl);
final PipedOutputStream out = new PipedOutputStream();
final PipedInputStream in = new PipedInputStream(out);
WebResourceResponse xResponse = new WebResourceResponse(mimeType, "UTF-8", in);
if (Utils.hasLollipop()) {
Map<String, String> headers = new HashMap<>();
headers.put("Access-Control-Allow-Origin", "*");
xResponse.setResponseHeaders(headers);
}
final String url = requestUrl;
webView.post(new Runnable() {
@Override
public void run() {
new Thread(new ResourceRequest(url, out, in)).start();
}
});
return xResponse;
} catch (IOException e) {
e.printStackTrace();
LogUtils.e(TAG, "url : " + requestUrl + " " + e.getMessage());
return super.shouldInterceptRequest(webView, requestUrl);
} catch (Throwable e) {
e.printStackTrace();
LogUtils.e(TAG, "url : " + requestUrl + " " + e.getMessage());
return super.shouldInterceptRequest(webView, requestUrl);
}
啥意思呢,先返回这个空response,但是异步往里面写数据。ResourceRequest里又是一套匹配缓存-请求-缓存-写返回的逻辑。这个地方第一次知道WebResourceResponse可以这么玩,新鲜干货。这里还包含了Container请求的处理逻辑。
这里的Container就是说,注册一个指定url,客户端会把这个路径识别为js->native的method call,然后客户端处理后以JSON的格式返回,请求既不走JsPompt也不走JsInterface。
widget实际上也是注册一个url,只是这个url回调在shouldOverrideUrlLoading,以douban://开头。功能是一样的,可能逻辑上定义成了两套组件。就是说widget被认为是界面相关的,container被认为是功能相关的。
好了,拆轮子拆完了。。。学到了一些,但是离期待学到的不够多啊。。。