編輯:關於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 WeakReferencemUriLoadCallback = 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 ListmWidgets = 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()) { Mapheaders = 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()) { Mapheaders = 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被認為是功能相關的。
好了,拆輪子拆完了。。。學到了一些,但是離期待學到的不夠多啊。。。
神秘的Android NDK開發往往眾多程序員感到興奮,但又不知它為何物,由於近期開發應用時,為了是開發的.apk文件不被他人解讀(反編譯),查閱了很多資料,其中有提到使
互聯網的發展是非常迅猛的,剛剛覺得自己適應了eclipse的用法,突然發現它已經被淘汰了。OK,今天不是來說eclipse和Android studio的褒貶。我們是來學
和大家一起分享一下學習經驗,如何實現Android文件下載進度顯示功能,希望對廣大初學者有幫助。先上效果圖: 上方的藍色進度條,會根據文件下載量的百分比進行加載,中部的
本文來自http://blog.csdn.net/hellogv/ ,引用必須注明出處! 上次講了Android手機與BLE終端之間的通信,而最常見的BLE終端應該是蘋果
Android的Notification是android系統中很重要的一