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