Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Rexxaar android筆記

Rexxaar android筆記

編輯:關於Android編程

跟著代碼看一看豆瓣開源的混合開發框架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的封裝。



    

        
    

    

    

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 mUriLoadCallback = new WeakReference(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(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(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 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 mWidgets = new ArrayList<>();

    /**
     * 自定義url攔截處理
     *
     * @param widget
     */
    public void addRexxarWidget(RexxarWidget widget) {
        if (null != widget) {
            mWidgets.add(widget);
        }
    }

    public List 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);
    }

    /**
     * 攔截資源請求,部分資源需要返回本地資源
     *

*

* html,js資源直接渲染進程返回,圖片等其他資源先返回空的數據流再異步向流中寫數據 *

*

* 這個方法會在渲染線程執行,如果做了耗時操作會block渲染 */ 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("")) { 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 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 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)} 異步攔截 *

* 先返回一個空的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 headers) { for (Map.Entry 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 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 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. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved