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缓存说明
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被认为是功能相关的。

好了,拆轮子拆完了。。。学到了一些,但是离期待学到的不够多啊。。。

1 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 手机百度云和谐怎么办 服务器连接不上怎么办 微粒贷请求失败怎么办 房子里手机信号不好怎么办 屋里上网没信号怎么办 屋子里面没信号怎么办 crm服务器关机了怎么办 日本代金券诈骗怎么办 发票已认证 作废怎么办 淘宝不发发票怎么办 ipd密码忘记了怎么办 apple id闪退怎么办 信用卡持卡人死亡欠款怎么办 信用卡名字错了怎么办 ios超出手机内存怎么办 app id停用了怎么办 相机储存空间不足怎么办 苹果icloud8满了怎么办 红米云空间已满怎么办 忘记手机密码怎么办oppo oppo云相册丢失怎么办 oppoa79密码忘了怎么办 云存储空间满了怎么办 云备份空间不足怎么办 安卓没有中文怎么办 燃气表显示异常怎么办 is语音登录不了怎么办 is语音禁止登录怎么办 淘宝竞争不过同行怎么办 碰到比价的顾客怎么办 淘宝同行恶意捣乱怎么办 怀孕吃了打虫药怎么办 阿迪鞋微信中签不能取怎么办 康乃馨花叶长斑怎么办 杜鹃花叶长斑怎么办 青苹果竹芋烂根怎么办 朱槿花叶子发黄怎么办 猫眼叶子卷了怎么办 鞋底氧化变黄怎么办 白色旅游鞋变黄怎么办 如意皇后不张新叶片怎么办