Rexxaar android笔记
来源:互联网 发布:黑科技和人工智能 编辑:程序博客网 时间:2024/06/11 10:30
跟着代码看一看豆瓣开源的混合开发框架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被认为是功能相关的。
好了,拆轮子拆完了。。。学到了一些,但是离期待学到的不够多啊。。。
- Rexxaar android笔记
- android笔记
- android笔记
- android笔记
- Android笔记
- Android笔记
- android笔记
- android 笔记
- android笔记
- Android笔记
- android笔记
- Android笔记
- Android笔记
- android笔记
- android笔记
- android笔记
- android 笔记
- android 笔记
- [精编][转]do{}while(0)宏详解
- 今天做的笔试的算法题。。。泪目啊
- Android 开发 之 JNI入门 - NDK从入门到精通
- 交叉熵代价函数
- Android 开发注意事项
- Rexxaar android笔记
- 恐龙技术生存体验(二 初出茅庐)
- 深度学习(二十四)矩阵分解之基于k-means的特征表达学习
- iOS CoreAnimation详解和使用
- 欢迎使用CSDN-markdown编辑器
- 设置按钮显示特性(setBacgroundDrawable)替换方法
- android 各国语言对应的缩写
- Apache RewriteCond RewriteRule 跳转故障解决
- python字符串连接的N种方式