Rexxaar android笔记

本文详细探讨了豆瓣开源的混合开发框架Rexxaar,重点介绍了其初始化过程,包括RouteManager、ResourceProxy的功能。Rexxaar通过OkHttp处理网络请求并缓存数据,RouteManager负责请求路由,ResourceProxy管理资源。文中还分析了WebView的定制,如RexxarWebViewCore的安全措施,并揭示了资源加载和缓存机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

跟着代码看一看豆瓣开源的混合开发框架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被认为是功能相关的。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值