1 概述
安卓webview经验汇总
2 引擎
2.1 自带
安卓自带有一个webview引擎,优点是安装包比较小,缺点是兼容性有很多问题,而且还各种bug,4.2以下的addJavascriptInterface的注入攻击。
2.2 Crosswalk
Intel赞助的一个项目,将chromium移植到手机上,安装包上包含的就是一整个chromium的webview引擎,所以安装包比较大,22m,优点是快,兼容性好,稳定性好,缺点是大,炒鸡大。
后来出了Crosswalk-Lite,说是将Chromium中的webgl等不必要的部分删掉,精简成一个只包含常用功能的webview引擎,有用过,可是比较不稳定,而且第一次启动时还会因为解压压缩包的原因导致超级慢,不推荐使用。
2.3 X5
QQ家的项目,由于微信与QQ中内置了一个自带的浏览器内核,所以开放出浏览器内核X5引擎,当用户装有微信或QQ时,会自动切换为X5内核的浏览器,否则会使用自带的浏览器。支持的html5特性没有Crosswalk标准而且全面,但胜在微信与QQ的安装率很高,所以在对安装包有严格限制的情况下,非常推荐使用X5引擎。另外,X5引擎自带有video全屏,图片上传等功能,这点比Crosswalk要好。
2.4 总结
如果是纯粹的hybrid项目,是比较建议使用Crosswalk的,质量很好,我们线上的app平均崩溃率只有0.2%。如果对安装包大小有要求,或者webview只是app中嵌套的一个小功能,则建议使用X5引擎。
3 webview通信
安卓webview通信,安卓的webview与javascript之间如何安全可靠高效的通信,这个看似简单的问题,其实很头疼。
3.1 原始方法
//设置支持javascript
setting.setJavaScriptEnabled(true);
//javascript->Java
webView.addJavascriptInterface(new Object(){
public void startPhone(String num){
Intent intent=new Intent();
intent.setAction(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:"+num));
startActivity(intent);
}
}, "demo");
//Java->javascript
webView.loadUrl("javascript:alert(123)");
使用webview下的addJavascriptInterface就能实现javascript通信给java,而loadUrl就能实现java通信给javascript。这个方案优点在于简单可靠,缺点是无法传入javascript中的闭包参数,而且4.2版本下的安卓还有安全性问题
3.2 拦截url
public WebResourceResponse shouldInterceptRequest(WebView view,String url) {
return m_jsInterface.shouldInterceptLoadRequest(url);
}
安卓的setWebViewClient中提供了可以拦截url请求的方法,如果我们将所有javascript对java的接口调用改为ajax请求,然后返回时像服务器一样将数据写入WebResourceResponse不就可以了。
public WebResourceResponse shouldInterceptLoadRequest(String url) {
try{
//校验协议
final Uri urlInfo = Uri.parse(url);
if( urlInfo.getScheme().equals("http") == false )
return null;
//校验pathInfo
List<String> pathInfo = urlInfo.getPathSegments();
if( pathInfo.size() != 3 ){
return null;
}
final String schemaName = pathInfo.get(0);
final String objectName = pathInfo.get(1);
final String methodName = pathInfo.get(2);
if( schemaName.equals("crossapi") == false )
return null;
Log.d("SafeWebView","crossapi request "+url);
final PipedOutputStream out = new PipedOutputStream();
PipedInputStream in = new PipedInputStream(out);
//调用接口
invokeBridge(objectName,methodName,new SafeWebViewJsInferfaceUriBridge(urlInfo,out));
return new WebResourceResponse("text/plain","utf-8",in);
}catch(Exception e){
e.printStackTrace();
return null;
}
}
写法还是蛮简单的,要注意的是使用PipedOutputStream来输出数据,避免在shouldInterceptLoadRequest长时间执行,导致UI线程阻塞
$.get("/crossapi/device/waitResponse",{test:"你好"},function(data){
console.log(JSON.stringify(data));
});
像ajax一样调用远程请求,来实现javascript调用java即可,java回调javascript则通过的是WebResponse写入。这种方法用很好的安全性,而且兼容性也很好,而且也支持javascript的闭包参数传入。
我们将这种办法也在线上沿用了很久,基本也没什么问题。可是,在做友盟推送接口发现,这种做法的系统消耗比较大,而且如果一个接口的阻塞比较久不返回,就会连累其他接口不及时返回。每次回调的次数只能是一次,不能说javascript请求一次java后,java能无限次地回调javascript接口,这个限制使得在做推送接口时也显得比较麻烦。
3.3 拦截confirm+loadUrl调用
@Override
public boolean onJsConfirm(WebView view, String url, String message,
JsResult result) {
boolean isHandle = m_jsInterface.onJsConfirm(view, message);
if( isHandle ){
result.confirm();
return true;
}
return super.onJsConfirm(view, url, message, result);
}
安卓的setWebChromeClient中提供了可以拦截confirm,alert和prompt,如果我们在这里将请求拦截,然后返回时使用loadUrl来注入javascript不就可以返回了。问题是,loadUrl只支持基本类型的回调,不支持javascript的闭包回调,怎么办?
var CrossapiCallbackPool = [];
var Crossapi = function(url,argv,callback){
CrossapiCallbackPool.push(callback);
var callbackId = CrossapiCallbackPool.length - 1;
var bundle = {
url:url,
argv:argv,
callback:callbackId,
};
var prefix = "CrossApiBridge:"+JSON.stringify(bundle);
confirm(prefix);
};
解决办法时,传入confirm前,将闭包数据转换为CallbackPool上一个整数ID就可以了,这样回调时使用
window.CrossapiCallbackPool[callbackId](xxxx)
就能实现java对javascript数据的闭包回调了
public boolean onJsConfirm(WebView webView,String message){
try{
//校验协议
String prefix = "CrossApiBridge:";
if( message.startsWith(prefix) == false ){
return false;
}
JSONTokener tokener = new JSONTokener(message.substring(prefix.length()));
JSONObject invokeInfo = (JSONObject)tokener.nextValue();
String url = invokeInfo.getString("url");
Object argv = invokeInfo.get("argv");
int callbackId = invokeInfo.getInt("callback");
String[] splitInfoTemp = url.split("/");
List<String> splitInfo = new ArrayList<String>();
for( String singleSplit : splitInfoTemp){
if( singleSplit.trim().length() == 0 ){
continue;
}
splitInfo.add(singleSplit.trim());
}
if( splitInfo.size() < 2 ){
return false;
}
if( argv instanceof JSONObject == false ){
return false;
}
final String objectName = splitInfo.get(0);
final String methodName = splitInfo.get(1);
//调用接口
invokeBridge(objectName,methodName,new SafeWebViewJsInferfacePromptBridge((JSONObject)argv,webView,callbackId));
return true;
}catch(Exception e){
e.printStackTrace();
return false;
}
}
写法还是挺简单的,要注意的是,这种方法需要预先在javascript环境中注入代码,不然回调中用不了。可以指定用户加载某个javascript文件,而这个文件默认在放在安卓本地环境,这样的话javascript用户用起来比较方便。
Crossapi("/device/waitResponse",{test:"你好"},function(data){
console.log(data);
});
调用时直接调用即可,这种方法较好地实现了,安全,高效,支持闭包传递的特性,解决了拦截url的多请求阻塞问题。我们也在逐步迁移到这种webviewbridge的手法上。这也是目前较为流行的做法
3.4 总结
怎样建立一个可靠的webviewbridge,这个问题其实也不简单噢,需要好好考虑一下。
4 配置
4.1 硬件加速
<application
android:hardwareAccelerated="true"
android:allowBackup="false"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:theme="@style/AppTheme"
tools:replace="android:allowBackup">
</application>
在AndroidManifest中设置android:hardwareAccelerated为true,开启硬件加速模式
4.2 javascript
this.getSettings().setJavaScriptEnabled(true);
开启webview的javascript,不然网页中的javascript代码默认是不启动的。
4.3 视频
全屏播放视频有两种模式,分别是webkit播放模式和x5播放模式
4.3.1 webkit播放模式
webkit播放模式是默认的播放模式,自带浏览器,Crosswalk浏览器和X5内核浏览器都支持的播放模式。其原理为在遇到需要全屏播放视频的页面时,webkit会将播放视频的view传递给开发者,开发者将播放view替代webview即可。
WebChromeClient chromeClient = new WebChromeClient() {
View myVideoView;
View myNormalView;
CustomViewCallback callback;
@Override
public void onShowCustomView(View view, CustomViewCallback customViewCallback) {
View normalView = X5WebView.this;
ViewGroup viewGroup = (ViewGroup) normalView.getParent();
viewGroup.removeView(normalView);
viewGroup.addView(view);
myVideoView = view;
myNormalView = normalView;
callback = customViewCallback;
}
@Override
public void onHideCustomView() {
if (callback != null) {
callback.onCustomViewHidden();
callback = null;
}
if (myVideoView != null) {
ViewGroup viewGroup = (ViewGroup) myVideoView.getParent();
viewGroup.removeView(myVideoView);
viewGroup.addView(myNormalView);
}
}
};
this.setWebChromeClient(chromeClient);
4.3.2 X5内核
X5浏览器支持三种播放视频的模式,分别是webkit播放模式与x5播放模式,x5播放模式是将视频文件跳转到一个特别的activity来单独播放,体验更好,速度也更流畅,推荐使用。但要注意的是,x5播放模式需要WebView的Context必须为Activity类型,我们试过在react-native中嵌套X5浏览器,传入webview的context是ReactContext,发现视频一直都是黑屏,坑爹呀。
<activity
android:name="com.tencent.smtt.sdk.VideoActivity"
android:alwaysRetainTaskState="true"
android:configChanges="orientation|screenSize|keyboardHidden"
android:exported="false"
android:launchMode="singleTask" >
<intent-filter>
<action android:name="com.tencent.smtt.tbs.video.PLAY" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
在AndroidManifest中加上这个activity
if(this.getX5WebViewExtension()!=null){
Bundle data = new Bundle();
data.putBoolean("standardFullScreen", false);//true表示标准全屏,false表示X5全屏;不设置默认false,
data.putBoolean("supportLiteWnd", false);//false:关闭小窗;true:开启小窗;不设置默认true,
data.putInt("DefaultVideoScreen", 2);//1:以页面内开始播放,2:以全屏开始播放;不设置默认:1
this.getX5WebViewExtension().invokeMiscMethod("setVideoParams", data);
}
在webview加上以上配置,那么在遇到带有视频的网页时,点击播放按钮就会跳转到com.tencent.smtt.sdk.VideoActivity来全屏播放视频了,非常方便。
4.4 对话框
有时候,我们需要更改console,alert,confirm,prompt这几种原生的对话框样式,我们可以这么做。
WebChromeClient chromeClient = new WebChromeClient() {
@Override
public boolean onConsoleMessage(ConsoleMessage var1) {
return super.onConsoleMessage(var1);
}
@Override
public boolean onJsConfirm(WebView arg0, String arg1, String arg2, JsResult arg3) {
//TODO
return super.onJsConfirm(arg0, arg1, arg2, arg3);
}
@Override
public boolean onJsAlert(WebView arg0, String arg1, String arg2, JsResult arg3) {
//TODO
return super.onJsAlert(null, "www.baidu.com", "aa", arg3);
}
@Override
public boolean onJsPrompt(WebView arg0, String arg1, String arg2, String arg3, JsPromptResult arg4) {
//TODO
return super.onJsPrompt(arg0, arg1, arg2, arg3, arg4);
}
};
this.setWebChromeClient(chromeClient);
修改一下WebChromeClient里面的回调就可以了,挺简单的。
4.5 缓存
webSetting.setAppCacheEnabled(true);
webSetting.setDatabaseEnabled(true);
webSetting.setDomStorageEnabled(true);
webSetting.setAppCacheMaxSize(Long.MAX_VALUE);
webSetting.setCacheMode(WebSettings.LOAD_NO_CACHE);
开启html5的各种缓存模式,database,appcache,以及domstorage,而cacheMode是用来配置http协议上的缓存。
4.6 拦截请求
4.6.1 页面加载
WebViewClient webviewClient = new WebViewClient() {
public void onPageFinished(WebView view, String url) {
//TODO
}
public void onPageStarted(WebView view, String url, Bitmap favicon) {
//TODO
}
}
this.setWebViewClient(webViewClient);
WebChromeClient webChromeClient = new WebChromeClient() {
public void onProgressChanged(WebView var1, int var2) {
//TODO
}
}
this.setWebChromeClient(webChromeClient);
webViewClient与webChromeClient中一起提供了页面开始加载,加载中,加载完成这三个事件,这也是大多数浏览器中实现顶部进度条的原理。
4.6.2 页面内请求
WebViewClient webviewClient = new WebViewClient() {
public WebResourceResponse shouldInterceptRequest (WebView view,
WebResourceRequest request) {
//TODO
return super.shouldInterceptRequest(view,request);
}
public WebResourceResponse shouldInterceptRequest(WebView view,
String url) {
//TODO
return super.shouldInterceptRequest(view,url);
}
}
this.setWebViewClient(webViewClient);
拦截页面中的ajax,文件等各种各样的请求,也有被用来做webview通信的。
4.6.3 页面外请求
WebViewClient webviewClient = new WebViewClient() {
/**
* 防止加载网页时调起系统浏览器
*/
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
}
this.setWebViewClient(webViewClient);
拦截页面中跳转到其他页面的请求,一般不做拦截的话,安卓浏览器中会弹出提示框,提示你是否跳转到系统浏览器中看这个页面。所以,要么是在重用本地的webview来加载新页面,要么是新开一个activity的webview来加载新页面,看业务了。
4.6.4 新窗口请求
WebChromeClient webChromeClient = new WebChromeClient() {
public boolean onCreateWindow(WebView var1, boolean var2, boolean var3, Message var4) {
return super.onCreateWindow(var1,var2,var3,var4);
}
}
this.setWebChromeClient(webChromeClient);
拦截用javascript来启动新窗口的请求,这个一般是不拦截的,不允许javascript来开启新窗口。
4.7 安全
this.getSettings().setAllowFileAccess(true);
this.getSettings().setAllowFileAccessFromFileURLs(true);
this.getSettings().setAllowUniversalAccessFromFileURLs(true);
设置file域名下页面的安全性,一般file域名是设置为无跨域的,简单暴力。
4.8 图片选择器
this.setWebChromeClient(new WebChromeClient(){
@Override
public void openFileChooser(final ValueCallback<Uri> var1, String var2, String var3) {
if( m_fileChooseListener == null ){
return;
}
m_fileChooseListener.onChoose(new FinishListener<Uri>() {
@Override
public void onFinish(int code, String message, Uri data) {
if( code != 0 ){
var1.onReceiveValue(null);
return;
}
Log.d("CrossWebView","onChoose "+data);
var1.onReceiveValue(data);
}
},var2);
}
@Override
public boolean onShowFileChooser(WebView var1, final ValueCallback<Uri[]> var2, WebChromeClient.FileChooserParams var3) {
if( m_fileChooseListener == null ){
return true;
}
String[] acceptTypes = var3.getAcceptTypes();
m_fileChooseListener.onChoose(new FinishListener<Uri>() {
@Override
public void onFinish(int code, String message, Uri data) {
if( code != 0 ){
var2.onReceiveValue(null);
return;
}
Log.d("CrossWebView","onChoose "+data);
var2.onReceiveValue(new Uri[]{data});
}
},acceptTypes[0]);
return true;
}
});
在setChromeClient中设置openFileChooser和onShowFileChooser两个回调就可以了
m_crossWebView.setOnFileChooserListener(new CrossWebView.OnFileChooserListener() {
@Override
public void onChoose(final FinishListener<Uri> callback, String accept) {
try {
JSONObject args = new JSONObject();
args.put("format", "url");
FinishListener<Object> argsCallback = new FinishListener<Object>() {
@Override
public void onFinish(int code, String msg, Object data) {
if (code != 0) {
callback.onFinish(code, msg, null);
return;
}
String dataStr = (String) data;
Uri uri = Uri.fromFile(new File(dataStr));
callback.onFinish(0, "", uri);
}
};
m_imageApi.preview(new CrossWebViewJsContextStub(args, argsCallback));
}catch (Exception e){
callback.onFinish(1, e.getMessage(), null);
}
}
要注意的是,我们打开图片浏览器后,获取的String类型的url最好用Uri.fromFile来转换成Uri后再吐给webview的回调就可以了
5 启动
5.1 loadUrl启动
webView.loadUrl("file:///android_asset/webpage/fullscreenVideo.html");
webView.loadUrl("http://www.baidu.com");
要么是加载本地资源上的文件,要么是加载网络上的文件,都可以。
5.2 loadData启动
view.loadDataWithBaseURL(view.getBaseUrl(), html, "text/html", view.getCharset(), null);
传入一段指定的html数据,让webview跑起来,这个还是比较简单的。
<!--错误演示-->
<div>HelloWorld</div>
<!--正确演示-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
<div>HelloWorld</div>
</body>
</html>
但是要注意的是,很多时候,我们是用这个loadData来展示富文本内容的,所以就直接将div扔进去loadData里面,这样会造成很多兼容性问题,所以,请务必在富文本前后加上恰当的html内容后才扔进webview内部。
6 优化
6.1 加载优化
- 并发加载优化
- 先拉缓存的md5,检查是否需要更新,然后使用
- 直接拉缓存,缓存更新由服务器更新
- 直接旧缓存,然后再检查是否需要更新,然后再刷新
7 FAQ
7.1 webview高度自适应
this.addJavascriptInterface(new Object(){
@JavascriptInterface
public void getContentHeight(double height){
//TODO height
}
},"NativeAndroid");
WebChromeClient webChromeClient = new WebChromeClient() {
public void onPageFinished(WebView view, String url) {
view.loadUrl("javascript:(function() { " + "NativeAndroid.getContentHeight(document.documentElement.scrollHeight);" + "})();");
}
}
this.setWebChromeClient(webChromeClient);
有时候需要webview的高度自适应其内部内容的高度。目前并没有什么好办法,就是在onPageFinished后执行一段javascript代码,让页面的高度用javascriptInterface的方法传递到native端。
7.2 视频在关闭后仍然播放
webView.getClass().getMethod("onPause").invoke(webView,(Object[])null);
webView.getClass().getMethod("onResume").invoke(webView,(Object[])null);
webView.removeAllViews()
webView.destroy()
安卓自带的webview有很多问题,其中一个大bug,就是关闭webview后,仍然会有视频的声音放出。解决办法是将页面onResume与onPause时调用webview的onPause与onRusume,在页面的onDestroy时调用webview的removeAllViews与destroy来彻底关闭视频。
7.3 弹出提示跳转浏览器
this.setWebViewClient(new WebViewClient(){
@Override
public boolean shouldOverrideUrlLoading(WebView var1,String var2){
CrossWebView.this.loadUrl(var2);
return true;
}
}
打开webview,弹出了一个对话框,让你选择什么浏览器来打开页面。解决办法很简单,覆盖setWebViewClient的shouldOverrideUrlLoading方法即可,让他默认使用原来的webview来加载网页,并且返回true代表你已经处理了这种情况。
7.4 支持下拉刷新的功能
@Override
public boolean canScrollVertically( int direction ){
if( direction < 0 ){
return this.getWebScrollY() > 0;
}else{
return 0;
}
}
不像ListView,ScrollView,这些原生的组件默认就与各大的下拉刷新组件相兼容,WebView默认与下拉组件不兼容,导致webview组件往下拉时没有触发下拉刷新。
7.5 onPageFinished与onRecieveError多次触发
@Override
public void onPageFinished(WebView var1, String var2) {
if( m_isLoadError == true ){
return;
}
postDelayed(new Runnable() {
@Override
public void run() {
if( m_isLoadError == true ){
return;
}
m_finishListener.onFinish();
}
},300);
}
@Override
public void onReceivedError(WebView var1, int var2, String var3, String var4) {
m_isLoadError = true;
m_errorListener.onError();
}
安卓的onPageFinished与onReceiveError触发时机有问题。例如,如果打开的页面是一个404页面,安卓会触发onReceiveError事件,然后触发onPageFinished。然后,如果你调用reload()来重新加载页面,安卓又会触发onPageFinished事件,然后触发onReceiveError事件。而我们希望的是,页面出错时只触发onReceiveError事件,页面正常打开时触发onPageFinished事件。所以,解决办法只能像上面这样了。注意,每次loadUrl与reload后,默认设置m_isLoadError为false。
7.6 内存泄漏
安卓与ios的webview中有严重的内存泄漏问题,页面关掉了以后,页面的webview仍然占据着内存空间
7.6.1 onDestry
mWebview.removeAllViews();
mWebview.destroy();
在Page的onDestroy回调中,手动调用webview的removeAllViews与destroy来回收垃圾。回收能力一般,不能解决全部问题
public void setConfigCallback(WindowManager windowManager) {
try {
Field field = WebView.class.getDeclaredField("mWebViewCore");
field = field.getType().getDeclaredField("mBrowserFrame");
field = field.getType().getDeclaredField("sConfigCallback");
field.setAccessible(true);
Object configCallback = field.get(null);
if (null == configCallback) {
return;
}
field = field.getType().getDeclaredField("mWindowManager");
field.setAccessible(true);
field.set(configCallback, windowManager);
} catch(Exception e) {
}
}
部分同学表示还需要用反射强制将webview的成员变量设置为null
webView.loadUrl('about:blank');
强制将webview加载到一个空页面,将原网页的图片,js等资源释放掉
7.6.2 杀死进程
将webview所在的activity放到一个单独的进程中,然后关闭activity时强制System.exit(0)来将进程关掉。暴力,好用,还能避免webview挂掉导致原进程挂了,目前微信和QQ打开网页用的就是这个办法,局限在于webview只有一个的场景,多webview的场景无能为力
7.6.3 webview池
@Override
public void onDestroy(){
super.onDestroy();
mWebView.loadUrl('about:blank');
pool.add(mWebView);
}
将用完的webview放入到一个池里面,下一次新建webView时优先从池里面取出,这样就能将新建的webView控制到一个可控的范围内,避免内存无限增大,但依然会有少许的内存泄漏。
7.7 cookie持久化
webView中的cookie不能很好地持久化到磁盘上,导致用户登录后,下次启动app还需要重新登录
7.7.1 强制持久化
@Override
public void onPause() {
super.onPause();
CookieStore.store(this);
}
在Activity的onPause回调中,将webview的cookie池全部写入到磁盘中
public class MyApplication extends Application{
@Override
public void onCreate(){
super.onCreate();
CookieStore.load(this);
}
}
在Application启动时,将磁盘的cookie读取到webview的cookie池中
public class CookieStore {
private static String getDomain(){
HomePageInfo.Setting setting = HomePageInfo.getSetting();
String url = setting.m_menuSetting.get(0).m_url;
Uri uri = Uri.parse(url);
return uri.getHost();
}
public static void store(Context context){
//获取cookie
String cookie = CookieManager.getInstance().getCookie("http://"+getDomain()+"/");
if( cookie == null ){
Log.d("mm_fish","storeCookie null");
return;
}
Log.d("mm_fish","storeCookie "+cookie);
Store.set(context,"cookie",cookie);
//同步cookie
CookieManager.getInstance().flush();
CookieSyncManager.getInstance().sync();
}
private static int loadSize = 0;
public static void load(Context context,final FinishListener<Object> listener){
//开启cookie
context = context.getApplicationContext();
CookieSyncManager.createInstance(context);
final CookieManager cookieManager = CookieManager.getInstance();
cookieManager.setAcceptCookie(true);
//设置cookie
String cookie = Store.get(context,"cookie");
if( cookie.isEmpty() ){
Log.d("mm_fish","loadCookie null");
listener.onFinish(0,"",null);
return;
}
Log.d("mm_fish","loadCookie "+cookie);
ValueCallback<Boolean> callback = new ValueCallback<Boolean>() {
@Override
public void onReceiveValue(Boolean aBoolean) {
Log.d("mm_fish2","setCookie "+aBoolean);
loadSize--;
if( loadSize == 0 ){
cookieManager.flush();
CookieSyncManager.getInstance().sync();
listener.onFinish(0,"",null);
}
}
};
String[] cookies = cookie.split(";");
loadSize = cookies.length;
for( String singleCookie : cookies ){
singleCookie += "; Max-Age=62208000; path=/; domain="+getDomain();
cookieManager.setCookie("http://"+getDomain()+"/", singleCookie,callback);
}
}
cookie的存储落地采用的是SharedPreference
7.7.2 x5与自带webview冲突
QbSdk.preInit(context.getApplicationContext(), new QbSdk.PreInitCallback() {
@Override
public void onCoreInitFinished() {
}
@Override
public void onViewInitFinished() {
listener.onFinish(0,"",new InitResult());
}
});
如果你是使用x5内核浏览服务,第一次启动时是用自带的webview,cookie写入到自带的cookie池中。第二次启动时是用的x5内核,使用的cookie池是x5的cookie池,导致第一次写入的cookie都丢失了。解决办法是,第一次启动就强制使用x5内核加载浏览服务。
7.7.3 接管网络请求
@Override
public WebResourceResponse shouldInterceptRequest(WebView var1, String var2) {
return m_jsBridge.shouldInterceptLoadRequest(var2);
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView var1, WebResourceRequest var2) {
return m_jsBridge.shouldInterceptLoadRequest(var2.getUrl().toString());
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView var1, WebResourceRequest var2, Bundle var3) {
return m_jsBridge.shouldInterceptLoadRequest(var2.getUrl().toString());
}
在WebViewClient中强制将所有的网络请求都走okhttp或volley,让cookie都给okhttp等的网络框架来做持久化的行为,安全可靠。唯一不足是,API 21以下的机器,这样做会导致post,put请求都丢失了,因为API 21以下的拦截网路请求中只给予了url,没有body体。
7.8 图片选择器拍照上传的延迟问题
自定义图片选择器以后,会发现当选择拍照上传的图片,webview不能马上读取到,需要延迟一会儿(大概1s)才能读取到。这个问题有点蛋疼,因为拍照时调用的是系统的拍照app,这个app在拍摄完后没有将数据及时落地到磁盘,导致了其他进程获取这个文件时会不齐全。但是,当你使用java的读文件接口来获取数据,却能避免这个情况。解决方法有两个:
- 接管webview中关于外部磁盘文件的读取工作,使得读取文件都是用Java的方式来读取,避免读取延迟的问题。
- 拍摄类的照片,加上5s的读取延迟
7.9 html页面禁止缓存
怎么做到只是html页面不缓存,而js,css等是走缓存的呢。实现办法是,在loadUrl上做手脚,让它带上随机数来避免缓存,但要注意的是随机数的参数必须加在query的地方,不要加在hash的地方,不然会影响跨页面的喵点跳转
private String getNoCacheUrl(String url){
Date date = new Date();
int queryIndex = url.indexOf('?');
int hashIndex = url.indexOf('#');
String query = "";
String hash = "";
String base = "";
if( queryIndex != -1 && hashIndex != -1 ){
if( queryIndex < hashIndex ){
base = url.substring(0,queryIndex);
query = url.substring(queryIndex+1,hashIndex);
hash = url.substring(hashIndex+1);
}else{
base = url.substring(0,hashIndex);
hash = url.substring(hashIndex+1,queryIndex);
query = url.substring(queryIndex+1);
}
}else if( queryIndex != -1 ){
base = url.substring(0,queryIndex);
query = url.substring(queryIndex+1);
hash = "";
}else if( hashIndex != -1 ){
base = url.substring(0,hashIndex);
hash = url.substring(hashIndex+1);
query = "";
}else{
base = url;
query = "";
hash = "";
}
if( query.isEmpty() == false ){
query += "&";
}
query += "_crossapinocache="+date.getTime();
String result = base + "?" + query+"#"+hash;
Log.d("CrossWebView","url "+url+","+" noCacheUrl: "+result);
return result;
}
- 本文作者: fishedee
- 版权声明: 本博客所有文章均采用 CC BY-NC-SA 3.0 CN 许可协议,转载必须注明出处!